Skip to main content

running_process/
lib.rs

1//! Cross-platform process execution, process-tree control, PTY handling, and
2//! broker integration primitives.
3//!
4//! The crate exposes a synchronous process API through [`NativeProcess`], a
5//! contained process-group helper through [`ContainedProcessGroup`], low-level
6//! spawn helpers through [`spawn()`] and [`spawn_daemon`], and optional
7//! daemon/broker modules behind feature flags.
8
9use std::collections::VecDeque;
10use std::io::Read;
11use std::process::{Child, ChildStdin, Command, Stdio};
12use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
13use std::sync::{Arc, Condvar, Mutex};
14use std::thread;
15use std::time::{Duration, Instant};
16
17use crate::observer::{ObserverEmitter, ProcessWatchEmitter};
18
19/// Explicit foreground commands preserving caller-controlled native launch state.
20pub use running_process_platform_internal::foreground;
21pub(crate) use running_process_platform_internal::platform;
22
23#[cfg(feature = "async-process")]
24mod async_process;
25#[cfg(feature = "async-process")]
26mod blocking_island;
27#[cfg(feature = "async-process")]
28pub use blocking_island::dispatch_blocking as blocking_island_dispatch;
29pub mod console_detect;
30pub mod containment;
31mod descendant_monitor;
32pub mod env_vars;
33pub mod environment;
34mod helpers;
35#[cfg(feature = "async-process")]
36mod process_runtime;
37#[cfg(feature = "window-icon")]
38pub mod window_icon;
39// Phase 1 of #221: process-observation capability model + portable
40// lifecycle baseline. Core-feature-clean (std-only: mpsc + SystemTime),
41// so the started/exited baseline is available to the base library
42// without pulling in the daemon runtime.
43/// Frozen v1 daemon manifest and service-definition registration substrate.
44///
45/// This direct persistence surface owns only the v1 registration records,
46/// SHA-256 seal/verify rules, host stamp, validated paths, and private-file
47/// behavior. It deliberately does not select an IPC endpoint, broker client,
48/// daemon runtime, identity probe, or async runtime.
49#[cfg(feature = "daemon-registration")]
50pub mod daemon_registration;
51/// Frozen v1 semantic registration compatibility contract.
52#[cfg(feature = "daemon-registration")]
53pub mod daemon_registration_compat;
54/// Frozen v2 service-definition registration writer substrate.
55///
56/// This direct persistence surface owns the established `.servicedef.v2`
57/// layout, generated service definition, validation, and owner-private file
58/// behavior. It deliberately does not select v2 manifests, a loader, broker
59/// negotiation, endpoint transport, identity, or an async runtime.
60#[cfg(feature = "daemon-registration-v2")]
61pub mod daemon_registration_v2;
62/// Limited shared-broker v2 registration compatibility contract.
63#[cfg(feature = "daemon-registration-v2")]
64pub mod daemon_registration_v2_compat;
65// The two registration writer features share only the small path, name, error,
66// and owner-private-directory substrate. Keeping it separate from either
67// public module prevents v2 persistence from selecting v1's SHA-256 manifest
68// support, while retaining exact v1 type identity through re-exports.
69/// Canonical semantic v1 frame compatibility contract, retaining raw values.
70#[cfg(feature = "frame-v1-codec")]
71pub mod daemon_frame_v1;
72#[cfg(any(feature = "daemon-registration", feature = "daemon-registration-v2"))]
73pub(crate) mod daemon_registration_common;
74/// Frozen v1 `Frame` envelope codec and consumer-protocol registry.
75///
76/// This direct, transport-free surface is available without broker IPC,
77/// daemon identity, hashing, or an async runtime. Broad broker paths
78/// re-export these exact items for compatibility.
79#[cfg(feature = "frame-v1-codec")]
80pub mod frame_v1;
81// Host facts are shared by the direct identity probe and persisted v1
82// registration. The implementation is deliberately private; registration
83// exposes its stable public host-identity path from `daemon_registration`.
84#[cfg(any(feature = "backend-identity", feature = "daemon-registration"))]
85#[path = "broker/host_identity.rs"]
86pub(crate) mod daemon_host_identity;
87pub mod observer;
88#[cfg(feature = "originator-scan")]
89pub mod originator;
90pub mod output_log;
91// The IPC client owns the generated protocol dependency.  Keeping code
92// generation in that optional package means process-only consumers do not
93// compile broker schemas or their build dependencies (#1144).
94#[cfg(feature = "client")]
95/// Prost-generated daemon protocol types used by the client transport.
96pub mod proto {
97    /// Generated Rust bindings for the `running_process.daemon.v1` protobuf package.
98    pub use running_process_protocol::daemon;
99}
100
101#[cfg(feature = "client")]
102pub mod client;
103
104// Phase 0 of #228: v1 broker module — prost-generated wire types from
105// `proto/broker_v1_*.proto`. The broad broker remains a `client` API. The
106// narrow identity substrate compiles this module privately so its direct
107// facade can preserve the frozen v1 probe/frame bytes without exposing broker
108// ownership, configuration, or client APIs.
109#[cfg(feature = "client")]
110pub mod broker;
111// The direct facade imports a deliberately small subset of the legacy
112// namespace while its compatibility re-exports remain available for type
113// identity.  The remaining client-only paths are intentionally dormant here.
114#[cfg(all(feature = "backend-identity", not(feature = "client")))]
115#[allow(dead_code, unused_imports)]
116mod broker;
117
118/// Direct daemon-identity substrate for an existing endpoint.
119///
120/// This is intentionally a small facade over the frozen v1 identity probe,
121/// sidecar, and sans-I/O endpoint mux. It does not adopt the broker client or
122/// daemon runtime, and it leaves endpoint naming and application payloads to
123/// the caller.
124#[cfg(feature = "backend-identity")]
125pub mod backend_identity;
126
127// #891: content-hash primitive (`blake3_file`) for dev daemon-identity
128// isolation. The direct identity facade needs it internally, but it remains a
129// public client-only utility rather than widening the direct facade.
130#[cfg(feature = "client")]
131pub mod content_hash;
132#[cfg(all(feature = "backend-identity", not(feature = "client")))]
133mod content_hash;
134
135/// Probe client facade (#633). Gated on the `probe` feature so a build
136/// without it contains none of this code.
137#[cfg(feature = "probe")]
138pub mod probe;
139
140// Phase 1 of #228 (issue #230): maintenance subcommands exposed via
141// the `runpm` CLI. Currently just `release-handles` — a cross-platform
142// scaffold for the Windows worktree-teardown handle-race fix
143// (soldr#710). Gated behind `feature = "client"` because the CLI that
144// drives it is.
145#[cfg(feature = "client")]
146pub mod maintenance;
147
148#[cfg(feature = "client")]
149pub mod cleanup;
150
151// Phase 4 of #222 (#427): per-OS boot autostart for the runpm daemon.
152// Gated behind `feature = "client"` because the only consumer is the
153// `runpm` CLI binary, which is itself client-gated.
154#[cfg(feature = "client")]
155pub mod boot_autostart;
156
157// Phase 5 of #222 (#428): `runpm.toml` parser used by the `runpm` CLI
158// to batch-start `[[app]]` entries. Lives in the library (not under
159// `src/bin/`) so the integration test in `tests/runpm/runpm_toml_config.rs`
160// can drive the same code path the binary uses.
161#[cfg(feature = "client")]
162pub mod runpm_config;
163
164// #415: consumer-consumable conformance test kit. Gated behind the
165// off-by-default `test-support` cargo feature (which implies `client`)
166// so the helpers ship in the published crate but only compile when a
167// consumer opts in as a dev-dependency.
168#[cfg(feature = "test-support")]
169pub mod test_support;
170
171// Lightweight tee sink primitives for callers that want transcript/log
172// fan-out without pulling in the full daemon runtime.
173//
174// The file lives under `daemon/` because that is who else uses it, and the
175// `daemon` feature loads it there as `daemon::telemetry`. Declaring it as a
176// module here too would load one file as two modules -- two copies of every
177// type, which are then not the same type -- so when both features are on this
178// re-exports the daemon's module instead of declaring a second one.
179#[cfg(all(feature = "telemetry", not(feature = "daemon")))]
180#[path = "daemon/telemetry.rs"]
181pub mod telemetry;
182
183#[cfg(all(feature = "telemetry", feature = "daemon"))]
184pub use daemon::telemetry;
185
186/// `telemetry` and `daemon::telemetry` must name one module, not two copies.
187///
188/// A `#[path]` module declaration alongside the daemon's own would compile --
189/// that was the bug -- but it would mint a second, incompatible set of types
190/// from the same source file, so a `TeeHandle` obtained through one path
191/// could not be passed to a function expecting the other. This conversion is
192/// the identity only while both paths resolve to the same item; if the
193/// duplicate declaration ever comes back, it stops compiling here rather than
194/// at whichever caller first tried to mix the two.
195#[cfg(all(feature = "telemetry", feature = "daemon"))]
196const _: fn(crate::telemetry::TeeHandle) -> daemon::telemetry::TeeHandle = |handle| handle;
197
198// Wave 5 of #165: daemon runtime absorbed from `running-process-daemon`.
199// Heavy deps (tokio, sqlite, etc.) gated behind `feature = "daemon"`.
200#[cfg(feature = "daemon")]
201/// Daemon runtime APIs and helpers enabled by the `daemon` feature.
202pub mod daemon;
203// `kill_tree` is established 4.x containment surface and remains available to
204// `default-features = false` callers. Its sysinfo-backed platform primitive is
205// the explicit Phase 0.5 compatibility exception; public inspection APIs stay
206// behind `process-inspection`.
207#[cfg(feature = "independent-spawn")]
208pub mod independent_spawn;
209pub mod process_tree;
210#[cfg(feature = "pty")]
211/// PTY-backed process APIs.
212pub mod pty;
213mod public_symbols;
214mod rust_debug;
215pub mod spawn;
216mod spawn_contract;
217pub use spawn_contract::{IndependentBackend, SpawnLifetime, SpawnMode, SpawnOptions};
218#[cfg(feature = "independent-spawn")]
219mod spawn_dispatch;
220#[cfg(feature = "independent-spawn")]
221pub use spawn_dispatch::{spawn_with_options, SpawnExit, SpawnHandle};
222pub mod systemd_killmode;
223#[cfg(feature = "terminal-graphics")]
224pub mod terminal_graphics;
225mod types;
226#[cfg(unix)]
227mod unix;
228#[cfg(windows)]
229mod windows;
230
231#[cfg(feature = "async-process")]
232pub use async_process::{
233    AsyncCapturedOutput, AsyncProcess, AsyncProcessBuilder, AsyncProcessSession,
234    AsyncProcessSessionChunk, AsyncProcessSessionControl, AsyncProcessSessionEvent,
235    AsyncProcessSessionOptions, AsyncProcessSessionOutput, AsyncStdio, ProcessTreeKill,
236};
237pub use console_detect::{monitor_console_windows, ConsoleWindowInfo};
238pub use containment::{ContainedProcessGroup, ORIGINATOR_ENV_VAR};
239// #891: content-hash primitive for dev daemon-identity isolation.
240#[cfg(feature = "client")]
241pub use content_hash::blake3_file;
242pub use observer::{
243    CapabilitySupport, CaptureSource, CategoryCapability, DumpResult, EventCategory,
244    ObservationGrade, ObservationPolicy, ObserverCapabilities, ObserverConfig, ObserverEvent,
245    ObserverEventKind, ObserverSubscriber, ProcessEvent, ProcessEventKind, ProcessIdentity,
246    ProcessObservation, ProcessObservationCapabilities, ProcessObservationError, ProcessWatch,
247    ProcessWatchConfigurationError, ProcessWatchCursor, ProcessWatchGap, ProcessWatchLoss,
248    ProcessWatchMatch, ProcessWatchRead, ProcessWatchSubscriber, StackCapture, StackDump,
249};
250#[cfg(feature = "originator-scan")]
251pub use originator::{
252    find_declared_daemon_pids, find_processes_by_originator, OriginatorProcessInfo,
253};
254pub use output_log::{
255    CursorRead, OutputCursor, OutputLog, OutputRecord, SharedOutputCursor, SharedOutputLog,
256};
257/// Executable naming and image-relative discovery, for binaries in this
258/// workspace that must name a sibling program without spelling it per host.
259#[doc(hidden)]
260pub use running_process_platform_internal::platform::executable as platform_executable;
261#[cfg(target_os = "linux")]
262pub use running_process_platform_internal::platform::process::current_executable_build_id;
263/// Canonical native process-inspection errors, preserving their host detail.
264pub use running_process_platform_internal::platform::process::{
265    ProcessInspectError, ProcessInspectErrorKind,
266};
267/// Resolve the current executable image for a live PID.
268pub use running_process_platform_internal::process_executable_path;
269/// Compare executable path spellings using the host-native policy.
270pub use running_process_platform_internal::process_same_executable_path;
271/// Retained native process-liveness observation.
272pub use running_process_platform_internal::ProcessLiveness;
273pub use rust_debug::{render_rust_debug_traces, RustDebugScopeGuard};
274pub use spawn::{
275    spawn, spawn_daemon, spawn_daemon_breaking_away_from_job,
276    spawn_daemon_breaking_away_with_env_policy, spawn_daemon_with_clear_env,
277    spawn_daemon_with_env_policy, spawn_daemon_with_environment,
278    spawn_daemon_with_explicit_environment, spawn_daemon_with_stdio,
279    spawn_daemon_with_stdio_and_env_policy, spawn_with_env_policy, spawn_with_environment,
280    spawn_with_explicit_environment, DaemonChild, DaemonStdio, DaemonStdioSource,
281    EnvironmentPolicy, SpawnStdio, SpawnedChild, SpawnedChildControl, StdioSource, SyncEnvironment,
282    DAEMON_MARKER_ENV_VAR,
283};
284#[cfg(feature = "client-async")]
285pub use spawn::{spawn_tokio, TokioSpawnOptions};
286#[cfg(feature = "terminal-graphics")]
287pub use terminal_graphics::{
288    current_terminal_capabilities, current_terminal_capabilities_with_timeout,
289    detect_terminal_capabilities, CapabilityStatus, EvidenceStrength, GraphicsCapability,
290    GraphicsProtocol, TerminalCapabilities, TerminalCapabilityInput, TerminalGraphicsCapabilities,
291    TerminalProbeEvidence,
292};
293pub use types::{
294    CommandSpec, ProcessConfig, ProcessError, ReadStatus, RunOutput, StderrMode, StdinMode,
295    StreamEvent, StreamKind,
296};
297#[cfg(feature = "window-icon")]
298pub use window_icon::{
299    host_icon_support, icon_support, set_host_icon, set_icon, IconError, IconScope, IconSource,
300    IconSupport, StockIcon,
301};
302
303#[cfg(unix)]
304pub(crate) use helpers::{child_try_wait_error_is_retryable, poll_mutex_until};
305pub(crate) use helpers::{exit_code, feed_chunk, kill_drain_deadline, log_spawned_child_pid};
306/// Convert a native process exit status to the portable integer convention.
307pub use running_process_platform_internal::exit_code as native_exit_code;
308pub use running_process_platform_internal::ProcessPriority;
309#[cfg(feature = "async-process")]
310pub use running_process_platform_internal::SpawnAdmission;
311#[cfg(unix)]
312pub use unix::{unix_set_priority, unix_signal_process, unix_signal_process_group, UnixSignal};
313#[cfg(windows)]
314pub(crate) use windows::{assign_child_to_windows_kill_on_close_job_impl, WindowsJobHandle};
315
316#[macro_export]
317/// Create a scoped Rust debug trace label for the current function body.
318macro_rules! rp_rust_debug_scope {
319    ($label:expr) => {
320        let _running_process_rust_debug_scope =
321            $crate::RustDebugScopeGuard::enter($label, file!(), line!());
322    };
323}
324
325#[derive(Default)]
326struct QueueState {
327    stdout_queue: VecDeque<Vec<u8>>,
328    stderr_queue: VecDeque<Vec<u8>>,
329    combined_queue: VecDeque<StreamEvent>,
330    stdout_history: VecDeque<Vec<u8>>,
331    stderr_history: VecDeque<Vec<u8>>,
332    combined_history: VecDeque<StreamEvent>,
333    /// Byte-exact stream chunks. Unlike the logical line queues these retain
334    /// delimiters, unterminated tails, and non-UTF-8 bytes; callers consume
335    /// them with `drain_stream_raw`.
336    stdout_raw: VecDeque<Vec<u8>>,
337    stderr_raw: VecDeque<Vec<u8>>,
338    stdout_raw_bytes: usize,
339    stderr_raw_bytes: usize,
340    stdout_history_bytes: usize,
341    stderr_history_bytes: usize,
342    combined_history_bytes: usize,
343    stdout_closed: bool,
344    stderr_closed: bool,
345}
346
347/// Sentinel value for returncode atomic: process has not exited yet.
348const RETURNCODE_NOT_SET: i64 = i64::MIN;
349
350struct SharedState {
351    queues: Mutex<QueueState>,
352    condvar: Condvar,
353    capture_limit: Option<usize>,
354    capture_overflowed: AtomicBool,
355    active_capture_readers: std::sync::atomic::AtomicUsize,
356    /// Atomic exit code. `RETURNCODE_NOT_SET` means "not exited yet".
357    /// Updated by a background waiter thread — reading is lock-free.
358    returncode: AtomicI64,
359    /// Phase 1 of #221: optional lifecycle-event emitter. `None` means
360    /// observation is off (the off-by-default path), so the lifecycle
361    /// hooks are inert. When `Some`, `started` is emitted once at spawn
362    /// and `exited` exactly once on the first returncode transition.
363    observer: Option<ObserverEmitter>,
364    /// Guards against emitting more than one `exited` event when several
365    /// code paths (waiter thread, `poll`, `kill`) race to record the exit.
366    observer_exit_emitted: AtomicBool,
367}
368
369struct ChildState {
370    child: ChildHandle,
371    #[cfg(windows)]
372    _job: WindowsJobHandle,
373}
374
375enum ChildHandle {
376    Standard(Child),
377    ExactTrace(running_process_platform_internal::platform::process::TracedChild),
378}
379
380impl ChildHandle {
381    fn id(&self) -> u32 {
382        match self {
383            Self::Standard(child) => child.id(),
384            Self::ExactTrace(child) => child.id(),
385        }
386    }
387
388    fn try_wait_code(&mut self) -> std::io::Result<Option<i32>> {
389        match self {
390            Self::Standard(child) => child.try_wait().map(|status| status.map(exit_code)),
391            Self::ExactTrace(child) => child.try_wait_code(),
392        }
393    }
394
395    fn kill(&mut self) -> std::io::Result<()> {
396        match self {
397            Self::Standard(child) => child.kill(),
398            Self::ExactTrace(child) => child.kill(),
399        }
400    }
401
402    fn take_stdin(&mut self) -> Option<ChildStdin> {
403        match self {
404            Self::Standard(child) => child.stdin.take(),
405            Self::ExactTrace(child) => child.take_stdin(),
406        }
407    }
408
409    fn take_stdout(&mut self) -> Option<std::process::ChildStdout> {
410        match self {
411            Self::Standard(child) => child.stdout.take(),
412            Self::ExactTrace(child) => child.take_stdout(),
413        }
414    }
415
416    fn take_stderr(&mut self) -> Option<std::process::ChildStderr> {
417        match self {
418            Self::Standard(child) => child.stderr.take(),
419            Self::ExactTrace(child) => child.take_stderr(),
420        }
421    }
422
423    #[cfg(windows)]
424    fn wait_code(&mut self) -> std::io::Result<i32> {
425        match self {
426            Self::Standard(child) => child.wait().map(exit_code),
427            Self::ExactTrace(child) => child.wait_code(),
428        }
429    }
430}
431
432#[cfg(test)]
433#[derive(Debug, Eq, PartialEq)]
434enum CapturePollAction {
435    Wait,
436    Read,
437    Cancel,
438}
439
440#[cfg(test)]
441fn capture_poll_action(capture_revents: i16, wake_revents: i16) -> CapturePollAction {
442    if wake_revents != 0 {
443        CapturePollAction::Cancel
444    } else if capture_revents != 0 {
445        CapturePollAction::Read
446    } else {
447        CapturePollAction::Wait
448    }
449}
450
451fn cleanup_child_after_start_error(child: ChildHandle) {
452    match child {
453        ChildHandle::Standard(mut child) => {
454            let _ = child.kill();
455            // Keep start bounded while retaining ownership until the child is
456            // eventually reaped, even if SIGKILL delivery takes time.
457            thread::spawn(move || {
458                let _ = child.wait();
459            });
460        }
461        ChildHandle::ExactTrace(mut child) => {
462            // The dedicated tracer remains the sole waiter and will reap it.
463            let _ = child.kill();
464        }
465    }
466}
467
468impl SharedState {
469    #[cfg(test)]
470    fn new(capture: bool) -> Self {
471        Self::with_observer_and_limit(capture, None, None)
472    }
473
474    fn with_observer_and_limit(
475        capture: bool,
476        observer: Option<ObserverEmitter>,
477        capture_limit: Option<usize>,
478    ) -> Self {
479        let queues = QueueState {
480            stdout_closed: !capture,
481            stderr_closed: !capture,
482            ..QueueState::default()
483        };
484        Self {
485            queues: Mutex::new(queues),
486            condvar: Condvar::new(),
487            capture_limit,
488            capture_overflowed: AtomicBool::new(false),
489            active_capture_readers: std::sync::atomic::AtomicUsize::new(0),
490            returncode: AtomicI64::new(RETURNCODE_NOT_SET),
491            observer,
492            observer_exit_emitted: AtomicBool::new(false),
493        }
494    }
495
496    /// Emit the lifecycle `exited` event exactly once, regardless of which
497    /// code path first observes the exit. No-op when observation is off.
498    fn emit_exited(&self, pid: u32, exit_code: i32) {
499        let Some(emitter) = self.observer.as_ref() else {
500            return;
501        };
502        if self
503            .observer_exit_emitted
504            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
505            .is_ok()
506        {
507            emitter.emit_exited(pid, exit_code);
508        }
509    }
510}
511
512/// A cross-platform child process with optional output capture.
513///
514/// `NativeProcess` wraps [`std::process::Command`] with the crate's
515/// process-tree containment, capture draining, timeout, and terminal-control
516/// behavior. Methods are synchronous and are safe to call from ordinary
517/// blocking code.
518pub struct NativeProcess {
519    config: ProcessConfig,
520    command_override: Mutex<Option<Command>>,
521    child: Arc<Mutex<Option<ChildState>>>,
522    stdin: Mutex<Option<ChildStdin>>,
523    shared: Arc<SharedState>,
524    process_watch: Option<Arc<ProcessWatchEmitter>>,
525    // This remains a constructor-only policy for the bounded std::Command
526    // entrypoint. General NativeProcess callers keep their established
527    // platform policy surface.
528    kill_when_owner_dies: bool,
529    #[cfg(test)]
530    stdin_write_active: AtomicBool,
531    capture_cancellation:
532        Arc<running_process_platform_internal::platform::process::CaptureCancellation>,
533}
534
535impl NativeProcess {
536    /// Create a process wrapper from a [`ProcessConfig`].
537    ///
538    /// The child is not spawned until [`Self::start`] is called. Process
539    /// observation is **off by default**: no lifecycle events are emitted
540    /// unless [`Self::with_observer`] is used instead.
541    pub fn new(config: ProcessConfig) -> Self {
542        Self::new_with_options(config, None, None, None, None)
543    }
544
545    /// Create a process wrapper with process observation enabled (Phase 1
546    /// of #221).
547    ///
548    /// Returns the wrapper paired with an [`ObserverSubscriber`] that
549    /// receives a [`started`](crate::ObserverEventKind::Started) event when
550    /// [`Self::start`] spawns the child and exactly one
551    /// [`exited`](crate::ObserverEventKind::Exited) event when the child is
552    /// reaped — for the categories the `config` requests that are actually
553    /// `Supported` (only [`Lifecycle`](crate::EventCategory::Lifecycle) in
554    /// Phase 1; see [`ObserverCapabilities::negotiate`](crate::ObserverCapabilities::negotiate)).
555    ///
556    /// The emitter never blocks on a slow or dropped subscriber.
557    pub fn with_observer(
558        config: ProcessConfig,
559        observer: crate::observer::ObserverConfig,
560    ) -> (Self, ObserverSubscriber) {
561        let (emitter, subscriber) = ObserverEmitter::new(observer);
562        let process = Self::new_with_options(config, Some(emitter), None, None, None);
563        (process, subscriber)
564    }
565
566    /// Create a process wrapper from a caller-configured
567    /// [`std::process::Command`] with process observation enabled.
568    ///
569    /// [`ProcessConfig`]'s declarative surface deliberately cannot represent
570    /// everything a `Command` can carry — `env_remove` scrubs of inherited
571    /// variables, non-Unicode (`OsString`) argv/env values, a pre-resolved
572    /// working directory — so a caller that already owns a fully configured
573    /// `Command` (a compiler front door wrapping cargo, zackees/soldr#2546)
574    /// would otherwise have to lossily re-encode it. This pairs the observer
575    /// machinery with the same command-override seam the capture-limit
576    /// constructors use: `command` is spawned verbatim, while `config` still
577    /// governs stdio routing, capture, containment, and limits (its
578    /// `command` / `cwd` / `env` fields are ignored in favor of the
579    /// override, matching `build_command`).
580    pub fn with_observer_and_command(
581        command: Command,
582        config: ProcessConfig,
583        observer: crate::observer::ObserverConfig,
584    ) -> (Self, ObserverSubscriber) {
585        let (emitter, subscriber) = ObserverEmitter::new(observer);
586        let process = Self::new_with_options(config, Some(emitter), None, Some(command), None);
587        (process, subscriber)
588    }
589
590    /// Create a process with launched-tree watch matching configured before
591    /// spawn. Exact tracing, when selected, owns the launch-time wait events.
592    pub fn with_process_watches(
593        config: ProcessConfig,
594        watches: Vec<ProcessWatch>,
595        policy: ObservationPolicy,
596    ) -> Result<(Self, ProcessWatchSubscriber), ProcessObservationError> {
597        let (emitter, subscriber) = ProcessWatchEmitter::new(watches, policy)?;
598        let process = Self::new_with_options(config, None, None, None, Some(emitter));
599        Ok((process, subscriber))
600    }
601
602    /// Describe exact launched-tree observation support on this host.
603    pub fn process_observation_capabilities() -> ProcessObservationCapabilities {
604        ProcessObservationCapabilities::current()
605    }
606
607    fn new_with_capture_limit(config: ProcessConfig, capture_limit: usize) -> Self {
608        Self::new_with_options(config, None, Some(capture_limit), None, None)
609    }
610
611    fn new_with_command_capture_limit(
612        command: Command,
613        config: ProcessConfig,
614        capture_limit: usize,
615        kill_when_owner_dies: bool,
616    ) -> Self {
617        let mut process =
618            Self::new_with_options(config, None, Some(capture_limit), Some(command), None);
619        process.kill_when_owner_dies = kill_when_owner_dies;
620        process
621    }
622
623    fn new_with_options(
624        config: ProcessConfig,
625        observer: Option<ObserverEmitter>,
626        capture_limit: Option<usize>,
627        command_override: Option<Command>,
628        process_watch: Option<Arc<ProcessWatchEmitter>>,
629    ) -> Self {
630        let shared = SharedState::with_observer_and_limit(config.capture, observer, capture_limit);
631        Self {
632            shared: Arc::new(shared),
633            process_watch,
634            command_override: Mutex::new(command_override),
635            child: Arc::new(Mutex::new(None)),
636            stdin: Mutex::new(None),
637            kill_when_owner_dies: false,
638            #[cfg(test)]
639            stdin_write_active: AtomicBool::new(false),
640            config,
641            capture_cancellation: Arc::new(Default::default()),
642        }
643    }
644
645    // Preserve a stable Rust frame here in release user dumps.
646    #[inline(never)]
647    /// Spawn the configured child process.
648    ///
649    /// Returns [`ProcessError::AlreadyStarted`] if the same wrapper already
650    /// owns a running child.
651    pub fn start(&self) -> Result<(), ProcessError> {
652        public_symbols::rp_native_process_start_public(self)
653    }
654
655    fn start_impl(&self) -> Result<(), ProcessError> {
656        crate::rp_rust_debug_scope!("running_process::NativeProcess::start");
657        let mut guard = self.child.lock().expect("child mutex poisoned");
658        if guard.is_some() {
659            return Err(ProcessError::AlreadyStarted);
660        }
661
662        let mut command = self.build_command();
663        let exact_trace = self
664            .process_watch
665            .as_ref()
666            .is_some_and(|watch| watch.uses_exact_trace());
667        match self.config.stdin_mode {
668            StdinMode::Inherit => {}
669            StdinMode::Piped => {
670                command.stdin(Stdio::piped());
671            }
672            StdinMode::Null => {
673                command.stdin(Stdio::null());
674            }
675        }
676        if self.config.capture {
677            command.stdout(Stdio::piped());
678            command.stderr(Stdio::piped());
679        }
680
681        let mut child = if exact_trace {
682            let event_watch = Arc::clone(self.process_watch.as_ref().expect("exact watch checked"));
683            let completion_watch = Arc::clone(&event_watch);
684            match running_process_platform_internal::platform::process::start_exact_trace(
685                command,
686                Box::new(move |event| event_watch.emit_exact(event)),
687                Box::new(move || completion_watch.close()),
688            ) {
689                Ok(child) => ChildHandle::ExactTrace(child),
690                Err(error) => {
691                    if let Some(watch) = self.process_watch.as_ref() {
692                        watch.close();
693                    }
694                    return Err(ProcessError::Spawn(error));
695                }
696            }
697        } else {
698            ChildHandle::Standard(command.spawn().map_err(ProcessError::Spawn)?)
699        };
700        log_spawned_child_pid(child.id()).map_err(ProcessError::Spawn)?;
701        // Phase 1 of #221: emit the lifecycle `started` event. No-op when
702        // observation is off (the common, off-by-default path).
703        if let Some(emitter) = self.shared.observer.as_ref() {
704            emitter.emit_started(child.id());
705        }
706        // #539 slice 2: when the observer requests EventCategory::Process,
707        // associate an IOCP with the per-spawn Job Object so a pump thread
708        // can forward descendant lifecycle events. The Lifecycle category
709        // is still served by emit_started / emit_exited above and below.
710        #[cfg(windows)]
711        let job = {
712            let descendant_sink = self
713                .shared
714                .observer
715                .as_ref()
716                .and_then(|e| e.descendant_sink());
717            let job_result = match &child {
718                ChildHandle::Standard(standard_child) => {
719                    let direct_pid = standard_child.id();
720                    public_symbols::rp_assign_child_to_windows_kill_on_close_job_with_observer_public(
721                        standard_child,
722                        descendant_sink,
723                        self.process_watch.clone(),
724                        direct_pid,
725                        self.config.address_space_limit_bytes,
726                    )
727                }
728                ChildHandle::ExactTrace(_) => unreachable!("Windows exact tracing is unavailable"),
729            };
730            match job_result {
731                Ok(job) => job,
732                Err(error) => {
733                    if let Some(watch) = self.process_watch.as_ref() {
734                        watch.close();
735                    }
736                    cleanup_child_after_start_error(child);
737                    return Err(ProcessError::Spawn(error));
738                }
739            }
740        };
741        if !exact_trace {
742            descendant_monitor::start(
743                child.id(),
744                self.shared.observer.as_ref(),
745                self.process_watch.as_ref(),
746            );
747        }
748        if self.config.capture {
749            let stdout = child.take_stdout().expect("stdout pipe missing");
750            let stderr = child.take_stderr().expect("stderr pipe missing");
751            let stdout =
752                match running_process_platform_internal::platform::process::prepare_capture_reader(
753                    stdout,
754                    &self.capture_cancellation,
755                    running_process_platform_internal::platform::process::CaptureStream::Stdout,
756                ) {
757                    Ok(stdout) => stdout,
758                    Err(error) => {
759                        cleanup_child_after_start_error(child);
760                        return Err(ProcessError::Spawn(error));
761                    }
762                };
763            let stderr =
764                match running_process_platform_internal::platform::process::prepare_capture_reader(
765                    stderr,
766                    &self.capture_cancellation,
767                    running_process_platform_internal::platform::process::CaptureStream::Stderr,
768                ) {
769                    Ok(stderr) => stderr,
770                    Err(error) => {
771                        running_process_platform_internal::platform::process::capture_reader_done(
772                        &self.capture_cancellation,
773                        running_process_platform_internal::platform::process::CaptureStream::Stdout,
774                    );
775                        cleanup_child_after_start_error(child);
776                        return Err(ProcessError::Spawn(error));
777                    }
778                };
779            self.spawn_reader(
780                stdout,
781                StreamKind::Stdout,
782                StreamKind::Stdout,
783                self.pipe_done_callback(StreamKind::Stdout),
784            );
785            self.spawn_reader(
786                stderr,
787                StreamKind::Stderr,
788                match self.config.stderr_mode {
789                    StderrMode::Stdout => StreamKind::Stdout,
790                    StderrMode::Pipe => StreamKind::Stderr,
791                },
792                self.pipe_done_callback(StreamKind::Stderr),
793            );
794        }
795        *self.stdin.lock().expect("stdin mutex poisoned") = child.take_stdin();
796        *guard = Some(ChildState {
797            child,
798            #[cfg(windows)]
799            _job: job,
800        });
801        drop(guard);
802        self.spawn_exit_waiter();
803        Ok(())
804    }
805
806    /// Background thread that polls for process exit and stores the exit code
807    /// atomically. This makes `returncode` auto-update without explicit `poll()`.
808    fn spawn_exit_waiter(&self) {
809        let child = Arc::clone(&self.child);
810        let shared = Arc::clone(&self.shared);
811        let capture = self.config.capture;
812        let capture_cancellation = Arc::clone(&self.capture_cancellation);
813        thread::spawn(move || {
814            loop {
815                if shared.returncode.load(Ordering::Acquire) != RETURNCODE_NOT_SET {
816                    return;
817                }
818                let exited = {
819                    let mut guard = child.lock().expect("child mutex poisoned");
820                    if let Some(child_state) = guard.as_mut() {
821                        let pid = child_state.child.id();
822                        match child_state.child.try_wait_code() {
823                            Ok(Some(code)) => {
824                                shared.returncode.store(code as i64, Ordering::Release);
825                                // Phase 1 of #221: lifecycle `exited`. Emit
826                                // before notifying waiters and is guarded so
827                                // only the first exit-observer fires.
828                                shared.emit_exited(pid, code);
829                                shared.condvar.notify_all();
830                                true
831                            }
832                            Ok(None) => false,
833                            Err(_error) => {
834                                #[cfg(unix)]
835                                if child_try_wait_error_is_retryable(&_error) {
836                                    false
837                                } else {
838                                    return;
839                                }
840                                #[cfg(windows)]
841                                return;
842                            }
843                        }
844                    } else {
845                        return;
846                    }
847                };
848                if exited {
849                    // The direct child has exited. Bound the capture-completion
850                    // wait so wait()/close()/read_* on the natural-exit path
851                    // cannot wedge forever when a grandchild inherited the pipe
852                    // and outlives the child (issue #590, cluster A). Unlike
853                    // `kill_impl` we do NOT cancel the reader up front: a
854                    // short-lived grandchild may still emit output the caller
855                    // expects to capture, so the reader is left to drain
856                    // naturally within the grace window. Only if the window
857                    // elapses with the pipe still held open do we cancel, to
858                    // release the otherwise-leaked reader thread (Windows:
859                    // CancelIoEx; Unix: a per-reader wake socket). The child
860                    // lock is released before this
861                    // potentially-blocking finalize so poll()/kill() are never
862                    // held off.
863                    if capture {
864                        let drained = finalize_capture_completion(&shared, kill_drain_deadline());
865                        if !drained {
866                            running_process_platform_internal::platform::process::cancel_capture_reader(
867                                &capture_cancellation,
868                            );
869                        }
870                    }
871                    // Non-invasive watch EOF is owned by the platform
872                    // descendant backend: Linux/macOS perform one final
873                    // reconciliation and Windows waits for
874                    // ACTIVE_PROCESS_ZERO. Closing here would race those
875                    // final descendant notifications.
876                    return;
877                }
878                // #199: intentional — capture thread polling for
879                // child-exit. `try_wait` is non-blocking by design;
880                // we can't block here because the thread also drains
881                // pipe state alongside the exit check. 10ms keeps the
882                // CPU cost negligible while staying responsive.
883                thread::sleep(Duration::from_millis(10));
884            }
885        });
886    }
887
888    /// Write bytes to the child's stdin and then close stdin.
889    pub fn write_stdin(&self, data: &[u8]) -> Result<(), ProcessError> {
890        if self.child.lock().expect("child mutex poisoned").is_none() {
891            return Err(ProcessError::NotRunning);
892        }
893        let mut guard = self.stdin.lock().expect("stdin mutex poisoned");
894        let stdin = guard.as_mut().ok_or(ProcessError::StdinUnavailable)?;
895        use std::io::Write;
896        #[cfg(test)]
897        self.stdin_write_active.store(true, Ordering::Release);
898        let write_result = stdin.write_all(data);
899        #[cfg(test)]
900        self.stdin_write_active.store(false, Ordering::Release);
901        write_result.map_err(ProcessError::Io)?;
902        stdin.flush().map_err(ProcessError::Io)?;
903        drop(guard.take());
904        Ok(())
905    }
906
907    /// Write to the child's stdin without closing it afterwards, so the
908    /// caller can issue additional writes. Used by interactive
909    /// pipe-backed sessions (#130 milestone 3) where the daemon keeps
910    /// stdin open across multiple client input frames.
911    pub fn write_stdin_streaming(&self, data: &[u8]) -> Result<(), ProcessError> {
912        if self.child.lock().expect("child mutex poisoned").is_none() {
913            return Err(ProcessError::NotRunning);
914        }
915        let mut guard = self.stdin.lock().expect("stdin mutex poisoned");
916        let stdin = guard.as_mut().ok_or(ProcessError::StdinUnavailable)?;
917        use std::io::Write;
918        #[cfg(test)]
919        self.stdin_write_active.store(true, Ordering::Release);
920        let write_result = stdin.write_all(data);
921        #[cfg(test)]
922        self.stdin_write_active.store(false, Ordering::Release);
923        write_result.map_err(ProcessError::Io)?;
924        stdin.flush().map_err(ProcessError::Io)?;
925        Ok(())
926    }
927
928    /// Explicitly close the child's stdin (signals EOF to the child).
929    /// Idempotent: returns Ok if stdin was already closed.
930    pub fn close_stdin(&self) -> Result<(), ProcessError> {
931        if self.child.lock().expect("child mutex poisoned").is_none() {
932            return Err(ProcessError::NotRunning);
933        }
934        drop(self.stdin.lock().expect("stdin mutex poisoned").take());
935        Ok(())
936    }
937
938    /// Check whether the child has exited without blocking.
939    ///
940    /// Returns `Ok(None)` while the process is still running.
941    pub fn poll(&self) -> Result<Option<i32>, ProcessError> {
942        // Fast path: check atomic set by background waiter thread.
943        if let Some(code) = self.returncode() {
944            return Ok(Some(code));
945        }
946        let mut guard = self.child.lock().expect("child mutex poisoned");
947        let Some(child_state) = guard.as_mut() else {
948            return Ok(self.returncode());
949        };
950        let pid = child_state.child.id();
951        let child = &mut child_state.child;
952        let status = child.try_wait_code().map_err(ProcessError::Io)?;
953        if let Some(code) = status {
954            self.set_returncode(code);
955            self.shared.emit_exited(pid, code);
956            return Ok(Some(code));
957        }
958        Ok(None)
959    }
960
961    // Preserve a stable Rust frame here in release user dumps.
962    #[inline(never)]
963    /// Wait for the child to exit.
964    ///
965    /// When `timeout` is `Some`, returns [`ProcessError::Timeout`] if the
966    /// child does not exit before the duration elapses.
967    pub fn wait(&self, timeout: Option<Duration>) -> Result<i32, ProcessError> {
968        public_symbols::rp_native_process_wait_public(self, timeout)
969    }
970
971    fn wait_impl(&self, timeout: Option<Duration>) -> Result<i32, ProcessError> {
972        crate::rp_rust_debug_scope!("running_process::NativeProcess::wait");
973        if self.child.lock().expect("child mutex poisoned").is_none() {
974            return self.returncode().ok_or(ProcessError::NotRunning);
975        }
976        // Fast path: already exited.
977        if let Some(code) = self.returncode() {
978            self.finish_capture_drain();
979            return Ok(code);
980        }
981        let start = Instant::now();
982        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
983        loop {
984            // Check returncode (set by exit-waiter thread via atomic + condvar).
985            let rc = self.shared.returncode.load(Ordering::Acquire);
986            if rc != RETURNCODE_NOT_SET {
987                drop(guard);
988                let code = rc as i32;
989                self.finish_capture_drain();
990                return Ok(code);
991            }
992            if let Some(limit) = timeout {
993                let elapsed = start.elapsed();
994                if elapsed >= limit {
995                    return Err(ProcessError::Timeout);
996                }
997                let remaining = limit - elapsed;
998                // Wait on condvar with timeout, capped at 50ms to recheck.
999                let wait_time = remaining.min(Duration::from_millis(50));
1000                guard = self
1001                    .shared
1002                    .condvar
1003                    .wait_timeout(guard, wait_time)
1004                    .expect("queue mutex poisoned")
1005                    .0;
1006            } else {
1007                // Wait on condvar with periodic recheck.
1008                guard = self
1009                    .shared
1010                    .condvar
1011                    .wait_timeout(guard, Duration::from_millis(50))
1012                    .expect("queue mutex poisoned")
1013                    .0;
1014            }
1015        }
1016    }
1017
1018    // Preserve a stable Rust frame here in release user dumps.
1019    #[inline(never)]
1020    /// Forcefully terminate the child process.
1021    pub fn kill(&self) -> Result<(), ProcessError> {
1022        public_symbols::rp_native_process_kill_public(self)
1023    }
1024
1025    fn kill_impl(&self) -> Result<(), ProcessError> {
1026        crate::rp_rust_debug_scope!("running_process::NativeProcess::kill");
1027        #[cfg(windows)]
1028        {
1029            let mut guard = self.child.lock().expect("child mutex poisoned");
1030            let child = &mut guard.as_mut().ok_or(ProcessError::NotRunning)?.child;
1031            let pid = child.id();
1032            child.kill().map_err(ProcessError::Io)?;
1033            let code = child.wait_code().map_err(ProcessError::Io)?;
1034            self.set_returncode(code);
1035            // Phase 1 of #221: a killed child still produces a lifecycle
1036            // `exited` event (guarded against double-emit by the waiter).
1037            self.shared.emit_exited(pid, code);
1038        }
1039        #[cfg(unix)]
1040        {
1041            let deadline = kill_drain_deadline();
1042            let (pid, already_reaped) = {
1043                let mut state = self.child.lock().expect("child mutex poisoned");
1044                let child = &mut state.as_mut().ok_or(ProcessError::NotRunning)?.child;
1045                let pid = child.id();
1046                if let Some(code) = child.try_wait_code().map_err(ProcessError::Io)? {
1047                    (pid, Some(code))
1048                } else {
1049                    let group_signaled = self.config.create_process_group
1050                        && unix_signal_process_group(pid as i32, UnixSignal::Kill).is_ok();
1051                    if !group_signaled {
1052                        child.kill().map_err(ProcessError::Io)?;
1053                    }
1054                    (pid, None)
1055                }
1056            };
1057
1058            // Wake capture readers immediately after signal delivery. In
1059            // particular, this prevents a surviving pipe-owning descendant
1060            // from extending the bounded reap window.
1061            self.cancel_capture_io();
1062            let reaped = already_reaped.or_else(|| {
1063                let reap_result =
1064                    poll_mutex_until(&self.child, deadline, Duration::from_millis(10), |state| {
1065                        match state.as_mut() {
1066                            Some(child) => child.child.try_wait_code(),
1067                            None => Ok(None),
1068                        }
1069                    });
1070                match reap_result {
1071                    Ok(Some(code)) => Some(code),
1072                    _ => None,
1073                }
1074            });
1075            if let Some(code) = reaped {
1076                self.set_returncode(code);
1077                self.shared.emit_exited(pid, code);
1078            }
1079            public_symbols::rp_native_process_wait_for_capture_completion_with_deadline_public(
1080                self, deadline,
1081            );
1082            Ok(())
1083        }
1084        #[cfg(windows)]
1085        {
1086            // Interrupt any pending capture `read()` in the per-stream reader
1087            // threads so they fall out of their loops immediately. This is what
1088            // makes the grandchild-pipe-orphan
1089            // case (FastLED Bug B: uv.exe spawns a python.exe grandchild
1090            // that inherits the pipe and outlives uv) wake up in
1091            // microseconds instead of waiting for the bounded-drain
1092            // safety-net deadline below.
1093            self.cancel_capture_io();
1094            // Synchronize with the per-stream reader threads so that by the
1095            // time kill() returns, the capture queues have flipped from
1096            // "blocked on read" to "closed" and downstream pollers (e.g.
1097            // take_combined_line) observe EOS instead of timeout. Without
1098            // this, callers that hit a wait()-timeout path see Python code
1099            // raise TimeoutError, kill the child, then race the reader
1100            // threads — a 10ms poll loop can miss the EOS flip entirely.
1101            //
1102            // The deadline remains a safety-net if the platform wake mechanism
1103            // does not fire.
1104            public_symbols::rp_native_process_wait_for_capture_completion_with_deadline_public(
1105                self,
1106                kill_drain_deadline(),
1107            );
1108            Ok(())
1109        }
1110    }
1111
1112    /// Terminate the child process.
1113    ///
1114    /// This currently uses the same hard-kill path as [`Self::kill`].
1115    pub fn terminate(&self) -> Result<(), ProcessError> {
1116        self.kill()
1117    }
1118
1119    /// Send the OS-appropriate soft termination signal to the child's
1120    /// process group (POSIX: SIGTERM to `-pid`; Windows: Ctrl+Break).
1121    ///
1122    /// Requires `ProcessConfig.create_process_group=true` on POSIX so
1123    /// that `-pid` resolves to the child's own group. With the default
1124    /// `create_process_group=false`, the kill would walk back to the
1125    /// caller's group; the method silently no-ops in that case to avoid
1126    /// signaling the wrong tree.
1127    ///
1128    /// Used by the daemon-side pipe sessions (#130 M4 follow-up) so
1129    /// that `TerminationOutcome::SoftExit` becomes meaningful on POSIX.
1130    pub fn terminate_group_soft(&self) -> Result<(), ProcessError> {
1131        if !self.config.create_process_group {
1132            // A group signal would otherwise reach the caller's own group.
1133            return Ok(());
1134        }
1135        let pid = self.pid().ok_or(ProcessError::NotRunning)?;
1136        running_process_platform_internal::platform::process::soft_terminate_process_group(pid)
1137            .map_err(ProcessError::Io)
1138    }
1139
1140    // Preserve a stable Rust frame here in release user dumps.
1141    #[inline(never)]
1142    /// Close the process wrapper by terminating the child when it is running.
1143    pub fn close(&self) -> Result<(), ProcessError> {
1144        public_symbols::rp_native_process_close_public(self)
1145    }
1146
1147    fn close_impl(&self) -> Result<(), ProcessError> {
1148        crate::rp_rust_debug_scope!("running_process::NativeProcess::close");
1149        if self.child.lock().expect("child mutex poisoned").is_none() {
1150            return Ok(());
1151        }
1152        if self.poll()?.is_none() {
1153            self.kill()?;
1154        } else {
1155            self.finish_capture_drain();
1156        }
1157        if let Some(watch) = self.process_watch.as_ref() {
1158            watch.close();
1159        }
1160        Ok(())
1161    }
1162
1163    /// Return the child process id when the wrapper currently owns a child.
1164    pub fn pid(&self) -> Option<u32> {
1165        self.child
1166            .lock()
1167            .expect("child mutex poisoned")
1168            .as_ref()
1169            .map(|state| state.child.id())
1170    }
1171
1172    /// Return the cached exit code when the child has exited.
1173    pub fn returncode(&self) -> Option<i32> {
1174        let v = self.shared.returncode.load(Ordering::Acquire);
1175        if v == RETURNCODE_NOT_SET {
1176            None
1177        } else {
1178            Some(v as i32)
1179        }
1180    }
1181
1182    /// Return whether captured output is queued for one stream.
1183    pub fn has_pending_stream(&self, stream: StreamKind) -> bool {
1184        if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
1185            return false;
1186        }
1187        let guard = self.shared.queues.lock().expect("queue mutex poisoned");
1188        match stream {
1189            StreamKind::Stdout => !guard.stdout_queue.is_empty(),
1190            StreamKind::Stderr => !guard.stderr_queue.is_empty(),
1191        }
1192    }
1193
1194    /// Return whether captured combined output is queued.
1195    pub fn has_pending_combined(&self) -> bool {
1196        let guard = self.shared.queues.lock().expect("queue mutex poisoned");
1197        !guard.combined_queue.is_empty()
1198    }
1199
1200    /// Drain and return all queued output for one stream.
1201    pub fn drain_stream(&self, stream: StreamKind) -> Vec<Vec<u8>> {
1202        if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
1203            return Vec::new();
1204        }
1205        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1206        let queue = match stream {
1207            StreamKind::Stdout => &mut guard.stdout_queue,
1208            StreamKind::Stderr => &mut guard.stderr_queue,
1209        };
1210        queue.drain(..).collect()
1211    }
1212
1213    /// Consume and return all byte-exact output currently captured for one
1214    /// stream.
1215    ///
1216    /// This is independent of the logical-line queues used by
1217    /// [`Self::read_stream`] and [`Self::drain_stream`]. It preserves CRLF/LF
1218    /// delimiters, unterminated tails, and non-UTF-8 bytes exactly as accepted
1219    /// from the pipe reader. Calling it after EOF returns an empty vector once
1220    /// the stream has been drained.
1221    pub fn drain_stream_raw(&self, stream: StreamKind) -> Vec<u8> {
1222        if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
1223            return Vec::new();
1224        }
1225        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1226        match stream {
1227            StreamKind::Stdout => {
1228                let mut output = Vec::with_capacity(guard.stdout_raw_bytes);
1229                for chunk in guard.stdout_raw.drain(..) {
1230                    output.extend_from_slice(&chunk);
1231                }
1232                guard.stdout_raw_bytes = 0;
1233                output
1234            }
1235            StreamKind::Stderr => {
1236                let mut output = Vec::with_capacity(guard.stderr_raw_bytes);
1237                for chunk in guard.stderr_raw.drain(..) {
1238                    output.extend_from_slice(&chunk);
1239                }
1240                guard.stderr_raw_bytes = 0;
1241                output
1242            }
1243        }
1244    }
1245
1246    /// Drain and return all queued combined output events.
1247    pub fn drain_combined(&self) -> Vec<StreamEvent> {
1248        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1249        guard.combined_queue.drain(..).collect()
1250    }
1251
1252    /// Read the next captured chunk from one stream.
1253    ///
1254    /// Returns [`ReadStatus::Timeout`] when `timeout` elapses before output or
1255    /// EOF is observed.
1256    pub fn read_stream(
1257        &self,
1258        stream: StreamKind,
1259        timeout: Option<Duration>,
1260    ) -> ReadStatus<Vec<u8>> {
1261        let deadline = timeout.map(|limit| Instant::now() + limit);
1262        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1263
1264        loop {
1265            if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
1266                return ReadStatus::Eof;
1267            }
1268
1269            let queue = match stream {
1270                StreamKind::Stdout => &mut guard.stdout_queue,
1271                StreamKind::Stderr => &mut guard.stderr_queue,
1272            };
1273            if let Some(line) = queue.pop_front() {
1274                return ReadStatus::Line(line);
1275            }
1276
1277            let closed = match stream {
1278                StreamKind::Stdout => {
1279                    if self.config.stderr_mode == StderrMode::Stdout {
1280                        guard.stdout_closed && guard.stderr_closed
1281                    } else {
1282                        guard.stdout_closed
1283                    }
1284                }
1285                StreamKind::Stderr => guard.stderr_closed,
1286            };
1287            if closed {
1288                return ReadStatus::Eof;
1289            }
1290
1291            match deadline {
1292                Some(deadline) => {
1293                    let now = Instant::now();
1294                    if now >= deadline {
1295                        return ReadStatus::Timeout;
1296                    }
1297                    let wait = deadline.saturating_duration_since(now);
1298                    let result = self
1299                        .shared
1300                        .condvar
1301                        .wait_timeout(guard, wait)
1302                        .expect("queue mutex poisoned");
1303                    guard = result.0;
1304                    if result.1.timed_out() {
1305                        return ReadStatus::Timeout;
1306                    }
1307                }
1308                None => {
1309                    guard = self
1310                        .shared
1311                        .condvar
1312                        .wait(guard)
1313                        .expect("queue mutex poisoned");
1314                }
1315            }
1316        }
1317    }
1318
1319    // Preserve a stable Rust frame here in release user dumps.
1320    #[inline(never)]
1321    /// Read the next captured combined stream event.
1322    pub fn read_combined(&self, timeout: Option<Duration>) -> ReadStatus<StreamEvent> {
1323        public_symbols::rp_native_process_read_combined_public(self, timeout)
1324    }
1325
1326    fn read_combined_impl(&self, timeout: Option<Duration>) -> ReadStatus<StreamEvent> {
1327        crate::rp_rust_debug_scope!("running_process::NativeProcess::read_combined");
1328        let deadline = timeout.map(|limit| Instant::now() + limit);
1329        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1330
1331        loop {
1332            if let Some(event) = guard.combined_queue.pop_front() {
1333                return ReadStatus::Line(event);
1334            }
1335            if guard.stdout_closed && guard.stderr_closed {
1336                return ReadStatus::Eof;
1337            }
1338
1339            match deadline {
1340                Some(deadline) => {
1341                    let now = Instant::now();
1342                    if now >= deadline {
1343                        return ReadStatus::Timeout;
1344                    }
1345                    let wait = deadline.saturating_duration_since(now);
1346                    let result = self
1347                        .shared
1348                        .condvar
1349                        .wait_timeout(guard, wait)
1350                        .expect("queue mutex poisoned");
1351                    guard = result.0;
1352                    if result.1.timed_out() {
1353                        return ReadStatus::Timeout;
1354                    }
1355                }
1356                None => {
1357                    guard = self
1358                        .shared
1359                        .condvar
1360                        .wait(guard)
1361                        .expect("queue mutex poisoned");
1362                }
1363            }
1364        }
1365    }
1366
1367    /// Return the retained stdout history.
1368    pub fn captured_stdout(&self) -> Vec<Vec<u8>> {
1369        self.shared
1370            .queues
1371            .lock()
1372            .expect("queue mutex poisoned")
1373            .stdout_history
1374            .clone()
1375            .into_iter()
1376            .collect()
1377    }
1378
1379    fn captured_stdout_raw(&self) -> Vec<u8> {
1380        let guard = self.shared.queues.lock().expect("queue mutex poisoned");
1381        guard.stdout_raw.iter().flatten().copied().collect()
1382    }
1383
1384    /// Return the retained stderr history.
1385    pub fn captured_stderr(&self) -> Vec<Vec<u8>> {
1386        if self.config.stderr_mode == StderrMode::Stdout {
1387            return Vec::new();
1388        }
1389        self.shared
1390            .queues
1391            .lock()
1392            .expect("queue mutex poisoned")
1393            .stderr_history
1394            .clone()
1395            .into_iter()
1396            .collect()
1397    }
1398
1399    fn captured_stderr_raw(&self) -> Vec<u8> {
1400        if self.config.stderr_mode == StderrMode::Stdout {
1401            return Vec::new();
1402        }
1403        let guard = self.shared.queues.lock().expect("queue mutex poisoned");
1404        guard.stderr_raw.iter().flatten().copied().collect()
1405    }
1406
1407    /// Return the retained combined stdout/stderr event history.
1408    pub fn captured_combined(&self) -> Vec<StreamEvent> {
1409        self.shared
1410            .queues
1411            .lock()
1412            .expect("queue mutex poisoned")
1413            .combined_history
1414            .clone()
1415            .into_iter()
1416            .collect()
1417    }
1418
1419    /// Return the retained byte count for one captured stream.
1420    pub fn captured_stream_bytes(&self, stream: StreamKind) -> usize {
1421        if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
1422            return 0;
1423        }
1424        let guard = self.shared.queues.lock().expect("queue mutex poisoned");
1425        match stream {
1426            StreamKind::Stdout => guard.stdout_history_bytes,
1427            StreamKind::Stderr => guard.stderr_history_bytes,
1428        }
1429    }
1430
1431    /// Return the retained byte count for combined captured output.
1432    pub fn captured_combined_bytes(&self) -> usize {
1433        self.shared
1434            .queues
1435            .lock()
1436            .expect("queue mutex poisoned")
1437            .combined_history_bytes
1438    }
1439
1440    /// Clear retained output history for one stream and return freed bytes.
1441    ///
1442    /// This releases both the logical-line history and the byte-exact queue
1443    /// behind [`Self::drain_stream_raw`], so it stays the single memory-release
1444    /// valve for a captured stream. The returned count is the logical-line
1445    /// history only, unchanged. A caller consuming byte-exact output should
1446    /// drain it with [`Self::drain_stream_raw`] — which frees it too — rather
1447    /// than interleaving this call.
1448    pub fn clear_captured_stream(&self, stream: StreamKind) -> usize {
1449        if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
1450            return 0;
1451        }
1452        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1453        match stream {
1454            StreamKind::Stdout => {
1455                let released = guard.stdout_history_bytes;
1456                guard.stdout_history.clear();
1457                guard.stdout_raw.clear();
1458                guard.stdout_raw_bytes = 0;
1459                guard.stdout_history_bytes = 0;
1460                released
1461            }
1462            StreamKind::Stderr => {
1463                let released = guard.stderr_history_bytes;
1464                guard.stderr_history.clear();
1465                guard.stderr_raw.clear();
1466                guard.stderr_raw_bytes = 0;
1467                guard.stderr_history_bytes = 0;
1468                released
1469            }
1470        }
1471    }
1472
1473    /// Clear retained combined output history and return freed bytes.
1474    pub fn clear_captured_combined(&self) -> usize {
1475        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1476        let released = guard.combined_history_bytes;
1477        guard.combined_history.clear();
1478        guard.combined_history_bytes = 0;
1479        released
1480    }
1481
1482    fn build_command(&self) -> Command {
1483        let command_override = self
1484            .command_override
1485            .lock()
1486            .expect("command override mutex poisoned")
1487            .take();
1488        let mut command = match command_override {
1489            Some(command) => command,
1490            None => {
1491                let mut command = match &self.config.command {
1492                    CommandSpec::Shell(command) => shell_command(command),
1493                    CommandSpec::Argv(argv) => {
1494                        let mut command = Command::new(&argv[0]);
1495                        if argv.len() > 1 {
1496                            command.args(&argv[1..]);
1497                        }
1498                        command
1499                    }
1500                };
1501                if let Some(cwd) = &self.config.cwd {
1502                    command.current_dir(cwd);
1503                }
1504                if let Some(env) = &self.config.env {
1505                    command.env_clear();
1506                    command.envs(env.iter().map(|(k, v)| (k, v)));
1507                }
1508                command
1509            }
1510        };
1511        let platform_config =
1512            running_process_platform_internal::platform::process::ProcessCommandConfig {
1513                creation_flags: self.config.creationflags,
1514                create_process_group: self.config.create_process_group,
1515                nice: self.config.nice,
1516                address_space_limit_bytes: self.config.address_space_limit_bytes,
1517            };
1518        let configured = if self.kill_when_owner_dies {
1519            running_process_platform_internal::platform::process::
1520                configure_process_command_for_bounded_owner_death(&mut command, platform_config)
1521        } else {
1522            running_process_platform_internal::platform::process::configure_process_command(
1523                &mut command,
1524                platform_config,
1525            )
1526        };
1527        configured.expect("platform command configuration must be valid");
1528        command
1529    }
1530
1531    fn spawn_reader<R>(
1532        &self,
1533        pipe: R,
1534        source_stream: StreamKind,
1535        visible_stream: StreamKind,
1536        on_pipe_done: Box<dyn FnOnce() + Send>,
1537    ) where
1538        R: Read + Send + 'static,
1539    {
1540        let shared = Arc::clone(&self.shared);
1541        shared.active_capture_readers.fetch_add(1, Ordering::AcqRel);
1542        thread::spawn(move || {
1543            let mut reader = pipe;
1544            let mut chunk = vec![0_u8; 65536];
1545            let mut pending = Vec::new();
1546
1547            loop {
1548                match reader.read(&mut chunk) {
1549                    Ok(0) => break,
1550                    Ok(n) => {
1551                        if append_raw(&shared, visible_stream, &chunk[..n]) {
1552                            let lines = feed_chunk(&mut pending, &chunk[..n]);
1553                            emit_lines(&shared, visible_stream, lines);
1554                        } else {
1555                            pending.clear();
1556                        }
1557                    }
1558                    Err(_) => break,
1559                }
1560            }
1561
1562            if !pending.is_empty() && !shared.capture_overflowed.load(Ordering::Acquire) {
1563                emit_lines(&shared, visible_stream, vec![std::mem::take(&mut pending)]);
1564            }
1565
1566            // Clear the parent-side pipe-handle slot under its mutex
1567            // before dropping the reader. After this returns,
1568            // `kill_impl` can no longer try to `CancelIoEx` on us, so
1569            // it's safe for `reader`'s drop to close the HANDLE.
1570            on_pipe_done();
1571            drop(reader);
1572
1573            let mut guard = shared.queues.lock().expect("queue mutex poisoned");
1574            match source_stream {
1575                StreamKind::Stdout => guard.stdout_closed = true,
1576                StreamKind::Stderr => guard.stderr_closed = true,
1577            }
1578            shared.active_capture_readers.fetch_sub(1, Ordering::AcqRel);
1579            shared.condvar.notify_all();
1580        });
1581    }
1582
1583    fn pipe_done_callback(&self, stream: StreamKind) -> Box<dyn FnOnce() + Send> {
1584        let cancellation = Arc::clone(&self.capture_cancellation);
1585        Box::new(move || {
1586            let stream = match stream {
1587                StreamKind::Stdout => {
1588                    running_process_platform_internal::platform::process::CaptureStream::Stdout
1589                }
1590                StreamKind::Stderr => {
1591                    running_process_platform_internal::platform::process::CaptureStream::Stderr
1592                }
1593            };
1594            running_process_platform_internal::platform::process::capture_reader_done(
1595                &cancellation,
1596                stream,
1597            );
1598        })
1599    }
1600
1601    /// Cancel pending capture reads so reader threads return immediately.
1602    /// Used by `kill_impl` to break the grandchild-orphan deadlock without
1603    /// waiting on `wait_for_capture_completion_with_deadline`'s safety-net.
1604    fn cancel_capture_io(&self) {
1605        crate::rp_rust_debug_scope!("running_process::NativeProcess::cancel_capture_io");
1606        running_process_platform_internal::platform::process::cancel_capture_reader(
1607            &self.capture_cancellation,
1608        );
1609    }
1610
1611    fn set_returncode(&self, code: i32) {
1612        self.shared.returncode.store(code as i64, Ordering::Release);
1613        self.shared.condvar.notify_all();
1614    }
1615
1616    /// Bounded capture drain for the natural-exit and `close` paths
1617    /// (issue #590, cluster A). Waits at most `kill_drain_deadline` for the
1618    /// reader threads to flip the closed flags, force-setting them on
1619    /// timeout so `wait()`/`close()` return in bounded time instead of
1620    /// wedging in the previously-unbounded `wait_for_capture_completion`.
1621    /// Unlike `kill_impl` the reader is not cancelled up front — a
1622    /// short-lived grandchild's output is allowed to drain within the
1623    /// grace window — but if the window elapses with the pipe still held
1624    /// open the reader is cancelled to release the leaked thread.
1625    fn finish_capture_drain(&self) {
1626        self.finish_capture_drain_with_deadline(kill_drain_deadline());
1627    }
1628
1629    fn finish_capture_drain_with_deadline(&self, deadline: Instant) {
1630        let drained = self.wait_for_capture_completion_with_deadline_impl(deadline);
1631        if !drained {
1632            self.cancel_capture_io();
1633        }
1634    }
1635
1636    /// Returns `true` if the reader threads flipped both closed flags on their
1637    /// own before `deadline`, `false` if the deadline forced completion.
1638    fn wait_for_capture_completion_with_deadline_impl(&self, deadline: Instant) -> bool {
1639        crate::rp_rust_debug_scope!(
1640            "running_process::NativeProcess::wait_for_capture_completion_with_deadline"
1641        );
1642        if !self.config.capture {
1643            return true;
1644        }
1645        finalize_capture_completion(&self.shared, deadline)
1646    }
1647
1648    fn wait_for_capture_readers_with_deadline(&self, deadline: Instant) -> bool {
1649        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1650        while self.shared.active_capture_readers.load(Ordering::Acquire) != 0 {
1651            let now = Instant::now();
1652            if now >= deadline {
1653                return false;
1654            }
1655            let (next_guard, result) = self
1656                .shared
1657                .condvar
1658                .wait_timeout(guard, deadline - now)
1659                .expect("queue mutex poisoned");
1660            guard = next_guard;
1661            if result.timed_out() && self.shared.active_capture_readers.load(Ordering::Acquire) != 0
1662            {
1663                return false;
1664            }
1665        }
1666        true
1667    }
1668}
1669
1670/// Cancel any pending blocking `read()` on the parent-side capture pipes
1671/// so the reader threads' `read()` calls return `ERROR_OPERATION_ABORTED`
1672/// immediately. Shared by `kill_impl`, `poll`, and the natural-exit
1673/// waiter thread (issue #590) — anywhere the child is observed to exit
1674/// while a grandchild may still hold the pipe open.
1675/// Wait until both capture streams report closed or `deadline` elapses.
1676/// On deadline, force-set the closed flags (and notify all waiters) so
1677/// downstream pollers observe EOF instead of blocking forever. Returns
1678/// `true` if the reader threads flipped the flags on their own, `false`
1679/// if the deadline forced them. A reader thread that later unblocks and
1680/// re-sets `closed = true` is a harmless no-op.
1681fn finalize_capture_completion(shared: &SharedState, deadline: Instant) -> bool {
1682    let mut guard = shared.queues.lock().expect("queue mutex poisoned");
1683    while !(guard.stdout_closed && guard.stderr_closed) {
1684        let now = Instant::now();
1685        if now >= deadline {
1686            guard.stdout_closed = true;
1687            guard.stderr_closed = true;
1688            shared.condvar.notify_all();
1689            return false;
1690        }
1691        let (next_guard, result) = shared
1692            .condvar
1693            .wait_timeout(guard, deadline - now)
1694            .expect("queue mutex poisoned");
1695        guard = next_guard;
1696        if result.timed_out() && !(guard.stdout_closed && guard.stderr_closed) {
1697            guard.stdout_closed = true;
1698            guard.stderr_closed = true;
1699            shared.condvar.notify_all();
1700            return false;
1701        }
1702    }
1703    true
1704}
1705
1706fn emit_lines(shared: &Arc<SharedState>, stream: StreamKind, lines: Vec<Vec<u8>>) {
1707    if lines.is_empty() || shared.capture_overflowed.load(Ordering::Acquire) {
1708        return;
1709    }
1710    let mut guard = shared.queues.lock().expect("queue mutex poisoned");
1711    if shared.capture_overflowed.load(Ordering::Acquire) {
1712        return;
1713    }
1714    for line in lines {
1715        let line_len = line.len();
1716        match stream {
1717            StreamKind::Stdout => {
1718                guard.stdout_history_bytes += line_len;
1719                guard.stdout_history.push_back(line.clone());
1720                guard.stdout_queue.push_back(line.clone());
1721            }
1722            StreamKind::Stderr => {
1723                guard.stderr_history_bytes += line_len;
1724                guard.stderr_history.push_back(line.clone());
1725                guard.stderr_queue.push_back(line.clone());
1726            }
1727        }
1728        let event = StreamEvent { stream, line };
1729        guard.combined_history_bytes += line_len;
1730        guard.combined_history.push_back(event.clone());
1731        guard.combined_queue.push_back(event);
1732    }
1733    shared.condvar.notify_all();
1734}
1735
1736fn append_raw(shared: &Arc<SharedState>, stream: StreamKind, chunk: &[u8]) -> bool {
1737    if chunk.is_empty() {
1738        return true;
1739    }
1740    let mut guard = shared.queues.lock().expect("queue mutex poisoned");
1741    let accepted = match shared.capture_limit {
1742        Some(limit) => {
1743            let retained = guard
1744                .stdout_raw_bytes
1745                .saturating_add(guard.stderr_raw_bytes);
1746            chunk.len().min(limit.saturating_sub(retained))
1747        }
1748        None => chunk.len(),
1749    };
1750    if accepted != 0 {
1751        let accepted_chunk = chunk[..accepted].to_vec();
1752        match stream {
1753            StreamKind::Stdout => {
1754                guard.stdout_raw_bytes += accepted;
1755                guard.stdout_raw.push_back(accepted_chunk);
1756            }
1757            StreamKind::Stderr => {
1758                guard.stderr_raw_bytes += accepted;
1759                guard.stderr_raw.push_back(accepted_chunk);
1760            }
1761        }
1762    }
1763    if accepted != chunk.len() {
1764        shared.capture_overflowed.store(true, Ordering::Release);
1765        false
1766    } else {
1767        shared.condvar.notify_all();
1768        true
1769    }
1770}
1771
1772mod bounded;
1773pub use bounded::{
1774    run_command, run_command_bounded, run_std_command_bounded,
1775    run_std_command_bounded_with_options, BoundedRunOptions,
1776};
1777
1778pub(crate) fn shell_command(command: &str) -> Command {
1779    running_process_platform_internal::platform::process::compat_shell_command(command)
1780}
1781
1782#[cfg(test)]
1783mod tests;