running_process_platform_internal/platform_linux/
foreground.rs1use std::io;
4use std::process::Command;
5
6pub 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 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 let descriptor = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_DUPFD, 64) };
149 assert!(descriptor >= 64);
150 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}