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}