Skip to main content

running_process_core/
lib.rs

1use std::collections::VecDeque;
2use std::fs::OpenOptions;
3use std::io::Read;
4use std::io::Write;
5use std::path::PathBuf;
6use std::process::{Child, Command, Stdio};
7use std::sync::atomic::{AtomicI64, Ordering};
8use std::sync::{Arc, Condvar, Mutex};
9use std::thread;
10use std::time::{Duration, Instant};
11
12use thiserror::Error;
13
14pub mod console_detect;
15pub mod containment;
16#[cfg(feature = "originator-scan")]
17pub mod originator;
18pub mod pty;
19mod public_symbols;
20mod rust_debug;
21pub mod spawn;
22
23pub use console_detect::{monitor_console_windows, ConsoleWindowInfo};
24pub use containment::{ContainedProcessGroup, ORIGINATOR_ENV_VAR};
25#[cfg(feature = "originator-scan")]
26pub use originator::{find_processes_by_originator, OriginatorProcessInfo};
27pub use rust_debug::{render_rust_debug_traces, RustDebugScopeGuard};
28pub use spawn::{
29    spawn, spawn_daemon, spawn_daemon_with_clear_env, DaemonChild, SpawnStdio, SpawnedChild,
30    StdioSource,
31};
32
33#[macro_export]
34macro_rules! rp_rust_debug_scope {
35    ($label:expr) => {
36        let _running_process_rust_debug_scope =
37            $crate::RustDebugScopeGuard::enter($label, file!(), line!());
38    };
39}
40
41const CHILD_PID_LOG_PATH_ENV: &str = "RUNNING_PROCESS_CHILD_PID_LOG_PATH";
42
43/// Hard cap on how long `kill_impl()` will block on
44/// `wait_for_capture_completion` after the direct child has been
45/// reaped. Override via the `RUNNING_PROCESS_KILL_DRAIN_TIMEOUT_MS`
46/// env var (milliseconds). The default of 2 s gives normal children
47/// time to flush their pipe buffers while preventing indefinite hangs
48/// when a grandchild inherits the pipe and outlives the parent (FastLED
49/// Bug B).
50const DEFAULT_KILL_DRAIN_TIMEOUT: Duration = Duration::from_secs(2);
51const KILL_DRAIN_TIMEOUT_ENV: &str = "RUNNING_PROCESS_KILL_DRAIN_TIMEOUT_MS";
52
53fn kill_drain_deadline() -> Instant {
54    let timeout = std::env::var(KILL_DRAIN_TIMEOUT_ENV)
55        .ok()
56        .and_then(|raw| raw.trim().parse::<u64>().ok())
57        .map(Duration::from_millis)
58        .unwrap_or(DEFAULT_KILL_DRAIN_TIMEOUT);
59    Instant::now() + timeout
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum StreamKind {
64    Stdout,
65    Stderr,
66}
67
68impl StreamKind {
69    pub fn as_str(self) -> &'static str {
70        match self {
71            Self::Stdout => "stdout",
72            Self::Stderr => "stderr",
73        }
74    }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct StreamEvent {
79    pub stream: StreamKind,
80    pub line: Vec<u8>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum ReadStatus<T> {
85    Line(T),
86    Timeout,
87    Eof,
88}
89
90#[derive(Debug, Error)]
91pub enum ProcessError {
92    #[error("process already started")]
93    AlreadyStarted,
94    #[error("process is not running")]
95    NotRunning,
96    #[error("process stdin is not available")]
97    StdinUnavailable,
98    #[error("failed to spawn process: {0}")]
99    Spawn(std::io::Error),
100    #[error("failed to read process output: {0}")]
101    Io(std::io::Error),
102    #[error("process timed out")]
103    Timeout,
104}
105
106#[derive(Debug, Clone)]
107pub enum CommandSpec {
108    Shell(String),
109    Argv(Vec<String>),
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum StdinMode {
114    Inherit,
115    Piped,
116    Null,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum StderrMode {
121    Stdout,
122    Pipe,
123}
124
125#[cfg(unix)]
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum UnixSignal {
128    Interrupt,
129    Terminate,
130    Kill,
131}
132
133#[derive(Debug, Clone)]
134pub struct ProcessConfig {
135    pub command: CommandSpec,
136    pub cwd: Option<PathBuf>,
137    pub env: Option<Vec<(String, String)>>,
138    pub capture: bool,
139    pub stderr_mode: StderrMode,
140    pub creationflags: Option<u32>,
141    pub create_process_group: bool,
142    pub stdin_mode: StdinMode,
143    pub nice: Option<i32>,
144}
145
146#[derive(Default)]
147struct QueueState {
148    stdout_queue: VecDeque<Vec<u8>>,
149    stderr_queue: VecDeque<Vec<u8>>,
150    combined_queue: VecDeque<StreamEvent>,
151    stdout_history: VecDeque<Vec<u8>>,
152    stderr_history: VecDeque<Vec<u8>>,
153    combined_history: VecDeque<StreamEvent>,
154    stdout_history_bytes: usize,
155    stderr_history_bytes: usize,
156    combined_history_bytes: usize,
157    stdout_closed: bool,
158    stderr_closed: bool,
159}
160
161/// Sentinel value for returncode atomic: process has not exited yet.
162const RETURNCODE_NOT_SET: i64 = i64::MIN;
163
164struct SharedState {
165    queues: Mutex<QueueState>,
166    condvar: Condvar,
167    /// Atomic exit code. `RETURNCODE_NOT_SET` means "not exited yet".
168    /// Updated by a background waiter thread — reading is lock-free.
169    returncode: AtomicI64,
170}
171
172#[cfg(windows)]
173struct WindowsJobHandle(usize);
174
175#[cfg(windows)]
176impl Drop for WindowsJobHandle {
177    fn drop(&mut self) {
178        unsafe {
179            winapi::um::handleapi::CloseHandle(self.0 as winapi::shared::ntdef::HANDLE);
180        }
181    }
182}
183
184struct ChildState {
185    child: Child,
186    #[cfg(windows)]
187    _job: WindowsJobHandle,
188}
189
190/// Parent-side handles for the captured stdout/stderr pipes, kept so
191/// that `kill_impl` can call `CancelIoEx` to interrupt a reader thread
192/// blocked in `read()`. Stored as `usize` because `RawHandle` (a raw
193/// pointer) is not `Send` and we share this via `Arc<Mutex<...>>`.
194///
195/// The reader thread clears its slot (under the mutex) immediately
196/// before dropping its `ChildStd*`, so `kill_impl` never calls
197/// `CancelIoEx` on a closed (and potentially reused) handle.
198#[cfg(windows)]
199#[derive(Default)]
200struct CapturePipeHandles {
201    stdout: Option<usize>,
202    stderr: Option<usize>,
203}
204
205impl SharedState {
206    fn new(capture: bool) -> Self {
207        let queues = QueueState {
208            stdout_closed: !capture,
209            stderr_closed: !capture,
210            ..QueueState::default()
211        };
212        Self {
213            queues: Mutex::new(queues),
214            condvar: Condvar::new(),
215            returncode: AtomicI64::new(RETURNCODE_NOT_SET),
216        }
217    }
218}
219
220pub struct NativeProcess {
221    config: ProcessConfig,
222    child: Arc<Mutex<Option<ChildState>>>,
223    shared: Arc<SharedState>,
224    #[cfg(windows)]
225    capture_pipe_handles: Arc<Mutex<CapturePipeHandles>>,
226}
227
228impl NativeProcess {
229    pub fn new(config: ProcessConfig) -> Self {
230        Self {
231            shared: Arc::new(SharedState::new(config.capture)),
232            child: Arc::new(Mutex::new(None)),
233            config,
234            #[cfg(windows)]
235            capture_pipe_handles: Arc::new(Mutex::new(CapturePipeHandles::default())),
236        }
237    }
238
239    // Preserve a stable Rust frame here in release user dumps.
240    #[inline(never)]
241    pub fn start(&self) -> Result<(), ProcessError> {
242        public_symbols::rp_native_process_start_public(self)
243    }
244
245    fn start_impl(&self) -> Result<(), ProcessError> {
246        crate::rp_rust_debug_scope!("running_process_core::NativeProcess::start");
247        let mut guard = self.child.lock().expect("child mutex poisoned");
248        if guard.is_some() {
249            return Err(ProcessError::AlreadyStarted);
250        }
251
252        let mut command = self.build_command();
253        match self.config.stdin_mode {
254            StdinMode::Inherit => {}
255            StdinMode::Piped => {
256                command.stdin(Stdio::piped());
257            }
258            StdinMode::Null => {
259                command.stdin(Stdio::null());
260            }
261        }
262        if self.config.capture {
263            command.stdout(Stdio::piped());
264            command.stderr(Stdio::piped());
265        }
266
267        let mut child = command.spawn().map_err(ProcessError::Spawn)?;
268        log_spawned_child_pid(child.id()).map_err(ProcessError::Spawn)?;
269        #[cfg(windows)]
270        let job = public_symbols::rp_assign_child_to_windows_kill_on_close_job_public(&child)
271            .map_err(ProcessError::Spawn)?;
272        if self.config.capture {
273            let stdout = child.stdout.take().expect("stdout pipe missing");
274            let stderr = child.stderr.take().expect("stderr pipe missing");
275            #[cfg(windows)]
276            {
277                use std::os::windows::io::AsRawHandle;
278                let mut handles = self
279                    .capture_pipe_handles
280                    .lock()
281                    .expect("capture pipe handles mutex poisoned");
282                handles.stdout = Some(stdout.as_raw_handle() as usize);
283                handles.stderr = Some(stderr.as_raw_handle() as usize);
284            }
285            self.spawn_reader(
286                stdout,
287                StreamKind::Stdout,
288                StreamKind::Stdout,
289                self.pipe_done_callback(StreamKind::Stdout),
290            );
291            self.spawn_reader(
292                stderr,
293                StreamKind::Stderr,
294                match self.config.stderr_mode {
295                    StderrMode::Stdout => StreamKind::Stdout,
296                    StderrMode::Pipe => StreamKind::Stderr,
297                },
298                self.pipe_done_callback(StreamKind::Stderr),
299            );
300        }
301        *guard = Some(ChildState {
302            child,
303            #[cfg(windows)]
304            _job: job,
305        });
306        drop(guard);
307        self.spawn_exit_waiter();
308        Ok(())
309    }
310
311    /// Background thread that polls for process exit and stores the exit code
312    /// atomically. This makes `returncode` auto-update without explicit `poll()`.
313    fn spawn_exit_waiter(&self) {
314        let child = Arc::clone(&self.child);
315        let shared = Arc::clone(&self.shared);
316        thread::spawn(move || loop {
317            if shared.returncode.load(Ordering::Acquire) != RETURNCODE_NOT_SET {
318                return;
319            }
320            {
321                let mut guard = child.lock().expect("child mutex poisoned");
322                if let Some(child_state) = guard.as_mut() {
323                    match child_state.child.try_wait() {
324                        Ok(Some(status)) => {
325                            let code = exit_code(status);
326                            shared.returncode.store(code as i64, Ordering::Release);
327                            shared.condvar.notify_all();
328                            return;
329                        }
330                        Ok(None) => {}
331                        Err(_) => return,
332                    }
333                } else {
334                    return;
335                }
336            }
337            thread::sleep(Duration::from_millis(10));
338        });
339    }
340
341    pub fn write_stdin(&self, data: &[u8]) -> Result<(), ProcessError> {
342        let mut guard = self.child.lock().expect("child mutex poisoned");
343        let child = &mut guard.as_mut().ok_or(ProcessError::NotRunning)?.child;
344        let stdin = child.stdin.as_mut().ok_or(ProcessError::StdinUnavailable)?;
345        use std::io::Write;
346        stdin.write_all(data).map_err(ProcessError::Io)?;
347        stdin.flush().map_err(ProcessError::Io)?;
348        drop(child.stdin.take());
349        Ok(())
350    }
351
352    /// Write to the child's stdin without closing it afterwards, so the
353    /// caller can issue additional writes. Used by interactive
354    /// pipe-backed sessions (#130 milestone 3) where the daemon keeps
355    /// stdin open across multiple client input frames.
356    pub fn write_stdin_streaming(&self, data: &[u8]) -> Result<(), ProcessError> {
357        let mut guard = self.child.lock().expect("child mutex poisoned");
358        let child = &mut guard.as_mut().ok_or(ProcessError::NotRunning)?.child;
359        let stdin = child.stdin.as_mut().ok_or(ProcessError::StdinUnavailable)?;
360        use std::io::Write;
361        stdin.write_all(data).map_err(ProcessError::Io)?;
362        stdin.flush().map_err(ProcessError::Io)?;
363        Ok(())
364    }
365
366    /// Explicitly close the child's stdin (signals EOF to the child).
367    /// Idempotent: returns Ok if stdin was already closed.
368    pub fn close_stdin(&self) -> Result<(), ProcessError> {
369        let mut guard = self.child.lock().expect("child mutex poisoned");
370        let child = &mut guard.as_mut().ok_or(ProcessError::NotRunning)?.child;
371        drop(child.stdin.take());
372        Ok(())
373    }
374
375    pub fn poll(&self) -> Result<Option<i32>, ProcessError> {
376        // Fast path: check atomic set by background waiter thread.
377        if let Some(code) = self.returncode() {
378            return Ok(Some(code));
379        }
380        let mut guard = self.child.lock().expect("child mutex poisoned");
381        let Some(child_state) = guard.as_mut() else {
382            return Ok(self.returncode());
383        };
384        let child = &mut child_state.child;
385        let status = child.try_wait().map_err(ProcessError::Io)?;
386        if let Some(status) = status {
387            let code = exit_code(status);
388            self.set_returncode(code);
389            return Ok(Some(code));
390        }
391        Ok(None)
392    }
393
394    // Preserve a stable Rust frame here in release user dumps.
395    #[inline(never)]
396    pub fn wait(&self, timeout: Option<Duration>) -> Result<i32, ProcessError> {
397        public_symbols::rp_native_process_wait_public(self, timeout)
398    }
399
400    fn wait_impl(&self, timeout: Option<Duration>) -> Result<i32, ProcessError> {
401        crate::rp_rust_debug_scope!("running_process_core::NativeProcess::wait");
402        if self.child.lock().expect("child mutex poisoned").is_none() {
403            return self.returncode().ok_or(ProcessError::NotRunning);
404        }
405        // Fast path: already exited.
406        if let Some(code) = self.returncode() {
407            public_symbols::rp_native_process_wait_for_capture_completion_public(self);
408            return Ok(code);
409        }
410        let start = Instant::now();
411        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
412        loop {
413            // Check returncode (set by exit-waiter thread via atomic + condvar).
414            let rc = self.shared.returncode.load(Ordering::Acquire);
415            if rc != RETURNCODE_NOT_SET {
416                drop(guard);
417                let code = rc as i32;
418                public_symbols::rp_native_process_wait_for_capture_completion_public(self);
419                return Ok(code);
420            }
421            if let Some(limit) = timeout {
422                let elapsed = start.elapsed();
423                if elapsed >= limit {
424                    return Err(ProcessError::Timeout);
425                }
426                let remaining = limit - elapsed;
427                // Wait on condvar with timeout, capped at 50ms to recheck.
428                let wait_time = remaining.min(Duration::from_millis(50));
429                guard = self
430                    .shared
431                    .condvar
432                    .wait_timeout(guard, wait_time)
433                    .expect("queue mutex poisoned")
434                    .0;
435            } else {
436                // Wait on condvar with periodic recheck.
437                guard = self
438                    .shared
439                    .condvar
440                    .wait_timeout(guard, Duration::from_millis(50))
441                    .expect("queue mutex poisoned")
442                    .0;
443            }
444        }
445    }
446
447    // Preserve a stable Rust frame here in release user dumps.
448    #[inline(never)]
449    pub fn kill(&self) -> Result<(), ProcessError> {
450        public_symbols::rp_native_process_kill_public(self)
451    }
452
453    fn kill_impl(&self) -> Result<(), ProcessError> {
454        crate::rp_rust_debug_scope!("running_process_core::NativeProcess::kill");
455        {
456            let mut guard = self.child.lock().expect("child mutex poisoned");
457            let child = &mut guard.as_mut().ok_or(ProcessError::NotRunning)?.child;
458            child.kill().map_err(ProcessError::Io)?;
459            let status = child.wait().map_err(ProcessError::Io)?;
460            self.set_returncode(exit_code(status));
461        }
462        // On Windows, interrupt any pending blocking `read()` in the
463        // per-stream reader threads so they fall out of their loops
464        // immediately. This is what makes the grandchild-pipe-orphan
465        // case (FastLED Bug B: uv.exe spawns a python.exe grandchild
466        // that inherits the pipe and outlives uv) wake up in
467        // microseconds instead of waiting for the bounded-drain
468        // safety-net deadline below.
469        #[cfg(windows)]
470        self.cancel_capture_io();
471        // Synchronize with the per-stream reader threads so that by the
472        // time kill() returns, the capture queues have flipped from
473        // "blocked on read" to "closed" and downstream pollers (e.g.
474        // take_combined_line) observe EOS instead of timeout. Without
475        // this, callers that hit a wait()-timeout path see Python code
476        // raise TimeoutError, kill the child, then race the reader
477        // threads — a 10ms poll loop can miss the EOS flip entirely.
478        //
479        // The deadline is the safety-net: on Windows `CancelIoEx`
480        // above almost always wakes the readers first; on POSIX, and
481        // in any pathological Windows case where `CancelIoEx` doesn't
482        // fire, the deadline guarantees `kill()` still returns.
483        public_symbols::rp_native_process_wait_for_capture_completion_with_deadline_public(
484            self,
485            kill_drain_deadline(),
486        );
487        Ok(())
488    }
489
490    pub fn terminate(&self) -> Result<(), ProcessError> {
491        self.kill()
492    }
493
494    /// Send the OS-appropriate soft termination signal to the child's
495    /// process group (POSIX: SIGTERM to `-pid`; Windows: no soft path
496    /// implemented yet — returns Ok without doing anything so callers
497    /// can run the same code on both platforms and rely on the post-
498    /// grace hard kill).
499    ///
500    /// Requires `ProcessConfig.create_process_group=true` on POSIX so
501    /// that `-pid` resolves to the child's own group. With the default
502    /// `create_process_group=false`, the kill would walk back to the
503    /// caller's group; the method silently no-ops in that case to avoid
504    /// signaling the wrong tree.
505    ///
506    /// Used by the daemon-side pipe sessions (#130 M4 follow-up) so
507    /// that `TerminationOutcome::SoftExit` becomes meaningful on POSIX.
508    pub fn terminate_group_soft(&self) -> Result<(), ProcessError> {
509        #[cfg(unix)]
510        {
511            if !self.config.create_process_group {
512                return Ok(());
513            }
514            let pid = match self.pid() {
515                Some(p) => p as i32,
516                None => return Err(ProcessError::NotRunning),
517            };
518            let result = unsafe { libc::kill(-pid, libc::SIGTERM) };
519            if result != 0 {
520                let err = std::io::Error::last_os_error();
521                if err.raw_os_error() != Some(libc::ESRCH) {
522                    return Err(ProcessError::Io(err));
523                }
524            }
525            Ok(())
526        }
527        #[cfg(windows)]
528        {
529            if !self.config.create_process_group {
530                // GenerateConsoleCtrlEvent only routes to children
531                // spawned with CREATE_NEW_PROCESS_GROUP, and the
532                // event would otherwise hit the daemon's own group.
533                // No-op so the hard-kill schedule still wins.
534                return Ok(());
535            }
536            let pid = match self.pid() {
537                Some(p) => p,
538                None => return Err(ProcessError::NotRunning),
539            };
540            // GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT=1, pid).
541            // SAFETY: the FFI call is the standard Windows API; no
542            // borrowed Rust state is involved.
543            let ok = unsafe {
544                winapi::um::wincon::GenerateConsoleCtrlEvent(
545                    winapi::um::wincon::CTRL_BREAK_EVENT,
546                    pid,
547                )
548            };
549            if ok == 0 {
550                let err = std::io::Error::last_os_error();
551                // ERROR_INVALID_HANDLE means the child has already
552                // exited or has detached from the console — treat as
553                // success because the soft step's only goal is to
554                // give the child a chance to exit cleanly, and a
555                // dead/detached child does not need one.
556                if err.raw_os_error() != Some(6) {
557                    return Err(ProcessError::Io(err));
558                }
559            }
560            Ok(())
561        }
562    }
563
564    // Preserve a stable Rust frame here in release user dumps.
565    #[inline(never)]
566    pub fn close(&self) -> Result<(), ProcessError> {
567        public_symbols::rp_native_process_close_public(self)
568    }
569
570    fn close_impl(&self) -> Result<(), ProcessError> {
571        crate::rp_rust_debug_scope!("running_process_core::NativeProcess::close");
572        if self.child.lock().expect("child mutex poisoned").is_none() {
573            return Ok(());
574        }
575        if self.poll()?.is_none() {
576            self.kill()?;
577        } else {
578            public_symbols::rp_native_process_wait_for_capture_completion_public(self);
579        }
580        Ok(())
581    }
582
583    pub fn pid(&self) -> Option<u32> {
584        self.child
585            .lock()
586            .expect("child mutex poisoned")
587            .as_ref()
588            .map(|state| state.child.id())
589    }
590
591    pub fn returncode(&self) -> Option<i32> {
592        let v = self.shared.returncode.load(Ordering::Acquire);
593        if v == RETURNCODE_NOT_SET {
594            None
595        } else {
596            Some(v as i32)
597        }
598    }
599
600    pub fn has_pending_stream(&self, stream: StreamKind) -> bool {
601        if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
602            return false;
603        }
604        let guard = self.shared.queues.lock().expect("queue mutex poisoned");
605        match stream {
606            StreamKind::Stdout => !guard.stdout_queue.is_empty(),
607            StreamKind::Stderr => !guard.stderr_queue.is_empty(),
608        }
609    }
610
611    pub fn has_pending_combined(&self) -> bool {
612        let guard = self.shared.queues.lock().expect("queue mutex poisoned");
613        !guard.combined_queue.is_empty()
614    }
615
616    pub fn drain_stream(&self, stream: StreamKind) -> Vec<Vec<u8>> {
617        if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
618            return Vec::new();
619        }
620        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
621        let queue = match stream {
622            StreamKind::Stdout => &mut guard.stdout_queue,
623            StreamKind::Stderr => &mut guard.stderr_queue,
624        };
625        queue.drain(..).collect()
626    }
627
628    pub fn drain_combined(&self) -> Vec<StreamEvent> {
629        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
630        guard.combined_queue.drain(..).collect()
631    }
632
633    pub fn read_stream(
634        &self,
635        stream: StreamKind,
636        timeout: Option<Duration>,
637    ) -> ReadStatus<Vec<u8>> {
638        let deadline = timeout.map(|limit| Instant::now() + limit);
639        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
640
641        loop {
642            if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
643                return ReadStatus::Eof;
644            }
645
646            let queue = match stream {
647                StreamKind::Stdout => &mut guard.stdout_queue,
648                StreamKind::Stderr => &mut guard.stderr_queue,
649            };
650            if let Some(line) = queue.pop_front() {
651                return ReadStatus::Line(line);
652            }
653
654            let closed = match stream {
655                StreamKind::Stdout => {
656                    if self.config.stderr_mode == StderrMode::Stdout {
657                        guard.stdout_closed && guard.stderr_closed
658                    } else {
659                        guard.stdout_closed
660                    }
661                }
662                StreamKind::Stderr => guard.stderr_closed,
663            };
664            if closed {
665                return ReadStatus::Eof;
666            }
667
668            match deadline {
669                Some(deadline) => {
670                    let now = Instant::now();
671                    if now >= deadline {
672                        return ReadStatus::Timeout;
673                    }
674                    let wait = deadline.saturating_duration_since(now);
675                    let result = self
676                        .shared
677                        .condvar
678                        .wait_timeout(guard, wait)
679                        .expect("queue mutex poisoned");
680                    guard = result.0;
681                    if result.1.timed_out() {
682                        return ReadStatus::Timeout;
683                    }
684                }
685                None => {
686                    guard = self
687                        .shared
688                        .condvar
689                        .wait(guard)
690                        .expect("queue mutex poisoned");
691                }
692            }
693        }
694    }
695
696    // Preserve a stable Rust frame here in release user dumps.
697    #[inline(never)]
698    pub fn read_combined(&self, timeout: Option<Duration>) -> ReadStatus<StreamEvent> {
699        public_symbols::rp_native_process_read_combined_public(self, timeout)
700    }
701
702    fn read_combined_impl(&self, timeout: Option<Duration>) -> ReadStatus<StreamEvent> {
703        crate::rp_rust_debug_scope!("running_process_core::NativeProcess::read_combined");
704        let deadline = timeout.map(|limit| Instant::now() + limit);
705        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
706
707        loop {
708            if let Some(event) = guard.combined_queue.pop_front() {
709                return ReadStatus::Line(event);
710            }
711            if guard.stdout_closed && guard.stderr_closed {
712                return ReadStatus::Eof;
713            }
714
715            match deadline {
716                Some(deadline) => {
717                    let now = Instant::now();
718                    if now >= deadline {
719                        return ReadStatus::Timeout;
720                    }
721                    let wait = deadline.saturating_duration_since(now);
722                    let result = self
723                        .shared
724                        .condvar
725                        .wait_timeout(guard, wait)
726                        .expect("queue mutex poisoned");
727                    guard = result.0;
728                    if result.1.timed_out() {
729                        return ReadStatus::Timeout;
730                    }
731                }
732                None => {
733                    guard = self
734                        .shared
735                        .condvar
736                        .wait(guard)
737                        .expect("queue mutex poisoned");
738                }
739            }
740        }
741    }
742
743    pub fn captured_stdout(&self) -> Vec<Vec<u8>> {
744        self.shared
745            .queues
746            .lock()
747            .expect("queue mutex poisoned")
748            .stdout_history
749            .clone()
750            .into_iter()
751            .collect()
752    }
753
754    pub fn captured_stderr(&self) -> Vec<Vec<u8>> {
755        if self.config.stderr_mode == StderrMode::Stdout {
756            return Vec::new();
757        }
758        self.shared
759            .queues
760            .lock()
761            .expect("queue mutex poisoned")
762            .stderr_history
763            .clone()
764            .into_iter()
765            .collect()
766    }
767
768    pub fn captured_combined(&self) -> Vec<StreamEvent> {
769        self.shared
770            .queues
771            .lock()
772            .expect("queue mutex poisoned")
773            .combined_history
774            .clone()
775            .into_iter()
776            .collect()
777    }
778
779    pub fn captured_stream_bytes(&self, stream: StreamKind) -> usize {
780        if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
781            return 0;
782        }
783        let guard = self.shared.queues.lock().expect("queue mutex poisoned");
784        match stream {
785            StreamKind::Stdout => guard.stdout_history_bytes,
786            StreamKind::Stderr => guard.stderr_history_bytes,
787        }
788    }
789
790    pub fn captured_combined_bytes(&self) -> usize {
791        self.shared
792            .queues
793            .lock()
794            .expect("queue mutex poisoned")
795            .combined_history_bytes
796    }
797
798    pub fn clear_captured_stream(&self, stream: StreamKind) -> usize {
799        if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
800            return 0;
801        }
802        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
803        match stream {
804            StreamKind::Stdout => {
805                let released = guard.stdout_history_bytes;
806                guard.stdout_history.clear();
807                guard.stdout_history_bytes = 0;
808                released
809            }
810            StreamKind::Stderr => {
811                let released = guard.stderr_history_bytes;
812                guard.stderr_history.clear();
813                guard.stderr_history_bytes = 0;
814                released
815            }
816        }
817    }
818
819    pub fn clear_captured_combined(&self) -> usize {
820        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
821        let released = guard.combined_history_bytes;
822        guard.combined_history.clear();
823        guard.combined_history_bytes = 0;
824        released
825    }
826
827    fn build_command(&self) -> Command {
828        let mut command = match &self.config.command {
829            CommandSpec::Shell(command) => shell_command(command),
830            CommandSpec::Argv(argv) => {
831                let mut command = Command::new(&argv[0]);
832                if argv.len() > 1 {
833                    command.args(&argv[1..]);
834                }
835                command
836            }
837        };
838        if let Some(cwd) = &self.config.cwd {
839            command.current_dir(cwd);
840        }
841        if let Some(env) = &self.config.env {
842            command.env_clear();
843            command.envs(env.iter().map(|(k, v)| (k, v)));
844        }
845        #[cfg(windows)]
846        {
847            use std::os::windows::process::CommandExt;
848
849            // CREATE_NEW_PROCESS_GROUP makes GenerateConsoleCtrlEvent
850            // with CTRL_BREAK_EVENT route to this child's group
851            // (rather than the daemon's group) — required for the
852            // pipe-session soft-signal path on Windows (#130 M4).
853            const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
854            let extra = if self.config.create_process_group {
855                CREATE_NEW_PROCESS_GROUP
856            } else {
857                0
858            };
859            let flags = self.config.creationflags.unwrap_or(0)
860                | extra
861                | windows_priority_flags(self.config.nice);
862            if flags != 0 {
863                command.creation_flags(flags);
864            }
865        }
866        #[cfg(unix)]
867        {
868            let create_process_group = self.config.create_process_group;
869            let nice = self.config.nice;
870
871            if create_process_group || nice.is_some() {
872                use std::os::unix::process::CommandExt;
873
874                unsafe {
875                    command.pre_exec(move || {
876                        if create_process_group && libc::setpgid(0, 0) == -1 {
877                            return Err(std::io::Error::last_os_error());
878                        }
879                        if let Some(nice) = nice {
880                            let result = libc::setpriority(libc::PRIO_PROCESS, 0, nice);
881                            if result == -1 {
882                                return Err(std::io::Error::last_os_error());
883                            }
884                        }
885                        Ok(())
886                    });
887                }
888            }
889        }
890        command
891    }
892
893    fn spawn_reader<R>(
894        &self,
895        pipe: R,
896        source_stream: StreamKind,
897        visible_stream: StreamKind,
898        on_pipe_done: Box<dyn FnOnce() + Send>,
899    ) where
900        R: Read + Send + 'static,
901    {
902        let shared = Arc::clone(&self.shared);
903        thread::spawn(move || {
904            let mut reader = pipe;
905            let mut chunk = vec![0_u8; 65536];
906            let mut pending = Vec::new();
907
908            loop {
909                match reader.read(&mut chunk) {
910                    Ok(0) => break,
911                    Ok(n) => {
912                        let lines = feed_chunk(&mut pending, &chunk[..n]);
913                        emit_lines(&shared, visible_stream, lines);
914                    }
915                    Err(_) => break,
916                }
917            }
918
919            if !pending.is_empty() {
920                emit_lines(&shared, visible_stream, vec![std::mem::take(&mut pending)]);
921            }
922
923            // Clear the parent-side pipe-handle slot under its mutex
924            // before dropping the reader. After this returns,
925            // `kill_impl` can no longer try to `CancelIoEx` on us, so
926            // it's safe for `reader`'s drop to close the HANDLE.
927            on_pipe_done();
928            drop(reader);
929
930            let mut guard = shared.queues.lock().expect("queue mutex poisoned");
931            match source_stream {
932                StreamKind::Stdout => guard.stdout_closed = true,
933                StreamKind::Stderr => guard.stderr_closed = true,
934            }
935            shared.condvar.notify_all();
936        });
937    }
938
939    #[cfg(windows)]
940    fn pipe_done_callback(&self, stream: StreamKind) -> Box<dyn FnOnce() + Send> {
941        let handles = Arc::clone(&self.capture_pipe_handles);
942        Box::new(move || {
943            let mut guard = handles
944                .lock()
945                .expect("capture pipe handles mutex poisoned");
946            match stream {
947                StreamKind::Stdout => guard.stdout = None,
948                StreamKind::Stderr => guard.stderr = None,
949            }
950        })
951    }
952
953    #[cfg(not(windows))]
954    fn pipe_done_callback(&self, _stream: StreamKind) -> Box<dyn FnOnce() + Send> {
955        Box::new(|| {})
956    }
957
958    /// Cancel any pending blocking `read()` on the parent-side capture
959    /// pipes so the reader threads' `read()` calls return
960    /// `ERROR_OPERATION_ABORTED` immediately. Used by `kill_impl` to
961    /// break the grandchild-orphan deadlock without waiting on
962    /// `wait_for_capture_completion_with_deadline`'s safety-net.
963    #[cfg(windows)]
964    fn cancel_capture_io(&self) {
965        crate::rp_rust_debug_scope!("running_process_core::NativeProcess::cancel_capture_io");
966        use winapi::shared::ntdef::HANDLE;
967        use winapi::um::ioapiset::CancelIoEx;
968        let guard = self
969            .capture_pipe_handles
970            .lock()
971            .expect("capture pipe handles mutex poisoned");
972        if let Some(h) = guard.stdout {
973            // SAFETY: the slot is `Some` only while the owning reader
974            // thread still holds the `ChildStdout`, so the HANDLE is
975            // valid for the duration of this call. The reader is
976            // blocked in `lock()` on the same mutex if it's racing us
977            // toward exit, so it cannot drop the pipe and close the
978            // HANDLE until we return.
979            unsafe {
980                CancelIoEx(h as HANDLE, std::ptr::null_mut());
981            }
982        }
983        if let Some(h) = guard.stderr {
984            unsafe {
985                CancelIoEx(h as HANDLE, std::ptr::null_mut());
986            }
987        }
988    }
989
990    fn set_returncode(&self, code: i32) {
991        self.shared.returncode.store(code as i64, Ordering::Release);
992        self.shared.condvar.notify_all();
993    }
994
995    fn wait_for_capture_completion_impl(&self) {
996        crate::rp_rust_debug_scope!(
997            "running_process_core::NativeProcess::wait_for_capture_completion"
998        );
999        if !self.config.capture {
1000            return;
1001        }
1002
1003        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1004        while !(guard.stdout_closed && guard.stderr_closed) {
1005            guard = self
1006                .shared
1007                .condvar
1008                .wait(guard)
1009                .expect("queue mutex poisoned");
1010        }
1011    }
1012
1013    /// Like `wait_for_capture_completion_impl` but bounded by `deadline`.
1014    /// Returns `true` if the reader threads flipped both closed flags on
1015    /// their own, `false` if the deadline elapsed first. On timeout the
1016    /// closed flags are force-set (and waiters notified) so downstream
1017    /// pollers stop seeing `Timeout` and start seeing `Eof`. A reader
1018    /// thread that eventually unblocks after the OS releases the pipe
1019    /// will assign `closed = true` again, which is a harmless no-op.
1020    fn wait_for_capture_completion_with_deadline_impl(&self, deadline: Instant) -> bool {
1021        crate::rp_rust_debug_scope!(
1022            "running_process_core::NativeProcess::wait_for_capture_completion_with_deadline"
1023        );
1024        if !self.config.capture {
1025            return true;
1026        }
1027
1028        let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1029        while !(guard.stdout_closed && guard.stderr_closed) {
1030            let now = Instant::now();
1031            if now >= deadline {
1032                guard.stdout_closed = true;
1033                guard.stderr_closed = true;
1034                self.shared.condvar.notify_all();
1035                return false;
1036            }
1037            let (next_guard, result) = self
1038                .shared
1039                .condvar
1040                .wait_timeout(guard, deadline - now)
1041                .expect("queue mutex poisoned");
1042            guard = next_guard;
1043            if result.timed_out() && !(guard.stdout_closed && guard.stderr_closed) {
1044                guard.stdout_closed = true;
1045                guard.stderr_closed = true;
1046                self.shared.condvar.notify_all();
1047                return false;
1048            }
1049        }
1050        true
1051    }
1052}
1053
1054#[cfg(unix)]
1055pub fn unix_set_priority(pid: u32, nice: i32) -> Result<(), std::io::Error> {
1056    let result = unsafe { libc::setpriority(libc::PRIO_PROCESS, pid, nice) };
1057    if result == -1 {
1058        return Err(std::io::Error::last_os_error());
1059    }
1060    Ok(())
1061}
1062
1063#[cfg(unix)]
1064pub fn unix_signal_process(pid: u32, signal: UnixSignal) -> Result<(), std::io::Error> {
1065    let result = unsafe { libc::kill(pid as i32, unix_signal_raw(signal)) };
1066    if result == -1 {
1067        return Err(std::io::Error::last_os_error());
1068    }
1069    Ok(())
1070}
1071
1072#[cfg(unix)]
1073pub fn unix_signal_process_group(pid: i32, signal: UnixSignal) -> Result<(), std::io::Error> {
1074    let result = unsafe { libc::killpg(pid, unix_signal_raw(signal)) };
1075    if result == -1 {
1076        return Err(std::io::Error::last_os_error());
1077    }
1078    Ok(())
1079}
1080
1081fn log_spawned_child_pid(pid: u32) -> Result<(), std::io::Error> {
1082    let Some(path) = std::env::var_os(CHILD_PID_LOG_PATH_ENV) else {
1083        return Ok(());
1084    };
1085
1086    let mut file = OpenOptions::new().create(true).append(true).open(path)?;
1087    file.write_all(format!("{pid}\n").as_bytes())?;
1088    file.flush()?;
1089    Ok(())
1090}
1091
1092#[cfg(windows)]
1093fn assign_child_to_windows_kill_on_close_job_impl(
1094    child: &Child,
1095) -> Result<WindowsJobHandle, std::io::Error> {
1096    crate::rp_rust_debug_scope!("running_process_core::assign_child_to_windows_kill_on_close_job");
1097    use std::mem::zeroed;
1098    use std::os::windows::io::AsRawHandle;
1099
1100    use winapi::shared::minwindef::FALSE;
1101    use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
1102    use winapi::um::jobapi2::{
1103        AssignProcessToJobObject, CreateJobObjectW, SetInformationJobObject,
1104    };
1105    use winapi::um::winnt::{
1106        JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
1107        JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
1108    };
1109
1110    let handle = child.as_raw_handle();
1111    let job = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };
1112    if job.is_null() || job == INVALID_HANDLE_VALUE {
1113        return Err(std::io::Error::last_os_error());
1114    }
1115
1116    let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { zeroed() };
1117    info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
1118    let ok = unsafe {
1119        SetInformationJobObject(
1120            job,
1121            JobObjectExtendedLimitInformation,
1122            (&mut info as *mut JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
1123            std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
1124        )
1125    };
1126    if ok == FALSE {
1127        let err = std::io::Error::last_os_error();
1128        unsafe { CloseHandle(job) };
1129        return Err(err);
1130    }
1131
1132    let ok = unsafe { AssignProcessToJobObject(job, handle.cast()) };
1133    if ok == FALSE {
1134        let err = std::io::Error::last_os_error();
1135        unsafe { CloseHandle(job) };
1136        return Err(err);
1137    }
1138
1139    Ok(WindowsJobHandle(job as usize))
1140}
1141
1142fn feed_chunk(pending: &mut Vec<u8>, chunk: &[u8]) -> Vec<Vec<u8>> {
1143    let mut lines = Vec::new();
1144    let mut start = 0;
1145    let mut index = 0;
1146
1147    while index < chunk.len() {
1148        if chunk[index] == b'\n' {
1149            let end = if index > start && chunk[index - 1] == b'\r' {
1150                index - 1
1151            } else {
1152                index
1153            };
1154            pending.extend_from_slice(&chunk[start..end]);
1155            if !pending.is_empty() {
1156                lines.push(std::mem::take(pending));
1157            }
1158            start = index + 1;
1159        }
1160        index += 1;
1161    }
1162
1163    pending.extend_from_slice(&chunk[start..]);
1164    lines
1165}
1166
1167fn emit_lines(shared: &Arc<SharedState>, stream: StreamKind, lines: Vec<Vec<u8>>) {
1168    if lines.is_empty() {
1169        return;
1170    }
1171    let mut guard = shared.queues.lock().expect("queue mutex poisoned");
1172    for line in lines {
1173        let line_len = line.len();
1174        match stream {
1175            StreamKind::Stdout => {
1176                guard.stdout_history_bytes += line_len;
1177                guard.stdout_history.push_back(line.clone());
1178                guard.stdout_queue.push_back(line.clone());
1179            }
1180            StreamKind::Stderr => {
1181                guard.stderr_history_bytes += line_len;
1182                guard.stderr_history.push_back(line.clone());
1183                guard.stderr_queue.push_back(line.clone());
1184            }
1185        }
1186        let event = StreamEvent { stream, line };
1187        guard.combined_history_bytes += line_len;
1188        guard.combined_history.push_back(event.clone());
1189        guard.combined_queue.push_back(event);
1190    }
1191    shared.condvar.notify_all();
1192}
1193
1194fn shell_command(command: &str) -> Command {
1195    #[cfg(windows)]
1196    {
1197        use std::os::windows::process::CommandExt;
1198
1199        let mut cmd = Command::new("cmd");
1200        cmd.raw_arg("/D /S /C \"");
1201        cmd.raw_arg(command);
1202        cmd.raw_arg("\"");
1203        cmd
1204    }
1205    #[cfg(not(windows))]
1206    {
1207        let mut cmd = Command::new("sh");
1208        cmd.arg("-lc").arg(command);
1209        cmd
1210    }
1211}
1212
1213fn exit_code(status: std::process::ExitStatus) -> i32 {
1214    #[cfg(unix)]
1215    {
1216        use std::os::unix::process::ExitStatusExt;
1217        status
1218            .code()
1219            .unwrap_or_else(|| -status.signal().unwrap_or(1))
1220    }
1221    #[cfg(not(unix))]
1222    {
1223        status.code().unwrap_or(1)
1224    }
1225}
1226
1227#[cfg(unix)]
1228fn unix_signal_raw(signal: UnixSignal) -> i32 {
1229    match signal {
1230        UnixSignal::Interrupt => libc::SIGINT,
1231        UnixSignal::Terminate => libc::SIGTERM,
1232        UnixSignal::Kill => libc::SIGKILL,
1233    }
1234}
1235
1236#[cfg(windows)]
1237fn windows_priority_flags(nice: Option<i32>) -> u32 {
1238    const IDLE_PRIORITY_CLASS: u32 = 0x0000_0040;
1239    const BELOW_NORMAL_PRIORITY_CLASS: u32 = 0x0000_4000;
1240    const ABOVE_NORMAL_PRIORITY_CLASS: u32 = 0x0000_8000;
1241    const HIGH_PRIORITY_CLASS: u32 = 0x0000_0080;
1242
1243    match nice {
1244        Some(value) if value >= 15 => IDLE_PRIORITY_CLASS,
1245        Some(value) if value >= 1 => BELOW_NORMAL_PRIORITY_CLASS,
1246        Some(value) if value <= -15 => HIGH_PRIORITY_CLASS,
1247        Some(value) if value <= -1 => ABOVE_NORMAL_PRIORITY_CLASS,
1248        _ => 0,
1249    }
1250}
1251#[cfg(test)]
1252mod tests;