dolang_vfs/process/foreign.rs
1//! Inspection and control of processes this VFS did not spawn.
2//!
3//! Where [`Child`](super::Child) addresses a process by parentage, everything
4//! here addresses one that already exists. The distinction is not cosmetic: a
5//! parent has a reaping relationship with its child that nothing here can
6//! reproduce, so [`ProcessExit`] carries strictly less than
7//! [`ProcessStatus`](super::ProcessStatus).
8//!
9//! A [`Process`] holds a kernel handle rather than a PID. A bare PID makes
10//! every operation check-then-act — enumerate, then signal, and in between the
11//! PID may have been recycled onto something else — so [`ProcessInfo`] carries
12//! a [`StartTime`] that [`Vfs::open_process_info`](crate::Vfs::open_process_info)
13//! compares after opening. A recycled PID necessarily has a later start time,
14//! so a match proves the handle refers to the intended process.
15
16use std::fmt;
17
18use serde::{Deserialize, Serialize};
19use uuid::Uuid;
20
21use crate::{
22 client, direct,
23 error::{Error, ErrorKind, Result},
24 path,
25 security::{UnixSecurityInfo, WindowsTokenInfo},
26};
27
28/// When a process started, in whatever units its platform reports.
29///
30/// Deliberately opaque, and compared only for equality: it exists to
31/// distinguish a process from a later one that inherited its PID, and
32/// normalizing it to a wall-clock time would cost precision on every platform
33/// for no gain. Linux reports clock ticks since boot, the BSDs and macOS a
34/// microsecond timestamp, and Windows 100-nanosecond intervals since 1601.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub struct StartTime(pub(crate) u64);
37
38/// A snapshot of one process, as of when it was taken.
39///
40/// Every field but `pid`, `name`, and `start` is optional, because no field
41/// beyond those is available on every platform, for every target process, to
42/// every caller. See the accessors for what limits each one.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct ProcessInfo {
45 pub(crate) session: Uuid,
46 pub(crate) pid: u32,
47 pub(crate) ppid: Option<u32>,
48 pub(crate) name: String,
49 pub(crate) start: StartTime,
50 pub(crate) exe: Option<path::PathBuf>,
51 pub(crate) cmdline: Option<Vec<String>>,
52 pub(crate) cwd: Option<path::PathBuf>,
53 pub(crate) family: ProcessFamily,
54 pub(crate) exit: Option<ProcessExit>,
55}
56
57/// The platform-specific half of a [`ProcessInfo`].
58///
59/// Which variant a record carries follows from the target it was captured on,
60/// so the enum answers "does this platform have such a thing at all" once, and
61/// the `Option` inside each variant is left to mean only "not obtained for this
62/// process".
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub(crate) enum ProcessFamily {
65 /// Unix credentials, absent if they could not be read.
66 Unix(Option<UnixSecurityInfo>),
67 Windows {
68 /// Access token information, absent if it was not read.
69 token: Option<WindowsTokenInfo>,
70 /// The command line as the process itself sees it, before it was split
71 /// into [`ProcessInfo::cmdline`]. Windows has no argument vector — the
72 /// split is a convention applied by the C runtime — so the unsplit
73 /// form is kept for a caller who needs to apply a different one.
74 cmdline: Option<String>,
75 },
76}
77
78impl ProcessInfo {
79 /// Returns the process ID.
80 pub fn pid(&self) -> u32 {
81 self.pid
82 }
83
84 /// Returns the parent process ID.
85 ///
86 /// The process that created this one, which may have exited since. Unix
87 /// reparents an orphan to `init` and so reports a live process; Windows
88 /// keeps reporting the original creator, whose PID may by then belong to
89 /// something unrelated.
90 pub fn parent_pid(&self) -> Option<u32> {
91 self.ppid
92 }
93
94 /// Returns the process name.
95 ///
96 /// The kernel's short name for the process, not its executable path: Linux
97 /// and the BSDs truncate it, and it reflects whatever the process last set
98 /// rather than what it was launched as.
99 pub fn name(&self) -> &str {
100 &self.name
101 }
102
103 /// Returns when the process started.
104 pub fn start_time(&self) -> StartTime {
105 self.start
106 }
107
108 /// Returns the path to the process executable.
109 pub fn exe(&self) -> Option<path::Path<'_>> {
110 self.exe.as_ref().map(Into::into)
111 }
112
113 /// Returns the process command line.
114 ///
115 /// Best-effort: macOS restricts it to processes owned by the same user.
116 ///
117 /// On Windows this is a reconstruction. The kernel stores one string and
118 /// leaves splitting it to the process, so what is reported here is that
119 /// string split by the MSVC convention that Rust and every C runtime
120 /// follow — which is what the target process almost certainly did with it,
121 /// but not something the system guarantees. [`windows_command_line`] has
122 /// the original.
123 ///
124 /// [`windows_command_line`]: Self::windows_command_line
125 pub fn command_line(&self) -> Option<&[String]> {
126 self.cmdline.as_deref()
127 }
128
129 /// Returns the process command line as a single unsplit string.
130 ///
131 /// Windows only, where this is the form the kernel actually stores.
132 ///
133 /// # Errors
134 ///
135 /// [`ErrorKind::Unsupported`](crate::error::ErrorKind::Unsupported) if the
136 /// record came from a Unix target, where the argument vector is what the
137 /// kernel holds and there is no original string to fall back to.
138 pub fn windows_command_line(&self) -> Result<Option<&str>> {
139 match &self.family {
140 ProcessFamily::Windows { cmdline, .. } => Ok(cmdline.as_deref()),
141 ProcessFamily::Unix(_) => Err(Error::new(
142 ErrorKind::Unsupported,
143 "Unix processes have an argument vector, not a command line string",
144 )),
145 }
146 }
147
148 /// Returns the process working directory.
149 ///
150 /// On Windows this is the process's own bookkeeping rather than something
151 /// the system tracks: NT has no per-process current directory, so what is
152 /// reported is the Win32 one the target keeps in its own memory, read from
153 /// there. A process is free to put anything in that field.
154 pub fn cwd(&self) -> Option<path::Path<'_>> {
155 self.cwd.as_ref().map(Into::into)
156 }
157
158 /// Returns the process's Unix credentials.
159 ///
160 /// The group list holds the supplementary groups alone. The BSDs keep the
161 /// effective group in the first slot of the credential's group array and
162 /// report it that way, so it is dropped here to leave the field meaning
163 /// the same thing on every target — it is reported by
164 /// [`UnixSecurityInfo::effective_gid`] instead.
165 ///
166 /// On macOS the supplementary group list is the kernel credential list,
167 /// capped at `NGROUPS` (16). That is narrower than what
168 /// [`SecurityInfo::current`](crate::security::SecurityInfo::current)
169 /// reports for this process, which resolves extended memberships through
170 /// opendirectoryd — an interface that answers only for the caller — so a
171 /// foreign macOS group list can be a truncated view where the
172 /// current-process one is not.
173 ///
174 /// # Errors
175 ///
176 /// [`ErrorKind::Unsupported`](crate::error::ErrorKind::Unsupported) if the
177 /// record came from a Windows target, which has no such credentials to
178 /// report.
179 pub fn identity(&self) -> Result<Option<&UnixSecurityInfo>> {
180 match &self.family {
181 ProcessFamily::Unix(identity) => Ok(identity.as_ref()),
182 ProcessFamily::Windows { .. } => Err(Error::new(
183 ErrorKind::Unsupported,
184 "Windows processes have no Unix credentials",
185 )),
186 }
187 }
188
189 /// Returns the process's access token information.
190 ///
191 /// `None` where it could not be read, which every route reports the same
192 /// way: a process that refuses to be opened, or one whose token the caller
193 /// has no right to. Nothing about which call produced the record changes
194 /// what is here.
195 ///
196 /// # Errors
197 ///
198 /// [`ErrorKind::Unsupported`](crate::error::ErrorKind::Unsupported) if the
199 /// record came from a Unix target, which has no access tokens.
200 pub fn token(&self) -> Result<Option<&WindowsTokenInfo>> {
201 match &self.family {
202 ProcessFamily::Windows { token, .. } => Ok(token.as_ref()),
203 ProcessFamily::Unix(_) => Err(Error::new(
204 ErrorKind::Unsupported,
205 "Unix processes have no access token",
206 )),
207 }
208 }
209
210 /// Returns how the process ended, or `None` if it was still running.
211 ///
212 /// A process can outlive itself in the table: Unix keeps a zombie until its
213 /// parent reaps it, and Windows keeps an exited process addressable while
214 /// any handle to it remains open. Such a record still reports what the
215 /// kernel holds — the PID, the parent, when it started — while everything
216 /// read out of its address space, the command line and working directory
217 /// included, is gone. This is what distinguishes that from a live process
218 /// whose fields were merely denied.
219 ///
220 /// Only Windows reports a code, for the reason [`ProcessExit`] gives.
221 ///
222 /// `None` is also what a record carries when its process could not be
223 /// examined closely enough to tell, which on Windows means one that could
224 /// not be opened at all.
225 pub fn exit(&self) -> Option<ProcessExit> {
226 self.exit
227 }
228
229 /// Returns the identity of the target session this was captured from.
230 pub fn session(&self) -> Uuid {
231 self.session
232 }
233}
234
235/// How a process this VFS did not spawn ended.
236///
237/// Weaker than [`ProcessStatus`](super::ProcessStatus), which describes a
238/// child. Only Windows can report an exit code for an arbitrary process;
239/// `waitid(P_PIDFD)` and `EVFILT_PROC`'s `NOTE_EXITSTATUS` are both restricted
240/// to the parent, so a Unix target can report only that the process is gone.
241///
242/// The type is the same on every platform rather than being absent on Unix, so
243/// that portable code does not have to branch on the target OS to call
244/// [`Process::wait`].
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246pub struct ProcessExit {
247 pub(crate) code: Option<i32>,
248}
249
250impl ProcessExit {
251 /// Returns the exit code, on a Windows target.
252 pub fn code(self) -> Option<i32> {
253 self.code
254 }
255}
256
257pub(crate) enum ProcessesInner {
258 Client(client::Processes),
259 Direct(direct::Processes),
260}
261
262impl fmt::Debug for Processes {
263 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264 f.debug_struct("Processes").finish_non_exhaustive()
265 }
266}
267
268/// A forward enumeration of the target's process table.
269///
270/// Entries are produced lazily. This matters on Linux, where the per-process
271/// cost is a pair of `/proc` reads, so a search that stops early does not pay
272/// for the whole table.
273pub struct Processes {
274 inner: ProcessesInner,
275}
276
277impl Processes {
278 pub(crate) fn client(processes: client::Processes) -> Self {
279 Self {
280 inner: ProcessesInner::Client(processes),
281 }
282 }
283
284 pub(crate) fn direct(processes: direct::Processes) -> Self {
285 Self {
286 inner: ProcessesInner::Direct(processes),
287 }
288 }
289
290 /// Returns the next process, or `None` once the table is exhausted.
291 ///
292 /// Processes that exit partway through enumeration are skipped rather than
293 /// reported as errors: the table is a moving target on every platform, and
294 /// a caller cannot act on the difference.
295 pub async fn next_entry(&mut self) -> Result<Option<ProcessInfo>> {
296 match &mut self.inner {
297 ProcessesInner::Client(processes) => processes.next_entry().await,
298 ProcessesInner::Direct(processes) => processes.next_entry().await,
299 }
300 }
301}
302
303pub(crate) enum ProcessInner {
304 Client(client::Process),
305 Direct(direct::Process),
306}
307
308impl fmt::Debug for Process {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 f.debug_struct("Process").field("pid", &self.pid).finish()
311 }
312}
313
314/// A handle to a process this VFS did not spawn.
315///
316/// Holding one is what makes the operations below refer to a single process
317/// rather than to whatever currently owns a PID. How strong that guarantee is
318/// varies:
319///
320/// - Linux (`pidfd`) and Windows (a process handle pins the PID against reuse)
321/// are race-free for the whole life of the handle.
322/// - FreeBSD and macOS validate identity at open and cannot maintain it:
323/// `kill(2)` takes a PID, and Capsicum mints process descriptors only through
324/// `pdfork`, so there is no handle to hold for a process this one did not
325/// fork. [`Process::signal`] and [`Process::terminate`] are racy there.
326pub struct Process {
327 pid: u32,
328 inner: ProcessInner,
329}
330
331impl Process {
332 pub(crate) fn client(pid: u32, process: client::Process) -> Self {
333 Self {
334 pid,
335 inner: ProcessInner::Client(process),
336 }
337 }
338
339 pub(crate) fn direct(pid: u32, process: direct::Process) -> Self {
340 Self {
341 pid,
342 inner: ProcessInner::Direct(process),
343 }
344 }
345
346 /// Returns the process ID.
347 ///
348 /// The only attribute projected directly: it is fixed for the life of the
349 /// handle, where everything else has to be re-read to be true.
350 pub fn pid(&self) -> u32 {
351 self.pid
352 }
353
354 /// Takes a fresh snapshot of the process.
355 pub async fn info(&self) -> Result<ProcessInfo> {
356 match &self.inner {
357 ProcessInner::Client(process) => process.info().await,
358 ProcessInner::Direct(process) => process.info().await,
359 }
360 }
361
362 /// Sends a signal to the process.
363 ///
364 /// Fails with [`ErrorKind::Unsupported`](crate::error::ErrorKind::Unsupported)
365 /// on a Windows target. The method exists there regardless, because the
366 /// target may be remote: whether signals exist is a property of the target,
367 /// not of the host this code was compiled for.
368 pub async fn signal(&self, signal: super::Signal) -> Result<()> {
369 match &self.inner {
370 ProcessInner::Client(process) => process.signal(signal).await,
371 ProcessInner::Direct(process) => process.signal(signal).await,
372 }
373 }
374
375 /// Asks the process to terminate.
376 ///
377 /// `SIGTERM` on Unix, `TerminateProcess` on Windows. Unconditional, with no
378 /// grace period and no escalation, unlike
379 /// [`Child::terminate`](super::Child::terminate): the graceful half of that
380 /// path relies on children being spawned into a process group of their own,
381 /// which is not something that can be arranged after the fact. Compose
382 /// `terminate`, [`wait`](Self::wait), a timeout, and [`kill`](Self::kill)
383 /// for grace with escalation.
384 ///
385 /// Note the asymmetry this leaves on Windows, where `TerminateProcess` is
386 /// not a request the target can decline or clean up after.
387 pub async fn terminate(&self) -> Result<()> {
388 match &self.inner {
389 ProcessInner::Client(process) => process.terminate().await,
390 ProcessInner::Direct(process) => process.terminate().await,
391 }
392 }
393
394 /// Kills the process.
395 ///
396 /// `SIGKILL` on Unix, `TerminateProcess` on Windows — the strongest stop
397 /// the target offers, and one the process cannot catch or clean up after.
398 ///
399 /// On a Windows target this is [`terminate`](Self::terminate) under another
400 /// name, since `TerminateProcess` is already unconditional and there is
401 /// nothing harder to escalate to. The pair exists because on Unix the
402 /// distinction is real, and a portable caller should be able to ask for
403 /// either without branching on the target.
404 pub async fn kill(&self) -> Result<()> {
405 match &self.inner {
406 ProcessInner::Client(process) => process.kill().await,
407 ProcessInner::Direct(process) => process.kill().await,
408 }
409 }
410
411 /// Waits for the process to exit.
412 pub async fn wait(&self) -> Result<ProcessExit> {
413 match &self.inner {
414 ProcessInner::Client(process) => process.wait().await,
415 ProcessInner::Direct(process) => process.wait().await,
416 }
417 }
418
419 /// Closes the handle.
420 ///
421 /// Not required: a dropped [`Process`] closes itself, and a remote one is
422 /// released when the session's object table is torn down. This exists so a
423 /// caller can observe a close failure instead of discarding it.
424 pub async fn close(self) -> Result<()> {
425 match self.inner {
426 ProcessInner::Client(process) => process.close().await,
427 ProcessInner::Direct(process) => process.close().await,
428 }
429 }
430}