1mod capture;
4use crate::worker::{CancelToken, Failure, FailureKind};
5pub use capture::{
6 capture, capture_with, stream_with, CaptureError, CapturePolicy, CommandOutput, StdinPolicy,
7 StreamError, StreamOutput, StreamPolicy,
8};
9use parking_lot::Mutex;
10use std::io;
11use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus};
12use std::sync::Arc;
13
14#[derive(Default)]
15struct Group {
16 pid: Option<u32>,
17 cancelled: bool,
18 #[cfg(unix)]
19 lease: Option<std::os::unix::net::UnixStream>,
20}
21impl Group {
22 fn signal(&self) -> Result<(), Failure> {
23 #[cfg(unix)]
24 if let Some(lease) = &self.lease {
25 return match lease.shutdown(std::net::Shutdown::Write) {
26 Ok(()) => Ok(()),
27 Err(error) if error.kind() == io::ErrorKind::NotConnected => Ok(()),
28 Err(error) => Err(Failure::new(
29 FailureKind::Io,
30 format!("close process lease: {error}"),
31 )),
32 };
33 }
34 #[cfg(unix)]
35 if let Some(pid) = self.pid {
36 if unsafe { libc::kill(-(pid as libc::pid_t), libc::SIGKILL) } == -1 {
40 let error = io::Error::last_os_error();
41 if error.raw_os_error() != Some(libc::ESRCH) {
42 return Err(Failure::new(
43 FailureKind::Io,
44 format!("kill process group: {error}"),
45 ));
46 }
47 }
48 }
49 Ok(())
50 }
51 fn cancel(&mut self) -> Result<(), Failure> {
52 self.cancelled = true;
53 self.signal()
54 }
55}
56
57pub struct OwnedProcess {
60 child: Child,
61 group: Arc<Mutex<Group>>,
62 token: CancelToken,
63 reaped: bool,
64}
65impl OwnedProcess {
66 pub fn spawn(command: &mut Command, token: &CancelToken) -> Result<Self, Failure> {
67 #[cfg(not(unix))]
68 return Err(Failure::new(
69 FailureKind::Unavailable,
70 "process-group supervision requires Unix",
71 ));
72 #[cfg(unix)]
73 Self::spawn_unix(command, token, None)
74 }
75
76 #[cfg(unix)]
79 pub fn spawn_leased(
80 command: &mut Command,
81 token: &CancelToken,
82 lease: std::os::unix::net::UnixStream,
83 ) -> Result<Self, Failure> {
84 Self::spawn_unix(command, token, Some(lease))
85 }
86
87 #[cfg(unix)]
88 fn spawn_unix(
89 command: &mut Command,
90 token: &CancelToken,
91 lease: Option<std::os::unix::net::UnixStream>,
92 ) -> Result<Self, Failure> {
93 use std::os::unix::process::CommandExt;
94 if lease.is_none() {
95 command.process_group(0);
96 }
97 let group = Arc::new(Mutex::new(Group {
98 lease,
99 ..Group::default()
100 }));
101 let callback = group.clone();
102 token.register_cancel_resource(move || callback.lock().cancel())?;
103 if token.is_cancelled() {
104 token.clear_cancel_resource();
105 return Err(Failure::new(
106 FailureKind::Unavailable,
107 "process cancelled before spawn",
108 ));
109 }
110 let child = match command.spawn() {
111 Ok(child) => child,
112 Err(error) => {
113 token.clear_cancel_resource();
114 return Err(Failure::new(FailureKind::Spawn, error.to_string()));
115 }
116 };
117 let process = Self {
118 child,
119 group,
120 token: token.clone(),
121 reaped: false,
122 };
123 {
124 let mut group = process.group.lock();
125 group.pid = Some(process.child.id());
126 if group.cancelled || token.is_cancelled() {
127 group.cancel()?;
128 }
129 }
130 Ok(process)
131 }
132 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
133 self.child.stdin.take()
134 }
135 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
136 self.child.stdout.take()
137 }
138 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
139 self.child.stderr.take()
140 }
141 pub fn terminate(&mut self) -> Result<(), Failure> {
142 self.group.lock().signal()
143 }
144
145 pub fn has_exited(&self) -> Result<bool, Failure> {
148 child_has_exited(&self.child)
149 }
150 pub fn wait(&mut self) -> Result<ExitStatus, Failure> {
153 while !self.reaped && !self.has_exited()? {
156 std::thread::park_timeout(std::time::Duration::from_millis(20));
157 }
158 self.group.lock().pid = None;
159 loop {
160 match self.child.wait() {
161 Ok(status) => {
162 self.reaped = true;
163 self.token.clear_cancel_resource();
164 return Ok(status);
165 }
166 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
167 Err(error) => return Err(Failure::new(FailureKind::Wait, error.to_string())),
168 }
169 }
170 }
171}
172impl Drop for OwnedProcess {
173 fn drop(&mut self) {
174 if !self.reaped {
175 let _ = self.terminate();
176 #[cfg(unix)]
179 if self.group.lock().lease.is_none() {
180 let _ = self.child.kill();
181 }
182 #[cfg(not(unix))]
183 let _ = self.child.kill();
184 let _ = self.wait();
185 }
186 self.token.clear_cancel_resource();
187 }
188}
189
190pub fn child_has_exited(child: &Child) -> Result<bool, Failure> {
193 #[cfg(not(unix))]
194 return Err(Failure::new(
195 FailureKind::Unavailable,
196 "process supervision requires Unix",
197 ));
198 #[cfg(unix)]
199 {
200 let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
202 loop {
203 let result = unsafe {
206 libc::waitid(
207 libc::P_PID,
208 child.id() as libc::id_t,
209 &mut info,
210 libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
211 )
212 };
213 if result == 0 {
214 return Ok(unsafe { info.si_pid() } != 0);
216 }
217 let error = io::Error::last_os_error();
218 if error.kind() != io::ErrorKind::Interrupted {
219 return Err(Failure::new(FailureKind::Wait, error.to_string()));
220 }
221 }
222 }
223}
224
225#[cfg(all(test, unix))]
226mod tests {
227 use super::*;
228 use crate::worker::{self, CancelReason, Outcome};
229 use std::io::Read;
230 use std::os::fd::OwnedFd;
231 use std::os::unix::net::UnixStream;
232 use std::process::Stdio;
233 use std::sync::mpsc;
234 use std::time::Duration;
235
236 #[test]
237 fn leased_cancellation_allows_helper_cleanup_before_reaping() {
238 let (ready_tx, ready_rx) = mpsc::channel();
239 let (done_tx, done_rx) = mpsc::channel();
240 let handle = worker::spawn_effect(
241 "leased-cleanup-test",
242 move |outcome| {
243 done_tx.send(outcome).unwrap();
244 },
245 move |token| {
246 let (lease, child_lease) = UnixStream::pair().unwrap();
247 let mut command = Command::new("/bin/sh");
248 command
249 .args(["-c", "printf READY; cat >/dev/null; printf CLEANED"])
250 .stdin(Stdio::from(OwnedFd::from(child_lease)))
251 .stdout(Stdio::piped())
252 .stderr(Stdio::null());
253 let mut process = OwnedProcess::spawn_leased(&mut command, &token, lease).unwrap();
254 let mut stdout = process.take_stdout().unwrap();
255 let mut ready = [0; 5];
256 stdout.read_exact(&mut ready).unwrap();
257 assert_eq!(&ready, b"READY");
258 ready_tx.send(()).unwrap();
259 let mut output = Vec::new();
260 stdout.read_to_end(&mut output).unwrap();
261 let status = process.wait().unwrap();
262 Outcome::Success((output, status.success()))
263 },
264 );
265 ready_rx.recv_timeout(Duration::from_secs(5)).unwrap();
266 handle.cancel(CancelReason::Shutdown);
267 let outcome = done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
268 let Outcome::Success((output, successful)) = outcome else {
269 panic!("cleanup outcome lost: {outcome:?}");
270 };
271 assert!(successful);
272 assert_eq!(output, b"CLEANED");
273 }
274}