1use 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.env\" ]; then . \"$p.env\"; fi; if [ -f \"$p\" ]; then exec sh \"$p\"; fi; exec \"$@\"".into(),
28 "start-command-handoff".into(),
29 marker_path.into(),
30 ];
31 args.extend_from_slice(main_args);
32 args
33}
34
35pub fn build_command_handoff_script(command: &str, shell: &str, keep_alive: bool) -> String {
36 use crate::isolation::isolation_shell::{
37 docker_shell_args, is_interactive_shell_command, is_shell_invocation_with_args,
38 };
39 if !keep_alive
40 && (is_interactive_shell_command(command) || is_shell_invocation_with_args(command))
41 {
42 return format!(
43 "exec {}\n",
44 docker_shell_args(command, "sh", false)
45 .iter()
46 .map(|arg| shell_quote(arg))
47 .collect::<Vec<_>>()
48 .join(" ")
49 );
50 }
51 if shell == "auto" || shell.is_empty() {
52 let effective = if keep_alive {
53 format!("{}; exec \"$__START_COMMAND_HANDOFF_SHELL\"", command)
54 } else {
55 command.into()
56 };
57 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));
58 }
59 let interactive = if matches!(shell.rsplit('/').next(), Some("bash" | "zsh")) {
60 " -i"
61 } else {
62 ""
63 };
64 let effective = if keep_alive {
65 format!("{}; exec {}", command, shell_quote(shell))
66 } else {
67 command.into()
68 };
69 format!(
70 "exec {}{} -c {}\n",
71 shell_quote(shell),
72 interactive,
73 shell_quote(&effective)
74 )
75}
76
77fn copy_into_container<R: CommandRunner + ?Sized>(
78 name: &str,
79 target: &str,
80 content: &str,
81 runner: &R,
82) -> Result<CommandRunOutput, String> {
83 let dir = std::env::temp_dir().join(format!("start-handoff-{}", uuid::Uuid::new_v4()));
84 fs::create_dir(&dir).map_err(|e| e.to_string())?;
85 let file = dir.join("command");
86 let result = (|| {
87 fs::write(&file, content).map_err(|e| e.to_string())?;
88 #[cfg(unix)]
89 {
90 use std::os::unix::fs::PermissionsExt;
91 fs::set_permissions(&file, fs::Permissions::from_mode(0o644))
92 .map_err(|e| e.to_string())?;
93 }
94 Ok(runner.run(
95 &docker_command().to_string_lossy(),
96 &[
97 "cp".into(),
98 file.to_string_lossy().into(),
99 format!("{}:{}", name, target),
100 ],
101 ))
102 })();
103 let _ = fs::remove_dir_all(&dir);
104 result
105}
106
107pub fn write_command_handoff<R: CommandRunner + ?Sized>(
108 name: &str,
109 command: &str,
110 shell: &str,
111 keep_alive: bool,
112 runner: &R,
113) -> Result<CommandRunOutput, String> {
114 copy_into_container(
115 name,
116 &command_handoff_path(name),
117 &build_command_handoff_script(command, shell, keep_alive),
118 runner,
119 )
120}
121
122pub fn write_execution_env_handoff<R: CommandRunner + ?Sized>(
125 name: &str,
126 env: &crate::execution_env::ExecutionEnv,
127 runner: &R,
128) -> Result<CommandRunOutput, String> {
129 let statement = crate::execution_env::build_execution_env_export(env);
130 let content = if statement.is_empty() {
131 statement
132 } else {
133 format!("{}\n", statement)
134 };
135 copy_into_container(
136 name,
137 &format!("{}.env", command_handoff_path(name)),
138 &content,
139 runner,
140 )
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use std::process::Command;
147
148 #[test]
149 fn selector_preserves_argv_and_replaces_only_the_command() {
150 let dir = tempfile::tempdir().unwrap();
151 let marker = dir.path().join("command");
152 let args = build_command_handoff_args(
153 &["sh".into(), "-c".into(), "printf original".into()],
154 "command",
155 );
156 let run = || {
157 Command::new(&args[0])
158 .args(&args[1..])
159 .current_dir(dir.path())
160 .output()
161 .unwrap()
162 .stdout
163 };
164 assert_eq!(run(), b"original");
165 fs::write(&marker, "exec sh -c 'printf replacement'\n").unwrap();
166 assert_eq!(run(), b"replacement");
167 assert_eq!(run(), b"replacement");
168 }
169
170 #[test]
171 fn handoff_preserves_direct_shell_argv_and_compound_status() {
172 for (command, expected) in [
173 (
174 "sh -c 'printf \"%s:%s\" \"$0\" \"$1\"' tag 'two words'",
175 "tag:two words",
176 ),
177 ("sh -c 'exit 7'; printf 'after%s' \"$?\"", "after7"),
178 ] {
179 let result = Command::new("sh")
180 .args(["-c", &build_command_handoff_script(command, "sh", false)])
181 .output()
182 .unwrap();
183 assert!(
184 result.status.success(),
185 "{}",
186 String::from_utf8_lossy(&result.stderr)
187 );
188 assert_eq!(String::from_utf8_lossy(&result.stdout), expected);
189 }
190 }
191
192 #[test]
193 fn auto_handoff_chooses_an_installed_shell_inside_the_container() {
194 let script = build_command_handoff_script("printf auto", "auto", false);
195 assert!(script.contains("command -v bash || command -v zsh || command -v sh"));
196 let result = Command::new("sh").args(["-c", &script]).output().unwrap();
197 assert!(
198 result.status.success(),
199 "{}",
200 String::from_utf8_lossy(&result.stderr)
201 );
202 assert_eq!(result.stdout, b"auto");
203 }
204}