Skip to main content

runtime/backlog/
jobs.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Which processes belong to which run (`crates/runtime/spec/Tools/Exec.lean` ยง8-13-3).
7//!
8//! A dispatched `cargo test` spends its memory in the `rustc` and test
9//! processes it starts, not in `cargo`. On Windows every run therefore
10//! gets one anonymous Job Object, and each command the backlog starts
11//! for that run joins it the moment the command enters the table; the
12//! processes a member of a job starts join the same job by themselves,
13//! so the job's process list is the run's whole process tree. The job
14//! is not kill-on-close: dropping it at `release` stops nothing, because
15//! stopping belongs to `release` and `halt` alone.
16
17use std::collections::{BTreeMap, BTreeSet};
18
19use kernel::{AxError, RunId};
20
21use super::{Backlog, BacklogId, Body, Claim, Member};
22
23/// The processes one run has in the backlog right now.
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct RunProcesses {
26    /// Each command's own pid, and every process its run's job holds.
27    /// A command that ended but was not yet harvested is still listed;
28    /// a reader finds no counters behind its pid.
29    pub pids: BTreeSet<u32>,
30    /// How many of the run's commands were read as themselves only,
31    /// without the processes they started: on Windows the ones that
32    /// could not join the job (all of them when the job's list could
33    /// not be read), and every one of them elsewhere, where there is
34    /// no Job Object.
35    pub unfollowed: u32,
36}
37
38/// Each run's job, and how many of its commands did not get into it.
39#[derive(Default)]
40pub(super) struct Jobs {
41    #[cfg(windows)]
42    runs: BTreeMap<RunId, RunJob>,
43}
44
45impl Backlog {
46    /// The processes of every run that has a command in the table.
47    /// A command released to nobody belongs to no run.
48    ///
49    /// # Errors
50    /// `E_STORAGE_FATAL` when the table cannot be reached.
51    pub fn processes(&self) -> Result<BTreeMap<RunId, RunProcesses>, AxError> {
52        let table = self.hold()?;
53        let mut runs: BTreeMap<RunId, RunProcesses> = BTreeMap::new();
54        for member in table.members.values() {
55            if let Body::Command {
56                child,
57                claim: Claim::Window(owner) | Claim::Run(owner),
58                ..
59            } = &member.body
60            {
61                runs.entry(*owner).or_default().pids.insert(child.id());
62            }
63        }
64        table.jobs.follow(&mut runs);
65        Ok(runs)
66    }
67
68    /// Enters a member into the table, and a command into its run's job.
69    pub(super) fn enrol(&self, id: BacklogId, member: Member) -> Result<(), AxError> {
70        let mut table = self.hold()?;
71        table.jobs.enter(&member);
72        table.members.insert(id, member);
73        Ok(())
74    }
75}
76
77impl Jobs {
78    /// Lets the run's job go; its processes keep running.
79    #[cfg(windows)]
80    pub(super) fn forget(&mut self, owner: RunId) {
81        self.runs.remove(&owner);
82    }
83
84    #[cfg(not(windows))]
85    pub(super) fn forget(&mut self, _owner: RunId) {}
86
87    #[cfg(windows)]
88    fn enter(&mut self, member: &Member) {
89        let Body::Command {
90            child,
91            claim: Claim::Window(owner) | Claim::Run(owner),
92            ..
93        } = &member.body
94        else {
95            return;
96        };
97        let run = self.runs.entry(*owner).or_default();
98        if run.join(child).is_none() {
99            run.unjoined = run.unjoined.saturating_add(1);
100        }
101    }
102
103    #[cfg(not(windows))]
104    fn enter(&mut self, _member: &Member) {}
105
106    #[cfg(windows)]
107    fn follow(&self, readings: &mut BTreeMap<RunId, RunProcesses>) {
108        for (owner, run) in &self.runs {
109            let reading = readings.entry(*owner).or_default();
110            let listed = run.job.as_ref().map(win32job::Job::query_process_id_list);
111            if let Some(Ok(pids)) = listed {
112                // A Windows pid is a DWORD; the list only widens it.
113                reading
114                    .pids
115                    .extend(pids.into_iter().filter_map(|pid| u32::try_from(pid).ok()));
116                reading.unfollowed = reading.unfollowed.saturating_add(run.unjoined);
117            } else {
118                reading.unfollowed = u32::try_from(reading.pids.len()).unwrap_or(u32::MAX);
119            }
120        }
121    }
122
123    #[cfg(not(windows))]
124    fn follow(&self, readings: &mut BTreeMap<RunId, RunProcesses>) {
125        for reading in readings.values_mut() {
126            reading.unfollowed = u32::try_from(reading.pids.len()).unwrap_or(u32::MAX);
127        }
128    }
129}
130
131/// One run's job, created when its first command joins.
132#[cfg(windows)]
133#[derive(Default)]
134struct RunJob {
135    job: Option<win32job::Job>,
136    unjoined: u32,
137}
138
139#[cfg(windows)]
140impl RunJob {
141    /// Puts `child` into this run's job, creating the job on first use.
142    /// `None` when the job cannot be made or the process cannot join it.
143    fn join(&mut self, child: &std::process::Child) -> Option<()> {
144        use std::os::windows::io::AsRawHandle;
145        let handle = isize::try_from(child.as_raw_handle().addr()).ok()?;
146        let job = match self.job.take() {
147            Some(job) => job,
148            None => win32job::Job::create().ok()?,
149        };
150        let joined = job.assign_process(handle).ok();
151        self.job = Some(job);
152        joined
153    }
154}