Skip to main content

running_process_platform_internal/platform/
process.rs

1//! Process spawning, containment, inspection, termination, and stdio.
2
3pub use crate::{
4    assign_child_to_windows_job, cancel_capture_reader, canonical_environment_pairs,
5    capture_reader_done, compat_shell_command, configure_exact_trace, configure_process_command,
6    configure_sync_contained_command, configure_sync_daemon_command, configure_trampoline_command,
7    current_executable_build_id, exact_trace_capability, exit_code, monitor_console_windows,
8    parent_has_console, prepare_capture_reader, set_process_name, shell_command,
9    soft_terminate_process_group, spawn_sync, spawn_sync_daemon, start_descendant_monitor,
10    start_exact_trace, sync_child_native_handle, trampoline_exit_code,
11    unix_mark_extra_fds_close_on_exec, CaptureCancellation, TracedChild, WindowsJobHandle,
12};
13
14#[cfg(feature = "async-process")]
15pub use crate::{
16    PlatformChild, PlatformEmergencySignal, PlatformLifecycle, PlatformOutput, PlatformStdin,
17    SpawnSpec, StreamMode,
18};
19
20#[cfg(feature = "process-inspection")]
21pub use crate::{kill_tree, process_snapshot, process_snapshot_for_pid};
22
23/// Host-neutral command options selected by the caller before spawning.
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
25pub struct ProcessCommandConfig {
26    pub creation_flags: Option<u32>,
27    pub create_process_group: bool,
28    pub nice: Option<i32>,
29    pub address_space_limit_bytes: Option<u64>,
30}
31
32/// Availability of an invasive, lossless launched-tree trace backend.
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct ExactTraceCapability {
35    pub available: bool,
36    pub backend: &'static str,
37    pub reason: &'static str,
38    pub non_invasive_backend: &'static str,
39    pub non_invasive_grade: NonInvasiveObservationGrade,
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum NonInvasiveObservationGrade {
44    KernelNotification,
45    KernelHintReconciled,
46    SnapshotInferred,
47}
48
49/// A raw, bounded spawning-thread capture collected while a tracee is stopped.
50#[derive(Clone, Debug, Default, Eq, PartialEq)]
51pub struct TraceOriginArtifact {
52    pub origin_pid: u32,
53    pub thread_id: u32,
54    pub architecture: String,
55    pub register_format: String,
56    pub executable: Option<std::path::PathBuf>,
57    pub registers: Vec<u8>,
58    pub stack_pointer: Option<u64>,
59    pub instruction_pointer: Option<u64>,
60    pub stack: Vec<u8>,
61    pub truncated: bool,
62    pub module_map: Vec<u8>,
63    pub module_map_truncated: bool,
64}
65
66/// Native launched-tree event produced by an exact trace backend.
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct ExactTraceEvent {
69    pub sequence: u64,
70    pub pid: u32,
71    pub parent_pid: Option<u32>,
72    pub parent_start_key: Option<u64>,
73    pub start_key: Option<u64>,
74    pub timestamp: std::time::SystemTime,
75    pub kind: ExactTraceEventKind,
76    pub executable: Option<std::path::PathBuf>,
77    pub argv: Option<Vec<std::ffi::OsString>>,
78    pub origin: Option<TraceOriginArtifact>,
79}
80
81#[derive(Clone, Debug, Eq, PartialEq)]
82pub enum ExactTraceEventKind {
83    Spawn,
84    Exec,
85    Exit {
86        exit_code: Option<i32>,
87        signal: Option<i32>,
88        raw_status: i64,
89    },
90    Loss {
91        reason: String,
92    },
93}
94
95/// A descendant lifecycle fact reported by the host monitor.
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub enum DescendantEvent {
98    Started {
99        pid: u32,
100        /// Immediate parent of the new descendant, when the discovery
101        /// mechanism knows it: the Linux `children`-file walk and the
102        /// macOS process-snapshot inversion both do; the Windows job
103        /// IOCP notification is PID-only, so it reports `None` rather
104        /// than paying a racy toolhelp scan per event.
105        parent_pid: Option<u32>,
106    },
107    Exited(u32),
108    /// The platform backend has completed its final reconciliation and no
109    /// further descendant events can arrive.
110    Completed,
111}
112
113/// Shared cancellation handle for a host-native descendant monitor.
114pub struct DescendantMonitorStop {
115    stopped: std::sync::atomic::AtomicBool,
116    mutex: std::sync::Mutex<()>,
117    wake: std::sync::Condvar,
118}
119
120impl DescendantMonitorStop {
121    /// Create an untriggered monitor cancellation handle.
122    pub fn new() -> Self {
123        Self {
124            stopped: std::sync::atomic::AtomicBool::new(false),
125            mutex: std::sync::Mutex::new(()),
126            wake: std::sync::Condvar::new(),
127        }
128    }
129
130    /// Report whether monitoring was cancelled.
131    pub fn is_stopped(&self) -> bool {
132        self.stopped.load(std::sync::atomic::Ordering::Acquire)
133    }
134
135    /// Cancel monitoring and wake a sleeping monitor immediately.
136    pub fn stop(&self) {
137        let _guard = self.mutex.lock().unwrap_or_else(|error| error.into_inner());
138        if !self.stopped.swap(true, std::sync::atomic::Ordering::AcqRel) {
139            self.wake.notify_all();
140        }
141    }
142
143    /// Wait until cancelled or `timeout` expires, returning whether cancelled.
144    pub fn wait_timeout(&self, timeout: std::time::Duration) -> bool {
145        if self.is_stopped() {
146            return true;
147        }
148        let guard = self.mutex.lock().unwrap_or_else(|error| error.into_inner());
149        if self.is_stopped() {
150            return true;
151        }
152        let (_guard, _wait_result) = self
153            .wake
154            .wait_timeout(guard, timeout)
155            .unwrap_or_else(|error| error.into_inner());
156        self.is_stopped()
157    }
158}
159
160impl Default for DescendantMonitorStop {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166/// Identifies one captured child output stream.
167#[derive(Clone, Copy)]
168pub enum CaptureStream {
169    Stdout,
170    Stderr,
171}
172
173/// Metadata about one visible window observed by console-popup monitoring.
174#[derive(Debug, Clone)]
175pub struct ConsoleWindowInfo {
176    pub pid: u32,
177    pub title: String,
178    pub hwnd: u64,
179}
180
181/// A platform-owned identity record used when observing a process tree.
182/// The timestamp fields are opaque host-native creation-time components and
183/// must only be compared for equality.
184#[derive(Clone, Copy, Debug, Eq, PartialEq)]
185pub struct ProcessSnapshot {
186    pub pid: u32,
187    pub parent_pid: u32,
188    pub start_time_a: u64,
189    pub start_time_b: u64,
190}
191
192/// Environment base selected by the shared caller for a synchronous spawn.
193///
194/// Explicit `Command::env` additions and removals remain on the command and
195/// are applied after this base by the selected platform implementation.
196#[derive(Clone, Debug, Eq, PartialEq)]
197pub enum SyncEnvironment {
198    /// Start with the spawning process's ambient environment.
199    Inherit,
200    /// Start with this complete, caller-assembled base environment.
201    Explicit(Vec<(std::ffi::OsString, std::ffi::OsString)>),
202}
203
204/// Caller-supplied stdio bindings for a contained synchronous child.
205///
206/// Each stream is independently configured. `drain_timeout` bounds how long
207/// wrapper-owned pipe ends remain open after the child exits; `None` leaves
208/// pipe closure entirely to the caller. `show_console` only affects Windows.
209pub struct SpawnStdio<'a> {
210    /// Child standard input source.
211    pub stdin: StdioSource<'a>,
212    /// Child standard output destination.
213    pub stdout: StdioSource<'a>,
214    /// Child standard error destination.
215    pub stderr: StdioSource<'a>,
216    /// Maximum post-exit pipe drain interval.
217    pub drain_timeout: Option<std::time::Duration>,
218    /// Whether a Windows child may inherit or allocate a visible console.
219    pub show_console: bool,
220}
221
222impl Default for SpawnStdio<'_> {
223    fn default() -> Self {
224        Self {
225            stdin: StdioSource::Null,
226            stdout: StdioSource::Parent,
227            stderr: StdioSource::Parent,
228            drain_timeout: Some(std::time::Duration::from_secs(2)),
229            show_console: false,
230        }
231    }
232}
233
234/// Caller-supplied output bindings for a detached synchronous child.
235///
236/// Detached children may write only to the platform null device or to a
237/// caller-owned file. Parent stdio and anonymous pipes are intentionally not
238/// available because either can retain or depend on the launching process.
239pub struct DaemonStdio<'a> {
240    /// Child standard output destination.
241    pub stdout: DaemonStdioSource<'a>,
242    /// Child standard error destination.
243    pub stderr: DaemonStdioSource<'a>,
244}
245
246impl Default for DaemonStdio<'_> {
247    fn default() -> Self {
248        Self {
249            stdout: DaemonStdioSource::Null,
250            stderr: DaemonStdioSource::Null,
251        }
252    }
253}
254
255/// Output destination accepted by the detached-child path.
256pub enum DaemonStdioSource<'a> {
257    /// Route output to the platform null device.
258    Null,
259    /// Duplicate a caller-owned file into the child.
260    File(&'a std::fs::File),
261}
262
263/// Standard-stream source or destination for a contained child.
264pub enum StdioSource<'a> {
265    /// Route the stream to the platform null device.
266    Null,
267    /// Inherit the matching stream from the parent process.
268    Parent,
269    /// Duplicate a caller-owned file into the child.
270    File(&'a std::fs::File),
271    /// Create and return an anonymous parent/child pipe pair.
272    Pipe,
273}
274
275/// Handle for a detached child that is not terminated when dropped.
276pub struct DaemonChild {
277    pub(crate) pid: u32,
278    pub(crate) inner: Box<dyn DaemonChildControl>,
279}
280
281pub(crate) trait DaemonChildControl:
282    Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe
283{
284    fn kill(&mut self) -> std::io::Result<()>;
285    fn wait(&mut self) -> std::io::Result<i32>;
286    fn try_wait(&mut self) -> std::io::Result<Option<i32>>;
287}
288
289impl DaemonChild {
290    /// Return the operating-system process identifier.
291    pub fn id(&self) -> u32 {
292        self.pid
293    }
294
295    /// Terminate the child process.
296    pub fn kill(&mut self) -> std::io::Result<()> {
297        self.inner.kill()
298    }
299
300    /// Wait for the child and return its numeric exit code.
301    pub fn wait(&mut self) -> std::io::Result<i32> {
302        self.inner.wait()
303    }
304
305    /// Return the exit code if the child has finished without blocking.
306    pub fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
307        self.inner.try_wait()
308    }
309}
310
311/// Handle and optional parent pipe ends for a contained child.
312///
313/// Dropping this value shuts down the contained process group.
314pub struct SpawnedChild {
315    /// Writable parent end when standard input was configured as a pipe.
316    pub stdin: Option<std::process::ChildStdin>,
317    /// Readable parent end when standard output was configured as a pipe.
318    pub stdout: Option<std::process::ChildStdout>,
319    /// Readable parent end when standard error was configured as a pipe.
320    pub stderr: Option<std::process::ChildStderr>,
321    pub(crate) pid: u32,
322    pub(crate) inner: Box<dyn SpawnedChildControl>,
323}
324
325pub(crate) trait SpawnedChildControl:
326    Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe
327{
328    fn kill(&mut self) -> std::io::Result<()>;
329    fn wait(&mut self) -> std::io::Result<i32>;
330    fn try_wait(&mut self) -> std::io::Result<Option<i32>>;
331    fn shutdown(&mut self);
332}
333
334impl SpawnedChild {
335    /// Return the operating-system process identifier.
336    pub fn id(&self) -> u32 {
337        self.pid
338    }
339
340    /// Forcibly terminate the child on a best-effort basis.
341    pub fn kill(&mut self) -> std::io::Result<()> {
342        self.inner.kill()
343    }
344
345    /// Wait for the child and return its numeric exit code.
346    pub fn wait(&mut self) -> std::io::Result<i32> {
347        self.inner.wait()
348    }
349
350    /// Return the exit code if the child has finished without blocking.
351    pub fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
352        self.inner.try_wait()
353    }
354}
355
356impl Drop for SpawnedChild {
357    fn drop(&mut self) {
358        self.inner.shutdown();
359    }
360}
361
362#[derive(Clone, Copy)]
363pub enum ObserverScope {
364    SystemWide,
365    LaunchedProcessTree,
366}
367#[derive(Clone, Copy)]
368pub enum ObserverCategory {
369    File,
370    Network,
371    Process,
372}
373#[derive(Clone, Copy)]
374pub enum ObserverSupport {
375    Supported,
376    Partial,
377    Unavailable,
378}
379#[derive(Clone, Copy)]
380pub struct ObserverBackend {
381    pub support: ObserverSupport,
382    pub backend: &'static str,
383    pub reason: &'static str,
384}
385pub use crate::platform_imp::observer_backend;
386pub use crate::platform_imp::read_process_cmdline;
387pub use crate::platform_imp::read_process_file_handles;
388
389/// Platform-neutral Unix signal selectors used by the compatibility facade.
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391pub enum UnixSignalKind {
392    Interrupt,
393    Terminate,
394    Kill,
395}
396
397pub use crate::{
398    unix_set_priority, unix_signal_process, unix_signal_process_group, unix_signal_raw,
399};
400
401/// What this host installed so a child outlives its owner no longer than it
402/// should.
403///
404/// The variants name the *guarantee*, not the call that produced it. A caller
405/// deciding whether to spawn a supervisor cares that the kernel will not do
406/// the reaping for it; whether the kernel would have used a parent-death
407/// signal or a job object is not a distinction it can act on.
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub enum OwnerDeathCleanup {
410    /// The kernel signals this process when its owner exits.
411    OwnerDeathSignal,
412    /// This process belongs to a container the kernel destroys with its owner.
413    KillOnOwnerHandleClose,
414    /// This process was already in such a container, installed by someone else.
415    AlreadyContained,
416    /// The host offers no kernel mechanism; a supervisor must do the reaping.
417    SupervisorRequired,
418    /// The host offers nothing and no supervisor contract is defined here.
419    Unsupported,
420}
421
422/// Which step of installing owner-death containment failed.
423///
424/// The caller's operator-facing messages distinguish these, and rightly: not
425/// being allowed to *build* a container is a different situation from
426/// building one and not being allowed to *join* it. Collapsing both into one
427/// error would make the two indistinguishable in a log, so the stage travels
428/// with the error rather than being inferred from the host.
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub enum OwnerDeathCleanupStage {
431    /// Asking the kernel to signal this process when its owner exits.
432    RequestSignal,
433    /// Creating the container that the kernel destroys with its owner.
434    CreateContainer,
435    /// Placing this process inside that container.
436    JoinContainer,
437}
438
439/// A failure to install owner-death containment, and the step it failed at.
440#[derive(Debug)]
441pub struct OwnerDeathCleanupError {
442    /// The step that failed.
443    pub stage: OwnerDeathCleanupStage,
444    /// What the host reported.
445    pub source: std::io::Error,
446}
447
448impl std::fmt::Display for OwnerDeathCleanupError {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        write!(f, "{:?}: {}", self.stage, self.source)
451    }
452}
453
454impl std::error::Error for OwnerDeathCleanupError {
455    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
456        Some(&self.source)
457    }
458}
459
460pub use crate::{
461    process_install_owner_death_cleanup as install_owner_death_cleanup,
462    process_owner_death_cleanup_target as owner_death_cleanup_target,
463};
464
465/// Why a host could not answer a question about a process.
466///
467/// The three named cases are the ones a caller can act on: a PID that could
468/// never name a process, a process that is not there, and a question this
469/// host does not answer. Everything else is the host's own report, kept
470/// whole rather than flattened into one of the three.
471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
472pub enum ProcessInspectErrorKind {
473    /// The PID is outside the range this host issues.
474    InvalidPid,
475    /// No process on this host currently has that PID.
476    NotFound,
477    /// This host has no such primitive.
478    Unsupported,
479    /// The host was asked and refused, or failed.
480    Host,
481}
482
483/// A failure to inspect or signal a process, and what kind of failure it was.
484#[derive(Debug)]
485pub struct ProcessInspectError {
486    /// Which of the four situations this is.
487    pub kind: ProcessInspectErrorKind,
488    /// What the host reported.
489    pub source: std::io::Error,
490}
491
492impl ProcessInspectError {
493    /// Build an error of `kind` carrying the host's last reported error.
494    pub fn last_os_error(kind: ProcessInspectErrorKind) -> Self {
495        Self {
496            kind,
497            source: std::io::Error::last_os_error(),
498        }
499    }
500
501    /// Build an error of `kind` with a message this crate composed itself.
502    pub fn stated(kind: ProcessInspectErrorKind, message: &str) -> Self {
503        Self {
504            kind,
505            source: std::io::Error::other(message.to_string()),
506        }
507    }
508}
509
510impl std::fmt::Display for ProcessInspectError {
511    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
512        write!(f, "{:?}: {}", self.kind, self.source)
513    }
514}
515
516impl std::error::Error for ProcessInspectError {
517    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
518        Some(&self.source)
519    }
520}
521
522pub use crate::{
523    process_executable_path as executable_path, process_force_kill as force_kill,
524    process_same_executable_path as same_executable_path,
525    process_signal_terminate as signal_terminate, ProcessLiveness,
526};
527
528/// A standing request from the host that this process shut down.
529///
530/// Hosts deliver this differently -- a POSIX signal, a Windows console
531/// control event injected on a thread of the OS's choosing -- but both arrive
532/// in a context where almost nothing is safe to do. A handler may not
533/// allocate, log, take a lock, or join a thread. So neither host runs the
534/// caller's code: each sets one flag, and the caller reads it whenever it is
535/// somewhere it can act.
536///
537/// That is why this is a poll rather than a callback. A callback would invite
538/// exactly the work the delivery context forbids.
539pub struct ShutdownRequest {
540    flag: &'static std::sync::atomic::AtomicBool,
541}
542
543impl ShutdownRequest {
544    /// Build a handle watching a flag the caller already owns.
545    ///
546    /// The host implementations use this to hand out a view of their own
547    /// static. It is public because a caller that already has a shutdown flag
548    /// -- one set by a supervisor protocol, or by a test -- can present it
549    /// through the same type rather than the loop it feeds needing two shapes
550    /// of "should I stop".
551    ///
552    /// `'static` is not incidental: a handler set by the OS outlives any
553    /// scope, so the flag it writes has to as well.
554    pub fn watching(flag: &'static std::sync::atomic::AtomicBool) -> Self {
555        Self { flag }
556    }
557
558    /// Whether the host has asked this process to shut down.
559    ///
560    /// Latching, not edge-triggered: once true it stays true, so a caller that
561    /// checks between two pieces of work cannot miss a request delivered while
562    /// it was busy.
563    pub fn requested(&self) -> bool {
564        self.flag.load(std::sync::atomic::Ordering::Relaxed)
565    }
566}
567
568impl std::fmt::Debug for ShutdownRequest {
569    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570        f.debug_struct("ShutdownRequest")
571            .field("requested", &self.requested())
572            .finish()
573    }
574}
575
576pub use crate::process_install_shutdown_request_handler as install_shutdown_request_handler;
577
578/// Whether this host can replace the running image with another program.
579///
580/// Unix can: `execve` keeps the process -- its PID, its open descriptors,
581/// its place in the process tree -- and swaps the program underneath.
582/// Windows has no equivalent; the nearest thing is starting a successor and
583/// exiting, which is a *different* process with a different PID and does not
584/// keep anything a parent or supervisor was holding onto.
585///
586/// Callers that can accept a successor should ask this and fall back. Callers
587/// that genuinely need the same process to continue have no fallback, and
588/// should treat `false` as unsupported rather than approximating it.
589pub use crate::{
590    process_can_replace_current_image as can_replace_current_image,
591    process_replace_current_image as replace_current_image,
592};