docker.rs

  1use anyhow::Context;
  2use anyhow::Result;
  3use anyhow::anyhow;
  4use async_trait::async_trait;
  5use collections::HashMap;
  6use parking_lot::Mutex;
  7use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
  8use semver::Version as SemanticVersion;
  9use std::collections::BTreeMap;
 10use std::time::Instant;
 11use std::{
 12    path::{Path, PathBuf},
 13    sync::Arc,
 14};
 15use util::ResultExt;
 16use util::command::Stdio;
 17use util::shell::ShellKind;
 18use util::{
 19    paths::{PathStyle, RemotePathBuf},
 20    rel_path::RelPath,
 21};
 22
 23use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender};
 24use gpui::{App, AppContext, AsyncApp, Task};
 25use rpc::proto::Envelope;
 26
 27use crate::{
 28    RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, RemoteOs, RemotePlatform,
 29    remote_client::{CommandTemplate, Interactive},
 30    transport::parse_platform,
 31};
 32
 33#[derive(
 34    Debug,
 35    Default,
 36    Clone,
 37    PartialEq,
 38    Eq,
 39    Hash,
 40    PartialOrd,
 41    Ord,
 42    serde::Serialize,
 43    serde::Deserialize,
 44)]
 45pub struct DockerConnectionOptions {
 46    pub name: String,
 47    pub container_id: String,
 48    pub remote_user: String,
 49    pub upload_binary_over_docker_exec: bool,
 50    pub use_podman: bool,
 51    pub remote_env: BTreeMap<String, String>,
 52}
 53
 54pub(crate) struct DockerExecConnection {
 55    proxy_process: Mutex<Option<u32>>,
 56    remote_dir_for_server: String,
 57    remote_binary_relpath: Option<Arc<RelPath>>,
 58    connection_options: DockerConnectionOptions,
 59    remote_platform: Option<RemotePlatform>,
 60    path_style: Option<PathStyle>,
 61    shell: String,
 62}
 63
 64impl DockerExecConnection {
 65    pub async fn new(
 66        connection_options: DockerConnectionOptions,
 67        delegate: Arc<dyn RemoteClientDelegate>,
 68        cx: &mut AsyncApp,
 69    ) -> Result<Self> {
 70        let mut this = Self {
 71            proxy_process: Mutex::new(None),
 72            remote_dir_for_server: "/".to_string(),
 73            remote_binary_relpath: None,
 74            connection_options,
 75            remote_platform: None,
 76            path_style: None,
 77            shell: "sh".to_owned(),
 78        };
 79        let (release_channel, version, commit) = cx.update(|cx| {
 80            (
 81                ReleaseChannel::global(cx),
 82                AppVersion::global(cx),
 83                AppCommitSha::try_global(cx),
 84            )
 85        });
 86        let remote_platform = this.check_remote_platform().await?;
 87
 88        this.path_style = match remote_platform.os {
 89            RemoteOs::Windows => Some(PathStyle::Windows),
 90            _ => Some(PathStyle::Posix),
 91        };
 92
 93        this.remote_platform = Some(remote_platform);
 94        log::info!("Remote platform discovered: {:?}", this.remote_platform);
 95
 96        this.shell = this.discover_shell().await;
 97        log::info!("Remote shell discovered: {}", this.shell);
 98
 99        this.remote_dir_for_server = this.docker_user_home_dir().await?.trim().to_string();
100
101        this.remote_binary_relpath = Some(
102            this.ensure_server_binary(
103                &delegate,
104                release_channel,
105                version,
106                &this.remote_dir_for_server,
107                commit,
108                cx,
109            )
110            .await?,
111        );
112
113        Ok(this)
114    }
115
116    fn docker_cli(&self) -> &str {
117        if self.connection_options.use_podman {
118            "podman"
119        } else {
120            "docker"
121        }
122    }
123
124    async fn discover_shell(&self) -> String {
125        let default_shell = "sh";
126        match self
127            .run_docker_exec("sh", None, &Default::default(), &["-c", "echo $SHELL"])
128            .await
129        {
130            Ok(shell) => match shell.trim() {
131                "" => {
132                    log::info!("$SHELL is not set, checking passwd for user");
133                }
134                shell => {
135                    return shell.to_owned();
136                }
137            },
138            Err(e) => {
139                log::error!("Failed to get $SHELL: {e}. Checking passwd for user");
140            }
141        }
142
143        match self
144            .run_docker_exec(
145                "sh",
146                None,
147                &Default::default(),
148                &["-c", "getent passwd \"$(id -un)\" | cut -d: -f7"],
149            )
150            .await
151        {
152            Ok(shell) => match shell.trim() {
153                "" => {
154                    log::info!("No shell found in passwd, falling back to {default_shell}");
155                }
156                shell => {
157                    return shell.to_owned();
158                }
159            },
160            Err(e) => {
161                log::info!("Error getting shell from passwd: {e}. Falling back to {default_shell}");
162            }
163        }
164        default_shell.to_owned()
165    }
166
167    async fn check_remote_platform(&self) -> Result<RemotePlatform> {
168        let uname = self
169            .run_docker_exec("uname", None, &Default::default(), &["-sm"])
170            .await?;
171        parse_platform(&uname)
172    }
173
174    async fn ensure_server_binary(
175        &self,
176        delegate: &Arc<dyn RemoteClientDelegate>,
177        release_channel: ReleaseChannel,
178        version: SemanticVersion,
179        remote_dir_for_server: &str,
180        commit: Option<AppCommitSha>,
181        cx: &mut AsyncApp,
182    ) -> Result<Arc<RelPath>> {
183        let remote_platform = self
184            .remote_platform
185            .context("No remote platform defined; cannot proceed.")?;
186
187        let version_str = match release_channel {
188            ReleaseChannel::Nightly => {
189                let commit = commit.map(|s| s.full()).unwrap_or_default();
190                format!("{}-{}", version, commit)
191            }
192            ReleaseChannel::Dev => "build".to_string(),
193            _ => version.to_string(),
194        };
195        let binary_name = format!(
196            "zed-remote-server-{}-{}",
197            release_channel.dev_name(),
198            version_str
199        );
200        let dst_path =
201            paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
202
203        let binary_exists_on_server = self
204            .run_docker_exec(
205                &dst_path.display(self.path_style()),
206                Some(&remote_dir_for_server),
207                &Default::default(),
208                &["version"],
209            )
210            .await
211            .is_ok();
212        #[cfg(any(debug_assertions, feature = "build-remote-server-binary"))]
213        if let Some(remote_server_path) = super::build_remote_server_from_source(
214            &remote_platform,
215            delegate.as_ref(),
216            binary_exists_on_server,
217            cx,
218        )
219        .await?
220        {
221            let tmp_path = paths::remote_server_dir_relative().join(
222                RelPath::unix(&format!(
223                    "download-{}-{}",
224                    std::process::id(),
225                    remote_server_path.file_name().unwrap().to_string_lossy()
226                ))
227                .unwrap(),
228            );
229            self.upload_local_server_binary(
230                &remote_server_path,
231                &tmp_path,
232                &remote_dir_for_server,
233                delegate,
234                cx,
235            )
236            .await?;
237            self.extract_server_binary(&dst_path, &tmp_path, &remote_dir_for_server, delegate, cx)
238                .await?;
239            return Ok(dst_path);
240        }
241
242        if binary_exists_on_server {
243            return Ok(dst_path);
244        }
245
246        let wanted_version = cx.update(|cx| match release_channel {
247            ReleaseChannel::Nightly => Ok(None),
248            ReleaseChannel::Dev => {
249                anyhow::bail!(
250                    "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})",
251                    dst_path
252                )
253            }
254            _ => Ok(Some(AppVersion::global(cx))),
255        })?;
256
257        let tmp_path_gz = paths::remote_server_dir_relative().join(
258            RelPath::unix(&format!(
259                "{}-download-{}.gz",
260                binary_name,
261                std::process::id()
262            ))
263            .unwrap(),
264        );
265        if !self.connection_options.upload_binary_over_docker_exec
266            && let Some(url) = delegate
267                .get_download_url(remote_platform, release_channel, wanted_version.clone(), cx)
268                .await?
269        {
270            match self
271                .download_binary_on_server(&url, &tmp_path_gz, &remote_dir_for_server, delegate, cx)
272                .await
273            {
274                Ok(_) => {
275                    self.extract_server_binary(
276                        &dst_path,
277                        &tmp_path_gz,
278                        &remote_dir_for_server,
279                        delegate,
280                        cx,
281                    )
282                    .await
283                    .context("extracting server binary")?;
284                    return Ok(dst_path);
285                }
286                Err(e) => {
287                    log::error!(
288                        "Failed to download binary on server, attempting to download locally and then upload it the server: {e:#}",
289                    )
290                }
291            }
292        }
293
294        let src_path = delegate
295            .download_server_binary_locally(remote_platform, release_channel, wanted_version, cx)
296            .await
297            .context("downloading server binary locally")?;
298        self.upload_local_server_binary(
299            &src_path,
300            &tmp_path_gz,
301            &remote_dir_for_server,
302            delegate,
303            cx,
304        )
305        .await
306        .context("uploading server binary")?;
307        self.extract_server_binary(
308            &dst_path,
309            &tmp_path_gz,
310            &remote_dir_for_server,
311            delegate,
312            cx,
313        )
314        .await
315        .context("extracting server binary")?;
316        Ok(dst_path)
317    }
318
319    async fn docker_user_home_dir(&self) -> Result<String> {
320        let inner_program = self.shell();
321        self.run_docker_exec(
322            &inner_program,
323            None,
324            &Default::default(),
325            &["-c", "echo $HOME"],
326        )
327        .await
328    }
329
330    async fn extract_server_binary(
331        &self,
332        dst_path: &RelPath,
333        tmp_path: &RelPath,
334        remote_dir_for_server: &str,
335        delegate: &Arc<dyn RemoteClientDelegate>,
336        cx: &mut AsyncApp,
337    ) -> Result<()> {
338        delegate.set_status(Some("Extracting remote development server"), cx);
339        let server_mode = 0o755;
340
341        let shell_kind = ShellKind::Posix;
342        let orig_tmp_path = tmp_path.display(self.path_style());
343        let server_mode = format!("{:o}", server_mode);
344        let server_mode = shell_kind
345            .try_quote(&server_mode)
346            .context("shell quoting")?;
347        let dst_path = dst_path.display(self.path_style());
348        let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?;
349        let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
350            let orig_tmp_path = shell_kind
351                .try_quote(&orig_tmp_path)
352                .context("shell quoting")?;
353            let tmp_path = shell_kind.try_quote(&tmp_path).context("shell quoting")?;
354            format!(
355                "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
356            )
357        } else {
358            let orig_tmp_path = shell_kind
359                .try_quote(&orig_tmp_path)
360                .context("shell quoting")?;
361            format!("chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",)
362        };
363        let args = shell_kind.args_for_shell(false, script.to_string());
364        self.run_docker_exec(
365            "sh",
366            Some(&remote_dir_for_server),
367            &Default::default(),
368            &args,
369        )
370        .await
371        .log_err();
372        Ok(())
373    }
374
375    async fn upload_local_server_binary(
376        &self,
377        src_path: &Path,
378        tmp_path_gz: &RelPath,
379        remote_dir_for_server: &str,
380        delegate: &Arc<dyn RemoteClientDelegate>,
381        cx: &mut AsyncApp,
382    ) -> Result<()> {
383        if let Some(parent) = tmp_path_gz.parent() {
384            self.run_docker_exec(
385                "mkdir",
386                Some(remote_dir_for_server),
387                &Default::default(),
388                &["-p", parent.display(self.path_style()).as_ref()],
389            )
390            .await?;
391        }
392
393        let src_stat = smol::fs::metadata(&src_path).await?;
394        let size = src_stat.len();
395
396        let t0 = Instant::now();
397        delegate.set_status(Some("Uploading remote development server"), cx);
398        log::info!(
399            "uploading remote development server to {:?} ({}kb)",
400            tmp_path_gz,
401            size / 1024
402        );
403        self.upload_file(src_path, tmp_path_gz, remote_dir_for_server)
404            .await
405            .context("failed to upload server binary")?;
406        log::info!("uploaded remote development server in {:?}", t0.elapsed());
407        Ok(())
408    }
409
410    async fn upload_and_chown(
411        docker_cli: String,
412        connection_options: DockerConnectionOptions,
413        src_path: String,
414        dst_path: String,
415    ) -> Result<()> {
416        let mut command = util::command::new_command(&docker_cli);
417        command.arg("cp");
418        command.arg("-a");
419        command.arg(&src_path);
420        command.arg(format!("{}:{}", connection_options.container_id, dst_path));
421
422        let output = command.output().await?;
423
424        if !output.status.success() {
425            let stderr = String::from_utf8_lossy(&output.stderr);
426            log::debug!("failed to upload via docker cp {src_path} -> {dst_path}: {stderr}",);
427            anyhow::bail!(
428                "failed to upload via docker cp {} -> {}: {}",
429                src_path,
430                dst_path,
431                stderr,
432            );
433        }
434
435        let mut chown_command = util::command::new_command(&docker_cli);
436        chown_command.arg("exec");
437        chown_command.arg(connection_options.container_id);
438        chown_command.arg("chown");
439        chown_command.arg(format!(
440            "{}:{}",
441            connection_options.remote_user, connection_options.remote_user,
442        ));
443        chown_command.arg(&dst_path);
444
445        let output = chown_command.output().await?;
446
447        if output.status.success() {
448            return Ok(());
449        }
450
451        let stderr = String::from_utf8_lossy(&output.stderr);
452        log::debug!("failed to change ownership for via chown: {stderr}",);
453        anyhow::bail!(
454            "failed to change ownership for zed_remote_server via chown: {}",
455            stderr,
456        );
457    }
458
459    async fn upload_file(
460        &self,
461        src_path: &Path,
462        dest_path: &RelPath,
463        remote_dir_for_server: &str,
464    ) -> Result<()> {
465        log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
466
467        let src_path_display = src_path.display().to_string();
468        let dest_path_str = dest_path.display(self.path_style());
469        let full_server_path = format!("{}/{}", remote_dir_for_server, dest_path_str);
470
471        Self::upload_and_chown(
472            self.docker_cli().to_string(),
473            self.connection_options.clone(),
474            src_path_display,
475            full_server_path,
476        )
477        .await
478    }
479
480    async fn run_docker_command(
481        &self,
482        subcommand: &str,
483        args: &[impl AsRef<str>],
484    ) -> Result<String> {
485        let mut command = util::command::new_command(self.docker_cli());
486        command.arg(subcommand);
487        for arg in args {
488            command.arg(arg.as_ref());
489        }
490        let output = command.output().await?;
491        log::debug!("{:?}: {:?}", command, output);
492        anyhow::ensure!(
493            output.status.success(),
494            "failed to run command {command:?}: {}",
495            String::from_utf8_lossy(&output.stderr)
496        );
497        Ok(String::from_utf8_lossy(&output.stdout).to_string())
498    }
499
500    async fn run_docker_exec(
501        &self,
502        inner_program: &str,
503        working_directory: Option<&str>,
504        env: &HashMap<String, String>,
505        program_args: &[impl AsRef<str>],
506    ) -> Result<String> {
507        let mut args = match working_directory {
508            Some(dir) => vec!["-w".to_string(), dir.to_string()],
509            None => vec![],
510        };
511
512        args.push("-u".to_string());
513        args.push(self.connection_options.remote_user.clone());
514
515        for (k, v) in self.connection_options.remote_env.iter() {
516            args.push("-e".to_string());
517            args.push(format!("{k}={v}"));
518        }
519
520        for (k, v) in env.iter() {
521            args.push("-e".to_string());
522            args.push(format!("{k}={v}"));
523        }
524
525        args.push(self.connection_options.container_id.clone());
526        args.push(inner_program.to_string());
527
528        for arg in program_args {
529            args.push(arg.as_ref().to_owned());
530        }
531        self.run_docker_command("exec", args.as_ref()).await
532    }
533
534    async fn download_binary_on_server(
535        &self,
536        url: &str,
537        tmp_path_gz: &RelPath,
538        remote_dir_for_server: &str,
539        delegate: &Arc<dyn RemoteClientDelegate>,
540        cx: &mut AsyncApp,
541    ) -> Result<()> {
542        if let Some(parent) = tmp_path_gz.parent() {
543            self.run_docker_exec(
544                "mkdir",
545                Some(remote_dir_for_server),
546                &Default::default(),
547                &["-p", parent.display(self.path_style()).as_ref()],
548            )
549            .await?;
550        }
551
552        delegate.set_status(Some("Downloading remote development server on host"), cx);
553
554        match self
555            .run_docker_exec(
556                "curl",
557                Some(remote_dir_for_server),
558                &Default::default(),
559                &[
560                    "-f",
561                    "-L",
562                    url,
563                    "-o",
564                    &tmp_path_gz.display(self.path_style()),
565                ],
566            )
567            .await
568        {
569            Ok(_) => {}
570            Err(e) => {
571                if self
572                    .run_docker_exec("which", None, &Default::default(), &["curl"])
573                    .await
574                    .is_ok()
575                {
576                    return Err(e);
577                }
578
579                log::info!("curl is not available, trying wget");
580                match self
581                    .run_docker_exec(
582                        "wget",
583                        Some(remote_dir_for_server),
584                        &Default::default(),
585                        &[url, "-O", &tmp_path_gz.display(self.path_style())],
586                    )
587                    .await
588                {
589                    Ok(_) => {}
590                    Err(e) => {
591                        if self
592                            .run_docker_exec("which", None, &Default::default(), &["wget"])
593                            .await
594                            .is_ok()
595                        {
596                            return Err(e);
597                        } else {
598                            anyhow::bail!("Neither curl nor wget is available");
599                        }
600                    }
601                }
602            }
603        }
604        Ok(())
605    }
606
607    fn kill_inner(&self) -> Result<()> {
608        if let Some(pid) = self.proxy_process.lock().take() {
609            if let Ok(_) = util::command::new_command("kill")
610                .arg(pid.to_string())
611                .spawn()
612            {
613                Ok(())
614            } else {
615                Err(anyhow::anyhow!("Failed to kill process"))
616            }
617        } else {
618            Ok(())
619        }
620    }
621}
622
623#[async_trait(?Send)]
624impl RemoteConnection for DockerExecConnection {
625    fn has_wsl_interop(&self) -> bool {
626        false
627    }
628    fn start_proxy(
629        &self,
630        unique_identifier: String,
631        reconnect: bool,
632        incoming_tx: UnboundedSender<Envelope>,
633        outgoing_rx: UnboundedReceiver<Envelope>,
634        connection_activity_tx: Sender<()>,
635        delegate: Arc<dyn RemoteClientDelegate>,
636        cx: &mut AsyncApp,
637    ) -> Task<Result<i32>> {
638        // We'll try connecting anew every time we open a devcontainer, so proactively try to kill any old connections.
639        if !self.has_been_killed() {
640            if let Err(e) = self.kill_inner() {
641                return Task::ready(Err(e));
642            };
643        }
644
645        delegate.set_status(Some("Starting proxy"), cx);
646
647        let Some(remote_binary_relpath) = self.remote_binary_relpath.clone() else {
648            return Task::ready(Err(anyhow!("Remote binary path not set")));
649        };
650
651        let mut docker_args = vec!["exec".to_string()];
652
653        for (k, v) in self.connection_options.remote_env.iter() {
654            docker_args.push("-e".to_string());
655            docker_args.push(format!("{k}={v}"));
656        }
657        for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
658            if let Some(value) = std::env::var(env_var).ok() {
659                docker_args.push("-e".to_string());
660                docker_args.push(format!("{env_var}={value}"));
661            }
662        }
663
664        docker_args.extend([
665            "-u".to_string(),
666            self.connection_options.remote_user.to_string(),
667            "-w".to_string(),
668            self.remote_dir_for_server.clone(),
669            "-i".to_string(),
670            self.connection_options.container_id.to_string(),
671        ]);
672
673        let val = remote_binary_relpath
674            .display(self.path_style())
675            .into_owned();
676        docker_args.push(val);
677        docker_args.push("proxy".to_string());
678        docker_args.push("--identifier".to_string());
679        docker_args.push(unique_identifier);
680        if reconnect {
681            docker_args.push("--reconnect".to_string());
682        }
683        let mut command = util::command::new_command(self.docker_cli());
684        command
685            .kill_on_drop(true)
686            .stdin(Stdio::piped())
687            .stdout(Stdio::piped())
688            .stderr(Stdio::piped())
689            .args(docker_args);
690
691        let Ok(child) = command.spawn() else {
692            return Task::ready(Err(anyhow::anyhow!(
693                "Failed to start remote server process"
694            )));
695        };
696
697        let mut proxy_process = self.proxy_process.lock();
698        *proxy_process = Some(child.id());
699
700        cx.spawn(async move |cx| {
701            super::handle_rpc_messages_over_child_process_stdio(
702                child,
703                incoming_tx,
704                outgoing_rx,
705                connection_activity_tx,
706                cx,
707            )
708            .await
709            .and_then(|status| {
710                if status != 0 {
711                    anyhow::bail!("Remote server exited with status {status}");
712                }
713                Ok(0)
714            })
715        })
716    }
717
718    fn upload_directory(
719        &self,
720        src_path: PathBuf,
721        dest_path: RemotePathBuf,
722        cx: &App,
723    ) -> Task<Result<()>> {
724        let dest_path_str = dest_path.to_string();
725        let src_path_display = src_path.display().to_string();
726
727        let upload_task = Self::upload_and_chown(
728            self.docker_cli().to_string(),
729            self.connection_options.clone(),
730            src_path_display,
731            dest_path_str,
732        );
733
734        cx.background_spawn(upload_task)
735    }
736
737    async fn kill(&self) -> Result<()> {
738        self.kill_inner()
739    }
740
741    fn has_been_killed(&self) -> bool {
742        self.proxy_process.lock().is_none()
743    }
744
745    fn build_command(
746        &self,
747        program: Option<String>,
748        args: &[String],
749        env: &HashMap<String, String>,
750        working_dir: Option<String>,
751        _port_forward: Option<(u16, String, u16)>,
752        interactive: Interactive,
753    ) -> Result<CommandTemplate> {
754        let mut parsed_working_dir = None;
755
756        let path_style = self.path_style();
757
758        if let Some(working_dir) = working_dir {
759            let working_dir = RemotePathBuf::new(working_dir, path_style).to_string();
760
761            const TILDE_PREFIX: &'static str = "~/";
762            if working_dir.starts_with(TILDE_PREFIX) {
763                let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/");
764                parsed_working_dir = Some(format!("$HOME/{working_dir}"));
765            } else {
766                parsed_working_dir = Some(working_dir);
767            }
768        }
769
770        let mut inner_program = Vec::new();
771
772        if let Some(program) = program {
773            inner_program.push(program);
774            for arg in args {
775                inner_program.push(arg.clone());
776            }
777        } else {
778            inner_program.push(self.shell());
779            inner_program.push("-l".to_string());
780        };
781
782        let mut docker_args = vec![
783            "exec".to_string(),
784            "-u".to_string(),
785            self.connection_options.remote_user.clone(),
786        ];
787
788        if let Some(parsed_working_dir) = parsed_working_dir {
789            docker_args.push("-w".to_string());
790            docker_args.push(parsed_working_dir);
791        }
792
793        for (k, v) in self.connection_options.remote_env.iter() {
794            docker_args.push("-e".to_string());
795            docker_args.push(format!("{k}={v}"));
796        }
797
798        for (k, v) in env.iter() {
799            docker_args.push("-e".to_string());
800            docker_args.push(format!("{k}={v}"));
801        }
802
803        match interactive {
804            Interactive::Yes => docker_args.push("-it".to_string()),
805            Interactive::No => docker_args.push("-i".to_string()),
806        }
807        docker_args.push(self.connection_options.container_id.to_string());
808
809        docker_args.append(&mut inner_program);
810
811        Ok(CommandTemplate {
812            program: self.docker_cli().to_string(),
813            args: docker_args,
814            // Docker-exec pipes in environment via the "-e" argument
815            env: Default::default(),
816        })
817    }
818
819    fn build_forward_ports_command(
820        &self,
821        _forwards: Vec<(u16, String, u16)>,
822    ) -> Result<CommandTemplate> {
823        Err(anyhow::anyhow!("Not currently supported for docker_exec"))
824    }
825
826    fn connection_options(&self) -> RemoteConnectionOptions {
827        RemoteConnectionOptions::Docker(self.connection_options.clone())
828    }
829
830    fn path_style(&self) -> PathStyle {
831        self.path_style.unwrap_or(PathStyle::Posix)
832    }
833
834    fn shell(&self) -> String {
835        self.shell.clone()
836    }
837
838    fn default_system_shell(&self) -> String {
839        String::from("/bin/sh")
840    }
841}