diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index 1c6da9b82a9a661307fd5eec1cd647ceb6c292bb..e8349601b5303331c0a6a38aca306fe57ab07ed3 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -533,8 +533,8 @@ impl remote::RemoteClientDelegate for RemoteClientDelegate { AutoUpdater::download_remote_server_release( release_channel, version.clone(), - platform.os, - platform.arch, + platform.os.as_str(), + platform.arch.as_str(), move |status, cx| this.set_status(Some(status), cx), cx, ) @@ -564,8 +564,8 @@ impl remote::RemoteClientDelegate for RemoteClientDelegate { AutoUpdater::get_remote_server_release_url( release_channel, version, - platform.os, - platform.arch, + platform.os.as_str(), + platform.arch.as_str(), cx, ) .await diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 51b71c988a6dc57e875b3baa28103bef0d8fd729..2db918ecce331acac91bb974df1b784f0d6532b3 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -7,8 +7,9 @@ mod transport; #[cfg(target_os = "windows")] pub use remote_client::OpenWslPath; pub use remote_client::{ - ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent, - RemoteConnection, RemoteConnectionOptions, RemotePlatform, connect, + ConnectionIdentifier, ConnectionState, RemoteArch, RemoteClient, RemoteClientDelegate, + RemoteClientEvent, RemoteConnection, RemoteConnectionOptions, RemoteOs, RemotePlatform, + connect, }; pub use transport::docker::DockerConnectionOptions; pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption}; diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index 9c6508e5f16027eb23225f0eceb1cb691f4a33c9..79bdbe540d070bfa18a6417622b386458ff221a8 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -49,10 +49,58 @@ use util::{ paths::{PathStyle, RemotePathBuf}, }; +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum RemoteOs { + Linux, + MacOs, + Windows, +} + +impl RemoteOs { + pub fn as_str(&self) -> &'static str { + match self { + RemoteOs::Linux => "linux", + RemoteOs::MacOs => "macos", + RemoteOs::Windows => "windows", + } + } + + pub fn is_windows(&self) -> bool { + matches!(self, RemoteOs::Windows) + } +} + +impl std::fmt::Display for RemoteOs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum RemoteArch { + X86_64, + Aarch64, +} + +impl RemoteArch { + pub fn as_str(&self) -> &'static str { + match self { + RemoteArch::X86_64 => "x86_64", + RemoteArch::Aarch64 => "aarch64", + } + } +} + +impl std::fmt::Display for RemoteArch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + #[derive(Copy, Clone, Debug)] pub struct RemotePlatform { - pub os: &'static str, - pub arch: &'static str, + pub os: RemoteOs, + pub arch: RemoteArch, } #[derive(Clone, Debug)] @@ -89,7 +137,8 @@ pub trait RemoteClientDelegate: Send + Sync { const MAX_MISSED_HEARTBEATS: usize = 5; const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5); -const INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60); +const INITIAL_CONNECTION_TIMEOUT: Duration = + Duration::from_secs(if cfg!(debug_assertions) { 5 } else { 60 }); const MAX_RECONNECT_ATTEMPTS: usize = 3; diff --git a/crates/remote/src/transport.rs b/crates/remote/src/transport.rs index 4cafbf60eec338addbb43e46d156960621301ab0..ebf643352fce8a14d88b7c870b177d2c6b7e7de0 100644 --- a/crates/remote/src/transport.rs +++ b/crates/remote/src/transport.rs @@ -1,5 +1,5 @@ use crate::{ - RemotePlatform, + RemoteArch, RemoteOs, RemotePlatform, json_log::LogRecord, protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message}, }; @@ -26,8 +26,8 @@ fn parse_platform(output: &str) -> Result { }; let os = match os { - "Darwin" => "macos", - "Linux" => "linux", + "Darwin" => RemoteOs::MacOs, + "Linux" => RemoteOs::Linux, _ => anyhow::bail!( "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" ), @@ -39,9 +39,9 @@ fn parse_platform(output: &str) -> Result { || arch.starts_with("arm64") || arch.starts_with("aarch64") { - "aarch64" + RemoteArch::Aarch64 } else if arch.starts_with("x86") { - "x86_64" + RemoteArch::X86_64 } else { anyhow::bail!( "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" @@ -193,7 +193,8 @@ async fn build_remote_server_from_source( .await?; anyhow::ensure!( output.status.success(), - "Failed to run command: {command:?}" + "Failed to run command: {command:?}: output: {}", + String::from_utf8_lossy(&output.stderr) ); Ok(()) } @@ -203,14 +204,15 @@ async fn build_remote_server_from_source( "{}-{}", platform.arch, match platform.os { - "linux" => + RemoteOs::Linux => if use_musl { "unknown-linux-musl" } else { "unknown-linux-gnu" }, - "macos" => "apple-darwin", - _ => anyhow::bail!("can't cross compile for: {:?}", platform), + RemoteOs::MacOs => "apple-darwin", + RemoteOs::Windows if cfg!(windows) => "pc-windows-msvc", + RemoteOs::Windows => "pc-windows-gnu", } ); let mut rust_flags = match std::env::var("RUSTFLAGS") { @@ -221,7 +223,7 @@ async fn build_remote_server_from_source( String::new() } }; - if platform.os == "linux" && use_musl { + if platform.os == RemoteOs::Linux && use_musl { rust_flags.push_str(" -C target-feature=+crt-static"); if let Ok(path) = std::env::var("ZED_ZSTD_MUSL_LIB") { @@ -232,7 +234,9 @@ async fn build_remote_server_from_source( rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); } - if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS { + if platform.arch.as_str() == std::env::consts::ARCH + && platform.os.as_str() == std::env::consts::OS + { delegate.set_status(Some("Building remote server binary from source"), cx); log::info!("building remote server binary from source"); run_cmd( @@ -308,7 +312,8 @@ async fn build_remote_server_from_source( .join("remote_server") .join(&triple) .join("debug") - .join("remote_server"); + .join("remote_server") + .with_extension(if platform.os.is_windows() { "exe" } else { "" }); let path = if !build_remote_server.contains("nocompress") { delegate.set_status(Some("Compressing binary"), cx); @@ -374,35 +379,44 @@ mod tests { #[test] fn test_parse_platform() { let result = parse_platform("Linux x86_64\n").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "x86_64"); + assert_eq!(result.os, RemoteOs::Linux); + assert_eq!(result.arch, RemoteArch::X86_64); let result = parse_platform("Darwin arm64\n").unwrap(); - assert_eq!(result.os, "macos"); - assert_eq!(result.arch, "aarch64"); + assert_eq!(result.os, RemoteOs::MacOs); + assert_eq!(result.arch, RemoteArch::Aarch64); let result = parse_platform("Linux x86_64").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "x86_64"); + assert_eq!(result.os, RemoteOs::Linux); + assert_eq!(result.arch, RemoteArch::X86_64); let result = parse_platform("some shell init output\nLinux aarch64\n").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "aarch64"); + assert_eq!(result.os, RemoteOs::Linux); + assert_eq!(result.arch, RemoteArch::Aarch64); let result = parse_platform("some shell init output\nLinux aarch64").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "aarch64"); + assert_eq!(result.os, RemoteOs::Linux); + assert_eq!(result.arch, RemoteArch::Aarch64); - assert_eq!(parse_platform("Linux armv8l\n").unwrap().arch, "aarch64"); - assert_eq!(parse_platform("Linux aarch64\n").unwrap().arch, "aarch64"); - assert_eq!(parse_platform("Linux x86_64\n").unwrap().arch, "x86_64"); + assert_eq!( + parse_platform("Linux armv8l\n").unwrap().arch, + RemoteArch::Aarch64 + ); + assert_eq!( + parse_platform("Linux aarch64\n").unwrap().arch, + RemoteArch::Aarch64 + ); + assert_eq!( + parse_platform("Linux x86_64\n").unwrap().arch, + RemoteArch::X86_64 + ); let result = parse_platform( r#"Linux x86_64 - What you're referring to as Linux, is in fact, GNU/Linux...\n"#, ) .unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "x86_64"); + assert_eq!(result.os, RemoteOs::Linux); + assert_eq!(result.arch, RemoteArch::X86_64); assert!(parse_platform("Windows x86_64\n").is_err()); assert!(parse_platform("Linux armv7l\n").is_err()); diff --git a/crates/remote/src/transport/docker.rs b/crates/remote/src/transport/docker.rs index 09f5935ec621260e933f11f46aa57493a31ace6d..9c14aa874941a5cdcd824d4adaeb41d694e347d8 100644 --- a/crates/remote/src/transport/docker.rs +++ b/crates/remote/src/transport/docker.rs @@ -24,8 +24,8 @@ use gpui::{App, AppContext, AsyncApp, Task}; use rpc::proto::Envelope; use crate::{ - RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, RemotePlatform, - remote_client::CommandTemplate, + RemoteArch, RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, RemoteOs, + RemotePlatform, remote_client::CommandTemplate, }; #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] @@ -70,7 +70,7 @@ impl DockerExecConnection { let remote_platform = this.check_remote_platform().await?; this.path_style = match remote_platform.os { - "windows" => Some(PathStyle::Windows), + RemoteOs::Windows => Some(PathStyle::Windows), _ => Some(PathStyle::Posix), }; @@ -124,8 +124,8 @@ impl DockerExecConnection { }; let os = match os.trim() { - "Darwin" => "macos", - "Linux" => "linux", + "Darwin" => RemoteOs::MacOs, + "Linux" => RemoteOs::Linux, _ => anyhow::bail!( "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" ), @@ -136,9 +136,9 @@ impl DockerExecConnection { || arch.starts_with("arm64") || arch.starts_with("aarch64") { - "aarch64" + RemoteArch::Aarch64 } else if arch.starts_with("x86") { - "x86_64" + RemoteArch::X86_64 } else { anyhow::bail!( "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 37921b88637f19b68f9625170d9d99e85b5a9bdf..6c8eb49c1c2158322a275e064162b53e2f5f3d5e 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -1,5 +1,5 @@ use crate::{ - RemoteClientDelegate, RemotePlatform, + RemoteArch, RemoteClientDelegate, RemoteOs, RemotePlatform, remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, transport::{parse_platform, parse_shell}, }; @@ -402,30 +402,50 @@ impl RemoteConnection for SshRemoteConnection { delegate: Arc, cx: &mut AsyncApp, ) -> Task> { + const VARS: [&str; 3] = ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"]; delegate.set_status(Some("Starting proxy"), cx); let Some(remote_binary_path) = self.remote_binary_path.clone() else { return Task::ready(Err(anyhow!("Remote binary path not set"))); }; - let mut proxy_args = vec![]; - for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { - if let Some(value) = std::env::var(env_var).ok() { - proxy_args.push(format!("{}='{}'", env_var, value)); + let mut ssh_command = if self.ssh_platform.os.is_windows() { + // TODO: Set the `VARS` environment variables, we do not have `env` on windows + // so this needs a different approach + let mut proxy_args = vec![]; + proxy_args.push("proxy".to_owned()); + proxy_args.push("--identifier".to_owned()); + proxy_args.push(unique_identifier); + + if reconnect { + proxy_args.push("--reconnect".to_owned()); } - } - proxy_args.push(remote_binary_path.display(self.path_style()).into_owned()); - proxy_args.push("proxy".to_owned()); - proxy_args.push("--identifier".to_owned()); - proxy_args.push(unique_identifier); + self.socket.ssh_command( + self.ssh_shell_kind, + &remote_binary_path.display(self.path_style()), + &proxy_args, + false, + ) + } else { + let mut proxy_args = vec![]; + for env_var in VARS { + if let Some(value) = std::env::var(env_var).ok() { + proxy_args.push(format!("{}='{}'", env_var, value)); + } + } + proxy_args.push(remote_binary_path.display(self.path_style()).into_owned()); + proxy_args.push("proxy".to_owned()); + proxy_args.push("--identifier".to_owned()); + proxy_args.push(unique_identifier); - if reconnect { - proxy_args.push("--reconnect".to_owned()); - } + if reconnect { + proxy_args.push("--reconnect".to_owned()); + } + self.socket + .ssh_command(self.ssh_shell_kind, "env", &proxy_args, false) + }; - let ssh_proxy_process = match self - .socket - .ssh_command(self.ssh_shell_kind, "env", &proxy_args, false) + let ssh_proxy_process = match ssh_command // IMPORTANT: we kill this process when we drop the task that uses it. .kill_on_drop(true) .spawn() @@ -545,22 +565,20 @@ impl SshRemoteConnection { .await?; drop(askpass); - let ssh_shell = socket.shell().await; + let is_windows = socket.probe_is_windows().await; + log::info!("Remote is windows: {}", is_windows); + + let ssh_shell = socket.shell(is_windows).await; log::info!("Remote shell discovered: {}", ssh_shell); - let ssh_platform = socket.platform(ShellKind::new(&ssh_shell, false)).await?; + + let ssh_shell_kind = ShellKind::new(&ssh_shell, is_windows); + let ssh_platform = socket.platform(ssh_shell_kind, is_windows).await?; log::info!("Remote platform discovered: {:?}", ssh_platform); - let ssh_path_style = match ssh_platform.os { - "windows" => PathStyle::Windows, - _ => PathStyle::Posix, + + let (ssh_path_style, ssh_default_system_shell) = match ssh_platform.os { + RemoteOs::Windows => (PathStyle::Windows, ssh_shell.clone()), + _ => (PathStyle::Posix, String::from("/bin/sh")), }; - let ssh_default_system_shell = String::from("/bin/sh"); - let ssh_shell_kind = ShellKind::new( - &ssh_shell, - match ssh_platform.os { - "windows" => true, - _ => false, - }, - ); let mut this = Self { socket, @@ -596,9 +614,14 @@ impl SshRemoteConnection { _ => version.to_string(), }; let binary_name = format!( - "zed-remote-server-{}-{}", + "zed-remote-server-{}-{}{}", release_channel.dev_name(), - version_str + version_str, + if self.ssh_platform.os.is_windows() { + ".exe" + } else { + "" + } ); let dst_path = paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap()); @@ -710,14 +733,19 @@ impl SshRemoteConnection { cx: &mut AsyncApp, ) -> Result<()> { if let Some(parent) = tmp_path_gz.parent() { - self.socket + let res = self + .socket .run_command( self.ssh_shell_kind, "mkdir", &["-p", parent.display(self.path_style()).as_ref()], true, ) - .await?; + .await; + if !self.ssh_platform.os.is_windows() { + // mkdir fails on windows if the path already exists ... + res?; + } } delegate.set_status(Some("Downloading remote development server on host"), cx); @@ -805,17 +833,24 @@ impl SshRemoteConnection { cx: &mut AsyncApp, ) -> Result<()> { if let Some(parent) = tmp_path_gz.parent() { - self.socket + let res = self + .socket .run_command( self.ssh_shell_kind, "mkdir", &["-p", parent.display(self.path_style()).as_ref()], true, ) - .await?; + .await; + if !self.ssh_platform.os.is_windows() { + // mkdir fails on windows if the path already exists ... + res?; + } } - let src_stat = fs::metadata(&src_path).await?; + let src_stat = fs::metadata(&src_path) + .await + .with_context(|| format!("failed to get metadata for {:?}", src_path))?; let size = src_stat.len(); let t0 = Instant::now(); @@ -866,7 +901,7 @@ impl SshRemoteConnection { }; let args = shell_kind.args_for_shell(false, script.to_string()); self.socket - .run_command(shell_kind, "sh", &args, true) + .run_command(self.ssh_shell_kind, "sh", &args, true) .await?; Ok(()) } @@ -1054,6 +1089,7 @@ impl SshSocket { ) -> Result { let mut command = self.ssh_command(shell_kind, program, args, allow_pseudo_tty); let output = command.output().await?; + log::debug!("{:?}: {:?}", command, output); anyhow::ensure!( output.status.success(), "failed to run command {command:?}: {}", @@ -1125,12 +1161,71 @@ impl SshSocket { arguments } - async fn platform(&self, shell: ShellKind) -> Result { - let output = self.run_command(shell, "uname", &["-sm"], false).await?; + async fn platform(&self, shell: ShellKind, is_windows: bool) -> Result { + if is_windows { + self.platform_windows(shell).await + } else { + self.platform_posix(shell).await + } + } + + async fn platform_posix(&self, shell: ShellKind) -> Result { + let output = self + .run_command(shell, "uname", &["-sm"], false) + .await + .context("Failed to run 'uname -sm' to determine platform")?; parse_platform(&output) } - async fn shell(&self) -> String { + async fn platform_windows(&self, shell: ShellKind) -> Result { + let output = self + .run_command( + shell, + "cmd", + &["/c", "echo", "%PROCESSOR_ARCHITECTURE%"], + false, + ) + .await + .context( + "Failed to run 'echo %PROCESSOR_ARCHITECTURE%' to determine Windows architecture", + )?; + + Ok(RemotePlatform { + os: RemoteOs::Windows, + arch: match output.trim() { + "AMD64" => RemoteArch::X86_64, + "ARM64" => RemoteArch::Aarch64, + arch => anyhow::bail!( + "Prebuilt remote servers are not yet available for windows-{arch}. See https://zed.dev/docs/remote-development" + ), + }, + }) + } + + /// Probes whether the remote host is running Windows. + /// + /// This is done by attempting to run a simple Windows-specific command. + /// If it succeeds and returns Windows-like output, we assume it's Windows. + async fn probe_is_windows(&self) -> bool { + match self + .run_command(ShellKind::PowerShell, "cmd", &["/c", "ver"], false) + .await + { + // Windows 'ver' command outputs something like "Microsoft Windows [Version 10.0.19045.5011]" + Ok(output) => output.trim().contains("indows"), + Err(_) => false, + } + } + + async fn shell(&self, is_windows: bool) -> String { + if is_windows { + self.shell_windows().await + } else { + self.shell_posix().await + } + } + + async fn shell_posix(&self) -> String { const DEFAULT_SHELL: &str = "sh"; match self .run_command(ShellKind::Posix, "sh", &["-c", "echo $SHELL"], false) @@ -1143,6 +1238,13 @@ impl SshSocket { } } } + + async fn shell_windows(&self) -> String { + // powershell is always the default, and cannot really be removed from the system + // so we can rely on that fact and reasonably assume that we will be running in a + // powershell environment + "powershell.exe".to_owned() + } } fn parse_port_number(port_str: &str) -> Result { diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs index d27648e67840681765248ae1cce12c15d7a13228..32dd9ebe8247bb4a0b631a79b1a93deb621e6ed1 100644 --- a/crates/remote/src/transport/wsl.rs +++ b/crates/remote/src/transport/wsl.rs @@ -1,5 +1,5 @@ use crate::{ - RemoteClientDelegate, RemotePlatform, + RemoteArch, RemoteClientDelegate, RemoteOs, RemotePlatform, remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, transport::{parse_platform, parse_shell}, }; @@ -70,7 +70,10 @@ impl WslRemoteConnection { let mut this = Self { connection_options, remote_binary_path: None, - platform: RemotePlatform { os: "", arch: "" }, + platform: RemotePlatform { + os: RemoteOs::Linux, + arch: RemoteArch::X86_64, + }, shell: String::new(), shell_kind: ShellKind::Posix, default_system_shell: String::from("/bin/sh"),