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