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