/build/source/nativelink-worker/src/process_table.rs
Line | Count | Source |
1 | | // Copyright 2026 Trace Machina, Inc. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | //! The host's processes as the action resource sampler sees them: each |
16 | | //! process's parent and process group, the CPU time it has used, and the |
17 | | //! memory it holds. |
18 | | //! |
19 | | //! Linux reads `/proc`, macOS asks `libproc`, and Windows takes a Toolhelp |
20 | | //! snapshot. Windows has no process groups, so there every process is a |
21 | | //! group of its own: an action's membership follows parent links, and the |
22 | | //! groups remembered from earlier samples (see |
23 | | //! [`action_membership`](crate::running_actions_manager::action_membership)) |
24 | | //! keep a process whose parent has exited, since Windows never reparents. |
25 | | |
26 | | use core::time::Duration; |
27 | | use std::sync::Arc; |
28 | | |
29 | | use nativelink_util::spawn_blocking; |
30 | | use tokio::time::Instant; |
31 | | |
32 | | use crate::running_actions_manager::ProcEntry; |
33 | | |
34 | | /// Every process with the CPU ticks it has used, as [`live_process_table`] |
35 | | /// returns it. |
36 | | pub type ProcessTable = Arc<[(ProcEntry, Option<u64>)]>; |
37 | | |
38 | | /// A [`live_process_table`] snapshot shared by every action sampler on this |
39 | | /// worker. |
40 | | /// |
41 | | /// Each running action samples every `RESOURCE_USAGE_SAMPLE_INTERVAL`, and |
42 | | /// finding its members means reading every process on the host. Scanning |
43 | | /// once per action makes the cost (running actions) x (host processes) per |
44 | | /// interval, which on a busy shared host saturates dozens of cores. Samplers |
45 | | /// instead reuse a snapshot younger than `max_age`, one interval, so the |
46 | | /// host is scanned at most once per interval however many actions run. |
47 | | /// Member CPU time is still read fresh on every sample, and member memory |
48 | | /// as [`ExactMemoryGate`](crate::running_actions_manager::ExactMemoryGate) |
49 | | /// decides; only |
50 | | /// membership can lag, by at most one interval, as it already could between |
51 | | /// two samples. |
52 | | /// Kills keep their own live scan |
53 | | /// ([`action_membership_live`](crate::running_actions_manager::action_membership_live)). |
54 | | #[derive(Debug)] |
55 | | pub struct ProcessTableCache { |
56 | | max_age: Duration, |
57 | | snapshot: tokio::sync::Mutex<Option<(Instant, ProcessTable)>>, |
58 | | } |
59 | | |
60 | | impl ProcessTableCache { |
61 | 85 | pub const fn new(max_age: Duration) -> Self { |
62 | 85 | Self { |
63 | 85 | max_age, |
64 | 85 | snapshot: tokio::sync::Mutex::const_new(None), |
65 | 85 | } |
66 | 85 | } |
67 | | |
68 | | /// Returns a snapshot younger than `max_age`. Callers that arrive while |
69 | | /// a scan is running wait for it rather than starting their own. |
70 | 171 | pub async fn current(&self) -> ProcessTable { |
71 | 171 | let mut snapshot = self.snapshot.lock().await; |
72 | 171 | if let Some((taken_at126 , table126 )) = snapshot.as_ref() |
73 | 126 | && taken_at.elapsed() < self.max_age |
74 | | { |
75 | 63 | return table.clone(); |
76 | 108 | } |
77 | | // The scan is thousands of blocking reads; keep it off the async workers. |
78 | 108 | let table: ProcessTable = spawn_blocking!("process_table_scan", live_process_table) |
79 | 108 | .await |
80 | 108 | .map_or_else(|_| Arc::from0 (Vec::new0 ()), Arc::from); |
81 | 108 | *snapshot = Some((Instant::now(), table.clone())); |
82 | 108 | table |
83 | 171 | } |
84 | | } |
85 | | |
86 | | /// Every process on the host, with the CPU ticks each has used so far (see |
87 | | /// [`ticks_to_millis`]). Blocking; a process that cannot be read is left |
88 | | /// out, and its ticks are `None` when only they cannot be read. |
89 | 171 | pub fn live_process_table() -> Vec<(ProcEntry, Option<u64>)> { |
90 | 171 | platform::live_process_table() |
91 | 171 | } |
92 | | |
93 | | /// The memory `pid` holds, in KiB, as close as the platform gets to the |
94 | | /// process's share of its resident pages: `Pss` on Linux, the physical |
95 | | /// footprint on macOS, the private commit charge on Windows. `None` when |
96 | | /// the process cannot be read. |
97 | 130 | pub fn read_process_memory_kb(pid: u32) -> Option<u64> { |
98 | 130 | platform::read_process_memory_kb(pid) |
99 | 130 | } |
100 | | |
101 | | /// Whether [`read_process_memory_kb`] costs more than reading a counter. |
102 | | /// Only on Linux: `smaps_rollup` walks the page tables of every mapping, |
103 | | /// about 3 ms per resident GiB, where macOS and Windows read figures the |
104 | | /// kernel keeps. Only there does the sampler read |
105 | | /// [`read_process_memory_bound_kb`] first to skip the exact figure. |
106 | | pub const EXACT_MEMORY_READ_WALKS_PAGES: bool = cfg!(target_os = "linux"); |
107 | | |
108 | | /// A figure for `pid`, in KiB, that [`read_process_memory_kb`] never |
109 | | /// exceeds, read from a counter. On Linux `RSS` from `statm`, which counts |
110 | | /// every resident page in full where `Pss` counts a shared one in part; |
111 | | /// elsewhere the exact figure, itself a counter read. `None` when the |
112 | | /// process cannot be read. |
113 | 301 | pub fn read_process_memory_bound_kb(pid: u32) -> Option<u64> { |
114 | | #[cfg(target_os = "linux")] |
115 | 301 | return platform::read_process_resident_kb(pid); |
116 | | #[cfg(not(target_os = "linux"))] |
117 | | return platform::read_process_memory_kb(pid); |
118 | 301 | } |
119 | | |
120 | | /// The CPU ticks `pid` has used so far, read now rather than from a |
121 | | /// snapshot (see [`ticks_to_millis`]). `None` when the process cannot be |
122 | | /// read. |
123 | 300 | pub fn read_process_cpu_ticks(pid: u32) -> Option<u64> { |
124 | 300 | platform::read_process_cpu_ticks(pid) |
125 | 300 | } |
126 | | |
127 | | /// Converts the CPU ticks of [`live_process_table`] into milliseconds. |
128 | | // Only the Windows conversion, a division, could be const. |
129 | | #[cfg_attr(target_family = "windows", allow(clippy::missing_const_for_fn))] |
130 | 52 | pub fn ticks_to_millis(ticks: u64) -> u64 { |
131 | 52 | platform::ticks_to_millis(ticks) |
132 | 52 | } |
133 | | |
134 | | /// Whether `pid` is still a process on the host: on unix including one that |
135 | | /// has exited but not yet been waited for, on Windows only a running one. |
136 | 16 | pub fn process_exists(pid: u32) -> bool { |
137 | 16 | platform::process_exists(pid) |
138 | 16 | } |
139 | | |
140 | | /// The worker's own process group, excluded from every action's members. |
141 | | /// On Windows, which has no process groups, the worker's own pid. |
142 | | #[cfg(target_family = "unix")] |
143 | 219 | pub fn worker_pgid() -> u32 { |
144 | | // SAFETY: `getpgrp` takes no arguments and cannot fail. |
145 | 219 | u32::try_from(unsafe { libc::getpgrp() }).unwrap_or(0) |
146 | 219 | } |
147 | | |
148 | | #[cfg(target_family = "windows")] |
149 | | pub fn worker_pgid() -> u32 { |
150 | | std::process::id() |
151 | | } |
152 | | |
153 | | #[cfg(target_os = "linux")] |
154 | | mod platform { |
155 | | use std::path::Path; |
156 | | |
157 | | use crate::running_actions_manager::{ |
158 | | ProcEntry, parse_cpu_ticks_from_stat, parse_kb_field, parse_links_from_stat, |
159 | | }; |
160 | | |
161 | 171 | pub(super) fn live_process_table() -> Vec<(ProcEntry, Option<u64>)> { |
162 | 171 | let Ok(entries) = std::fs::read_dir("/proc") else { |
163 | 0 | return Vec::new(); |
164 | | }; |
165 | 171 | entries |
166 | 171 | .flatten() |
167 | 11.5k | .filter_map171 (|entry| { |
168 | 11.5k | let pid961 = entry.file_name().to_string_lossy().parse::<u32>().ok()?10.6k ; |
169 | 961 | let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?0 ; |
170 | 961 | let (parent, group) = parse_links_from_stat(&stat)?0 ; |
171 | 961 | Some(( |
172 | 961 | ProcEntry { |
173 | 961 | pid, |
174 | 961 | ppid: parent, |
175 | 961 | pgrp: group, |
176 | 961 | }, |
177 | 961 | parse_cpu_ticks_from_stat(&stat), |
178 | 961 | )) |
179 | 11.5k | }) |
180 | 171 | .collect() |
181 | 171 | } |
182 | | |
183 | | /// `Pss` from `smaps_rollup` divides a page shared by N processes N |
184 | | /// ways, so summed over the group it counts a forked parent's pages, |
185 | | /// shared libraries and mapped inputs once. `VmRSS` counts them once per |
186 | | /// process, and a test runner that forks eight children would sample at |
187 | | /// several times what the cgroup charges. Falls back to `VmRSS` where |
188 | | /// `smaps_rollup` cannot be read (kernels before 4.14). |
189 | 130 | pub(super) fn read_process_memory_kb(pid: u32) -> Option<u64> { |
190 | 130 | if let Ok(rollup103 ) = std::fs::read_to_string(format!("/proc/{pid}/smaps_rollup")) |
191 | 103 | && let Some(pss_kb) = parse_kb_field(&rollup, "Pss:") |
192 | | { |
193 | 103 | return Some(pss_kb); |
194 | 27 | } |
195 | 27 | let status13 = std::fs::read_to_string(format!("/proc/{pid}/status")).ok()?14 ; |
196 | 13 | parse_kb_field(&status, "VmRSS:") |
197 | 130 | } |
198 | | |
199 | | /// The resident pages `statm` reports, in KiB. Kernels 6.2 to 6.15 |
200 | | /// read these counters without folding in each CPU's pending updates, |
201 | | /// so there the figure can trail by up to a batch of pages per CPU, |
202 | | /// tens of MiB on a large host. |
203 | 301 | pub(super) fn read_process_resident_kb(pid: u32) -> Option<u64> { |
204 | 301 | let statm214 = std::fs::read_to_string(format!("/proc/{pid}/statm")).ok()?87 ; |
205 | 214 | let pages: u64 = statm.split_whitespace().nth(1)?0 .parse().ok()?0 ; |
206 | | // SAFETY: `sysconf` takes an int and returns a long, with no memory |
207 | | // safety considerations. |
208 | 214 | let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; |
209 | 214 | let page_kb = u64::try_from(page_size).ok().filter(|&size| size > 0)?0 / 1024; |
210 | 214 | Some(pages.saturating_mul(page_kb)) |
211 | 301 | } |
212 | | |
213 | 300 | pub(super) fn read_process_cpu_ticks(pid: u32) -> Option<u64> { |
214 | 300 | parse_cpu_ticks_from_stat214 (&std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?86 ) |
215 | 300 | } |
216 | | |
217 | | /// Ticks are clock ticks from `/proc`. `_SC_CLK_TCK` is 100 on every |
218 | | /// Linux we run on, but it is queryable so ask rather than assume, and |
219 | | /// fall back to 100 if the query fails. |
220 | 52 | pub(super) fn ticks_to_millis(ticks: u64) -> u64 { |
221 | | // SAFETY: `sysconf` takes an int and returns a long, with no memory |
222 | | // safety considerations. |
223 | 52 | let hz = unsafe { libc::sysconf(libc::_SC_CLK_TCK) }; |
224 | 52 | let hz = if hz > 0 { hz as u64 } else { 1000 }; |
225 | 52 | ticks.saturating_mul(1_000) / hz |
226 | 52 | } |
227 | | |
228 | 16 | pub(super) fn process_exists(pid: u32) -> bool { |
229 | 16 | Path::new(&format!("/proc/{pid}")).exists() |
230 | 16 | } |
231 | | } |
232 | | |
233 | | #[cfg(target_os = "macos")] |
234 | | mod platform { |
235 | | use core::ffi::c_int; |
236 | | use core::mem::size_of; |
237 | | |
238 | | use crate::running_actions_manager::ProcEntry; |
239 | | |
240 | | // libc deprecates its binding in favour of the mach2 crate; one call does |
241 | | // not warrant that dependency. |
242 | | #[repr(C)] |
243 | | struct MachTimebaseInfo { |
244 | | numerator: u32, |
245 | | denominator: u32, |
246 | | } |
247 | | |
248 | | unsafe extern "C" { |
249 | | fn mach_timebase_info(info: *mut MachTimebaseInfo) -> c_int; |
250 | | } |
251 | | |
252 | | pub(super) fn live_process_table() -> Vec<(ProcEntry, Option<u64>)> { |
253 | | // SAFETY: a null buffer asks only for the number of pids. |
254 | | let count = unsafe { libc::proc_listallpids(core::ptr::null_mut(), 0) }; |
255 | | let Ok(count) = usize::try_from(count) else { |
256 | | return Vec::new(); |
257 | | }; |
258 | | // Room for the processes that start between the two calls. |
259 | | let mut pids: Vec<libc::pid_t> = vec![0; count + 64]; |
260 | | let Ok(buffer_size) = c_int::try_from(pids.len() * size_of::<libc::pid_t>()) else { |
261 | | return Vec::new(); |
262 | | }; |
263 | | // SAFETY: the buffer holds `buffer_size` bytes and outlives the call. |
264 | | let listed = unsafe { libc::proc_listallpids(pids.as_mut_ptr().cast(), buffer_size) }; |
265 | | let Ok(listed) = usize::try_from(listed) else { |
266 | | return Vec::new(); |
267 | | }; |
268 | | pids.truncate(listed); |
269 | | pids.into_iter() |
270 | | .filter_map(|pid| { |
271 | | let pid = u32::try_from(pid).ok()?; |
272 | | let info = bsd_info(pid)?; |
273 | | Some(( |
274 | | ProcEntry { |
275 | | pid, |
276 | | ppid: info.pbi_ppid, |
277 | | pgrp: info.pbi_pgid, |
278 | | }, |
279 | | read_process_cpu_ticks(pid), |
280 | | )) |
281 | | }) |
282 | | .collect() |
283 | | } |
284 | | |
285 | | /// `ri_phys_footprint` is what macOS charges the process: its private |
286 | | /// resident and compressed pages, not the shared pages `RSS` counts once |
287 | | /// per process. |
288 | | pub(super) fn read_process_memory_kb(pid: u32) -> Option<u64> { |
289 | | rusage(pid).map(|usage| usage.ri_phys_footprint / 1024) |
290 | | } |
291 | | |
292 | | pub(super) fn read_process_cpu_ticks(pid: u32) -> Option<u64> { |
293 | | rusage(pid).map(|usage| usage.ri_user_time.saturating_add(usage.ri_system_time)) |
294 | | } |
295 | | |
296 | | /// Ticks are Mach absolute time units: nanoseconds on Intel, but not on |
297 | | /// Apple silicon, so convert through the timebase. |
298 | | pub(super) fn ticks_to_millis(ticks: u64) -> u64 { |
299 | | let mut timebase = MachTimebaseInfo { |
300 | | numerator: 0, |
301 | | denominator: 0, |
302 | | }; |
303 | | // SAFETY: fills the struct it is given, which outlives the call. |
304 | | let status = unsafe { mach_timebase_info(&raw mut timebase) }; |
305 | | let (numerator, denominator) = if status == 0 && timebase.denominator != 0 { |
306 | | ( |
307 | | u128::from(timebase.numerator), |
308 | | u128::from(timebase.denominator), |
309 | | ) |
310 | | } else { |
311 | | (1, 1) |
312 | | }; |
313 | | let millis = u128::from(ticks) * numerator / denominator / 1_000_000; |
314 | | u64::try_from(millis).unwrap_or(u64::MAX) |
315 | | } |
316 | | |
317 | | pub(super) fn process_exists(pid: u32) -> bool { |
318 | | let Ok(pid) = libc::pid_t::try_from(pid) else { |
319 | | return false; |
320 | | }; |
321 | | // SAFETY: signal 0 only checks that the process exists. |
322 | | let delivered = unsafe { libc::kill(pid, 0) } == 0; |
323 | | delivered || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM) |
324 | | } |
325 | | |
326 | | fn bsd_info(pid: u32) -> Option<libc::proc_bsdinfo> { |
327 | | let pid = c_int::try_from(pid).ok()?; |
328 | | let size = c_int::try_from(size_of::<libc::proc_bsdinfo>()).ok()?; |
329 | | // SAFETY: an all-zero proc_bsdinfo is valid; it is plain integers. |
330 | | let mut info: libc::proc_bsdinfo = unsafe { core::mem::zeroed() }; |
331 | | // SAFETY: the buffer is a proc_bsdinfo of `size` bytes that outlives |
332 | | // the call. |
333 | | let read = unsafe { |
334 | | libc::proc_pidinfo(pid, libc::PROC_PIDTBSDINFO, 0, (&raw mut info).cast(), size) |
335 | | }; |
336 | | (read == size).then_some(info) |
337 | | } |
338 | | |
339 | | fn rusage(pid: u32) -> Option<libc::rusage_info_v2> { |
340 | | let pid = c_int::try_from(pid).ok()?; |
341 | | // SAFETY: an all-zero rusage_info_v2 is valid; it is plain integers. |
342 | | let mut usage: libc::rusage_info_v2 = unsafe { core::mem::zeroed() }; |
343 | | // SAFETY: RUSAGE_INFO_V2 fills a rusage_info_v2, which outlives the |
344 | | // call. |
345 | | let status = |
346 | | unsafe { libc::proc_pid_rusage(pid, libc::RUSAGE_INFO_V2, (&raw mut usage).cast()) }; |
347 | | (status == 0).then_some(usage) |
348 | | } |
349 | | } |
350 | | |
351 | | #[cfg(target_family = "windows")] |
352 | | mod platform { |
353 | | use core::mem::size_of; |
354 | | use std::collections::HashMap; |
355 | | use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; |
356 | | |
357 | | use windows_sys::Win32::Foundation::{FILETIME, INVALID_HANDLE_VALUE, STILL_ACTIVE}; |
358 | | use windows_sys::Win32::System::Diagnostics::ToolHelp::{ |
359 | | CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, |
360 | | TH32CS_SNAPPROCESS, |
361 | | }; |
362 | | use windows_sys::Win32::System::ProcessStatus::{ |
363 | | GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX, |
364 | | }; |
365 | | use windows_sys::Win32::System::Threading::{ |
366 | | GetExitCodeProcess, GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, |
367 | | }; |
368 | | |
369 | | use crate::running_actions_manager::ProcEntry; |
370 | | |
371 | | pub(super) fn live_process_table() -> Vec<(ProcEntry, Option<u64>)> { |
372 | | let links = process_links(); |
373 | | let times: HashMap<u32, ProcessTimes> = links |
374 | | .iter() |
375 | | .filter_map(|&(pid, _)| Some((pid, process_times(pid)?))) |
376 | | .collect(); |
377 | | links |
378 | | .into_iter() |
379 | | .map(|(pid, ppid)| { |
380 | | let own = times.get(&pid); |
381 | | // Windows never reparents, and reuses ids, so a parent id can |
382 | | // name a process that started after the child: not its parent. |
383 | | let ppid = match (own, times.get(&ppid)) { |
384 | | (Some(own), Some(parent)) if parent.created > own.created => 0, |
385 | | _ => ppid, |
386 | | }; |
387 | | ( |
388 | | ProcEntry { |
389 | | pid, |
390 | | ppid, |
391 | | pgrp: pid, |
392 | | }, |
393 | | own.map(|times| times.cpu), |
394 | | ) |
395 | | }) |
396 | | .collect() |
397 | | } |
398 | | |
399 | | /// The private commit charge: memory the process committed that no |
400 | | /// other process shares, which is also what a job object's memory limit |
401 | | /// counts. The private working set would be closer to `Pss`, but |
402 | | /// Windows Server 2022 reports it as 0. |
403 | | pub(super) fn read_process_memory_kb(pid: u32) -> Option<u64> { |
404 | | let process = open(pid)?; |
405 | | let size = u32::try_from(size_of::<PROCESS_MEMORY_COUNTERS_EX>()).ok()?; |
406 | | let mut counters = PROCESS_MEMORY_COUNTERS_EX { |
407 | | cb: size, |
408 | | ..Default::default() |
409 | | }; |
410 | | // SAFETY: the handle is open and the buffer holds `size` bytes. |
411 | | let ok = unsafe { |
412 | | GetProcessMemoryInfo( |
413 | | process.as_raw_handle(), |
414 | | (&raw mut counters).cast::<PROCESS_MEMORY_COUNTERS>(), |
415 | | size, |
416 | | ) |
417 | | }; |
418 | | (ok != 0).then_some(counters.PrivateUsage as u64 / 1024) |
419 | | } |
420 | | |
421 | | pub(super) fn read_process_cpu_ticks(pid: u32) -> Option<u64> { |
422 | | process_times(pid).map(|times| times.cpu) |
423 | | } |
424 | | |
425 | | /// Ticks are the 100 ns units of `GetProcessTimes`. |
426 | | pub(super) const fn ticks_to_millis(ticks: u64) -> u64 { |
427 | | ticks / 10_000 |
428 | | } |
429 | | |
430 | | /// The process object outlives the process for as long as anyone holds |
431 | | /// a handle to it, which an antivirus scanner may, so ask whether it is |
432 | | /// still running instead. |
433 | | pub(super) fn process_exists(pid: u32) -> bool { |
434 | | let Some(process) = open(pid) else { |
435 | | return false; |
436 | | }; |
437 | | let mut exit_code = 0; |
438 | | // SAFETY: the handle is open and `exit_code` outlives the call. |
439 | | let ok = unsafe { GetExitCodeProcess(process.as_raw_handle(), &raw mut exit_code) }; |
440 | | ok != 0 && exit_code == STILL_ACTIVE.cast_unsigned() |
441 | | } |
442 | | |
443 | | struct ProcessTimes { |
444 | | created: u64, |
445 | | cpu: u64, |
446 | | } |
447 | | |
448 | | /// Every process's id and its parent's id, from one snapshot. |
449 | | fn process_links() -> Vec<(u32, u32)> { |
450 | | let Ok(entry_size) = u32::try_from(size_of::<PROCESSENTRY32W>()) else { |
451 | | return Vec::new(); |
452 | | }; |
453 | | // SAFETY: takes no pointers; the result is checked before use. |
454 | | let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }; |
455 | | if snapshot == INVALID_HANDLE_VALUE { |
456 | | return Vec::new(); |
457 | | } |
458 | | // SAFETY: a valid handle this function now owns. |
459 | | let snapshot = unsafe { OwnedHandle::from_raw_handle(snapshot) }; |
460 | | // SAFETY: an all-zero entry is valid; it is plain integers. |
461 | | let mut entry: PROCESSENTRY32W = unsafe { core::mem::zeroed() }; |
462 | | entry.dwSize = entry_size; |
463 | | let mut links = Vec::new(); |
464 | | // SAFETY: the snapshot is open and `entry` has its size set. |
465 | | let mut more = unsafe { Process32FirstW(snapshot.as_raw_handle(), &raw mut entry) } != 0; |
466 | | while more { |
467 | | links.push((entry.th32ProcessID, entry.th32ParentProcessID)); |
468 | | // SAFETY: as above. |
469 | | more = unsafe { Process32NextW(snapshot.as_raw_handle(), &raw mut entry) } != 0; |
470 | | } |
471 | | links |
472 | | } |
473 | | |
474 | | fn process_times(pid: u32) -> Option<ProcessTimes> { |
475 | | let process = open(pid)?; |
476 | | let mut created = FILETIME::default(); |
477 | | let mut exited = FILETIME::default(); |
478 | | let mut kernel = FILETIME::default(); |
479 | | let mut user = FILETIME::default(); |
480 | | // SAFETY: the handle is open and every out pointer outlives the call. |
481 | | let ok = unsafe { |
482 | | GetProcessTimes( |
483 | | process.as_raw_handle(), |
484 | | &raw mut created, |
485 | | &raw mut exited, |
486 | | &raw mut kernel, |
487 | | &raw mut user, |
488 | | ) |
489 | | }; |
490 | | (ok != 0).then(|| ProcessTimes { |
491 | | created: filetime_ticks(created), |
492 | | cpu: filetime_ticks(kernel).saturating_add(filetime_ticks(user)), |
493 | | }) |
494 | | } |
495 | | |
496 | | fn filetime_ticks(time: FILETIME) -> u64 { |
497 | | (u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime) |
498 | | } |
499 | | |
500 | | fn open(pid: u32) -> Option<OwnedHandle> { |
501 | | // SAFETY: takes no pointers; a null result is checked before use. |
502 | | let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) }; |
503 | | // SAFETY: a non-null handle from OpenProcess is open and ours to close. |
504 | | (!handle.is_null()).then(|| unsafe { OwnedHandle::from_raw_handle(handle) }) |
505 | | } |
506 | | } |