start_command/
docker_command_handoff.rs1use crate::docker_cleanup::docker_command;
3use crate::execution_control::{CommandRunOutput, CommandRunner};
4use crate::isolation::isolation_log::shell_quote;
5use std::fs;
6
7pub fn command_handoff_path(session_name: &str) -> String {
8 let safe: String = session_name
9 .chars()
10 .map(|c| {
11 if c.is_ascii_alphanumeric() || "_.-".contains(c) {
12 c
13 } else {
14 '_'
15 }
16 })
17 .collect();
18 format!("/.start-command-resume-{}", safe)
19}
20
21pub fn build_command_handoff_args(main_args: &[String], marker_path: &str) -> Vec<String> {
22 let mut args = vec![
23 "sh".into(),
24 "-c".into(),
25 "p=$1; shift; if [ -f \"$p\" ]; then exec sh \"$p\"; fi; exec \"$@\"".into(),
26 "start-command-handoff".into(),
27 marker_path.into(),
28 ];
29 args.extend_from_slice(main_args);
30 args
31}
32
33pub fn build_command_handoff_script(command: &str, shell: &str, keep_alive: bool) -> String {
34 use crate::isolation::isolation_shell::{
35 docker_shell_args, is_interactive_shell_command, is_shell_invocation_with_args,
36 };
37 if !keep_alive
38 && (is_interactive_shell_command(command) || is_shell_invocation_with_args(command))
39 {
40 return format!(
41 "exec {}\n",
42 docker_shell_args(command, "sh", false)
43 .iter()
44 .map(|arg| shell_quote(arg))
45 .collect::<Vec<_>>()
46 .join(" ")
47 );
48 }
49 if shell == "auto" || shell.is_empty() {
50 let effective = if keep_alive {
51 format!("{}; exec \"$__START_COMMAND_HANDOFF_SHELL\"", command)
52 } else {
53 command.into()
54 };
55 return format!("__START_COMMAND_HANDOFF_SHELL=$(command -v bash || command -v zsh || command -v sh); export __START_COMMAND_HANDOFF_SHELL\ncase \"$__START_COMMAND_HANDOFF_SHELL\" in */bash|bash|*/zsh|zsh) exec \"$__START_COMMAND_HANDOFF_SHELL\" -i -c {};; *) exec \"$__START_COMMAND_HANDOFF_SHELL\" -c {};; esac\n", shell_quote(&effective), shell_quote(&effective));
56 }
57 let interactive = if matches!(shell.rsplit('/').next(), Some("bash" | "zsh")) {
58 " -i"
59 } else {
60 ""
61 };
62 let effective = if keep_alive {
63 format!("{}; exec {}", command, shell_quote(shell))
64 } else {
65 command.into()
66 };
67 format!(
68 "exec {}{} -c {}\n",
69 shell_quote(shell),
70 interactive,
71 shell_quote(&effective)
72 )
73}
74
75pub fn write_command_handoff<R: CommandRunner + ?Sized>(
76 name: &str,
77 command: &str,
78 shell: &str,
79 keep_alive: bool,
80 runner: &R,
81) -> Result<CommandRunOutput, String> {
82 let dir = std::env::temp_dir().join(format!("start-handoff-{}", uuid::Uuid::new_v4()));
83 fs::create_dir(&dir).map_err(|e| e.to_string())?;
84 let file = dir.join("command");
85 let result = (|| {
86 fs::write(
87 &file,
88 build_command_handoff_script(command, shell, keep_alive),
89 )
90 .map_err(|e| e.to_string())?;
91 #[cfg(unix)]
92 {
93 use std::os::unix::fs::PermissionsExt;
94 fs::set_permissions(&file, fs::Permissions::from_mode(0o644))
95 .map_err(|e| e.to_string())?;
96 }
97 Ok(runner.run(
98 &docker_command().to_string_lossy(),
99 &[
100 "cp".into(),
101 file.to_string_lossy().into(),
102 format!("{}:{}", name, command_handoff_path(name)),
103 ],
104 ))
105 })();
106 let _ = fs::remove_dir_all(&dir);
107 result
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use std::process::Command;
114
115 #[test]
116 fn selector_preserves_argv_and_replaces_only_the_command() {
117 let dir = tempfile::tempdir().unwrap();
118 let marker = dir.path().join("command");
119 let args = build_command_handoff_args(
120 &["sh".into(), "-c".into(), "printf original".into()],
121 "command",
122 );
123 let run = || {
124 Command::new(&args[0])
125 .args(&args[1..])
126 .current_dir(dir.path())
127 .output()
128 .unwrap()
129 .stdout
130 };
131 assert_eq!(run(), b"original");
132 fs::write(&marker, "exec sh -c 'printf replacement'\n").unwrap();
133 assert_eq!(run(), b"replacement");
134 assert_eq!(run(), b"replacement");
135 }
136
137 #[test]
138 fn handoff_preserves_direct_shell_argv_and_compound_status() {
139 for (command, expected) in [
140 (
141 "sh -c 'printf \"%s:%s\" \"$0\" \"$1\"' tag 'two words'",
142 "tag:two words",
143 ),
144 ("sh -c 'exit 7'; printf 'after%s' \"$?\"", "after7"),
145 ] {
146 let result = Command::new("sh")
147 .args(["-c", &build_command_handoff_script(command, "sh", false)])
148 .output()
149 .unwrap();
150 assert!(
151 result.status.success(),
152 "{}",
153 String::from_utf8_lossy(&result.stderr)
154 );
155 assert_eq!(String::from_utf8_lossy(&result.stdout), expected);
156 }
157 }
158
159 #[test]
160 fn auto_handoff_chooses_an_installed_shell_inside_the_container() {
161 let script = build_command_handoff_script("printf auto", "auto", false);
162 assert!(script.contains("command -v bash || command -v zsh || command -v sh"));
163 let result = Command::new("sh").args(["-c", &script]).output().unwrap();
164 assert!(
165 result.status.success(),
166 "{}",
167 String::from_utf8_lossy(&result.stderr)
168 );
169 assert_eq!(result.stdout, b"auto");
170 }
171}