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}