logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

51
crates/remote/Cargo.toml Normal file
View File

@@ -0,0 +1,51 @@
[package]
name = "remote"
description = "Client-side subsystem for remote editing"
edition.workspace = true
version = "0.1.0"
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/remote.rs"
doctest = false
[features]
default = []
build-remote-server-binary = []
test-support = ["fs/test-support"]
[dependencies]
anyhow.workspace = true
askpass.workspace = true
async-trait.workspace = true
base64.workspace = true
collections.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
log.workspace = true
parking_lot.workspace = true
paths.workspace = true
prost.workspace = true
release_channel.workspace = true
rpc = { workspace = true, features = ["gpui"] }
schemars.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
tempfile.workspace = true
thiserror.workspace = true
urlencoding.workspace = true
util.workspace = true
which.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }

1
crates/remote/LICENSE-GPL Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,61 @@
use std::borrow::Cow;
use log::{Level, Log, Record};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug, Serialize)]
pub struct LogRecord<'a> {
pub level: usize,
pub module_path: Option<Cow<'a, str>>,
pub file: Option<Cow<'a, str>>,
pub line: Option<u32>,
pub message: Cow<'a, str>,
}
impl<'a> LogRecord<'a> {
pub fn new(record: &'a Record<'a>) -> Self {
Self {
level: serialize_level(record.level()),
module_path: record.module_path().map(Cow::Borrowed),
file: record.file().map(Cow::Borrowed),
line: record.line(),
message: Cow::Owned(record.args().to_string()),
}
}
pub fn log(&'a self, logger: &dyn Log) {
if let Some(level) = deserialize_level(self.level) {
logger.log(
&log::Record::builder()
.module_path(self.module_path.as_deref())
.target("remote_server")
.args(format_args!("{}", self.message))
.file(self.file.as_deref())
.line(self.line)
.level(level)
.build(),
)
}
}
}
fn serialize_level(level: Level) -> usize {
match level {
Level::Error => 1,
Level::Warn => 2,
Level::Info => 3,
Level::Debug => 4,
Level::Trace => 5,
}
}
fn deserialize_level(level: usize) -> Option<Level> {
match level {
1 => Some(Level::Error),
2 => Some(Level::Warn),
3 => Some(Level::Info),
4 => Some(Level::Debug),
5 => Some(Level::Trace),
_ => None,
}
}

View File

@@ -0,0 +1,76 @@
use anyhow::Result;
use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use prost::Message as _;
use rpc::proto::Envelope;
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub struct MessageId(pub u32);
pub type MessageLen = u32;
pub const MESSAGE_LEN_SIZE: usize = size_of::<MessageLen>();
pub fn message_len_from_buffer(buffer: &[u8]) -> MessageLen {
MessageLen::from_le_bytes(buffer.try_into().unwrap())
}
pub async fn read_message_with_len<S: AsyncRead + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
message_len: MessageLen,
) -> Result<Envelope> {
buffer.resize(message_len as usize, 0);
stream.read_exact(buffer).await?;
Ok(Envelope::decode(buffer.as_slice())?)
}
pub async fn read_message<S: AsyncRead + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
) -> Result<Envelope> {
buffer.resize(MESSAGE_LEN_SIZE, 0);
stream.read_exact(buffer).await?;
let len = message_len_from_buffer(buffer);
read_message_with_len(stream, buffer, len).await
}
pub async fn write_message<S: AsyncWrite + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
message: Envelope,
) -> Result<()> {
let message_len = message.encoded_len() as u32;
stream
.write_all(message_len.to_le_bytes().as_slice())
.await?;
buffer.clear();
buffer.reserve(message_len as usize);
message.encode(buffer)?;
stream.write_all(buffer).await?;
Ok(())
}
pub async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
) -> Result<()> {
let len = buffer.len() as u32;
stream.write_all(len.to_le_bytes().as_slice()).await?;
stream.write_all(buffer).await?;
Ok(())
}
pub async fn read_message_raw<S: AsyncRead + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
) -> Result<()> {
buffer.resize(MESSAGE_LEN_SIZE, 0);
stream.read_exact(buffer).await?;
let message_len = message_len_from_buffer(buffer);
buffer.resize(message_len as usize, 0);
stream.read_exact(buffer).await?;
Ok(())
}

View File

@@ -0,0 +1,24 @@
use thiserror::Error;
#[derive(Copy, Clone, Error, Debug)]
#[repr(i32)]
pub enum ProxyLaunchError {
// We're using 90 as the exit code, because 0-78 are often taken
// by shells and other conventions and >128 also has certain meanings
// in certain contexts.
#[error("Attempted reconnect, but server not running.")]
ServerNotRunning = 90,
}
impl ProxyLaunchError {
pub fn to_exit_code(self) -> i32 {
self as i32
}
pub fn from_exit_code(exit_code: i32) -> Option<Self> {
match exit_code {
90 => Some(Self::ServerNotRunning),
_ => None,
}
}
}

View File

