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