566 lines · 19.7 KB · Rust
RawDownload
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 })} /// The command (program + args) used to read a remote host's OS version, given/// its detected OS.////// The output is parsed by [`parse_os_version`].pub(crate) fn os_version_command(os: RemoteOs) -> (&'static str, &'static [&'static str]) {    match os {        // Matches the `/etc/os-release` parsing in `client::telemetry::os_version`.        RemoteOs::Linux => ("cat", &["/etc/os-release"]),        RemoteOs::MacOs => ("sw_vers", &["-productVersion"]),        // Prints e.g. "Microsoft Windows [Version 10.0.19045.5011]".        RemoteOs::Windows => ("cmd.exe", &["/c", "ver"]),    }} /// Parses the output of [`os_version_command`] into a human-readable version/// string, matching the conventions used by `client::telemetry::os_version`.////// For Linux this is `"{ID} {VERSION_ID}"` (e.g. `"ubuntu 24.04"`); for macOS it/// is the product version (e.g. `"15.6.1"`); for Windows it is the/// `major.minor.build` version (e.g. `"10.0.19045"`). Returns `None` if nothing/// usable could be parsed.pub(crate) fn parse_os_version(os: RemoteOs, output: &str) -> Option<String> {    let output = output.trim();    if output.is_empty() {        return None;    }    match os {        RemoteOs::Linux => util::parse_os_release(output),        RemoteOs::MacOs => {            // `sw_vers -productVersion` prints a single version line.            output                .lines()                .next_back()                .map(|line| line.trim().to_string())                .filter(|line| !line.is_empty())        }        RemoteOs::Windows => parse_windows_version(output),    }} /// Extracts a `major.minor.build` version from the output of `cmd.exe /c ver`,/// e.g. `"Microsoft Windows [Version 10.0.19045.5011]"` -> `"10.0.19045"`.////// Scans for the first dotted run of integers (rather than relying on the/// surrounding, potentially localized, text) and drops the trailing revision so/// the format matches `client::telemetry::os_version` on Windows.fn parse_windows_version(output: &str) -> Option<String> {    output        .split(|c: char| !c.is_ascii_digit() && c != '.')        .filter_map(|token| {            let parts: Vec<&str> = token.split('.').filter(|part| !part.is_empty()).collect();            (parts.len() >= 3 && parts.iter().all(|part| part.parse::<u32>().is_ok()))                .then(|| parts[..3].join("."))        })        .next()} /// 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())            .stderr(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);

Lines 1–500 of 566.