Skip to main content

strop_core/
process.rs

1//! Worker-only child ownership. Cancellation signals a private Unix process
2//! group promptly; only its owner may revoke the capability and reap the PID.
3mod capture;
4use crate::worker::{CancelToken, Failure, FailureKind};
5pub use capture::{capture, capture_with, CaptureError, CapturePolicy, CommandOutput, StdinPolicy};
6use parking_lot::Mutex;
7use std::io;
8use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus};
9use std::sync::Arc;
10
11#[derive(Default)]
12struct Group {
13    pid: Option<u32>,
14    cancelled: bool,
15}
16impl Group {
17    fn signal(&self) -> Result<(), Failure> {
18        #[cfg(unix)]
19        if let Some(pid) = self.pid {
20            // SAFETY: this positive PID belongs to our unreaped child, launched
21            // with process_group(0). The mutex serializes signalling and revocation;
22            // negation targets only that private group. std cannot signal groups.
23            if unsafe { libc::kill(-(pid as libc::pid_t), libc::SIGKILL) } == -1 {
24                let error = io::Error::last_os_error();
25                if error.raw_os_error() != Some(libc::ESRCH) {
26                    return Err(Failure::new(
27                        FailureKind::Io,
28                        format!("kill process group: {error}"),
29                    ));
30                }
31            }
32        }
33        Ok(())
34    }
35    fn cancel(&mut self) -> Result<(), Failure> {
36        self.cancelled = true;
37        self.signal()
38    }
39}
40
41/// Owns both the child and the cancellation capability. Never put this in editor
42/// state: Drop may wait, so it belongs exclusively to a worker stack.
43pub struct OwnedProcess {
44    child: Child,
45    group: Arc<Mutex<Group>>,
46    token: CancelToken,
47    reaped: bool,
48}
49impl OwnedProcess {
50    pub fn spawn(command: &mut Command, token: &CancelToken) -> Result<Self, Failure> {
51        #[cfg(not(unix))]
52        return Err(Failure::new(
53            FailureKind::Unavailable,
54            "process-group supervision requires Unix",
55        ));
56        #[cfg(unix)]
57        {
58            use std::os::unix::process::CommandExt;
59            let group = Arc::new(Mutex::new(Group::default()));
60            let callback = group.clone();
61            token.register_cancel_resource(move || callback.lock().cancel())?;
62            if token.is_cancelled() {
63                token.clear_cancel_resource();
64                return Err(Failure::new(
65                    FailureKind::Unavailable,
66                    "process cancelled before spawn",
67                ));
68            }
69            let child = match command.process_group(0).spawn() {
70                Ok(child) => child,
71                Err(error) => {
72                    token.clear_cancel_resource();
73                    return Err(Failure::new(FailureKind::Spawn, error.to_string()));
74                }
75            };
76            let process = Self {
77                child,
78                group,
79                token: token.clone(),
80                reaped: false,
81            };
82            {
83                let mut group = process.group.lock();
84                group.pid = Some(process.child.id());
85                // A callback may already have run before publication.
86                if group.cancelled || token.is_cancelled() {
87                    group.cancel()?;
88                }
89            }
90            Ok(process)
91        }
92    }
93    pub fn take_stdin(&mut self) -> Option<ChildStdin> {
94        self.child.stdin.take()
95    }
96    pub fn take_stdout(&mut self) -> Option<ChildStdout> {
97        self.child.stdout.take()
98    }
99    pub fn take_stderr(&mut self) -> Option<ChildStderr> {
100        self.child.stderr.take()
101    }
102    pub fn terminate(&mut self) -> Result<(), Failure> {
103        self.group.lock().signal()
104    }
105
106    /// Observe without reaping: descendants may still hold pipes, and cancellation
107    /// must retain its PID reservation until those descendants have been killed.
108    pub fn has_exited(&self) -> Result<bool, Failure> {
109        #[cfg(not(unix))]
110        return Err(Failure::new(
111            FailureKind::Unavailable,
112            "process supervision requires Unix",
113        ));
114        #[cfg(unix)]
115        {
116            // SAFETY: zero initializes siginfo_t; successful waitid writes it.
117            // WNOWAIT retains ownership of the child PID, WNOHANG never blocks.
118            let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
119            loop {
120                let result = unsafe {
121                    libc::waitid(
122                        libc::P_PID,
123                        self.child.id() as libc::id_t,
124                        &mut info,
125                        libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
126                    )
127                };
128                if result == 0 {
129                    // SAFETY: waitid successfully initialized the platform layout.
130                    return Ok(unsafe { info.si_pid() } != 0);
131                }
132                let error = io::Error::last_os_error();
133                if error.kind() != io::ErrorKind::Interrupted {
134                    return Err(Failure::new(FailureKind::Wait, error.to_string()));
135                }
136            }
137        }
138    }
139    /// Wait with cancellation intact, then revoke before reaping. A callback
140    /// detached from CancelToken can never signal a reused PID.
141    pub fn wait(&mut self) -> Result<ExitStatus, Failure> {
142        // Retain the signalling capability throughout the wait. Even a caller
143        // waiting before termination must remain cancellable.
144        while !self.reaped && !self.has_exited()? {
145            std::thread::park_timeout(std::time::Duration::from_millis(20));
146        }
147        self.group.lock().pid = None;
148        loop {
149            match self.child.wait() {
150                Ok(status) => {
151                    self.reaped = true;
152                    self.token.clear_cancel_resource();
153                    return Ok(status);
154                }
155                Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
156                Err(error) => return Err(Failure::new(FailureKind::Wait, error.to_string())),
157            }
158        }
159    }
160}
161impl Drop for OwnedProcess {
162    fn drop(&mut self) {
163        if !self.reaped {
164            let _ = self.terminate();
165            // Direct-child fallback if process-group signalling failed.
166            let _ = self.child.kill();
167            let _ = self.wait();
168        }
169        self.token.clear_cancel_resource();
170    }
171}