Detailed changes
@@ -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
@@ -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};
@@ -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;
@@ -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<RemotePlatform> {
};
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<RemotePlatform> {
|| 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());
@@ -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"
@@ -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<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Task<Result<i32>> {
+ 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<String> {
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<RemotePlatform> {
- let output = self.run_command(shell, "uname", &["-sm"], false).await?;
+ async fn platform(&self, shell: ShellKind, is_windows: bool) -> Result<RemotePlatform> {
+ if is_windows {
+ self.platform_windows(shell).await
+ } else {
+ self.platform_posix(shell).await
+ }
+ }
+
+ async fn platform_posix(&self, shell: ShellKind) -> Result<RemotePlatform> {
+ 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<RemotePlatform> {
+ 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<u16> {
@@ -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"),