Skip to main content

running_process_platform_internal/platform_linux/
foreground.rs

1//! Linux foreground execution: Unix process-image replacement.
2
3use std::io;
4use std::process::Command;
5
6/// Replace the current Unix process image using the supplied native command.
7/// Success never returns; failure returns the original OS error. This does not
8/// spawn a child or change the caller's requested argv, environment, stdio,
9/// pre-exec hooks, process group, or session policy. As with `CommandExt::exec`,
10/// failed execution may leave setup changes applied to the current process.
11pub fn exec(command: &mut Command) -> io::Error {
12    use std::os::unix::process::CommandExt;
13    command.exec()
14}
15
16#[cfg(test)]
17mod tests {
18    use super::exec;
19    use crate::foreground::{output, spawn, status};
20    use std::io;
21    use crate::foreground::tests::fixture_command;
22
23    #[test]
24    fn spawn_transfers_child_and_preserves_configured_output() {
25        use std::io::{Read, Seek};
26        use std::process::Stdio;
27        use std::time::{Duration, Instant};
28        let mut destination = tempfile::tempfile().expect("output file");
29        let mut command = fixture_command("/bin/sh");
30        command
31            .args(["-c", "printf '%s' \"$MARKER\"; exit 7"])
32            .env("MARKER", "caller-configured-output")
33            .stdin(Stdio::null())
34            .stderr(Stdio::null())
35            .stdout(Stdio::from(destination.try_clone().unwrap()));
36        let mut child = spawn(&mut command).expect("foreground spawn");
37        assert!(
38            child.stdout.is_none(),
39            "file binding must not become a capture pipe"
40        );
41        let deadline = Instant::now() + Duration::from_secs(5);
42        let status = loop {
43            match child.try_wait() {
44                Ok(Some(status)) => break status,
45                Ok(None) if Instant::now() < deadline => {
46                    std::thread::sleep(Duration::from_millis(10));
47                }
48                outcome => {
49                    let _ = child.kill();
50                    let _ = child.wait();
51                    panic!("foreground fixture did not complete: {outcome:?}");
52                }
53            }
54        };
55        assert_eq!(status.code(), Some(7));
56        destination.rewind().unwrap();
57        let mut bytes = Vec::new();
58        destination.read_to_end(&mut bytes).unwrap();
59        assert_eq!(bytes, b"caller-configured-output");
60    }
61
62    #[test]
63    fn exec_returns_native_missing_image_error() {
64        let directory = tempfile::tempdir().expect("private fixture directory");
65        let mut command = fixture_command(directory.path().join("absent-executable"));
66        // No stdio/cwd/env/hook changes: the failed exec leaves this test's
67        // process configuration untouched, and never runs another image.
68        let error = exec(&mut command);
69        assert_eq!(error.kind(), io::ErrorKind::NotFound);
70        assert_eq!(error.raw_os_error(), Some(libc::ENOENT));
71    }
72
73    #[test]
74    fn output_preserves_cwd_environment_literal_argv_and_native_nonzero() {
75        let directory = tempfile::tempdir().expect("fixture cwd");
76        let cwd = directory.path().canonicalize().expect("physical cwd");
77        let mut command = fixture_command("/bin/sh");
78        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", ""]);
79        command.current_dir(&cwd).env("MARKER", "foreground-env");
80        let output = output(&mut command).expect("foreground output");
81        assert_eq!(output.status.code(), Some(7));
82        let expected = format!(
83            "{}\nforeground-env\n$literal; not a command\n",
84            cwd.display()
85        );
86        assert_eq!(output.stdout, expected.as_bytes());
87        assert!(output.stderr.is_empty());
88    }
89
90    #[test]
91    fn both_execution_forms_preserve_explicit_stdio_overrides() {
92        use std::io::{Read, Seek, Write};
93        use std::process::Stdio;
94        for capture in [false, true] {
95            let mut input = tempfile::tempfile().unwrap();
96            input.write_all(b"caller-controlled input").unwrap();
97            input.rewind().unwrap();
98            let mut destination = tempfile::tempfile().unwrap();
99            let mut command = fixture_command("/bin/sh");
100            command
101                .args(["-c", "cat; exit 7"])
102                .stdin(Stdio::from(input))
103                .stdout(Stdio::from(destination.try_clone().unwrap()))
104                .stderr(Stdio::null());
105            if capture {
106                let captured = output(&mut command).unwrap();
107                assert_eq!(captured.status.code(), Some(7));
108                assert!(captured.stdout.is_empty());
109                assert!(captured.stderr.is_empty());
110            } else {
111                assert_eq!(status(&mut command).unwrap().code(), Some(7));
112            }
113            destination.rewind().unwrap();
114            let mut bytes = Vec::new();
115            destination.read_to_end(&mut bytes).unwrap();
116            assert_eq!(bytes, b"caller-controlled input");
117        }
118    }
119
120    #[test]
121    fn foreground_keeps_parent_process_group_and_session() {
122        let parent = std::fs::read_to_string("/proc/self/stat").expect("parent stat");
123        let mut command = fixture_command("/bin/sh");
124        command.args(["-c", "cut -d' ' -f5,6 /proc/self/stat"]);
125        let output = output(&mut command).expect("foreground child");
126        let parent_fields: Vec<_> = parent
127            .rsplit_once(") ")
128            .expect("stat")
129            .1
130            .split_whitespace()
131            .collect();
132        let child = String::from_utf8_lossy(&output.stdout);
133        assert_eq!(
134            child.trim(),
135            format!("{} {}", parent_fields[2], parent_fields[3])
136        );
137    }
138
139    #[test]
140    fn foreground_does_not_close_an_inheritable_high_descriptor() {
141        use std::io::{Seek, Write};
142        use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
143        let mut file = tempfile::tempfile().expect("fixture file");
144        file.write_all(b"inherited-resource").unwrap();
145        file.rewind().unwrap();
146        // SAFETY: F_DUPFD duplicates the live borrowed descriptor; it does
147        // not access userspace memory or change the source descriptor.
148        let descriptor = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_DUPFD, 64) };
149        assert!(descriptor >= 64);
150        // SAFETY: F_DUPFD returned a distinct owned descriptor.
151        let descriptor = unsafe { OwnedFd::from_raw_fd(descriptor) };
152        let mut command = fixture_command("/bin/sh");
153        command.args([
154            "-c",
155            "cat \"/proc/self/fd/$1\"",
156            "foreground",
157            &descriptor.as_raw_fd().to_string(),
158        ]);
159        let captured = output(&mut command).expect("inherited descriptor read");
160        assert!(captured.status.success());
161        assert_eq!(captured.stdout, b"inherited-resource");
162    }
163}