Skip to main content

start_command/
docker_command_handoff.rs

1//! Copy-free command handoff for newly launched detached Docker containers.
2use 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}