Skip to main content

running_process_platform_internal/
foreground.rs

1//! Caller-owned foreground command execution.
2//!
3//! Unlike contained, bounded, and daemon spawning, this module intentionally
4//! does not configure process groups, sessions, descriptor inheritance,
5//! consoles, owner-death policy, or environment. The supplied `Command` is
6//! the complete native contract.
7
8use std::io;
9use std::process::{Child, Command, ExitStatus, Output};
10
11/// Start a caller-configured command and transfer its native child ownership.
12/// No containment or drop cleanup is added: the caller remains responsible for
13/// consuming pipes, termination and reaping, exactly as with `Command::spawn`.
14/// Use a contained/session API when that ownership policy is desired instead.
15pub fn spawn(command: &mut Command) -> io::Result<Child> {
16    command.spawn()
17}
18
19/// Run and return the native exit status.
20///
21/// Standard streams retain exactly the caller's `Command` configuration; the
22/// std default is inherited streams, while explicit `stdin`/`stdout`/`stderr`
23/// overrides remain in force.
24pub fn status(command: &mut Command) -> io::Result<ExitStatus> {
25    command.status()
26}
27
28/// Run with std's concurrent captured-output behavior and native exit status.
29/// Its stdio behavior is exactly `Command::output`: unspecified stdout/stderr
30/// are captured while explicit stream overrides remain caller-controlled.
31/// Every other caller-owned command property remains unchanged.
32pub fn output(command: &mut Command) -> io::Result<Output> {
33    command.output()
34}
35
36/// Replace the current Unix process image using the supplied native command.
37/// Success never returns; failure returns the original OS error. This does not
38/// spawn a child or change the caller's requested argv, environment, stdio,
39/// pre-exec hooks, process group, or session policy. As with `CommandExt::exec`,
40/// failed execution may leave setup changes applied to the current process.
41#[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        // No stdio/cwd/env/hook changes: the failed exec leaves this test's
105        // process configuration untouched, and never runs another image.
106        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        // /D disables user AutoRun commands; CD emits the actual child cwd.
135        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        // SAFETY: F_DUPFD duplicates the live borrowed descriptor; it does
214        // not access userspace memory or change the source descriptor.
215        let descriptor = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_DUPFD, 64) };
216        assert!(descriptor >= 64);
217        // SAFETY: F_DUPFD returned a distinct owned descriptor.
218        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}