@@ -0,0 +1,27 @@
pub mod json_log;
pub mod protocol;
pub mod proxy;
pub mod remote_client;
pub mod remote_identity;
mod transport;
#[cfg(target_os = "windows")]
pub use remote_client::OpenWslPath;
pub use remote_client::{
CommandTemplate, ConnectionIdentifier, ConnectionState, Interactive, RemoteArch, RemoteClient,
RemoteClientDelegate, RemoteClientEvent, RemoteConnection, RemoteConnectionOptions, RemoteOs,
RemotePlatform, connect, has_active_connection,
};
pub use remote_identity::{
RemoteConnectionIdentity, remote_connection_identity, same_remote_connection_identity,
};
pub use transport::docker::DockerConnectionOptions;
pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};
pub use transport::wsl::WslConnectionOptions;
#[cfg(target_os = "windows")]
pub use transport::wsl::wsl_path_to_windows_path;
#[cfg(any(test, feature = "test-support"))]
pub use transport::mock::{
MockConnection, MockConnectionOptions, MockConnectionRegistry, MockDelegate,
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,168 @@
use crate::RemoteConnectionOptions;
/// A normalized remote identity for matching live remote hosts against
/// persisted remote metadata.
///
/// This mirrors workspace persistence identity semantics rather than full
/// `RemoteConnectionOptions` equality, so runtime-only fields like SSH
/// nicknames or Docker environment overrides do not affect matching.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RemoteConnectionIdentity {
Ssh {
host: String,
username: Option<String>,
port: Option<u16>,
},
Wsl {
distro_name: String,
user: Option<String>,
},
Docker {
container_id: String,
name: String,
remote_user: String,
},
#[cfg(any(test, feature = "test-support"))]
Mock { id: u64 },
}
impl From<&RemoteConnectionOptions> for RemoteConnectionIdentity {
fn from(options: &RemoteConnectionOptions) -> Self {
match options {
RemoteConnectionOptions::Ssh(options) => Self::Ssh {
host: options.host.to_string(),
username: options.username.clone(),
port: options.port,
},
RemoteConnectionOptions::Wsl(options) => Self::Wsl {
distro_name: options.distro_name.clone(),
user: options.user.clone(),
},
RemoteConnectionOptions::Docker(options) => Self::Docker {
container_id: options.container_id.clone(),
name: options.name.clone(),
remote_user: options.remote_user.clone(),
},
#[cfg(any(test, feature = "test-support"))]
RemoteConnectionOptions::Mock(options) => Self::Mock { id: options.id },
}
}
}
pub fn remote_connection_identity(options: &RemoteConnectionOptions) -> RemoteConnectionIdentity {
options.into()
}
pub fn same_remote_connection_identity(
left: Option<&RemoteConnectionOptions>,
right: Option<&RemoteConnectionOptions>,
) -> bool {
match (left, right) {
(Some(left), Some(right)) => {
remote_connection_identity(left) == remote_connection_identity(right)
}
(None, None) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::{DockerConnectionOptions, SshConnectionOptions, WslConnectionOptions};
#[test]
fn ssh_identity_ignores_non_persisted_runtime_fields() {
let left = RemoteConnectionOptions::Ssh(SshConnectionOptions {
host: "example.com".into(),
username: Some("anth".to_string()),
port: Some(2222),
password: Some("secret".to_string()),
args: Some(vec!["-v".to_string()]),
connection_timeout: Some(30),
nickname: Some("work".to_string()),
upload_binary_over_ssh: true,
..Default::default()
});
let right = RemoteConnectionOptions::Ssh(SshConnectionOptions {
host: "example.com".into(),
username: Some("anth".to_string()),
port: Some(2222),
password: None,
args: None,
connection_timeout: None,
nickname: None,
upload_binary_over_ssh: false,
..Default::default()
});
assert!(same_remote_connection_identity(Some(&left), Some(&right),));
}
#[test]
fn ssh_identity_distinguishes_persistence_key_fields() {
let left = RemoteConnectionOptions::Ssh(SshConnectionOptions {
host: "example.com".into(),
username: Some("anth".to_string()),
port: Some(2222),
..Default::default()
});
let right = RemoteConnectionOptions::Ssh(SshConnectionOptions {
host: "example.com".into(),
username: Some("anth".to_string()),
port: Some(2223),
..Default::default()
});
assert!(!same_remote_connection_identity(Some(&left), Some(&right),));
}
#[test]
fn wsl_identity_includes_user() {
let left = RemoteConnectionOptions::Wsl(WslConnectionOptions {
distro_name: "Ubuntu".to_string(),
user: Some("anth".to_string()),
});
let right = RemoteConnectionOptions::Wsl(WslConnectionOptions {
distro_name: "Ubuntu".to_string(),
user: Some("root".to_string()),
});
assert!(!same_remote_connection_identity(Some(&left), Some(&right),));
}
#[test]
fn docker_identity_ignores_non_persisted_runtime_fields() {
let left = RemoteConnectionOptions::Docker(DockerConnectionOptions {
name: "zed-dev".to_string(),
container_id: "container-123".to_string(),
remote_user: "anth".to_string(),
upload_binary_over_docker_exec: true,
use_podman: true,
remote_env: BTreeMap::from([("FOO".to_string(), "BAR".to_string())]),
});
let right = RemoteConnectionOptions::Docker(DockerConnectionOptions {
name: "zed-dev".to_string(),
container_id: "container-123".to_string(),
remote_user: "anth".to_string(),
upload_binary_over_docker_exec: false,
use_podman: false,
remote_env: BTreeMap::new(),
});
assert!(same_remote_connection_identity(Some(&left), Some(&right),));
}
#[test]
fn local_identity_matches_only_local_identity() {
let remote = RemoteConnectionOptions::Wsl(WslConnectionOptions {
distro_name: "Ubuntu".to_string(),
user: Some("anth".to_string()),
});
assert!(same_remote_connection_identity(None, None));
assert!(!same_remote_connection_identity(None, Some(&remote)));
}
}

View File

@@ -0,0 +1,466 @@
use std::io::Write;
use crate::{
RemoteArch, RemoteOs, RemotePlatform,
json_log::LogRecord,
protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message},
};
use anyhow::{Context as _, Result};
use futures::{
AsyncReadExt as _, FutureExt as _, StreamExt as _,
channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender},
};
use gpui::{AppContext as _, AsyncApp, Task};
use rpc::proto::Envelope;
use util::command::Child;
pub mod docker;
#[cfg(any(test, feature = "test-support"))]
pub mod mock;
pub mod ssh;
pub mod wsl;
/// Parses the output of `uname -sm` to determine the remote platform.
/// Takes the last line to skip possible shell initialization output.
fn parse_platform(output: &str) -> Result<RemotePlatform> {
let output = output.trim();
let uname = output.rsplit_once('\n').map_or(output, |(_, last)| last);
let Some((os, arch)) = uname.split_once(" ") else {
anyhow::bail!("unknown uname: {uname:?}")
};
let os = match os {
"Darwin" => RemoteOs::MacOs,
"Linux" => RemoteOs::Linux,
_ => anyhow::bail!(
"Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development"
),
};
// exclude armv5,6,7 as they are 32-bit.
let arch = if arch.starts_with("armv8")
|| arch.starts_with("armv9")
|| arch.starts_with("arm64")
|| arch.starts_with("aarch64")
{
RemoteArch::Aarch64
} else if arch.starts_with("x86") {
RemoteArch::X86_64
} else {
anyhow::bail!(
"Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development"
)
};
Ok(RemotePlatform { os, arch })
}
/// Parses the output of `echo $SHELL` to determine the remote shell.
/// Takes the last line to skip possible shell initialization output.
fn parse_shell(output: &str, fallback_shell: &str) -> String {
let output = output.trim();
let shell = output.rsplit_once('\n').map_or(output, |(_, last)| last);
if shell.is_empty() {
log::error!("$SHELL is not set, falling back to {fallback_shell}");
fallback_shell.to_owned()
} else {
shell.to_owned()
}
}
fn handle_rpc_messages_over_child_process_stdio(
mut remote_proxy_process: Child,
incoming_tx: UnboundedSender<Envelope>,
mut outgoing_rx: UnboundedReceiver<Envelope>,
mut connection_activity_tx: Sender<()>,
cx: &AsyncApp,
) -> Task<Result<i32>> {
let mut child_stderr = remote_proxy_process.stderr.take().unwrap();
let mut child_stdout = remote_proxy_process.stdout.take().unwrap();
let mut child_stdin = remote_proxy_process.stdin.take().unwrap();
let mut stdin_buffer = Vec::new();
let mut stdout_buffer = Vec::new();
let mut stderr_buffer = Vec::new();
let mut stderr_offset = 0;
let stdin_task = cx.background_spawn(async move {
while let Some(outgoing) = outgoing_rx.next().await {
write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
}
anyhow::Ok(())
});
let stdout_task = cx.background_spawn({
let mut connection_activity_tx = connection_activity_tx.clone();
async move {
loop {
stdout_buffer.resize(MESSAGE_LEN_SIZE, 0);
let len = child_stdout.read(&mut stdout_buffer).await?;
if len == 0 {
return anyhow::Ok(());
}
if len < MESSAGE_LEN_SIZE {
child_stdout.read_exact(&mut stdout_buffer[len..]).await?;
}
let message_len = message_len_from_buffer(&stdout_buffer);
let envelope =
read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len)
.await?;
connection_activity_tx.try_send(()).ok();
incoming_tx.unbounded_send(envelope).ok();
}
}
});
let stderr_task: Task<anyhow::Result<()>> = cx.background_spawn(async move {
loop {
stderr_buffer.resize(stderr_offset + 1024, 0);
let len = child_stderr
.read(&mut stderr_buffer[stderr_offset..])
.await?;
if len == 0 {
return anyhow::Ok(());
}
stderr_offset += len;
let mut start_ix = 0;
while let Some(ix) = stderr_buffer[start_ix..stderr_offset]
.iter()
.position(|b| b == &b'\n')
{
let line_ix = start_ix + ix;
let content = &stderr_buffer[start_ix..line_ix];
start_ix = line_ix + 1;
if let Ok(record) = serde_json::from_slice::<LogRecord>(content) {
record.log(log::logger())
} else {
std::io::stderr()
.write_fmt(format_args!(
"(remote) {}\n",
String::from_utf8_lossy(content)
))
.ok();
}
}
stderr_buffer.drain(0..start_ix);
stderr_offset -= start_ix;
connection_activity_tx.try_send(()).ok();
}
});
cx.background_spawn(async move {
let result = futures::select! {
result = stdin_task.fuse() => {
result.context("stdin")
}
result = stdout_task.fuse() => {
result.context("stdout")
}
result = stderr_task.fuse() => {
result.context("stderr")
}
};
let exit_status = remote_proxy_process.status().await?;
let status = exit_status.code().unwrap_or_else(|| {
#[cfg(unix)]
let status = std::os::unix::process::ExitStatusExt::signal(&exit_status).unwrap_or(1);
#[cfg(not(unix))]
let status = 1;
status
});
match result {
Ok(_) => Ok(status),
Err(error) => Err(error),
}
})
}
#[cfg(any(debug_assertions, feature = "build-remote-server-binary"))]
async fn build_remote_server_from_source(
platform: &crate::RemotePlatform,
delegate: &dyn crate::RemoteClientDelegate,
binary_exists_on_server: bool,
cx: &mut AsyncApp,
) -> Result<Option<std::path::PathBuf>> {
use std::env::VarError;
use std::path::Path;
use util::command::{Command, Stdio, new_command};
if let Ok(path) = std::env::var("ZED_COPY_REMOTE_SERVER") {
let path = std::path::PathBuf::from(path);
if path.exists() {
return Ok(Some(path));
} else {
log::warn!(
"ZED_COPY_REMOTE_SERVER path does not exist, falling back to ZED_BUILD_REMOTE_SERVER: {}",
path.display()
);
}
}
// By default, we make building remote server from source opt-out and we do not force artifact compression
// for quicker builds.
let build_remote_server =
std::env::var("ZED_BUILD_REMOTE_SERVER").unwrap_or("nocompress".into());
if let "never" = &*build_remote_server {
return Ok(None);
} else if let "false" | "no" | "off" | "0" = &*build_remote_server {
if binary_exists_on_server {
return Ok(None);
}
log::warn!("ZED_BUILD_REMOTE_SERVER is disabled, but no server binary exists on the server")
}
async fn run_cmd(command: &mut Command) -> Result<()> {
let output = command
.kill_on_drop(true)
.stdout(Stdio::inherit())
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"Failed to run command: {command:?}: output: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
let use_musl = !build_remote_server.contains("nomusl");
let triple = format!(
"{}-{}",
platform.arch,
match platform.os {
RemoteOs::Linux =>
if use_musl {
"unknown-linux-musl"
} else {
"unknown-linux-gnu"
},
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") {
Ok(val) => val,
Err(VarError::NotPresent) => String::new(),
Err(e) => {
log::error!("Failed to get env var `RUSTFLAGS` value: {e}");
String::new()
}
};
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") {
rust_flags.push_str(&format!(" -C link-arg=-L{path}"));
}
}
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(
new_command("cargo")
.current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
"--target",
&triple,
])
.env("RUSTFLAGS", &rust_flags),
)
.await?;
} else {
if which("zig", cx).await?.is_none() {
anyhow::bail!(if cfg!(not(windows)) {
"zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup)"
} else {
"zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup)"
});
}
let rustup = which("rustup", cx)
.await?
.context("rustup not found on $PATH, install rustup (see https://rustup.rs/)")?;
delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
log::info!("adding rustup target");
run_cmd(new_command(rustup).args(["target", "add"]).arg(&triple)).await?;
if which("cargo-zigbuild", cx).await?.is_none() {
delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
log::info!("installing cargo-zigbuild");
run_cmd(new_command("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?;
}
delegate.set_status(
Some(&format!(
"Building remote binary from source for {triple} with Zig"
)),
cx,
);
log::info!("building remote binary from source for {triple} with Zig");
run_cmd(
new_command("cargo")
.current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.args([
"zigbuild",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
"--target",
&triple,
])
.env("RUSTFLAGS", &rust_flags),
)
.await?;
};
let bin_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.join("target")
.join("remote_server")
.join(&triple)
.join("debug")
.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);
#[cfg(not(target_os = "windows"))]
let archive_path = {
run_cmd(new_command("gzip").arg("-f").arg(&bin_path)).await?;
bin_path.with_extension("gz")
};
#[cfg(target_os = "windows")]
let archive_path = {
let zip_path = bin_path.with_extension("zip");
if smol::fs::metadata(&zip_path).await.is_ok() {
smol::fs::remove_file(&zip_path).await?;
}
let compress_command = format!(
"Compress-Archive -Path '{}' -DestinationPath '{}' -Force",
bin_path.display(),
zip_path.display(),
);
run_cmd(new_command("powershell.exe").args([
"-NoProfile",
"-Command",
&compress_command,
]))
.await?;
zip_path
};
std::env::current_dir()?.join(archive_path)
} else {
bin_path
};
Ok(Some(path))
}
#[cfg(any(debug_assertions, feature = "build-remote-server-binary"))]
async fn which(
binary_name: impl AsRef<str>,
cx: &mut AsyncApp,
) -> Result<Option<std::path::PathBuf>> {
let binary_name = binary_name.as_ref().to_string();
let binary_name_cloned = binary_name.clone();
let res = cx
.background_spawn(async move { which::which(binary_name_cloned) })
.await;
match res {
Ok(path) => Ok(Some(path)),
Err(which::Error::CannotFindBinaryPath) => Ok(None),
Err(err) => Err(anyhow::anyhow!(
"Failed to run 'which' to find the binary '{binary_name}': {err}"
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_platform() {
let result = parse_platform("Linux x86_64\n").unwrap();
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, RemoteOs::MacOs);
assert_eq!(result.arch, RemoteArch::Aarch64);
let result = parse_platform("Linux x86_64").unwrap();
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, RemoteOs::Linux);
assert_eq!(result.arch, RemoteArch::Aarch64);
let result = parse_platform("some shell init output\nLinux aarch64").unwrap();
assert_eq!(result.os, RemoteOs::Linux);
assert_eq!(result.arch, RemoteArch::Aarch64);
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, 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());
}
#[test]
fn test_parse_shell() {
assert_eq!(parse_shell("/bin/bash\n", "sh"), "/bin/bash");
assert_eq!(parse_shell("/bin/zsh\n", "sh"), "/bin/zsh");
assert_eq!(parse_shell("/bin/bash", "sh"), "/bin/bash");
assert_eq!(
parse_shell("some shell init output\n/bin/bash\n", "sh"),
"/bin/bash"
);
assert_eq!(
parse_shell("some shell init output\n/bin/bash", "sh"),
"/bin/bash"
);
assert_eq!(parse_shell("", "sh"), "sh");
assert_eq!(parse_shell("\n", "sh"), "sh");
}
}

