Skip to main content

rightkit_child_process/
registry.rs

1//! The process-wide list of owned children, for [`terminate_all_owned`].
2//!
3//! Every child started by [`OwnedCommand::spawn`](crate::OwnedCommand::spawn) is listed here
4//! until its `OwnedChild` is dropped. Detached children
5//! ([`spawn_uncontained_detached`](crate::OwnedCommand::spawn_uncontained_detached)) are not
6//! owned and are never listed. The list holds only `Weak` references (Unix: to the shared
7//! child; Windows: to the kill-on-close job, plus a duplicated process handle), so listing a
8//! child never keeps it, its pipes or its job alive, and a dropped child leaves the list.
9//!
10//! Identity: on Unix every signal still goes through `UnixChild::signal`, which proves the
11//! pid is the child this process spawned and has not reaped before it is used, exactly as for
12//! the owner's own calls; an entry whose leader was reaped is reported `already_gone` and
13//! nothing is sent. On Windows termination uses the process handle and the job, which cannot
14//! name a recycled pid.
15
16use crate::{process_handle::ProcessHandle, TerminationOptions};
17use std::{
18    io,
19    sync::{Arc, Mutex, MutexGuard, Weak},
20};
21
22#[cfg(unix)]
23type PlatformChild = crate::unix_child::UnixChild;
24#[cfg(windows)]
25type PlatformChild = crate::spawn_windows::WindowsChild;
26
27struct Entry {
28    /// Address of the shared child; identifies the entry for `forget`.
29    key: usize,
30    pid: u32,
31    child: Weak<Mutex<PlatformChild>>,
32    #[cfg(windows)]
33    process: ProcessHandle,
34    #[cfg(windows)]
35    job: Weak<crate::process_windows::KillOnCloseJob>,
36}
37
38static OWNED: Mutex<Vec<Entry>> = Mutex::new(Vec::new());
39
40fn owned() -> MutexGuard<'static, Vec<Entry>> {
41    OWNED.lock().unwrap_or_else(|poison| poison.into_inner())
42}
43
44fn key_of(child: &Arc<Mutex<PlatformChild>>) -> usize {
45    Arc::as_ptr(child) as usize
46}
47
48#[cfg(unix)]
49pub(crate) fn register(child: &Arc<Mutex<PlatformChild>>, pid: u32) {
50    let mut list = owned();
51    list.retain(|entry| entry.child.strong_count() > 0);
52    list.push(Entry {
53        key: key_of(child),
54        pid,
55        child: Arc::downgrade(child),
56    });
57}
58
59#[cfg(windows)]
60pub(crate) fn register(
61    child: &Arc<Mutex<PlatformChild>>,
62    handle: &ProcessHandle,
63    job: Weak<crate::process_windows::KillOnCloseJob>,
64    pid: u32,
65) {
66    // A handle that cannot be duplicated leaves this child unlisted (it is still owned and
67    // killed on drop as before); the duplicate is what lets the list act without the child.
68    let Ok(process) = handle.try_clone() else {
69        return;
70    };
71    let mut list = owned();
72    list.retain(|entry| entry.child.strong_count() > 0);
73    list.push(Entry {
74        key: key_of(child),
75        pid,
76        child: Arc::downgrade(child),
77        process,
78        job,
79    });
80}
81
82/// Removes the entry for `child`, if any. Called when its `OwnedChild` is dropped.
83pub(crate) fn forget(child: &Arc<Mutex<PlatformChild>>) {
84    let key = key_of(child);
85    owned().retain(|entry| entry.key != key);
86}
87
88/// How many owned children are listed and still alive (their `OwnedChild` has not been
89/// dropped). A child that exited but was not dropped is still counted.
90pub fn owned_count() -> usize {
91    let mut list = owned();
92    list.retain(|entry| entry.child.strong_count() > 0);
93    list.len()
94}
95
96/// The result of [`terminate_all_owned`], as pids.
97#[derive(Debug, Default)]
98pub struct TerminateAllReport {
99    /// Stopped by this call.
100    pub terminated: Vec<u32>,
101    /// Nothing was left to stop: the leader had been reaped or its group was already empty
102    /// (Unix), or the process had exited and had no live job (Windows).
103    pub already_gone: Vec<u32>,
104    /// Could not be stopped, with the reason. The rest were still attempted.
105    pub failed: Vec<(u32, io::Error)>,
106}
107
108/// Stops every child this process owns (everything started with `OwnedCommand::spawn` whose
109/// `OwnedChild` has not been dropped), tree and all, in one call: for an application's
110/// shutdown path, replacing an app-kept list of running children.
111///
112/// Unix: `SIGTERM` to every child's process group first, then at most `options.grace` in
113/// total (not per child) for the groups to leave, then `SIGKILL` to every group, and a bounded
114/// reap of each leader (2 s in total). Windows: each child's retained job is terminated and
115/// the child itself, with `options.windows_exit_code`; there is no graceful stage and
116/// `grace` is not used. Every group signal is sent only while the leader is proven to be the
117/// child this process spawned and has not reaped (see `unix_child`), so a recycled pid is
118/// never signalled.
119///
120/// The `OwnedChild` values stay valid: `wait` returns the cached status, `Drop` finds nothing
121/// left to kill. Children started detached are not owned and are not touched; stop those with
122/// [`OwnedChild::terminate_tree_deep`](crate::OwnedChild::terminate_tree_deep).
123pub fn terminate_all_owned(options: TerminationOptions) -> TerminateAllReport {
124    #[cfg(unix)]
125    {
126        terminate_all_unix(options)
127    }
128    #[cfg(windows)]
129    {
130        terminate_all_windows(options)
131    }
132}
133
134#[cfg(unix)]
135fn terminate_all_unix(options: TerminationOptions) -> TerminateAllReport {
136    use crate::{
137        owned::{await_tree_shared, signal_shared},
138        unix_child::{SignalOutcome, SignalTarget},
139    };
140    use std::time::{Duration, Instant, SystemTime};
141
142    const REAP_BUDGET: Duration = Duration::from_secs(2);
143
144    let live: Vec<(u32, Arc<Mutex<PlatformChild>>)> = {
145        let mut list = owned();
146        list.retain(|entry| entry.child.strong_count() > 0);
147        list.iter()
148            .filter_map(|entry| entry.child.upgrade().map(|child| (entry.pid, child)))
149            .collect()
150    };
151    let mut report = TerminateAllReport::default();
152
153    // Stage 1: ask every group to stop.
154    let mut asked: Vec<(u32, Arc<Mutex<PlatformChild>>, ProcessHandle)> = Vec::new();
155    for (pid, child) in live {
156        let handle = match ProcessHandle::new(&child, SystemTime::now()) {
157            Ok(handle) => handle,
158            Err(error) => {
159                report.failed.push((pid, error));
160                continue;
161            }
162        };
163        match signal_shared(&child, SignalTarget::Group, libc::SIGTERM) {
164            Ok(SignalOutcome::Sent) => asked.push((pid, child, handle)),
165            Ok(_) => report.already_gone.push(pid),
166            Err(error) => report.failed.push((pid, error)),
167        }
168    }
169
170    // Stage 2: one shared grace.
171    let deadline = Instant::now().checked_add(options.grace);
172    for (pid, child, handle) in &asked {
173        let left = deadline.map_or(Duration::ZERO, |at| {
174            at.saturating_duration_since(Instant::now())
175        });
176        if let Err(error) = await_tree_shared(child, handle, left, true) {
177            report.failed.push((*pid, error));
178        }
179    }
180
181    // Stage 3: force every group down, then reap each leader within one shared budget.
182    let reap_deadline = Instant::now().checked_add(REAP_BUDGET);
183    for (pid, child, handle) in &asked {
184        match signal_shared(child, SignalTarget::Group, libc::SIGKILL) {
185            Ok(_) => {
186                let left = reap_deadline.map_or(Duration::ZERO, |at| {
187                    at.saturating_duration_since(Instant::now())
188                });
189                // A leader that does not reap in time is left to its owner; the group is dead.
190                let _ = handle.wait_timeout(left);
191                if !report.failed.iter().any(|(failed, _)| failed == pid) {
192                    report.terminated.push(*pid);
193                }
194            }
195            Err(error) => report.failed.push((*pid, error)),
196        }
197    }
198    report
199}
200
201#[cfg(windows)]
202fn terminate_all_windows(options: TerminationOptions) -> TerminateAllReport {
203    let live: Vec<(
204        u32,
205        ProcessHandle,
206        Option<Arc<crate::process_windows::KillOnCloseJob>>,
207    )> = {
208        let mut list = owned();
209        list.retain(|entry| entry.child.strong_count() > 0);
210        list.iter()
211            .filter_map(|entry| {
212                Some((
213                    entry.pid,
214                    entry.process.try_clone().ok()?,
215                    entry.job.upgrade(),
216                ))
217            })
218            .collect()
219    };
220    let mut report = TerminateAllReport::default();
221    for (pid, process, job) in live {
222        if job.is_none() && matches!(process.try_wait(), Ok(Some(_))) {
223            report.already_gone.push(pid);
224            continue;
225        }
226        match crate::process_windows::terminate(&process, job.as_deref(), options.windows_exit_code)
227        {
228            Ok(()) => report.terminated.push(pid),
229            Err(error) => report.failed.push((pid, error)),
230        }
231    }
232    report
233}