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