Skip to main content

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}