View File

@@ -0,0 +1,842 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use async_trait::async_trait;
use collections::HashMap;
use parking_lot::Mutex;
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use semver::Version as SemanticVersion;
use std::collections::BTreeMap;
use std::time::Instant;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::ResultExt;
use util::command::Stdio;
use util::shell::ShellKind;
use util::{
paths::{PathStyle, RemotePathBuf},
rel_path::RelPath,
};
use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender};
use gpui::{App, AppContext, AsyncApp, Task};
use rpc::proto::Envelope;
use crate::{
RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, RemoteOs, RemotePlatform,
remote_client::{CommandTemplate, Interactive},
transport::parse_platform,
};
#[derive(
Debug,
Default,
Clone,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
serde::Serialize,
serde::Deserialize,
)]
pub struct DockerConnectionOptions {
pub name: String,
pub container_id: String,
pub remote_user: String,
pub upload_binary_over_docker_exec: bool,
pub use_podman: bool,
pub remote_env: BTreeMap<String, String>,
}
pub(crate) struct DockerExecConnection {
proxy_process: Mutex<Option<u32>>,
remote_dir_for_server: String,
remote_binary_relpath: Option<Arc<RelPath>>,
connection_options: DockerConnectionOptions,
remote_platform: Option<RemotePlatform>,
path_style: Option<PathStyle>,
shell: String,
}
impl DockerExecConnection {
pub async fn new(
connection_options: DockerConnectionOptions,
delegate: Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut this = Self {
proxy_process: Mutex::new(None),
remote_dir_for_server: "/".to_string(),
remote_binary_relpath: None,
connection_options,
remote_platform: None,
path_style: None,
shell: "sh".to_owned(),
};
let (release_channel, version, commit) = cx.update(|cx| {
(
ReleaseChannel::global(cx),
AppVersion::global(cx),
AppCommitSha::try_global(cx),
)
});
let remote_platform = this.check_remote_platform().await?;
this.path_style = match remote_platform.os {
RemoteOs::Windows => Some(PathStyle::Windows),
_ => Some(PathStyle::Posix),
};
this.remote_platform = Some(remote_platform);
log::info!("Remote platform discovered: {:?}", this.remote_platform);
this.shell = this.discover_shell().await;
log::info!("Remote shell discovered: {}", this.shell);
this.remote_dir_for_server = this.docker_user_home_dir().await?.trim().to_string();
this.remote_binary_relpath = Some(
this.ensure_server_binary(
&delegate,
release_channel,
version,
&this.remote_dir_for_server,
commit,
cx,
)
.await?,
);
Ok(this)
}
fn docker_cli(&self) -> &str {
if self.connection_options.use_podman {
"podman"
} else {
"docker"
}
}
async fn discover_shell(&self) -> String {
let default_shell = "sh";
match self
.run_docker_exec("sh", None, &Default::default(), &["-c", "echo $SHELL"])
.await
{
Ok(shell) => match shell.trim() {
"" => {
log::info!("$SHELL is not set, checking passwd for user");
}
shell => {
return shell.to_owned();
}
},
Err(e) => {
log::error!("Failed to get $SHELL: {e}. Checking passwd for user");
}
}
match self
.run_docker_exec(
"sh",
None,
&Default::default(),
&["-c", "getent passwd \"$(id -un)\" | cut -d: -f7"],
)
.await
{
Ok(shell) => match shell.trim() {
"" => {
log::info!("No shell found in passwd, falling back to {default_shell}");
}
shell => {
return shell.to_owned();
}
},
Err(e) => {
log::info!("Error getting shell from passwd: {e}. Falling back to {default_shell}");
}
}
default_shell.to_owned()
}
async fn check_remote_platform(&self) -> Result<RemotePlatform> {
let uname = self
.run_docker_exec("uname", None, &Default::default(), &["-sm"])
.await?;
parse_platform(&uname)
}
async fn ensure_server_binary(
&self,
delegate: &Arc<dyn RemoteClientDelegate>,
release_channel: ReleaseChannel,
version: SemanticVersion,
remote_dir_for_server: &str,
commit: Option<AppCommitSha>,
cx: &mut AsyncApp,
) -> Result<Arc<RelPath>> {
let remote_platform = self
.remote_platform
.context("No remote platform defined; cannot proceed.")?;
let version_str = match release_channel {
ReleaseChannel::Nightly => {
let commit = commit.map(|s| s.full()).unwrap_or_default();
format!("{}-{}", version, commit)
}
ReleaseChannel::Dev => "build".to_string(),
_ => version.to_string(),
};
let binary_name = format!(
"zed-remote-server-{}-{}",
release_channel.dev_name(),
version_str
);
let dst_path =
paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
let binary_exists_on_server = self
.run_docker_exec(
&dst_path.display(self.path_style()),
Some(&remote_dir_for_server),
&Default::default(),
&["version"],
)
.await
.is_ok();
#[cfg(any(debug_assertions, feature = "build-remote-server-binary"))]
if let Some(remote_server_path) = super::build_remote_server_from_source(
&remote_platform,
delegate.as_ref(),
binary_exists_on_server,
cx,
)
.await?
{
let tmp_path = paths::remote_server_dir_relative().join(
RelPath::unix(&format!(
"download-{}-{}",
std::process::id(),
remote_server_path.file_name().unwrap().to_string_lossy()
))
.unwrap(),
);
self.upload_local_server_binary(
&remote_server_path,
&tmp_path,
&remote_dir_for_server,
delegate,
cx,
)
.await?;
self.extract_server_binary(&dst_path, &tmp_path, &remote_dir_for_server, delegate, cx)
.await?;
return Ok(dst_path);
}
if binary_exists_on_server {
return Ok(dst_path);
}
let wanted_version = cx.update(|cx| match release_channel {
ReleaseChannel::Nightly => Ok(None),
ReleaseChannel::Dev => {
anyhow::bail!(
"ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})",
dst_path
)
}
_ => Ok(Some(AppVersion::global(cx))),
})?;
let tmp_path_gz = paths::remote_server_dir_relative().join(
RelPath::unix(&format!(
"{}-download-{}.gz",
binary_name,
std::process::id()
))
.unwrap(),
);
if !self.connection_options.upload_binary_over_docker_exec
&& let Some(url) = delegate
.get_download_url(remote_platform, release_channel, wanted_version.clone(), cx)
.await?
{
match self
.download_binary_on_server(&url, &tmp_path_gz, &remote_dir_for_server, delegate, cx)
.await
{
Ok(_) => {
self.extract_server_binary(
&dst_path,
&tmp_path_gz,
&remote_dir_for_server,
delegate,
cx,
)
.await
.context("extracting server binary")?;
return Ok(dst_path);
}
Err(e) => {
log::error!(
"Failed to download binary on server, attempting to download locally and then upload it the server: {e:#}",
)
}
}
}
let src_path = delegate
.download_server_binary_locally(remote_platform, release_channel, wanted_version, cx)
.await
.context("downloading server binary locally")?;
self.upload_local_server_binary(
&src_path,
&tmp_path_gz,
&remote_dir_for_server,
delegate,
cx,
)
.await
.context("uploading server binary")?;
self.extract_server_binary(
&dst_path,
&tmp_path_gz,
&remote_dir_for_server,
delegate,
cx,
)
.await
.context("extracting server binary")?;
Ok(dst_path)
}
async fn docker_user_home_dir(&self) -> Result<String> {
let inner_program = self.shell();
self.run_docker_exec(
&inner_program,
None,
&Default::default(),
&["-c", "echo $HOME"],
)
.await
}
async fn extract_server_binary(
&self,
dst_path: &RelPath,
tmp_path: &RelPath,
remote_dir_for_server: &str,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
delegate.set_status(Some("Extracting remote development server"), cx);
let server_mode = 0o755;
let shell_kind = ShellKind::Posix;
let orig_tmp_path = tmp_path.display(self.path_style());
let server_mode = format!("{:o}", server_mode);
let server_mode = shell_kind
.try_quote(&server_mode)
.context("shell quoting")?;
let dst_path = dst_path.display(self.path_style());
let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?;
let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
let orig_tmp_path = shell_kind
.try_quote(&orig_tmp_path)
.context("shell quoting")?;
let tmp_path = shell_kind.try_quote(&tmp_path).context("shell quoting")?;
format!(
"gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
)
} else {
let orig_tmp_path = shell_kind
.try_quote(&orig_tmp_path)
.context("shell quoting")?;
format!("chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",)
};
let args = shell_kind.args_for_shell(false, script.to_string());
self.run_docker_exec(
"sh",
Some(&remote_dir_for_server),
&Default::default(),
&args,
)
.await
.log_err();
Ok(())
}
async fn upload_local_server_binary(
&self,
src_path: &Path,
tmp_path_gz: &RelPath,
remote_dir_for_server: &str,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
if let Some(parent) = tmp_path_gz.parent() {
self.run_docker_exec(
"mkdir",
Some(remote_dir_for_server),
&Default::default(),
&["-p", parent.display(self.path_style()).as_ref()],
)
.await?;
}
let src_stat = smol::fs::metadata(&src_path).await?;
let size = src_stat.len();
let t0 = Instant::now();
delegate.set_status(Some("Uploading remote development server"), cx);
log::info!(
"uploading remote development server to {:?} ({}kb)",
tmp_path_gz,
size / 1024
);
self.upload_file(src_path, tmp_path_gz, remote_dir_for_server)
.await
.context("failed to upload server binary")?;
log::info!("uploaded remote development server in {:?}", t0.elapsed());
Ok(())
}
async fn upload_and_chown(
docker_cli: String,
connection_options: DockerConnectionOptions,
src_path: String,
dst_path: String,
) -> Result<()> {
let mut command = util::command::new_command(&docker_cli);
command.arg("cp");
command.arg("-a");
command.arg(&src_path);
command.arg(format!("{}:{}", connection_options.container_id, dst_path));
let output = command.output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
log::debug!("failed to upload via docker cp {src_path} -> {dst_path}: {stderr}",);
anyhow::bail!(
"failed to upload via docker cp {} -> {}: {}",
src_path,
dst_path,
stderr,
);
}
let mut chown_command = util::command::new_command(&docker_cli);
chown_command.arg("exec");
chown_command.arg(connection_options.container_id);
chown_command.arg("chown");
chown_command.arg(format!(
"{}:{}",
connection_options.remote_user, connection_options.remote_user,
));
chown_command.arg(&dst_path);
let output = chown_command.output().await?;
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr);
log::debug!("failed to change ownership for via chown: {stderr}",);
anyhow::bail!(
"failed to change ownership for zed_remote_server via chown: {}",
stderr,
);
}
async fn upload_file(
&self,
src_path: &Path,
dest_path: &RelPath,
remote_dir_for_server: &str,
) -> Result<()> {
log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
let src_path_display = src_path.display().to_string();
let dest_path_str = dest_path.display(self.path_style());
let full_server_path = format!("{}/{}", remote_dir_for_server, dest_path_str);
Self::upload_and_chown(
self.docker_cli().to_string(),
self.connection_options.clone(),
src_path_display,
full_server_path,
)
.await
}
async fn run_docker_command(
&self,
subcommand: &str,
args: &[impl AsRef<str>],
) -> Result<String> {
let mut command = util::command::new_command(self.docker_cli());
command.arg(subcommand);
for arg in args {
command.arg(arg.as_ref());
}
let output = command.output().await?;
log::debug!("{:?}: {:?}", command, output);
anyhow::ensure!(
output.status.success(),
"failed to run command {command:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
async fn run_docker_exec(
&self,
inner_program: &str,
working_directory: Option<&str>,
env: &HashMap<String, String>,
program_args: &[impl AsRef<str>],
) -> Result<String> {
let mut args = match working_directory {
Some(dir) => vec!["-w".to_string(), dir.to_string()],
None => vec![],
};
args.push("-u".to_string());
args.push(self.connection_options.remote_user.clone());
for (k, v) in self.connection_options.remote_env.iter() {
args.push("-e".to_string());
args.push(format!("{k}={v}"));
}
for (k, v) in env.iter() {
args.push("-e".to_string());
args.push(format!("{k}={v}"));
}
args.push(self.connection_options.container_id.clone());
args.push(inner_program.to_string());
for arg in program_args {
args.push(arg.as_ref().to_owned());
}
self.run_docker_command("exec", args.as_ref()).await
}
async fn download_binary_on_server(
&self,
url: &str,
tmp_path_gz: &RelPath,
remote_dir_for_server: &str,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
if let Some(parent) = tmp_path_gz.parent() {
self.run_docker_exec(
"mkdir",
Some(remote_dir_for_server),
&Default::default(),
&["-p", parent.display(self.path_style()).as_ref()],
)
.await?;
}
delegate.set_status(Some("Downloading remote development server on host"), cx);
match self
.run_docker_exec(
"curl",
Some(remote_dir_for_server),
&Default::default(),
&[
"-f",
"-L",
url,
"-o",
&tmp_path_gz.display(self.path_style()),
],
)
.await
{
Ok(_) => {}
Err(e) => {
if self
.run_docker_exec("which", None, &Default::default(), &["curl"])
.await
.is_ok()
{
return Err(e);
}
log::info!("curl is not available, trying wget");
match self
.run_docker_exec(
"wget",
Some(remote_dir_for_server),
&Default::default(),
&[url, "-O", &tmp_path_gz.display(self.path_style())],
)
.await
{
Ok(_) => {}
Err(e) => {
if self
.run_docker_exec("which", None, &Default::default(), &["wget"])
.await
.is_ok()
{
return Err(e);
} else {
anyhow::bail!("Neither curl nor wget is available");
}
}
}
}
}
Ok(())
}
fn kill_inner(&self) -> Result<()> {
if let Some(pid) = self.proxy_process.lock().take() {
if let Ok(_) = util::command::new_command("kill")
.arg(pid.to_string())
.spawn()
{
Ok(())
} else {
Err(anyhow::anyhow!("Failed to kill process"))
}
} else {
Ok(())
}
}
}
#[async_trait(?Send)]
impl RemoteConnection for DockerExecConnection {
fn has_wsl_interop(&self) -> bool {
false
}
fn start_proxy(
&self,
unique_identifier: String,
reconnect: bool,
incoming_tx: UnboundedSender<Envelope>,
outgoing_rx: UnboundedReceiver<Envelope>,
connection_activity_tx: Sender<()>,
delegate: Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Task<Result<i32>> {
// We'll try connecting anew every time we open a devcontainer, so proactively try to kill any old connections.
if !self.has_been_killed() {
if let Err(e) = self.kill_inner() {
return Task::ready(Err(e));
};
}
delegate.set_status(Some("Starting proxy"), cx);
let Some(remote_binary_relpath) = self.remote_binary_relpath.clone() else {
return Task::ready(Err(anyhow!("Remote binary path not set")));
};
let mut docker_args = vec!["exec".to_string()];
for (k, v) in self.connection_options.remote_env.iter() {
docker_args.push("-e".to_string());
docker_args.push(format!("{k}={v}"));
}
for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
if let Some(value) = std::env::var(env_var).ok() {
docker_args.push("-e".to_string());
docker_args.push(format!("{env_var}={value}"));
}
}
docker_args.extend([
"-u".to_string(),
self.connection_options.remote_user.to_string(),
"-w".to_string(),
self.remote_dir_for_server.clone(),
"-i".to_string(),
self.connection_options.container_id.to_string(),
]);
let val = remote_binary_relpath
.display(self.path_style())
.into_owned();
docker_args.push(val);
docker_args.push("proxy".to_string());
docker_args.push("--identifier".to_string());
docker_args.push(unique_identifier);
if reconnect {
docker_args.push("--reconnect".to_string());
}
let mut command = util::command::new_command(self.docker_cli());
command
.kill_on_drop(true)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.args(docker_args);
let Ok(child) = command.spawn() else {
return Task::ready(Err(anyhow::anyhow!(
"Failed to start remote server process"
)));
};
let mut proxy_process = self.proxy_process.lock();
*proxy_process = Some(child.id());
cx.spawn(async move |cx| {
super::handle_rpc_messages_over_child_process_stdio(
child,
incoming_tx,
outgoing_rx,
connection_activity_tx,
cx,
)
.await
.and_then(|status| {
if status != 0 {
anyhow::bail!("Remote server exited with status {status}");
}
Ok(0)
})
})
}
fn upload_directory(
&self,
src_path: PathBuf,
dest_path: RemotePathBuf,
cx: &App,
) -> Task<Result<()>> {
let dest_path_str = dest_path.to_string();
let src_path_display = src_path.display().to_string();
let upload_task = Self::upload_and_chown(
self.docker_cli().to_string(),
self.connection_options.clone(),
src_path_display,
dest_path_str,
);
cx.background_spawn(upload_task)
}
async fn kill(&self) -> Result<()> {
self.kill_inner()
}
fn has_been_killed(&self) -> bool {
self.proxy_process.lock().is_none()
}
fn build_command(
&self,
program: Option<String>,
args: &[String],
env: &HashMap<String, String>,
working_dir: Option<String>,
_port_forward: Option<(u16, String, u16)>,
interactive: Interactive,
) -> Result<CommandTemplate> {
let mut parsed_working_dir = None;
let path_style = self.path_style();
if let Some(working_dir) = working_dir {
let working_dir = RemotePathBuf::new(working_dir, path_style).to_string();
const TILDE_PREFIX: &'static str = "~/";
if working_dir.starts_with(TILDE_PREFIX) {
let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/");
parsed_working_dir =
Some(format!("{}/{}", self.remote_dir_for_server, working_dir));
} else {
parsed_working_dir = Some(working_dir);
}
}
let mut inner_program = Vec::new();
if let Some(program) = program {
inner_program.push(program);
for arg in args {
inner_program.push(arg.clone());
}
} else {
inner_program.push(self.shell());
inner_program.push("-l".to_string());
};
let mut docker_args = vec![
"exec".to_string(),
"-u".to_string(),
self.connection_options.remote_user.clone(),
];
if let Some(parsed_working_dir) = parsed_working_dir {
docker_args.push("-w".to_string());
docker_args.push(parsed_working_dir);
}
for (k, v) in self.connection_options.remote_env.iter() {
docker_args.push("-e".to_string());
docker_args.push(format!("{k}={v}"));
}
for (k, v) in env.iter() {
docker_args.push("-e".to_string());
docker_args.push(format!("{k}={v}"));
}
match interactive {
Interactive::Yes => docker_args.push("-it".to_string()),
Interactive::No => docker_args.push("-i".to_string()),
}
docker_args.push(self.connection_options.container_id.to_string());
docker_args.append(&mut inner_program);
Ok(CommandTemplate {
program: self.docker_cli().to_string(),
args: docker_args,
// Docker-exec pipes in environment via the "-e" argument
env: Default::default(),
})
}
fn build_forward_ports_command(
&self,
_forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate> {
Err(anyhow::anyhow!("Not currently supported for docker_exec"))
}
fn connection_options(&self) -> RemoteConnectionOptions {
RemoteConnectionOptions::Docker(self.connection_options.clone())
}
fn path_style(&self) -> PathStyle {
self.path_style.unwrap_or(PathStyle::Posix)
}
fn shell(&self) -> String {
self.shell.clone()
}
fn default_system_shell(&self) -> String {
String::from("/bin/sh")
}
}

View File

@@ -0,0 +1,342 @@
//! Mock transport for testing remote connections.
//!
//! This module provides a mock implementation of the `RemoteConnection` trait
//! that allows testing remote editing functionality without actual SSH/WSL/Docker
//! connections.
//!
//! # Usage
//!
//! ```rust,ignore
//! use remote::{MockConnection, RemoteClient};
//!
//! #[gpui::test]
//! async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
//! let (opts, server_session) = MockConnection::new(cx, server_cx);
//!
//! // Create the headless project (server side)
//! server_cx.update(HeadlessProject::init);
//! let _headless = server_cx.new(|cx| {
//! HeadlessProject::new(
//! HeadlessAppState { session: server_session, /* ... */ },
//! false,
//! cx,
//! )
//! });
//!
//! // Create the client using the helper
//! let (client, server_client) = RemoteClient::new_mock(cx, server_cx).await;
//! // ... test logic ...
//! }
//! ```
use crate::remote_client::{
ChannelClient, CommandTemplate, Interactive, RemoteClientDelegate, RemoteConnection,
RemoteConnectionOptions,
};
use anyhow::Result;
use async_trait::async_trait;
use collections::HashMap;
use futures::{
FutureExt, SinkExt, StreamExt,
channel::{
mpsc::{self, Sender},
oneshot,
},
select_biased,
};
use gpui::{App, AppContext as _, AsyncApp, Global, Task, TestAppContext};
use rpc::{AnyProtoClient, proto::Envelope};
use std::{
path::PathBuf,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
use util::paths::{PathStyle, RemotePathBuf};
/// Unique identifier for a mock connection.
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct MockConnectionOptions {
pub id: u64,
}
/// A mock implementation of `RemoteConnection` for testing.
pub struct MockRemoteConnection {
options: MockConnectionOptions,
server_channel: Arc<ChannelClient>,
server_cx: SendableCx,
}
/// Wrapper to pass `AsyncApp` across thread boundaries in tests.
///
/// # Safety
///
/// This is safe because in test mode, GPUI is always single-threaded and so
/// having access to one async app means being on the same main thread.
pub(crate) struct SendableCx(AsyncApp);
impl SendableCx {
pub(crate) fn new(cx: &TestAppContext) -> Self {
Self(cx.to_async())
}
pub(crate) fn get(&self, _: &AsyncApp) -> AsyncApp {
self.0.clone()
}
}
// SAFETY: In test mode, GPUI is always single-threaded, and SendableCx
// is only accessed from the main thread via the get() method which
// requires a valid AsyncApp reference.
unsafe impl Send for SendableCx {}
unsafe impl Sync for SendableCx {}
/// Global registry that holds pre-created mock connections.
///
/// When `ConnectionPool::connect` is called with `MockConnectionOptions`,
/// it retrieves the connection from this registry.
#[derive(Default)]
pub struct MockConnectionRegistry {
pending: HashMap<u64, (oneshot::Receiver<()>, Arc<MockRemoteConnection>)>,
}
impl Global for MockConnectionRegistry {}
impl MockConnectionRegistry {
/// Called by `ConnectionPool::connect` to retrieve a pre-registered mock connection.
pub fn take(
&mut self,
opts: &MockConnectionOptions,
) -> Option<impl Future<Output = Arc<MockRemoteConnection>> + use<>> {
let (guard, con) = self.pending.remove(&opts.id)?;
Some(async move {
_ = guard.await;
con
})
}
}
/// Helper for creating mock connection pairs in tests.
pub struct MockConnection;
pub type ConnectGuard = oneshot::Sender<()>;
impl MockConnection {
/// Creates a new mock connection pair for testing.
///
/// This function:
/// 1. Creates a unique `MockConnectionOptions` identifier
/// 2. Sets up the server-side channel (returned as `AnyProtoClient`)
/// 3. Creates a `MockRemoteConnection` and registers it in the global registry
/// 4. The connection will be retrieved from the registry when `ConnectionPool::connect` is called
///
/// Returns:
/// - `MockConnectionOptions` to pass to `remote::connect()` or `RemoteClient` creation
/// - `AnyProtoClient` to pass to `HeadlessProject::new()` as the session
///
/// # Arguments
/// - `client_cx`: The test context for the client side
/// - `server_cx`: The test context for the server/headless side
pub(crate) fn new(
client_cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) -> (MockConnectionOptions, AnyProtoClient, ConnectGuard) {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
let opts = MockConnectionOptions { id };
let (server_client, connect_guard) =
Self::new_with_opts(opts.clone(), client_cx, server_cx);
(opts, server_client, connect_guard)
}
/// Creates a mock connection pair for existing `MockConnectionOptions`.
///
/// This is useful when simulating reconnection: after a connection is torn
/// down, register a new mock server under the same options so the next
/// `ConnectionPool::connect` call finds it.
pub(crate) fn new_with_opts(
opts: MockConnectionOptions,
client_cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) -> (AnyProtoClient, ConnectGuard) {
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
let server_client = server_cx
.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "mock-server", false));
let connection = Arc::new(MockRemoteConnection {
options: opts.clone(),
server_channel: server_client.clone(),
server_cx: SendableCx::new(server_cx),
});
let (tx, rx) = oneshot::channel();
client_cx.update(|cx| {
cx.default_global::<MockConnectionRegistry>()
.pending
.insert(opts.id, (rx, connection));
});
(server_client.into(), tx)
}
}
#[async_trait(?Send)]
impl RemoteConnection for MockRemoteConnection {
async fn kill(&self) -> Result<()> {
Ok(())
}
fn has_been_killed(&self) -> bool {
false
}
fn build_command(
&self,
program: Option<String>,
args: &[String],
env: &HashMap<String, String>,
_working_dir: Option<String>,
_port_forward: Option<(u16, String, u16)>,
_interactive: Interactive,
) -> Result<CommandTemplate> {
let shell_program = program.unwrap_or_else(|| "sh".to_string());
let mut shell_args = Vec::new();
shell_args.push(shell_program);
shell_args.extend(args.iter().cloned());
Ok(CommandTemplate {
program: "mock".into(),
args: shell_args,
env: env.clone(),
})
}
fn build_forward_ports_command(
&self,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate> {
Ok(CommandTemplate {
program: "mock".into(),
args: std::iter::once("-N".to_owned())
.chain(forwards.into_iter().map(|(local_port, host, remote_port)| {
format!("{local_port}:{host}:{remote_port}")
}))
.collect(),
env: Default::default(),
})
}
fn upload_directory(
&self,
_src_path: PathBuf,
_dest_path: RemotePathBuf,
_cx: &App,
) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn connection_options(&self) -> RemoteConnectionOptions {
RemoteConnectionOptions::Mock(self.options.clone())
}
fn simulate_disconnect(&self, cx: &AsyncApp) {
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
self.server_channel
.reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx));
}
fn start_proxy(
&self,
_unique_identifier: String,
_reconnect: bool,
mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
mut client_outgoing_rx: mpsc::UnboundedReceiver<Envelope>,
mut connection_activity_tx: Sender<()>,
_delegate: Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Task<Result<i32>> {
let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::<Envelope>();
let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::<Envelope>();
self.server_channel.reconnect(
server_incoming_rx,
server_outgoing_tx,
&self.server_cx.get(cx),
);
cx.background_spawn(async move {
loop {
select_biased! {
server_to_client = server_outgoing_rx.next().fuse() => {
let Some(server_to_client) = server_to_client else {
return Ok(1)
};
connection_activity_tx.try_send(()).ok();
client_incoming_tx.send(server_to_client).await.ok();
}
client_to_server = client_outgoing_rx.next().fuse() => {
let Some(client_to_server) = client_to_server else {
return Ok(1)
};
server_incoming_tx.send(client_to_server).await.ok();
}
}
}
})
}
fn path_style(&self) -> PathStyle {
PathStyle::local()
}
fn shell(&self) -> String {
"sh".to_owned()
}
fn default_system_shell(&self) -> String {
"sh".to_owned()
}
fn has_wsl_interop(&self) -> bool {
false
}
}
/// Mock delegate for tests that don't need delegate functionality.
pub struct MockDelegate;
impl RemoteClientDelegate for MockDelegate {
fn ask_password(
&self,
_prompt: String,
_sender: futures::channel::oneshot::Sender<askpass::EncryptedPassword>,
_cx: &mut AsyncApp,
) {
unreachable!("MockDelegate::ask_password should not be called in tests")
}
fn download_server_binary_locally(
&self,
_platform: crate::RemotePlatform,
_release_channel: release_channel::ReleaseChannel,
_version: Option<semver::Version>,
_cx: &mut AsyncApp,
) -> Task<Result<PathBuf>> {
unreachable!("MockDelegate::download_server_binary_locally should not be called in tests")
}
fn get_download_url(
&self,
_platform: crate::RemotePlatform,
_release_channel: release_channel::ReleaseChannel,
_version: Option<semver::Version>,
_cx: &mut AsyncApp,
) -> Task<Result<Option<String>>> {
unreachable!("MockDelegate::get_download_url should not be called in tests")
}
fn set_status(&self, _status: Option<&str>, _cx: &mut AsyncApp) {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,657 @@
use crate::{
RemoteArch, RemoteClientDelegate, RemoteOs, RemotePlatform,
remote_client::{CommandTemplate, Interactive, RemoteConnection, RemoteConnectionOptions},
transport::{parse_platform, parse_shell},
};
use anyhow::{Context, Result, anyhow, bail};
use async_trait::async_trait;
use collections::HashMap;
use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender};
use gpui::{App, AppContext as _, AsyncApp, Task};
use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::Envelope;
use semver::Version;
use smol::fs;
use std::{
ffi::OsStr,
fmt::Write as _,
path::{Path, PathBuf},
sync::Arc,
time::Instant,
};
use util::{
command::Stdio,
paths::{PathStyle, RemotePathBuf},
rel_path::RelPath,
shell::{Shell, ShellKind},
shell_builder::ShellBuilder,
};
#[derive(
Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct WslConnectionOptions {
pub distro_name: String,
pub user: Option<String>,
}
impl From<settings::WslConnection> for WslConnectionOptions {
fn from(val: settings::WslConnection) -> Self {
WslConnectionOptions {
distro_name: val.distro_name,
user: val.user,
}
}
}
#[derive(Debug)]
pub(crate) struct WslRemoteConnection {
remote_binary_path: Option<Arc<RelPath>>,
platform: RemotePlatform,
shell: String,
shell_kind: ShellKind,
default_system_shell: String,
has_wsl_interop: bool,
connection_options: WslConnectionOptions,
}
impl WslRemoteConnection {
pub(crate) async fn new(
connection_options: WslConnectionOptions,
delegate: Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<Self> {
log::info!(
"Connecting to WSL distro {} with user {:?}",
connection_options.distro_name,
connection_options.user
);
let (release_channel, version) =
cx.update(|cx| (ReleaseChannel::global(cx), AppVersion::global(cx)));
let mut this = Self {
connection_options,
remote_binary_path: None,
platform: RemotePlatform {
os: RemoteOs::Linux,
arch: RemoteArch::X86_64,
},
shell: String::new(),
shell_kind: ShellKind::Posix,
default_system_shell: String::from("/bin/sh"),
has_wsl_interop: false,
};
delegate.set_status(Some("Detecting WSL environment"), cx);
this.shell = this
.detect_shell()
.await
.context("failed detecting shell")?;
log::info!("Remote shell discovered: {}", this.shell);
this.shell_kind = ShellKind::new(&this.shell, false);
this.has_wsl_interop = this.detect_has_wsl_interop().await.unwrap_or_default();
log::info!(
"Remote has wsl interop {}",
if this.has_wsl_interop {
"enabled"
} else {
"disabled"
}
);
this.platform = this
.detect_platform()
.await
.context("failed detecting platform")?;
log::info!("Remote platform discovered: {:?}", this.platform);
this.remote_binary_path = Some(
this.ensure_server_binary(&delegate, release_channel, version, cx)
.await
.context("failed ensuring server binary")?,
);
log::debug!("Detected WSL environment: {this:#?}");
Ok(this)
}
async fn detect_platform(&self) -> Result<RemotePlatform> {
let program = self.shell_kind.prepend_command_prefix("uname");
let output = self.run_wsl_command_with_output(&program, &["-sm"]).await?;
parse_platform(&output)
}
async fn detect_shell(&self) -> Result<String> {
const DEFAULT_SHELL: &str = "sh";
match self
.run_wsl_command_with_output("sh", &["-c", "echo $SHELL"])
.await
{
Ok(output) => Ok(parse_shell(&output, DEFAULT_SHELL)),
Err(e) => {
log::error!("Failed to detect remote shell: {e}");
Ok(DEFAULT_SHELL.to_owned())
}
}
}
async fn detect_has_wsl_interop(&self) -> Result<bool> {
let interop = match self
.run_wsl_command_with_output("cat", &["/proc/sys/fs/binfmt_misc/WSLInterop"])
.await
{
Ok(interop) => interop,
Err(err) => self
.run_wsl_command_with_output("cat", &["/proc/sys/fs/binfmt_misc/WSLInterop-late"])
.await
.inspect_err(|err2| log::error!("Failed to detect wsl interop: {err}; {err2}"))?,
};
Ok(interop.contains("enabled"))
}
async fn windows_path_to_wsl_path(&self, source: &Path) -> Result<String> {
windows_path_to_wsl_path_impl(&self.connection_options, source).await
}
async fn run_wsl_command_with_output(&self, program: &str, args: &[&str]) -> Result<String> {
run_wsl_command_with_output_impl(&self.connection_options, program, args).await
}
async fn run_wsl_command(&self, program: &str, args: &[&str]) -> Result<()> {
run_wsl_command_impl(wsl_command_impl(
&self.connection_options,
program,
args,
false,
))
.await
.map(|_| ())
}
async fn ensure_server_binary(
&self,
delegate: &Arc<dyn RemoteClientDelegate>,
release_channel: ReleaseChannel,
version: Version,
cx: &mut AsyncApp,
) -> Result<Arc<RelPath>> {
let version_str = match release_channel {
ReleaseChannel::Dev => "build".to_string(),
_ => version.to_string(),
};
let binary_name = format!(
"zed-remote-server-{}-{}",
release_channel.dev_name(),
version_str
);
let dst_path =
paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
if let Some(parent) = dst_path.parent() {
let parent = parent.display(PathStyle::Posix);
let mkdir = self.shell_kind.prepend_command_prefix("mkdir");
self.run_wsl_command(&mkdir, &["-p", &parent])
.await
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
}
let binary_exists_on_server = self
.run_wsl_command(&dst_path.display(PathStyle::Posix), &["version"])
.await
.is_ok();
#[cfg(any(debug_assertions, feature = "build-remote-server-binary"))]
if let Some(remote_server_path) = super::build_remote_server_from_source(
&self.platform,
delegate.as_ref(),
binary_exists_on_server,
cx,
)
.await?
{
let tmp_path = paths::remote_server_dir_relative().join(
&RelPath::unix(&format!(
"download-{}-{}",
std::process::id(),
remote_server_path.file_name().unwrap().to_string_lossy()
))
.unwrap(),
);
self.upload_file(&remote_server_path, &tmp_path, delegate, cx)
.await?;
self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
.await?;
return Ok(dst_path);
}
if binary_exists_on_server {
return Ok(dst_path);
}
let wanted_version = match release_channel {
ReleaseChannel::Nightly | ReleaseChannel::Dev => None,
_ => Some(cx.update(|cx| AppVersion::global(cx))),
};
let src_path = delegate
.download_server_binary_locally(self.platform, release_channel, wanted_version, cx)
.await?;
let tmp_path = format!(
"{}.{}.gz",
dst_path.display(PathStyle::Posix),
std::process::id()
);
let tmp_path = RelPath::unix(&tmp_path).unwrap();
self.upload_file(&src_path, &tmp_path, delegate, cx).await?;
self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
.await?;
Ok(dst_path)
}
async fn upload_file(
&self,
src_path: &Path,
dst_path: &RelPath,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
delegate.set_status(Some("Uploading remote server"), cx);
if let Some(parent) = dst_path.parent() {
let parent = parent.display(PathStyle::Posix);
let mkdir = self.shell_kind.prepend_command_prefix("mkdir");
self.run_wsl_command(&mkdir, &["-p", &parent])
.await
.context("Failed to create directory when uploading file")?;
}
let t0 = Instant::now();
let src_stat = fs::metadata(&src_path)
.await
.with_context(|| format!("source path does not exist: {}", src_path.display()))?;
let size = src_stat.len();
log::info!(
"uploading remote server to WSL {:?} ({}kb)",
dst_path,
size / 1024
);
let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?;
let cp = self.shell_kind.prepend_command_prefix("cp");
self.run_wsl_command(
&cp,
&["-f", &src_path_in_wsl, &dst_path.display(PathStyle::Posix)],
)
.await
.map_err(|e| {
anyhow!(
"Failed to copy file {}({}) to WSL {:?}: {}",
src_path.display(),
src_path_in_wsl,
dst_path,
e
)
})?;
log::info!("uploaded remote server in {:?}", t0.elapsed());
Ok(())
}
async fn extract_and_install(
&self,
tmp_path: &RelPath,
dst_path: &RelPath,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
delegate.set_status(Some("Extracting remote server"), cx);
let tmp_path_str = tmp_path.display(PathStyle::Posix);
let dst_path_str = dst_path.display(PathStyle::Posix);
// Build extraction script with proper error handling
let script = if tmp_path_str.ends_with(".gz") {
let uncompressed = tmp_path_str.trim_end_matches(".gz");
format!(
"set -e; gunzip -f '{}' && chmod 755 '{}' && mv -f '{}' '{}'",
tmp_path_str, uncompressed, uncompressed, dst_path_str
)
} else {
format!(
"set -e; chmod 755 '{}' && mv -f '{}' '{}'",
tmp_path_str, tmp_path_str, dst_path_str
)
};
self.run_wsl_command("sh", &["-c", &script])
.await
.map_err(|e| anyhow!("Failed to extract server binary: {}", e))?;
Ok(())
}
}
#[async_trait(?Send)]
impl RemoteConnection for WslRemoteConnection {
fn start_proxy(
&self,
unique_identifier: String,
reconnect: bool,
incoming_tx: UnboundedSender<Envelope>,
outgoing_rx: UnboundedReceiver<Envelope>,
connection_activity_tx: Sender<()>,
delegate: Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Task<Result<i32>> {
delegate.set_status(Some("Starting proxy"), cx);
let Some(remote_binary_path) = &self.remote_binary_path 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() {
// We don't quote the value here as it seems excessive and may result in invalid envs for the
// proxy server. For example, `RUST_LOG='debug'` will result in a warning "invalid logging spec 'debug'', ignoring it"
// in the proxy server. Therefore, we pass the env vars as is.
proxy_args.push(format!("{}={}", env_var, value));
}
}
proxy_args.push(remote_binary_path.display(PathStyle::Posix).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());
}
let proxy_process =
match wsl_command_impl(&self.connection_options, "env", &proxy_args, true)
.kill_on_drop(true)
.spawn()
{
Ok(process) => process,
Err(error) => {
return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error)));
}
};
super::handle_rpc_messages_over_child_process_stdio(
proxy_process,
incoming_tx,
outgoing_rx,
connection_activity_tx,
cx,
)
}
fn upload_directory(
&self,
src_path: PathBuf,
dest_path: RemotePathBuf,
cx: &App,
) -> Task<Result<()>> {
cx.background_spawn({
let options = self.connection_options.clone();
async move {
let wsl_src = windows_path_to_wsl_path_impl(&options, &src_path).await?;
let command = wsl_command_impl(
&options,
"cp",
&["-r", &wsl_src, &dest_path.to_string()],
true,
);
run_wsl_command_impl(command).await.map_err(|e| {
anyhow!(
"failed to upload directory {} -> {}: {}",
src_path.display(),
dest_path,
e
)
})?;
Ok(())
}
})
}
async fn kill(&self) -> Result<()> {
Ok(())
}
fn has_been_killed(&self) -> bool {
false
}
fn shares_network_interface(&self) -> bool {
true
}
fn build_command(
&self,
program: Option<String>,
args: &[String],
env: &HashMap<String, String>,
working_dir: Option<String>,
port_forward: Option<(u16, String, u16)>,
_interactive: Interactive,
) -> Result<CommandTemplate> {
if port_forward.is_some() {
bail!("WSL shares the network interface with the host system");
}
let shell_kind = self.shell_kind;
let working_dir = working_dir
.map(|working_dir| RemotePathBuf::new(working_dir, PathStyle::Posix).to_string())
.unwrap_or("~".to_string());
let mut exec = String::from("exec env ");
for (key, value) in env.iter() {
let assignment = format!("{key}={value}");
let assignment = shell_kind.try_quote(&assignment).context("shell quoting")?;
write!(exec, "{assignment} ")?;
}
if let Some(program) = program {
write!(
exec,
"{}",
shell_kind
.try_quote_prefix_aware(&program)
.context("shell quoting")?
)?;
for arg in args {
let arg = shell_kind.try_quote(&arg).context("shell quoting")?;
write!(exec, " {}", &arg)?;
}
} else {
write!(&mut exec, "{} -l", self.shell)?;
}
let (command, args) =
ShellBuilder::new(&Shell::Program(self.shell.clone()), false).build(Some(exec), &[]);
let mut wsl_args = if let Some(user) = &self.connection_options.user {
vec![
"--distribution".to_string(),
self.connection_options.distro_name.clone(),
"--user".to_string(),
user.clone(),
"--cd".to_string(),
working_dir,
"--".to_string(),
command,
]
} else {
vec![
"--distribution".to_string(),
self.connection_options.distro_name.clone(),
"--cd".to_string(),
working_dir,
"--".to_string(),
command,
]
};
wsl_args.extend(args);
Ok(CommandTemplate {
program: "wsl.exe".to_string(),
args: wsl_args,
env: HashMap::default(),
})
}
fn build_forward_ports_command(
&self,
_: Vec<(u16, String, u16)>,
) -> anyhow::Result<CommandTemplate> {
Err(anyhow!("WSL shares a network interface with the host"))
}
fn connection_options(&self) -> RemoteConnectionOptions {
RemoteConnectionOptions::Wsl(self.connection_options.clone())
}
fn path_style(&self) -> PathStyle {
PathStyle::Posix
}
fn shell(&self) -> String {
self.shell.clone()
}
fn default_system_shell(&self) -> String {
self.default_system_shell.clone()
}
fn has_wsl_interop(&self) -> bool {
self.has_wsl_interop
}
}
/// `wslpath` is a executable available in WSL, it's a linux binary.
/// So it doesn't support Windows style paths.
async fn sanitize_path(path: &Path) -> Result<String> {
let path = smol::fs::canonicalize(path)
.await
.with_context(|| format!("Failed to canonicalize path {}", path.display()))?;
let path_str = path.to_string_lossy();
let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
Ok(sanitized.replace('\\', "/"))
}
fn run_wsl_command_with_output_impl(
options: &WslConnectionOptions,
program: &str,
args: &[&str],
) -> impl Future<Output = Result<String>> + use<> {
let exec_command = wsl_command_impl(options, program, args, true);
let command = wsl_command_impl(options, program, args, false);
async move {
match run_wsl_command_impl(exec_command).await {
Ok(res) => Ok(res),
Err(exec_err) => match run_wsl_command_impl(command).await {
Ok(res) => Ok(res),
Err(e) => Err(e.context(exec_err)),
},
}
}
}
impl WslConnectionOptions {
pub fn abs_windows_path_to_wsl_path(
&self,
source: &Path,
) -> impl Future<Output = Result<String>> + use<> {
let path_str = source.to_string_lossy();
let source = path_str.strip_prefix(r"\\?\").unwrap_or(&*path_str);
let source = source.replace('\\', "/");
run_wsl_command_with_output_impl(self, "wslpath", &["-u", &source])
}
}
async fn windows_path_to_wsl_path_impl(
options: &WslConnectionOptions,
source: &Path,
) -> Result<String> {
let source = sanitize_path(source).await?;
run_wsl_command_with_output_impl(options, "wslpath", &["-u", &source]).await
}
/// Converts a WSL/POSIX path to a Windows path using `wslpath -w`.
///
/// For example, `/home/user/project` becomes `\\wsl.localhost\Ubuntu\home\user\project`
#[cfg(target_os = "windows")]
pub fn wsl_path_to_windows_path(
options: &WslConnectionOptions,
wsl_path: &Path,
) -> impl Future<Output = Result<PathBuf>> + use<> {
let wsl_path_str = wsl_path.to_string_lossy().to_string();
let command = wsl_command_impl(options, "wslpath", &["-w", &wsl_path_str], true);
async move {
let windows_path = run_wsl_command_impl(command).await?;
Ok(PathBuf::from(windows_path))
}
}
fn run_wsl_command_impl(
mut command: util::command::Command,
) -> impl Future<Output = Result<String>> {
async move {
let output = command
.output()
.await
.with_context(|| format!("Failed to run command '{:?}'", command))?;
if !output.status.success() {
return Err(anyhow!(
"Command '{:?}' failed: {}",
command,
String::from_utf8_lossy(&output.stderr).trim()
));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}
}
/// Creates a new `wsl.exe` command that runs the given program with the given arguments.
///
/// If `exec` is true, the command will be executed in the WSL environment without spawning a new shell.
fn wsl_command_impl(
options: &WslConnectionOptions,
program: &str,
args: &[impl AsRef<OsStr>],
exec: bool,
) -> util::command::Command {
let mut command = util::command::new_command("wsl.exe");
if let Some(user) = &options.user {
command.arg("--user").arg(user);
}
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--distribution")
.arg(&options.distro_name)
.arg("--cd")
.arg("~");
if exec {
command.arg("--exec");
}
command.arg(program).args(args);
log::debug!("wsl {:?}", command);
command
}