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:
51
crates/remote/Cargo.toml
Normal file
51
crates/remote/Cargo.toml
Normal 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
1
crates/remote/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
61
crates/remote/src/json_log.rs
Normal file
61
crates/remote/src/json_log.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
76
crates/remote/src/protocol.rs
Normal file
76
crates/remote/src/protocol.rs
Normal 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(())
|
||||
}
|
||||
24
crates/remote/src/proxy.rs
Normal file
24
crates/remote/src/proxy.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
27
crates/remote/src/remote.rs
Normal file
27
crates/remote/src/remote.rs
Normal 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,
|
||||
};
|
||||
1995
crates/remote/src/remote_client.rs
Normal file
1995
crates/remote/src/remote_client.rs
Normal file
File diff suppressed because it is too large
Load Diff
168
crates/remote/src/remote_identity.rs
Normal file
168
crates/remote/src/remote_identity.rs
Normal 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)));
|
||||
}
|
||||
}
|
||||
466
crates/remote/src/transport.rs
Normal file
466
crates/remote/src/transport.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
842
crates/remote/src/transport/docker.rs
Normal file
842
crates/remote/src/transport/docker.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
342
crates/remote/src/transport/mock.rs
Normal file
342
crates/remote/src/transport/mock.rs
Normal 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) {}
|
||||
}
|
||||
2272
crates/remote/src/transport/ssh.rs
Normal file
2272
crates/remote/src/transport/ssh.rs
Normal file
File diff suppressed because it is too large
Load Diff
657
crates/remote/src/transport/wsl.rs
Normal file
657
crates/remote/src/transport/wsl.rs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user