player_platform_process/
unix_process.rs1use std::io;
2use std::os::unix::process::CommandExt;
3use std::process::Command;
4
5use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction};
6
7pub fn configure_background_process_group(command: &mut Command) {
15 command.process_group(0);
16
17 unsafe {
21 command.pre_exec(|| {
22 let action = SigAction::new(SigHandler::SigIgn, SaFlags::empty(), SigSet::empty());
23 sigaction(Signal::SIGTTOU, &action)
26 .map(|_| ())
27 .map_err(|error| io::Error::from_raw_os_error(error as i32))
28 });
29 }
30}
31
32#[cfg(test)]
33mod tests {
34 use super::configure_background_process_group;
35 use std::ffi::CString;
36 use std::fs::File;
37 use std::io::Read;
38 use std::os::unix::ffi::OsStrExt;
39 use std::process::{Command, Stdio};
40 use std::time::{Duration, Instant};
41
42 use nix::libc;
43 use nix::pty::{ForkptyResult, forkpty};
44 use nix::sys::signal::{Signal, killpg};
45 use nix::sys::termios::{LocalFlags, SetArg, tcgetattr, tcsetattr};
46 use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid};
47
48 const PTY_CONTROLLER_ENV: &str = "VESPER_PROCESS_TEST_PTY_CONTROLLER";
49
50 #[cfg(unix)]
51 #[test]
52 fn child_ignores_sigttou_before_exec() {
53 let mut command = Command::new("sh");
54 command.args(["-c", "kill -s TTOU $$"]);
55 configure_background_process_group(&mut command);
56
57 let mut child = command.spawn().expect("run SIGTTOU fixture");
58 let status = wait_with_deadline(&mut child, Duration::from_secs(2))
59 .unwrap_or_else(|| terminate_test_group(&mut child));
60
61 assert!(status.success(), "child was stopped by SIGTTOU: {status}");
62 }
63
64 #[test]
65 fn background_process_group_can_write_to_a_tostop_controlling_pty() {
66 if std::env::var_os(PTY_CONTROLLER_ENV).is_some() {
67 run_pty_controller();
68 return;
69 }
70
71 let env_program = CString::new("/usr/bin/env").expect("static env path has no NUL");
72 let controller_env = CString::new(format!("{PTY_CONTROLLER_ENV}=1"))
73 .expect("controller environment has no NUL");
74 let executable_path = std::env::current_exe().expect("locate test executable");
75 let executable = CString::new(executable_path.as_os_str().as_bytes())
76 .expect("test executable path has no NUL");
77 let exact = CString::new("--exact").expect("static argument has no NUL");
78 let test_name = CString::new(
79 "unix_process::tests::background_process_group_can_write_to_a_tostop_controlling_pty",
80 )
81 .expect("static test name has no NUL");
82 let nocapture = CString::new("--nocapture").expect("static argument has no NUL");
83 let argv = [
84 env_program.as_ptr(),
85 controller_env.as_ptr(),
86 executable.as_ptr(),
87 exact.as_ptr(),
88 test_name.as_ptr(),
89 nocapture.as_ptr(),
90 std::ptr::null(),
91 ];
92
93 let fork = unsafe { forkpty(None, None) }.expect("fork PTY controller");
98 let (controller, master) = match fork {
99 ForkptyResult::Child => {
100 unsafe {
105 libc::execv(env_program.as_ptr(), argv.as_ptr());
106 libc::_exit(127);
107 }
108 }
109 ForkptyResult::Parent { child, master } => (child, master),
110 };
111 let output_reader = std::thread::spawn(move || {
112 let master = File::from(master);
113 let mut output = Vec::new();
114 let result = master.take(64 * 1024).read_to_end(&mut output);
115 if let Err(error) = result
116 && error.raw_os_error() != Some(libc::EIO)
117 {
118 panic!("read PTY controller output: {error}");
119 }
120 output
121 });
122 let status = wait_for_pty_controller(controller, Duration::from_secs(5));
123 let controller_output = output_reader.join().expect("join PTY output reader");
124 assert!(
125 matches!(status, WaitStatus::Exited(_, 0)),
126 "PTY controller failed with {status:?}: {}",
127 String::from_utf8_lossy(&controller_output)
128 );
129 }
130
131 fn run_pty_controller() {
132 let stdin = std::io::stdin();
133 let mut termios = tcgetattr(&stdin).expect("read PTY terminal settings");
134 termios.local_flags.insert(LocalFlags::TOSTOP);
135 tcsetattr(&stdin, SetArg::TCSANOW, &termios).expect("enable TOSTOP on controlling PTY");
136
137 let mut command = Command::new("sh");
138 command
139 .args(["-c", "printf 'background diagnostic\\n' >&2"])
140 .stdin(Stdio::null())
141 .stdout(Stdio::null())
142 .stderr(Stdio::inherit());
143 configure_background_process_group(&mut command);
144
145 let mut child = command.spawn().expect("start background PTY writer");
146 let status = wait_with_deadline(&mut child, Duration::from_secs(2))
147 .unwrap_or_else(|| terminate_test_group(&mut child));
148 assert!(
149 status.success(),
150 "background PTY writer did not exit successfully: {status}"
151 );
152 }
153
154 fn wait_for_pty_controller(controller: nix::unistd::Pid, timeout: Duration) -> WaitStatus {
155 let deadline = Instant::now() + timeout;
156 loop {
157 match waitpid(
158 controller,
159 Some(WaitPidFlag::WNOHANG | WaitPidFlag::WUNTRACED),
160 )
161 .expect("poll PTY controller")
162 {
163 WaitStatus::StillAlive if Instant::now() < deadline => {
164 std::thread::sleep(Duration::from_millis(10));
165 }
166 WaitStatus::StillAlive => return terminate_pty_controller(controller),
167 status @ (WaitStatus::Exited(_, _)
168 | WaitStatus::Signaled(_, _, _)
169 | WaitStatus::Stopped(_, _)) => return status,
170 _ if Instant::now() < deadline => {
171 std::thread::sleep(Duration::from_millis(10));
172 }
173 _ => return terminate_pty_controller(controller),
174 }
175 }
176 }
177
178 fn terminate_pty_controller(controller: nix::unistd::Pid) -> WaitStatus {
179 let _ = killpg(controller, Signal::SIGCONT);
180 let _ = killpg(controller, Signal::SIGKILL);
181 waitpid(controller, None).expect("reap PTY controller")
182 }
183
184 fn wait_with_deadline(
185 child: &mut std::process::Child,
186 timeout: Duration,
187 ) -> Option<std::process::ExitStatus> {
188 let deadline = Instant::now() + timeout;
189 while Instant::now() < deadline {
190 match child.try_wait() {
191 Ok(Some(status)) => return Some(status),
192 Ok(None) => std::thread::sleep(Duration::from_millis(10)),
193 Err(error) => panic!("failed to poll process test child: {error}"),
194 }
195 }
196 None
197 }
198
199 fn terminate_test_group(child: &mut std::process::Child) -> std::process::ExitStatus {
200 use nix::sys::signal::{Signal, killpg};
201 use nix::unistd::Pid;
202
203 let process_group = i32::try_from(child.id()).expect("test child pid fits process group");
204 let _ = killpg(Pid::from_raw(process_group), Signal::SIGCONT);
205 let _ = killpg(Pid::from_raw(process_group), Signal::SIGKILL);
206 let _ = child.kill();
207 child.wait().expect("reap timed-out process test child")
208 }
209}