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:
co-authored by
Junkui Zhang
parent
1c2e2a00fe
commit
f78f3e7729
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user