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. Load more
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 }