Add initial support for WSL (#37035)

Closes #36188

## Todo

* [x] CLI
* [x] terminals
* [x] tasks

## For future PRs
* debugging
* UI for opening WSL projects
* fixing workspace state restoration

Release Notes:

- Windows alpha: Zed now supports editing folders in WSL.

---------

Co-authored-by: Junkui Zhang <364772080@qq.com>
This commit is contained in:
Max Brunsfeld
2025-08-29 17:18:52 -07:00
committed by GitHub
co-authored by Junkui Zhang
parent 1c2e2a00fe
commit f78f3e7729
27 changed files with 1701 additions and 786 deletions
+2 -1
View File
@@ -6,6 +6,7 @@ mod transport;
pub use remote_client::{
ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent,
RemotePlatform,
RemoteConnectionOptions, RemotePlatform,
};
pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};
pub use transport::wsl::WslConnectionOptions;
+90 -27
View File
@@ -1,6 +1,11 @@
use crate::{
SshConnectionOptions, protocol::MessageId, proxy::ProxyLaunchError,
transport::ssh::SshRemoteConnection,
SshConnectionOptions,
protocol::MessageId,
proxy::ProxyLaunchError,
transport::{
ssh::SshRemoteConnection,
wsl::{WslConnectionOptions, WslRemoteConnection},
},
};
use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait;
@@ -237,7 +242,7 @@ impl From<&State> for ConnectionState {
pub struct RemoteClient {
client: Arc<ChannelClient>,
unique_identifier: String,
connection_options: SshConnectionOptions,
connection_options: RemoteConnectionOptions,
path_style: PathStyle,
state: Option<State>,
}
@@ -290,6 +295,22 @@ impl RemoteClient {
cancellation: oneshot::Receiver<()>,
delegate: Arc<dyn RemoteClientDelegate>,
cx: &mut App,
) -> Task<Result<Option<Entity<Self>>>> {
Self::new(
unique_identifier,
RemoteConnectionOptions::Ssh(connection_options),
cancellation,
delegate,
cx,
)
}
pub fn new(
unique_identifier: ConnectionIdentifier,
connection_options: RemoteConnectionOptions,
cancellation: oneshot::Receiver<()>,
delegate: Arc<dyn RemoteClientDelegate>,
cx: &mut App,
) -> Task<Result<Option<Entity<Self>>>> {
let unique_identifier = unique_identifier.to_string(cx);
cx.spawn(async move |cx| {
@@ -424,7 +445,7 @@ impl RemoteClient {
}
let state = self.state.take().unwrap();
let (attempts, ssh_connection, delegate) = match state {
let (attempts, remote_connection, delegate) = match state {
State::Connected {
ssh_connection,
delegate,
@@ -482,15 +503,15 @@ impl RemoteClient {
};
}
if let Err(error) = ssh_connection
if let Err(error) = remote_connection
.kill()
.await
.context("Failed to kill ssh process")
{
failed!(error, attempts, ssh_connection, delegate);
failed!(error, attempts, remote_connection, delegate);
};
let connection_options = ssh_connection.connection_options();
let connection_options = remote_connection.connection_options();
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
@@ -519,7 +540,7 @@ impl RemoteClient {
{
Ok((ssh_connection, io_task)) => (ssh_connection, io_task),
Err(error) => {
failed!(error, attempts, ssh_connection, delegate);
failed!(error, attempts, remote_connection, delegate);
}
};
@@ -751,6 +772,13 @@ impl RemoteClient {
Some(self.state.as_ref()?.remote_connection()?.shell())
}
pub fn shares_network_interface(&self) -> bool {
self.state
.as_ref()
.and_then(|state| state.remote_connection())
.map_or(false, |connection| connection.shares_network_interface())
}
pub fn build_command(
&self,
program: Option<String>,
@@ -789,11 +817,7 @@ impl RemoteClient {
self.client.clone().into()
}
pub fn host(&self) -> String {
self.connection_options.host.clone()
}
pub fn connection_options(&self) -> SshConnectionOptions {
pub fn connection_options(&self) -> RemoteConnectionOptions {
self.connection_options.clone()
}
@@ -836,14 +860,14 @@ impl RemoteClient {
pub fn fake_server(
client_cx: &mut gpui::TestAppContext,
server_cx: &mut gpui::TestAppContext,
) -> (SshConnectionOptions, AnyProtoClient) {
) -> (RemoteConnectionOptions, AnyProtoClient) {
let port = client_cx
.update(|cx| cx.default_global::<ConnectionPool>().connections.len() as u16 + 1);
let opts = SshConnectionOptions {
let opts = RemoteConnectionOptions::Ssh(SshConnectionOptions {
host: "<fake>".to_string(),
port: Some(port),
..Default::default()
};
});
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
let server_client =
@@ -874,13 +898,13 @@ impl RemoteClient {
#[cfg(any(test, feature = "test-support"))]
pub async fn fake_client(
opts: SshConnectionOptions,
opts: RemoteConnectionOptions,
client_cx: &mut gpui::TestAppContext,
) -> Entity<Self> {
let (_tx, rx) = oneshot::channel();
client_cx
.update(|cx| {
Self::ssh(
Self::new(
ConnectionIdentifier::setup(),
opts,
rx,
@@ -901,7 +925,7 @@ enum ConnectionPoolEntry {
#[derive(Default)]
struct ConnectionPool {
connections: HashMap<SshConnectionOptions, ConnectionPoolEntry>,
connections: HashMap<RemoteConnectionOptions, ConnectionPoolEntry>,
}
impl Global for ConnectionPool {}
@@ -909,7 +933,7 @@ impl Global for ConnectionPool {}
impl ConnectionPool {
pub fn connect(
&mut self,
opts: SshConnectionOptions,
opts: RemoteConnectionOptions,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut App,
) -> Shared<Task<Result<Arc<dyn RemoteConnection>, Arc<anyhow::Error>>>> {
@@ -939,9 +963,18 @@ impl ConnectionPool {
let opts = opts.clone();
let delegate = delegate.clone();
async move |cx| {
let connection = SshRemoteConnection::new(opts.clone(), delegate, cx)
.await
.map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>);
let connection = match opts.clone() {
RemoteConnectionOptions::Ssh(opts) => {
SshRemoteConnection::new(opts, delegate, cx)
.await
.map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>)
}
RemoteConnectionOptions::Wsl(opts) => {
WslRemoteConnection::new(opts, delegate, cx)
.await
.map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>)
}
};
cx.update_global(|pool: &mut Self, _| {
debug_assert!(matches!(
@@ -972,6 +1005,33 @@ impl ConnectionPool {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RemoteConnectionOptions {
Ssh(SshConnectionOptions),
Wsl(WslConnectionOptions),
}
impl RemoteConnectionOptions {
pub fn display_name(&self) -> String {
match self {
RemoteConnectionOptions::Ssh(opts) => opts.host.clone(),
RemoteConnectionOptions::Wsl(opts) => opts.distro_name.clone(),
}
}
}
impl From<SshConnectionOptions> for RemoteConnectionOptions {
fn from(opts: SshConnectionOptions) -> Self {
RemoteConnectionOptions::Ssh(opts)
}
}
impl From<WslConnectionOptions> for RemoteConnectionOptions {
fn from(opts: WslConnectionOptions) -> Self {
RemoteConnectionOptions::Wsl(opts)
}
}
#[async_trait(?Send)]
pub(crate) trait RemoteConnection: Send + Sync {
fn start_proxy(
@@ -992,6 +1052,9 @@ pub(crate) trait RemoteConnection: Send + Sync {
) -> Task<Result<()>>;
async fn kill(&self) -> Result<()>;
fn has_been_killed(&self) -> bool;
fn shares_network_interface(&self) -> bool {
false
}
fn build_command(
&self,
program: Option<String>,
@@ -1000,7 +1063,7 @@ pub(crate) trait RemoteConnection: Send + Sync {
working_dir: Option<String>,
port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate>;
fn connection_options(&self) -> SshConnectionOptions;
fn connection_options(&self) -> RemoteConnectionOptions;
fn path_style(&self) -> PathStyle;
fn shell(&self) -> String;
@@ -1307,7 +1370,7 @@ impl ProtoClient for ChannelClient {
#[cfg(any(test, feature = "test-support"))]
mod fake {
use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform};
use crate::{SshConnectionOptions, remote_client::CommandTemplate};
use crate::remote_client::{CommandTemplate, RemoteConnectionOptions};
use anyhow::Result;
use async_trait::async_trait;
use collections::HashMap;
@@ -1326,7 +1389,7 @@ mod fake {
use util::paths::{PathStyle, RemotePathBuf};
pub(super) struct FakeRemoteConnection {
pub(super) connection_options: SshConnectionOptions,
pub(super) connection_options: RemoteConnectionOptions,
pub(super) server_channel: Arc<ChannelClient>,
pub(super) server_cx: SendableCx,
}
@@ -1386,7 +1449,7 @@ mod fake {
unreachable!()
}
fn connection_options(&self) -> SshConnectionOptions {
fn connection_options(&self) -> RemoteConnectionOptions {
self.connection_options.clone()
}
+335
View File
@@ -1 +1,336 @@
use crate::{
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 smol::process::Child;
pub mod ssh;
pub mod wsl;
fn handle_rpc_messages_over_child_process_stdio(
mut ssh_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 = ssh_proxy_process.stderr.take().unwrap();
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
let mut child_stdin = ssh_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 {
eprintln!("(remote) {}", String::from_utf8_lossy(content));
}
}
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 status = ssh_proxy_process.status().await?.code().unwrap_or(1);
match result {
Ok(_) => Ok(status),
Err(error) => Err(error),
}
})
}
#[cfg(debug_assertions)]
async fn build_remote_server_from_source(
platform: &crate::RemotePlatform,
delegate: &dyn crate::RemoteClientDelegate,
cx: &mut AsyncApp,
) -> Result<Option<std::path::PathBuf>> {
use std::path::Path;
let Some(build_remote_server) = std::env::var("ZED_BUILD_REMOTE_SERVER").ok() else {
return Ok(None);
};
use smol::process::{Command, Stdio};
use std::env::VarError;
async fn run_cmd(command: &mut Command) -> Result<()> {
let output = command
.kill_on_drop(true)
.stderr(Stdio::inherit())
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"Failed to run command: {command:?}"
);
Ok(())
}
let use_musl = !build_remote_server.contains("nomusl");
let triple = format!(
"{}-{}",
platform.arch,
match platform.os {
"linux" =>
if use_musl {
"unknown-linux-musl"
} else {
"unknown-linux-gnu"
},
"macos" => "apple-darwin",
_ => anyhow::bail!("can't cross compile for: {:?}", platform),
}
);
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 == "linux" && use_musl {
rust_flags.push_str(" -C target-feature=+crt-static");
}
if build_remote_server.contains("mold") {
rust_flags.push_str(" -C link-arg=-fuse-ld=mold");
}
if platform.arch == std::env::consts::ARCH && platform.os == 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(
Command::new("cargo")
.args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
"--target",
&triple,
])
.env("RUSTFLAGS", &rust_flags),
)
.await?;
} else if build_remote_server.contains("cross") {
#[cfg(target_os = "windows")]
use util::paths::SanitizedPath;
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
log::info!("installing cross");
run_cmd(Command::new("cargo").args([
"install",
"cross",
"--git",
"https://github.com/cross-rs/cross",
]))
.await?;
delegate.set_status(
Some(&format!(
"Building remote server binary from source for {} with Docker",
&triple
)),
cx,
);
log::info!("building remote server binary from source for {}", &triple);
// On Windows, the binding needs to be set to the canonical path
#[cfg(target_os = "windows")]
let src = SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string();
#[cfg(not(target_os = "windows"))]
let src = "./target";
run_cmd(
Command::new("cross")
.args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
"--target",
&triple,
])
.env(
"CROSS_CONTAINER_OPTS",
format!("--mount type=bind,src={src},dst=/app/target"),
)
.env("RUSTFLAGS", &rust_flags),
)
.await?;
} else {
let which = cx
.background_spawn(async move { which::which("zig") })
.await;
if which.is_err() {
#[cfg(not(target_os = "windows"))]
{
anyhow::bail!(
"zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
)
}
#[cfg(target_os = "windows")]
{
anyhow::bail!(
"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) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
)
}
}
delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
log::info!("adding rustup target");
run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?;
delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
log::info!("installing cargo-zigbuild");
run_cmd(Command::new("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(
Command::new("cargo")
.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("target")
.join("remote_server")
.join(&triple)
.join("debug")
.join("remote_server");
let path = if !build_remote_server.contains("nocompress") {
delegate.set_status(Some("Compressing binary"), cx);
#[cfg(not(target_os = "windows"))]
{
run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?;
}
#[cfg(target_os = "windows")]
{
// On Windows, we use 7z to compress the binary
let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?;
let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple);
if smol::fs::metadata(&gz_path).await.is_ok() {
smol::fs::remove_file(&gz_path).await?;
}
run_cmd(Command::new(seven_zip).args([
"a",
"-tgzip",
&gz_path,
&bin_path.to_string_lossy(),
]))
.await?;
}
let mut archive_path = bin_path;
archive_path.set_extension("gz");
std::env::current_dir()?.join(archive_path)
} else {
bin_path
};
Ok(Some(path))
}
+11 -330
View File
@@ -1,14 +1,12 @@
use crate::{
RemoteClientDelegate, RemotePlatform,
json_log::LogRecord,
protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message},
remote_client::{CommandTemplate, RemoteConnection},
remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions},
};
use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait;
use collections::HashMap;
use futures::{
AsyncReadExt as _, FutureExt as _, StreamExt as _,
AsyncReadExt as _, FutureExt as _,
channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender},
select_biased,
};
@@ -99,8 +97,8 @@ impl RemoteConnection for SshRemoteConnection {
self.master_process.lock().is_none()
}
fn connection_options(&self) -> SshConnectionOptions {
self.socket.connection_options.clone()
fn connection_options(&self) -> RemoteConnectionOptions {
RemoteConnectionOptions::Ssh(self.socket.connection_options.clone())
}
fn shell(&self) -> String {
@@ -267,7 +265,7 @@ impl RemoteConnection for SshRemoteConnection {
}
};
Self::multiplex(
super::handle_rpc_messages_over_child_process_stdio(
ssh_proxy_process,
incoming_tx,
outgoing_rx,
@@ -415,109 +413,6 @@ impl SshRemoteConnection {
Ok(this)
}
fn multiplex(
mut ssh_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 = ssh_proxy_process.stderr.take().unwrap();
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
let mut child_stdin = ssh_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 {
eprintln!("(remote) {}", String::from_utf8_lossy(content));
}
}
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 status = ssh_proxy_process.status().await?.code().unwrap_or(1);
match result {
Ok(_) => Ok(status),
Err(error) => Err(error),
}
})
}
#[allow(unused)]
async fn ensure_server_binary(
&self,
delegate: &Arc<dyn RemoteClientDelegate>,
@@ -544,19 +439,20 @@ impl SshRemoteConnection {
self.ssh_path_style,
);
let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok();
#[cfg(debug_assertions)]
if let Some(build_remote_server) = build_remote_server {
let src_path = self.build_local(build_remote_server, delegate, cx).await?;
if let Some(remote_server_path) =
super::build_remote_server_from_source(&self.ssh_platform, delegate.as_ref(), cx)
.await?
{
let tmp_path = RemotePathBuf::new(
paths::remote_server_dir_relative().join(format!(
"download-{}-{}",
std::process::id(),
src_path.file_name().unwrap().to_string_lossy()
remote_server_path.file_name().unwrap().to_string_lossy()
)),
self.ssh_path_style,
);
self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx)
self.upload_local_server_binary(&remote_server_path, &tmp_path, delegate, cx)
.await?;
self.extract_server_binary(&dst_path, &tmp_path, delegate, cx)
.await?;
@@ -794,221 +690,6 @@ impl SshRemoteConnection {
);
Ok(())
}
#[cfg(debug_assertions)]
async fn build_local(
&self,
build_remote_server: String,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<PathBuf> {
use smol::process::{Command, Stdio};
use std::env::VarError;
async fn run_cmd(command: &mut Command) -> Result<()> {
let output = command
.kill_on_drop(true)
.stderr(Stdio::inherit())
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"Failed to run command: {command:?}"
);
Ok(())
}
let use_musl = !build_remote_server.contains("nomusl");
let triple = format!(
"{}-{}",
self.ssh_platform.arch,
match self.ssh_platform.os {
"linux" =>
if use_musl {
"unknown-linux-musl"
} else {
"unknown-linux-gnu"
},
"macos" => "apple-darwin",
_ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform),
}
);
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 self.ssh_platform.os == "linux" && use_musl {
rust_flags.push_str(" -C target-feature=+crt-static");
}
if build_remote_server.contains("mold") {
rust_flags.push_str(" -C link-arg=-fuse-ld=mold");
}
if self.ssh_platform.arch == std::env::consts::ARCH
&& self.ssh_platform.os == 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(
Command::new("cargo")
.args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
"--target",
&triple,
])
.env("RUSTFLAGS", &rust_flags),
)
.await?;
} else if build_remote_server.contains("cross") {
#[cfg(target_os = "windows")]
use util::paths::SanitizedPath;
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
log::info!("installing cross");
run_cmd(Command::new("cargo").args([
"install",
"cross",
"--git",
"https://github.com/cross-rs/cross",
]))
.await?;
delegate.set_status(
Some(&format!(
"Building remote server binary from source for {} with Docker",
&triple
)),
cx,
);
log::info!("building remote server binary from source for {}", &triple);
// On Windows, the binding needs to be set to the canonical path
#[cfg(target_os = "windows")]
let src =
SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string();
#[cfg(not(target_os = "windows"))]
let src = "./target";
run_cmd(
Command::new("cross")
.args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
"--target",
&triple,
])
.env(
"CROSS_CONTAINER_OPTS",
format!("--mount type=bind,src={src},dst=/app/target"),
)
.env("RUSTFLAGS", &rust_flags),
)
.await?;
} else {
let which = cx
.background_spawn(async move { which::which("zig") })
.await;
if which.is_err() {
#[cfg(not(target_os = "windows"))]
{
anyhow::bail!(
"zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
)
}
#[cfg(target_os = "windows")]
{
anyhow::bail!(
"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) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
)
}
}
delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
log::info!("adding rustup target");
run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?;
delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
log::info!("installing cargo-zigbuild");
run_cmd(Command::new("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(
Command::new("cargo")
.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("target")
.join("remote_server")
.join(&triple)
.join("debug")
.join("remote_server");
let path = if !build_remote_server.contains("nocompress") {
delegate.set_status(Some("Compressing binary"), cx);
#[cfg(not(target_os = "windows"))]
{
run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?;
}
#[cfg(target_os = "windows")]
{
// On Windows, we use 7z to compress the binary
let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?;
let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple);
if smol::fs::metadata(&gz_path).await.is_ok() {
smol::fs::remove_file(&gz_path).await?;
}
run_cmd(Command::new(seven_zip).args([
"a",
"-tgzip",
&gz_path,
&bin_path.to_string_lossy(),
]))
.await?;
}
let mut archive_path = bin_path;
archive_path.set_extension("gz");
std::env::current_dir()?.join(archive_path)
} else {
bin_path
};
Ok(path)
}
}
impl SshSocket {
+494
View File
@@ -0,0 +1,494 @@
use crate::{
RemoteClientDelegate, RemotePlatform,
remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions},
};
use anyhow::{Result, anyhow, bail};
use async_trait::async_trait;
use collections::HashMap;
use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender};
use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task};
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use rpc::proto::Envelope;
use smol::{fs, process};
use std::{
fmt::Write as _,
path::{Path, PathBuf},
process::Stdio,
sync::Arc,
time::Instant,
};
use util::paths::{PathStyle, RemotePathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WslConnectionOptions {
pub distro_name: String,
pub user: Option<String>,
}
pub(crate) struct WslRemoteConnection {
remote_binary_path: Option<RemotePathBuf>,
platform: RemotePlatform,
shell: String,
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, commit) = cx.update(|cx| {
(
ReleaseChannel::global(cx),
AppVersion::global(cx),
AppCommitSha::try_global(cx),
)
})?;
let mut this = Self {
connection_options,
remote_binary_path: None,
platform: RemotePlatform { os: "", arch: "" },
shell: String::new(),
};
delegate.set_status(Some("Detecting WSL environment"), cx);
this.platform = this.detect_platform().await?;
this.shell = this.detect_shell().await?;
this.remote_binary_path = Some(
this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
.await?,
);
Ok(this)
}
async fn detect_platform(&self) -> Result<RemotePlatform> {
let arch_str = self.run_wsl_command("uname", &["-m"]).await?;
let arch_str = arch_str.trim().to_string();
let arch = match arch_str.as_str() {
"x86_64" => "x86_64",
"aarch64" | "arm64" => "aarch64",
_ => "x86_64",
};
Ok(RemotePlatform { os: "linux", arch })
}
async fn detect_shell(&self) -> Result<String> {
Ok(self
.run_wsl_command("sh", &["-c", "echo $SHELL"])
.await
.ok()
.and_then(|shell_path| shell_path.trim().split('/').next_back().map(str::to_string))
.unwrap_or_else(|| "bash".to_string()))
}
async fn windows_path_to_wsl_path(&self, source: &Path) -> Result<String> {
windows_path_to_wsl_path_impl(&self.connection_options, source).await
}
fn wsl_command(&self, program: &str, args: &[&str]) -> process::Command {
wsl_command_impl(&self.connection_options, program, args)
}
async fn run_wsl_command(&self, program: &str, args: &[&str]) -> Result<String> {
run_wsl_command_impl(&self.connection_options, program, args).await
}
async fn ensure_server_binary(
&self,
delegate: &Arc<dyn RemoteClientDelegate>,
release_channel: ReleaseChannel,
version: SemanticVersion,
commit: Option<AppCommitSha>,
cx: &mut AsyncApp,
) -> Result<RemotePathBuf> {
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 = RemotePathBuf::new(
paths::remote_wsl_server_dir_relative().join(binary_name),
PathStyle::Posix,
);
if let Some(parent) = dst_path.parent() {
self.run_wsl_command("mkdir", &["-p", &parent.to_string()])
.await
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
}
#[cfg(debug_assertions)]
if let Some(remote_server_path) =
super::build_remote_server_from_source(&self.platform, delegate.as_ref(), cx).await?
{
let tmp_path = RemotePathBuf::new(
paths::remote_wsl_server_dir_relative().join(format!(
"download-{}-{}",
std::process::id(),
remote_server_path.file_name().unwrap().to_string_lossy()
)),
PathStyle::Posix,
);
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 self
.run_wsl_command(&dst_path.to_string(), &["version"])
.await
.is_ok()
{
return Ok(dst_path);
}
delegate.set_status(Some("Installing remote server"), cx);
let wanted_version = match release_channel {
ReleaseChannel::Nightly => None,
ReleaseChannel::Dev => {
return Err(anyhow!("Dev builds require manual installation"));
}
_ => 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 = RemotePathBuf::new(
PathBuf::from(format!("{}.{}.tmp", dst_path, std::process::id())),
PathStyle::Posix,
);
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: &RemotePathBuf,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
delegate.set_status(Some("Uploading remote server to WSL"), cx);
if let Some(parent) = dst_path.parent() {
self.run_wsl_command("mkdir", &["-p", &parent.to_string()])
.await
.map_err(|e| anyhow!("Failed to create directory when uploading file: {}", e))?;
}
let t0 = Instant::now();
let src_stat = fs::metadata(&src_path).await?;
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?;
self.run_wsl_command("cp", &["-f", &src_path_in_wsl, &dst_path.to_string()])
.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: &RemotePathBuf,
dst_path: &RemotePathBuf,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
delegate.set_status(Some("Extracting remote server"), cx);
let tmp_path_str = tmp_path.to_string();
let dst_path_str = dst_path.to_string();
// 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_command = format!(
"exec {} proxy --identifier {}",
remote_binary_path, unique_identifier
);
if reconnect {
proxy_command.push_str(" --reconnect");
}
for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
if let Some(value) = std::env::var(env_var).ok() {
proxy_command = format!("{}='{}' {}", env_var, value, proxy_command);
}
}
let proxy_process = match self
.wsl_command("sh", &["-lc", &proxy_command])
.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?;
run_wsl_command_impl(&options, "cp", &["-r", &wsl_src, &dest_path.to_string()])
.await
.map_err(|e| {
anyhow!(
"failed to upload directory {} -> {}: {}",
src_path.display(),
dest_path.to_string(),
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)>,
) -> Result<CommandTemplate> {
if port_forward.is_some() {
bail!("WSL shares the network interface with the host system");
}
let working_dir = working_dir
.map(|working_dir| RemotePathBuf::new(working_dir.into(), PathStyle::Posix).to_string())
.unwrap_or("~".to_string());
let mut script = String::new();
for (k, v) in env.iter() {
write!(&mut script, "{}='{}' ", k, v).unwrap();
}
if let Some(program) = program {
let command = shlex::try_quote(&program)?;
script.push_str(&command);
for arg in args {
let arg = shlex::try_quote(&arg)?;
script.push_str(" ");
script.push_str(&arg);
}
} else {
write!(&mut script, "exec {} -l", self.shell).unwrap();
}
let 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(),
self.shell.clone(),
"-c".to_string(),
shlex::try_quote(&script)?.to_string(),
]
} else {
vec![
"--distribution".to_string(),
self.connection_options.distro_name.clone(),
"--cd".to_string(),
working_dir,
"--".to_string(),
self.shell.clone(),
"-c".to_string(),
shlex::try_quote(&script)?.to_string(),
]
};
Ok(CommandTemplate {
program: "wsl.exe".to_string(),
args: wsl_args,
env: HashMap::default(),
})
}
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()
}
}
/// `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?;
let path_str = path.to_string_lossy();
let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
Ok(sanitized.replace('\\', "/"))
}
async fn windows_path_to_wsl_path_impl(
options: &WslConnectionOptions,
source: &Path,
) -> Result<String> {
let source = sanitize_path(source).await?;
run_wsl_command_impl(options, "wslpath", &["-u", &source]).await
}
fn wsl_command_impl(
options: &WslConnectionOptions,
program: &str,
args: &[&str],
) -> process::Command {
let mut command = util::command::new_smol_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("~")
.arg(program)
.args(args);
command
}
async fn run_wsl_command_impl(
options: &WslConnectionOptions,
program: &str,
args: &[&str],
) -> Result<String> {
let output = wsl_command_impl(options, program, args).output().await?;
if !output.status.success() {
return Err(anyhow!(
"Command '{}' failed: {}",
program,
String::from_utf8_lossy(&output.stderr).trim()
));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}