1use std::path::Path;
13use std::process::{Command, Stdio};
14use std::time::Duration;
15
16use crate::engine::{capture, Captured, EngineRef};
17use crate::ContainerError;
18use strop_core::worker::CancelToken;
19use strop_workspace::ContainerId;
20
21pub fn exec_command(id: &ContainerId, program: &str, args: &[String], cwd: &Path) -> Command {
28 let mut command = Command::new("docker");
29 command
30 .arg("exec")
31 .arg("-i")
32 .arg("--workdir")
33 .arg(cwd)
34 .arg(id.as_str())
35 .arg(program)
36 .args(args)
37 .stdin(Stdio::piped())
38 .stdout(Stdio::piped())
39 .stderr(Stdio::piped());
40 command
41}
42
43const EXEC_DEADLINE: Duration = Duration::from_secs(30);
45
46pub fn exec_capture(
50 engine: &EngineRef,
51 id: &ContainerId,
52 program: &str,
53 args: &[String],
54 cwd: &Path,
55 stdout_limit: u64,
56 token: &CancelToken,
57) -> Result<Captured, ContainerError> {
58 let _ = engine;
59 let mut argv: Vec<&str> = vec!["exec", "--workdir"];
60 let Some(cwd_text) = cwd.to_str() else {
61 return Err(ContainerError::CapabilityRefused {
62 what: "exec: the working directory is not UTF-8".into(),
63 });
64 };
65 argv.push(cwd_text);
66 argv.push(id.as_str());
67 argv.push(program);
68 argv.extend(args.iter().map(String::as_str));
69 capture(&argv, stdout_limit, EXEC_DEADLINE, token)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 use std::path::PathBuf;
77
78 fn reference() -> ContainerId {
79 ContainerId::canonical("a".repeat(64)).unwrap()
80 }
81
82 #[test]
83 fn exec_command_is_argv_only_with_piped_stdio() {
84 let command = exec_command(
85 &reference(),
86 "pyright-langserver",
87 &["--stdio".into()],
88 &PathBuf::from("/src"),
89 );
90 let text = format!("{command:?}");
91 assert!(text.contains("exec"), "{text}");
92 assert!(text.contains("--workdir"), "{text}");
93 assert!(text.contains("/src"), "{text}");
94 assert!(text.contains(&"a".repeat(64)), "{text}");
95 assert!(text.contains("pyright-langserver"), "{text}");
96 assert!(!text.contains("sh -c"), "no shell, ever: {text}");
97 }
98}