877 lines · 27.8 KB · Rust
RawDownload
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::{    RemoteArch, 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>,    os_version: Option<String>,    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,            os_version: 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::Unix),        };         this.remote_platform = Some(remote_platform);        log::info!("Remote platform discovered: {:?}", this.remote_platform);         this.os_version = this.discover_os_version(remote_platform.os).await;        log::info!("Remote OS version discovered: {:?}", this.os_version);         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"        }    }     /// Run a shell command inside the container and reliably extract its output    /// using unique delimiters, so that shell initialization noise (e.g. from    /// BASH_ENV or .bashrc) does not corrupt the result.    async fn run_docker_exec_delimited(&self, script: &str) -> Result<String> {        const MARKER: &str = "=====ZED_DELIM_7f3a9c=====";        let wrapped =            format!("printf '{MARKER}'; {script}; __exit=$?; printf '{MARKER}'; exit $__exit");        let output = self            .run_docker_exec("sh", None, &Default::default(), &["-c", &wrapped])            .await?;        let start = output.find(MARKER).map(|i| i + MARKER.len()).unwrap_or(0);        let end = output[start..]            .find(MARKER)            .map(|i| start + i)            .unwrap_or(output.len());        Ok(output[start..end].to_string())    }     async fn discover_shell(&self) -> String {        let default_shell = "sh";        match self.run_docker_exec_delimited("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_delimited("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_delimited("uname -sm").await?;        parse_platform(&uname)    }     /// Best-effort detection of the container's OS version for telemetry.    async fn discover_os_version(&self, os: RemoteOs) -> Option<String> {        let (program, args) = super::os_version_command(os);        match self            .run_docker_exec(program, None, &Default::default(), args)            .await        {            Ok(output) => super::parse_os_version(os, &output),            Err(error) => {                log::warn!("Failed to determine remote OS version: {error:#}");                None            }        }    }     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::from_unix_str(&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::from_unix_str(&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.into());        }         if binary_exists_on_server {            return Ok(dst_path.into());        }         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::from_unix_str(&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.into());                }                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.into())    }     async fn docker_user_home_dir(&self) -> Result<String> {        self.run_docker_exec_delimited("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    }

Lines 1–500 of 877.