running_process_platform_internal/
foreground.rs1use std::io;
9use std::process::{Child, Command, ExitStatus, Output};
10
11pub fn spawn(command: &mut Command) -> io::Result<Child> {
16 command.spawn()
17}
18
19pub fn status(command: &mut Command) -> io::Result<ExitStatus> {
25 command.status()
26}
27
28pub fn output(command: &mut Command) -> io::Result<Output> {
33 command.output()
34}
35
36#[cfg(unix)]
42pub fn exec(command: &mut Command) -> io::Error {
43 use std::os::unix::process::CommandExt;
44 command.exec()
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50
51 #[test]
52 fn spawn_preserves_native_missing_program_error() {
53 let directory = tempfile::tempdir().expect("private fixture directory");
54 let mut command = Command::new(directory.path().join("absent-executable"));
55 let error = spawn(&mut command).expect_err("no child should be created");
56 assert_eq!(error.kind(), io::ErrorKind::NotFound);
57 }
58
59 #[cfg(unix)]
60 #[test]
61 fn spawn_transfers_child_and_preserves_configured_output() {
62 use std::io::{Read, Seek};
63 use std::process::Stdio;
64 use std::time::{Duration, Instant};
65 let mut destination = tempfile::tempfile().expect("output file");
66 let mut command = Command::new("/bin/sh");
67 command
68 .args(["-c", "printf '%s' \"$MARKER\"; exit 7"])
69 .env("MARKER", "caller-configured-output")
70 .stdin(Stdio::null())
71 .stderr(Stdio::null())
72 .stdout(Stdio::from(destination.try_clone().unwrap()));
73 let mut child = spawn(&mut command).expect("foreground spawn");
74 assert!(
75 child.stdout.is_none(),
76 "file binding must not become a capture pipe"
77 );
78 let deadline = Instant::now() + Duration::from_secs(5);
79 let status = loop {
80 match child.try_wait() {
81 Ok(Some(status)) => break status,
82 Ok(None) if Instant::now() < deadline => {
83 std::thread::sleep(Duration::from_millis(10));
84 }
85 outcome => {
86 let _ = child.kill();
87 let _ = child.wait();
88 panic!("foreground fixture did not complete: {outcome:?}");
89 }
90 }
91 };
92 assert_eq!(status.code(), Some(7));
93 destination.rewind().unwrap();
94 let mut bytes = Vec::new();
95 destination.read_to_end(&mut bytes).unwrap();
96 assert_eq!(bytes, b"caller-configured-output");
97 }
98
99 #[cfg(unix)]
100 #[test]
101 fn exec_returns_native_missing_image_error() {
102 let directory = tempfile::tempdir().expect("private fixture directory");
103 let mut command = Command::new(directory.path().join("absent-executable"));
104 let error = exec(&mut command);
107 assert_eq!(error.kind(), io::ErrorKind::NotFound);
108 assert_eq!(error.raw_os_error(), Some(libc::ENOENT));
109 }
110
111 #[cfg(unix)]
112 #[test]
113 fn output_preserves_cwd_environment_literal_argv_and_native_nonzero() {
114 let directory = tempfile::tempdir().expect("fixture cwd");
115 let cwd = directory.path().canonicalize().expect("physical cwd");
116 let mut command = Command::new("/bin/sh");
117 command.args(["-c", "[ \"$#\" = 2 ] && [ -z \"$2\" ] || exit 9; printf '%s\\n%s\\n%s\\n' \"$PWD\" \"$MARKER\" \"$1\"; exit 7", "foreground", "$literal; not a command", ""]);
118 command.current_dir(&cwd).env("MARKER", "foreground-env");
119 let output = output(&mut command).expect("foreground output");
120 assert_eq!(output.status.code(), Some(7));
121 let expected = format!(
122 "{}\nforeground-env\n$literal; not a command\n",
123 cwd.display()
124 );
125 assert_eq!(output.stdout, expected.as_bytes());
126 assert!(output.stderr.is_empty());
127 }
128
129 #[cfg(windows)]
130 #[test]
131 fn output_preserves_windows_environment_cwd_and_nonzero_status() {
132 let directory = tempfile::tempdir().expect("fixture cwd");
133 let mut command = Command::new("cmd.exe");
134 command.args(["/D", "/C", "cd & echo %MARKER% & exit /b 7"]);
136 command
137 .current_dir(directory.path())
138 .env("MARKER", "foreground-env");
139 let captured = output(&mut command).expect("foreground output");
140 assert_eq!(captured.status.code(), Some(7));
141 let text = String::from_utf8_lossy(&captured.stdout);
142 let mut lines = text.lines();
143 let actual = std::path::Path::new(lines.next().expect("cwd line"));
144 assert_eq!(
145 actual.canonicalize().unwrap(),
146 directory.path().canonicalize().unwrap()
147 );
148 assert_eq!(
149 lines.next().expect("environment line").trim(),
150 "foreground-env"
151 );
152 }
153
154 #[cfg(target_os = "linux")]
155 #[test]
156 fn foreground_keeps_parent_process_group_and_session() {
157 let parent = std::fs::read_to_string("/proc/self/stat").expect("parent stat");
158 let mut command = Command::new("/bin/sh");
159 command.args(["-c", "cut -d' ' -f5,6 /proc/self/stat"]);
160 let output = output(&mut command).expect("foreground child");
161 let parent_fields: Vec<_> = parent
162 .rsplit_once(") ")
163 .expect("stat")
164 .1
165 .split_whitespace()
166 .collect();
167 let child = String::from_utf8_lossy(&output.stdout);
168 assert_eq!(
169 child.trim(),
170 format!("{} {}", parent_fields[2], parent_fields[3])
171 );
172 }
173
174 #[cfg(unix)]
175 #[test]
176 fn both_execution_forms_preserve_explicit_stdio_overrides() {
177 use std::io::{Read, Seek, Write};
178 use std::process::Stdio;
179 for capture in [false, true] {
180 let mut input = tempfile::tempfile().unwrap();
181 input.write_all(b"caller-controlled input").unwrap();
182 input.rewind().unwrap();
183 let mut destination = tempfile::tempfile().unwrap();
184 let mut command = Command::new("/bin/sh");
185 command
186 .args(["-c", "cat; exit 7"])
187 .stdin(Stdio::from(input))
188 .stdout(Stdio::from(destination.try_clone().unwrap()))
189 .stderr(Stdio::null());
190 if capture {
191 let captured = output(&mut command).unwrap();
192 assert_eq!(captured.status.code(), Some(7));
193 assert!(captured.stdout.is_empty());
194 assert!(captured.stderr.is_empty());
195 } else {
196 assert_eq!(status(&mut command).unwrap().code(), Some(7));
197 }
198 destination.rewind().unwrap();
199 let mut bytes = Vec::new();
200 destination.read_to_end(&mut bytes).unwrap();
201 assert_eq!(bytes, b"caller-controlled input");
202 }
203 }
204
205 #[cfg(target_os = "linux")]
206 #[test]
207 fn foreground_does_not_close_an_inheritable_high_descriptor() {
208 use std::io::{Seek, Write};
209 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
210 let mut file = tempfile::tempfile().expect("fixture file");
211 file.write_all(b"inherited-resource").unwrap();
212 file.rewind().unwrap();
213 let descriptor = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_DUPFD, 64) };
216 assert!(descriptor >= 64);
217 let descriptor = unsafe { OwnedFd::from_raw_fd(descriptor) };
219 let mut command = Command::new("/bin/sh");
220 command.args([
221 "-c",
222 "cat \"/proc/self/fd/$1\"",
223 "foreground",
224 &descriptor.as_raw_fd().to_string(),
225 ]);
226 let captured = output(&mut command).expect("inherited descriptor read");
227 assert!(captured.status.success());
228 assert_eq!(captured.stdout, b"inherited-resource");
229 }
230}