1use 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;
18
19#[cfg(feature = "async-process")]
20mod async_process;
21#[cfg(feature = "async-process")]
22mod blocking_island;
23#[cfg(feature = "async-process")]
24pub use blocking_island::dispatch_blocking as blocking_island_dispatch;
25pub mod console_detect;
26pub mod containment;
27pub mod environment;
28mod helpers;
29#[cfg(feature = "async-process")]
30mod process_runtime;
31pub mod window_icon;
32pub mod observer;
37#[cfg(feature = "originator-scan")]
38pub mod originator;
39pub mod output_log;
40#[cfg(feature = "client")]
45pub mod proto {
47 #[allow(missing_docs)]
49 pub mod daemon {
50 include!(concat!(env!("OUT_DIR"), "/running_process.daemon.v1.rs"));
51 }
52}
53
54#[cfg(feature = "client")]
55pub mod client;
56
57#[cfg(feature = "client")]
62pub mod broker;
63
64#[cfg(feature = "client")]
68pub mod content_hash;
69
70#[cfg(feature = "probe")]
73pub mod probe;
74
75#[cfg(feature = "client")]
81pub mod maintenance;
82
83#[cfg(feature = "client")]
84pub mod cleanup;
85
86#[cfg(feature = "client")]
90pub mod boot_autostart;
91
92#[cfg(feature = "client")]
97pub mod runpm_config;
98
99#[cfg(feature = "test-support")]
104pub mod test_support;
105
106#[cfg(feature = "telemetry")]
109#[path = "daemon/telemetry.rs"]
110pub mod telemetry;
111
112#[cfg(feature = "daemon")]
115pub mod daemon;
117pub mod process_tree;
118#[cfg(feature = "pty")]
119pub mod pty;
121mod public_symbols;
122mod rust_debug;
123pub mod spawn;
124pub mod systemd_killmode;
125pub mod terminal_graphics;
126mod types;
127#[cfg(unix)]
128mod unix;
129#[cfg(windows)]
130mod windows;
131
132#[cfg(feature = "async-process")]
133pub use async_process::AsyncProcess;
134pub use console_detect::{monitor_console_windows, ConsoleWindowInfo};
135pub use containment::{ContainedProcessGroup, ORIGINATOR_ENV_VAR};
136#[cfg(feature = "client")]
138pub use content_hash::blake3_file;
139pub use observer::{
140 CapabilitySupport, CategoryCapability, EventCategory, ObserverCapabilities, ObserverConfig,
141 ObserverEvent, ObserverEventKind, ObserverSubscriber,
142};
143#[cfg(feature = "originator-scan")]
144pub use originator::{
145 find_declared_daemon_pids, find_processes_by_originator, OriginatorProcessInfo,
146};
147pub use output_log::{
148 CursorRead, OutputCursor, OutputLog, OutputRecord, SharedOutputCursor, SharedOutputLog,
149};
150pub use rust_debug::{render_rust_debug_traces, RustDebugScopeGuard};
151pub use spawn::{
152 spawn, spawn_daemon, spawn_daemon_breaking_away_from_job,
153 spawn_daemon_breaking_away_with_env_policy, spawn_daemon_with_clear_env,
154 spawn_daemon_with_env_policy, spawn_daemon_with_stdio, spawn_daemon_with_stdio_and_env_policy,
155 spawn_with_env_policy, DaemonChild, DaemonStdio, DaemonStdioSource, EnvironmentPolicy,
156 SpawnStdio, SpawnedChild, StdioSource, DAEMON_MARKER_ENV_VAR,
157};
158#[cfg(feature = "client-async")]
159pub use spawn::{spawn_tokio, TokioSpawnOptions};
160pub use terminal_graphics::{
161 current_terminal_capabilities, current_terminal_capabilities_with_timeout,
162 detect_terminal_capabilities, CapabilityStatus, EvidenceStrength, GraphicsCapability,
163 GraphicsProtocol, TerminalCapabilities, TerminalCapabilityInput, TerminalGraphicsCapabilities,
164 TerminalProbeEvidence,
165};
166pub use types::{
167 CommandSpec, ProcessConfig, ProcessError, ReadStatus, RunOutput, StderrMode, StdinMode,
168 StreamEvent, StreamKind,
169};
170pub use window_icon::{
171 host_icon_support, icon_support, set_host_icon, set_icon, IconError, IconScope, IconSource,
172 IconSupport, StockIcon,
173};
174
175#[cfg(unix)]
176pub(crate) use helpers::{
177 child_signal_disposition, child_try_wait_error_is_retryable, completed_reap_after_signal,
178 poll_mutex_until, with_child_lock_for_signal, ChildSignalDisposition,
179};
180pub(crate) use helpers::{exit_code, feed_chunk, kill_drain_deadline, log_spawned_child_pid};
181#[cfg(unix)]
182pub use unix::{unix_set_priority, unix_signal_process, unix_signal_process_group, UnixSignal};
183#[cfg(windows)]
184pub(crate) use windows::{
185 assign_child_to_windows_kill_on_close_job_impl, windows_creation_flags, WindowsJobHandle,
186};
187
188#[macro_export]
189macro_rules! rp_rust_debug_scope {
191 ($label:expr) => {
192 let _running_process_rust_debug_scope =
193 $crate::RustDebugScopeGuard::enter($label, file!(), line!());
194 };
195}
196
197#[derive(Default)]
198struct QueueState {
199 stdout_queue: VecDeque<Vec<u8>>,
200 stderr_queue: VecDeque<Vec<u8>>,
201 combined_queue: VecDeque<StreamEvent>,
202 stdout_history: VecDeque<Vec<u8>>,
203 stderr_history: VecDeque<Vec<u8>>,
204 combined_history: VecDeque<StreamEvent>,
205 stdout_raw: Vec<u8>,
206 stderr_raw: Vec<u8>,
207 stdout_history_bytes: usize,
208 stderr_history_bytes: usize,
209 combined_history_bytes: usize,
210 stdout_closed: bool,
211 stderr_closed: bool,
212}
213
214const RETURNCODE_NOT_SET: i64 = i64::MIN;
216
217struct SharedState {
218 queues: Mutex<QueueState>,
219 condvar: Condvar,
220 capture_limit: Option<usize>,
221 capture_overflowed: AtomicBool,
222 active_capture_readers: std::sync::atomic::AtomicUsize,
223 returncode: AtomicI64,
226 observer: Option<ObserverEmitter>,
231 observer_exit_emitted: AtomicBool,
234}
235
236struct ChildState {
237 child: Child,
238 #[cfg(windows)]
239 _job: WindowsJobHandle,
240}
241
242#[cfg(test)]
243#[derive(Debug, Eq, PartialEq)]
244enum CapturePollAction {
245 Wait,
246 Read,
247 Cancel,
248}
249
250#[cfg(test)]
251fn capture_poll_action(capture_revents: i16, wake_revents: i16) -> CapturePollAction {
252 if wake_revents != 0 {
253 CapturePollAction::Cancel
254 } else if capture_revents != 0 {
255 CapturePollAction::Read
256 } else {
257 CapturePollAction::Wait
258 }
259}
260
261fn cleanup_child_after_start_error(mut child: Child) {
262 let _ = child.kill();
263 thread::spawn(move || {
266 let _ = child.wait();
267 });
268}
269
270impl SharedState {
271 #[cfg(test)]
272 fn new(capture: bool) -> Self {
273 Self::with_observer_and_limit(capture, None, None)
274 }
275
276 fn with_observer_and_limit(
277 capture: bool,
278 observer: Option<ObserverEmitter>,
279 capture_limit: Option<usize>,
280 ) -> Self {
281 let queues = QueueState {
282 stdout_closed: !capture,
283 stderr_closed: !capture,
284 ..QueueState::default()
285 };
286 Self {
287 queues: Mutex::new(queues),
288 condvar: Condvar::new(),
289 capture_limit,
290 capture_overflowed: AtomicBool::new(false),
291 active_capture_readers: std::sync::atomic::AtomicUsize::new(0),
292 returncode: AtomicI64::new(RETURNCODE_NOT_SET),
293 observer,
294 observer_exit_emitted: AtomicBool::new(false),
295 }
296 }
297
298 fn emit_exited(&self, pid: u32, exit_code: i32) {
301 let Some(emitter) = self.observer.as_ref() else {
302 return;
303 };
304 if self
305 .observer_exit_emitted
306 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
307 .is_ok()
308 {
309 emitter.emit_exited(pid, exit_code);
310 }
311 }
312}
313
314pub struct NativeProcess {
321 config: ProcessConfig,
322 command_override: Mutex<Option<Command>>,
323 child: Arc<Mutex<Option<ChildState>>>,
324 stdin: Mutex<Option<ChildStdin>>,
325 shared: Arc<SharedState>,
326 #[cfg(test)]
327 stdin_write_active: AtomicBool,
328 capture_cancellation:
329 Arc<running_process_platform_internal::platform::process::CaptureCancellation>,
330}
331
332impl NativeProcess {
333 pub fn new(config: ProcessConfig) -> Self {
339 Self::new_with_options(config, None, None, None)
340 }
341
342 pub fn with_observer(
355 config: ProcessConfig,
356 observer: crate::observer::ObserverConfig,
357 ) -> (Self, ObserverSubscriber) {
358 let (emitter, subscriber) = ObserverEmitter::new(observer);
359 let process = Self::new_with_options(config, Some(emitter), None, None);
360 (process, subscriber)
361 }
362
363 fn new_with_capture_limit(config: ProcessConfig, capture_limit: usize) -> Self {
364 Self::new_with_options(config, None, Some(capture_limit), None)
365 }
366
367 fn new_with_command_capture_limit(
368 command: Command,
369 config: ProcessConfig,
370 capture_limit: usize,
371 ) -> Self {
372 Self::new_with_options(config, None, Some(capture_limit), Some(command))
373 }
374
375 fn new_with_options(
376 config: ProcessConfig,
377 observer: Option<ObserverEmitter>,
378 capture_limit: Option<usize>,
379 command_override: Option<Command>,
380 ) -> Self {
381 let shared = SharedState::with_observer_and_limit(config.capture, observer, capture_limit);
382 Self {
383 shared: Arc::new(shared),
384 command_override: Mutex::new(command_override),
385 child: Arc::new(Mutex::new(None)),
386 stdin: Mutex::new(None),
387 #[cfg(test)]
388 stdin_write_active: AtomicBool::new(false),
389 config,
390 capture_cancellation: Arc::new(Default::default()),
391 }
392 }
393
394 #[inline(never)]
396 pub fn start(&self) -> Result<(), ProcessError> {
401 public_symbols::rp_native_process_start_public(self)
402 }
403
404 fn start_impl(&self) -> Result<(), ProcessError> {
405 crate::rp_rust_debug_scope!("running_process::NativeProcess::start");
406 let mut guard = self.child.lock().expect("child mutex poisoned");
407 if guard.is_some() {
408 return Err(ProcessError::AlreadyStarted);
409 }
410
411 let mut command = self.build_command();
412 match self.config.stdin_mode {
413 StdinMode::Inherit => {}
414 StdinMode::Piped => {
415 command.stdin(Stdio::piped());
416 }
417 StdinMode::Null => {
418 command.stdin(Stdio::null());
419 }
420 }
421 if self.config.capture {
422 command.stdout(Stdio::piped());
423 command.stderr(Stdio::piped());
424 }
425
426 let mut child = command.spawn().map_err(ProcessError::Spawn)?;
427 log_spawned_child_pid(child.id()).map_err(ProcessError::Spawn)?;
428 if let Some(emitter) = self.shared.observer.as_ref() {
431 emitter.emit_started(child.id());
432 }
433 #[cfg(windows)]
438 let job = {
439 let descendant_sink = self
440 .shared
441 .observer
442 .as_ref()
443 .and_then(|e| e.descendant_sink());
444 let direct_pid = child.id();
445 public_symbols::rp_assign_child_to_windows_kill_on_close_job_with_observer_public(
446 &child,
447 descendant_sink,
448 direct_pid,
449 self.config.address_space_limit_bytes,
450 )
451 .map_err(ProcessError::Spawn)?
452 };
453 #[cfg(target_os = "linux")]
457 {
458 if let Some(emitter) = self.shared.observer.as_ref() {
459 if let Some((sink, stop)) = emitter.descendant_pump() {
460 crate::observer::descendants_linux::enable_subreaper();
461 crate::observer::descendants_linux::spawn_pump(child.id(), sink, stop);
462 }
463 }
464 }
465 #[cfg(target_os = "macos")]
469 {
470 if let Some(emitter) = self.shared.observer.as_ref() {
471 if let Some((sink, stop)) = emitter.descendant_pump() {
472 crate::observer::descendants_macos::spawn_pump(child.id(), sink, stop);
473 }
474 }
475 }
476 if self.config.capture {
477 let stdout = child.stdout.take().expect("stdout pipe missing");
478 let stderr = child.stderr.take().expect("stderr pipe missing");
479 let stdout =
480 match running_process_platform_internal::platform::process::prepare_capture_reader(
481 stdout,
482 &self.capture_cancellation,
483 running_process_platform_internal::platform::process::CaptureStream::Stdout,
484 ) {
485 Ok(stdout) => stdout,
486 Err(error) => {
487 cleanup_child_after_start_error(child);
488 return Err(ProcessError::Spawn(error));
489 }
490 };
491 let stderr =
492 match running_process_platform_internal::platform::process::prepare_capture_reader(
493 stderr,
494 &self.capture_cancellation,
495 running_process_platform_internal::platform::process::CaptureStream::Stderr,
496 ) {
497 Ok(stderr) => stderr,
498 Err(error) => {
499 running_process_platform_internal::platform::process::capture_reader_done(
500 &self.capture_cancellation,
501 running_process_platform_internal::platform::process::CaptureStream::Stdout,
502 );
503 cleanup_child_after_start_error(child);
504 return Err(ProcessError::Spawn(error));
505 }
506 };
507 self.spawn_reader(
508 stdout,
509 StreamKind::Stdout,
510 StreamKind::Stdout,
511 self.pipe_done_callback(StreamKind::Stdout),
512 );
513 self.spawn_reader(
514 stderr,
515 StreamKind::Stderr,
516 match self.config.stderr_mode {
517 StderrMode::Stdout => StreamKind::Stdout,
518 StderrMode::Pipe => StreamKind::Stderr,
519 },
520 self.pipe_done_callback(StreamKind::Stderr),
521 );
522 }
523 *self.stdin.lock().expect("stdin mutex poisoned") = child.stdin.take();
524 *guard = Some(ChildState {
525 child,
526 #[cfg(windows)]
527 _job: job,
528 });
529 drop(guard);
530 self.spawn_exit_waiter();
531 Ok(())
532 }
533
534 fn spawn_exit_waiter(&self) {
537 let child = Arc::clone(&self.child);
538 let shared = Arc::clone(&self.shared);
539 let capture = self.config.capture;
540 let capture_cancellation = Arc::clone(&self.capture_cancellation);
541 thread::spawn(move || {
542 loop {
543 if shared.returncode.load(Ordering::Acquire) != RETURNCODE_NOT_SET {
544 return;
545 }
546 let exited = {
547 let mut guard = child.lock().expect("child mutex poisoned");
548 if let Some(child_state) = guard.as_mut() {
549 let pid = child_state.child.id();
550 match child_state.child.try_wait() {
551 Ok(Some(status)) => {
552 let code = exit_code(status);
553 shared.returncode.store(code as i64, Ordering::Release);
554 shared.emit_exited(pid, code);
558 shared.condvar.notify_all();
559 true
560 }
561 Ok(None) => false,
562 Err(_error) => {
563 #[cfg(unix)]
564 if child_try_wait_error_is_retryable(&_error) {
565 false
566 } else {
567 return;
568 }
569 #[cfg(windows)]
570 return;
571 }
572 }
573 } else {
574 return;
575 }
576 };
577 if exited {
578 if capture {
593 let drained = finalize_capture_completion(&shared, kill_drain_deadline());
594 if !drained {
595 running_process_platform_internal::platform::process::cancel_capture_reader(
596 &capture_cancellation,
597 );
598 }
599 }
600 return;
601 }
602 thread::sleep(Duration::from_millis(10));
608 }
609 });
610 }
611
612 pub fn write_stdin(&self, data: &[u8]) -> Result<(), ProcessError> {
614 if self.child.lock().expect("child mutex poisoned").is_none() {
615 return Err(ProcessError::NotRunning);
616 }
617 let mut guard = self.stdin.lock().expect("stdin mutex poisoned");
618 let stdin = guard.as_mut().ok_or(ProcessError::StdinUnavailable)?;
619 use std::io::Write;
620 #[cfg(test)]
621 self.stdin_write_active.store(true, Ordering::Release);
622 let write_result = stdin.write_all(data);
623 #[cfg(test)]
624 self.stdin_write_active.store(false, Ordering::Release);
625 write_result.map_err(ProcessError::Io)?;
626 stdin.flush().map_err(ProcessError::Io)?;
627 drop(guard.take());
628 Ok(())
629 }
630
631 pub fn write_stdin_streaming(&self, data: &[u8]) -> Result<(), ProcessError> {
636 if self.child.lock().expect("child mutex poisoned").is_none() {
637 return Err(ProcessError::NotRunning);
638 }
639 let mut guard = self.stdin.lock().expect("stdin mutex poisoned");
640 let stdin = guard.as_mut().ok_or(ProcessError::StdinUnavailable)?;
641 use std::io::Write;
642 #[cfg(test)]
643 self.stdin_write_active.store(true, Ordering::Release);
644 let write_result = stdin.write_all(data);
645 #[cfg(test)]
646 self.stdin_write_active.store(false, Ordering::Release);
647 write_result.map_err(ProcessError::Io)?;
648 stdin.flush().map_err(ProcessError::Io)?;
649 Ok(())
650 }
651
652 pub fn close_stdin(&self) -> Result<(), ProcessError> {
655 if self.child.lock().expect("child mutex poisoned").is_none() {
656 return Err(ProcessError::NotRunning);
657 }
658 drop(self.stdin.lock().expect("stdin mutex poisoned").take());
659 Ok(())
660 }
661
662 pub fn poll(&self) -> Result<Option<i32>, ProcessError> {
666 if let Some(code) = self.returncode() {
668 return Ok(Some(code));
669 }
670 let mut guard = self.child.lock().expect("child mutex poisoned");
671 let Some(child_state) = guard.as_mut() else {
672 return Ok(self.returncode());
673 };
674 let pid = child_state.child.id();
675 let child = &mut child_state.child;
676 let status = child.try_wait().map_err(ProcessError::Io)?;
677 if let Some(status) = status {
678 let code = exit_code(status);
679 self.set_returncode(code);
680 self.shared.emit_exited(pid, code);
681 return Ok(Some(code));
682 }
683 Ok(None)
684 }
685
686 #[inline(never)]
688 pub fn wait(&self, timeout: Option<Duration>) -> Result<i32, ProcessError> {
693 public_symbols::rp_native_process_wait_public(self, timeout)
694 }
695
696 fn wait_impl(&self, timeout: Option<Duration>) -> Result<i32, ProcessError> {
697 crate::rp_rust_debug_scope!("running_process::NativeProcess::wait");
698 if self.child.lock().expect("child mutex poisoned").is_none() {
699 return self.returncode().ok_or(ProcessError::NotRunning);
700 }
701 if let Some(code) = self.returncode() {
703 self.finish_capture_drain();
704 return Ok(code);
705 }
706 let start = Instant::now();
707 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
708 loop {
709 let rc = self.shared.returncode.load(Ordering::Acquire);
711 if rc != RETURNCODE_NOT_SET {
712 drop(guard);
713 let code = rc as i32;
714 self.finish_capture_drain();
715 return Ok(code);
716 }
717 if let Some(limit) = timeout {
718 let elapsed = start.elapsed();
719 if elapsed >= limit {
720 return Err(ProcessError::Timeout);
721 }
722 let remaining = limit - elapsed;
723 let wait_time = remaining.min(Duration::from_millis(50));
725 guard = self
726 .shared
727 .condvar
728 .wait_timeout(guard, wait_time)
729 .expect("queue mutex poisoned")
730 .0;
731 } else {
732 guard = self
734 .shared
735 .condvar
736 .wait_timeout(guard, Duration::from_millis(50))
737 .expect("queue mutex poisoned")
738 .0;
739 }
740 }
741 }
742
743 #[inline(never)]
745 pub fn kill(&self) -> Result<(), ProcessError> {
747 public_symbols::rp_native_process_kill_public(self)
748 }
749
750 fn kill_impl(&self) -> Result<(), ProcessError> {
751 crate::rp_rust_debug_scope!("running_process::NativeProcess::kill");
752 #[cfg(windows)]
753 {
754 let mut guard = self.child.lock().expect("child mutex poisoned");
755 let child = &mut guard.as_mut().ok_or(ProcessError::NotRunning)?.child;
756 let pid = child.id();
757 child.kill().map_err(ProcessError::Io)?;
758 let status = child.wait().map_err(ProcessError::Io)?;
759 let code = exit_code(status);
760 self.set_returncode(code);
761 self.shared.emit_exited(pid, code);
764 }
765 #[cfg(unix)]
766 {
767 let deadline = kill_drain_deadline();
768 let (pid, already_reaped) = with_child_lock_for_signal(&self.child, |state| {
769 let child = &mut state.as_mut().ok_or(ProcessError::NotRunning)?.child;
770 let pid = child.id();
771 match child_signal_disposition(child.try_wait()).map_err(ProcessError::Io)? {
772 ChildSignalDisposition::AlreadyExited(status) => Ok((pid, Some(status))),
773 ChildSignalDisposition::Signal => {
774 let group_signaled = self.config.create_process_group
775 && unix_signal_process_group(pid as i32, UnixSignal::Kill).is_ok();
776 if !group_signaled {
777 child.kill().map_err(ProcessError::Io)?;
778 }
779 Ok((pid, None))
780 }
781 }
782 })?;
783
784 self.cancel_capture_io();
788 let reaped = already_reaped.or_else(|| {
789 let reap_result =
790 poll_mutex_until(&self.child, deadline, Duration::from_millis(10), |state| {
791 match state.as_mut() {
792 Some(child) => child.child.try_wait(),
793 None => Ok(None),
794 }
795 });
796 completed_reap_after_signal(reap_result)
797 });
798 if let Some(status) = reaped {
799 let code = exit_code(status);
800 self.set_returncode(code);
801 self.shared.emit_exited(pid, code);
802 }
803 public_symbols::rp_native_process_wait_for_capture_completion_with_deadline_public(
804 self, deadline,
805 );
806 Ok(())
807 }
808 #[cfg(windows)]
809 {
810 self.cancel_capture_io();
818 public_symbols::rp_native_process_wait_for_capture_completion_with_deadline_public(
829 self,
830 kill_drain_deadline(),
831 );
832 Ok(())
833 }
834 }
835
836 pub fn terminate(&self) -> Result<(), ProcessError> {
840 self.kill()
841 }
842
843 pub fn terminate_group_soft(&self) -> Result<(), ProcessError> {
855 if !self.config.create_process_group {
856 return Ok(());
858 }
859 let pid = self.pid().ok_or(ProcessError::NotRunning)?;
860 running_process_platform_internal::platform::process::soft_terminate_process_group(pid)
861 .map_err(ProcessError::Io)
862 }
863
864 #[inline(never)]
866 pub fn close(&self) -> Result<(), ProcessError> {
868 public_symbols::rp_native_process_close_public(self)
869 }
870
871 fn close_impl(&self) -> Result<(), ProcessError> {
872 crate::rp_rust_debug_scope!("running_process::NativeProcess::close");
873 if self.child.lock().expect("child mutex poisoned").is_none() {
874 return Ok(());
875 }
876 if self.poll()?.is_none() {
877 self.kill()?;
878 } else {
879 self.finish_capture_drain();
880 }
881 Ok(())
882 }
883
884 pub fn pid(&self) -> Option<u32> {
886 self.child
887 .lock()
888 .expect("child mutex poisoned")
889 .as_ref()
890 .map(|state| state.child.id())
891 }
892
893 pub fn returncode(&self) -> Option<i32> {
895 let v = self.shared.returncode.load(Ordering::Acquire);
896 if v == RETURNCODE_NOT_SET {
897 None
898 } else {
899 Some(v as i32)
900 }
901 }
902
903 pub fn has_pending_stream(&self, stream: StreamKind) -> bool {
905 if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
906 return false;
907 }
908 let guard = self.shared.queues.lock().expect("queue mutex poisoned");
909 match stream {
910 StreamKind::Stdout => !guard.stdout_queue.is_empty(),
911 StreamKind::Stderr => !guard.stderr_queue.is_empty(),
912 }
913 }
914
915 pub fn has_pending_combined(&self) -> bool {
917 let guard = self.shared.queues.lock().expect("queue mutex poisoned");
918 !guard.combined_queue.is_empty()
919 }
920
921 pub fn drain_stream(&self, stream: StreamKind) -> Vec<Vec<u8>> {
923 if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
924 return Vec::new();
925 }
926 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
927 let queue = match stream {
928 StreamKind::Stdout => &mut guard.stdout_queue,
929 StreamKind::Stderr => &mut guard.stderr_queue,
930 };
931 queue.drain(..).collect()
932 }
933
934 pub fn drain_combined(&self) -> Vec<StreamEvent> {
936 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
937 guard.combined_queue.drain(..).collect()
938 }
939
940 pub fn read_stream(
945 &self,
946 stream: StreamKind,
947 timeout: Option<Duration>,
948 ) -> ReadStatus<Vec<u8>> {
949 let deadline = timeout.map(|limit| Instant::now() + limit);
950 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
951
952 loop {
953 if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
954 return ReadStatus::Eof;
955 }
956
957 let queue = match stream {
958 StreamKind::Stdout => &mut guard.stdout_queue,
959 StreamKind::Stderr => &mut guard.stderr_queue,
960 };
961 if let Some(line) = queue.pop_front() {
962 return ReadStatus::Line(line);
963 }
964
965 let closed = match stream {
966 StreamKind::Stdout => {
967 if self.config.stderr_mode == StderrMode::Stdout {
968 guard.stdout_closed && guard.stderr_closed
969 } else {
970 guard.stdout_closed
971 }
972 }
973 StreamKind::Stderr => guard.stderr_closed,
974 };
975 if closed {
976 return ReadStatus::Eof;
977 }
978
979 match deadline {
980 Some(deadline) => {
981 let now = Instant::now();
982 if now >= deadline {
983 return ReadStatus::Timeout;
984 }
985 let wait = deadline.saturating_duration_since(now);
986 let result = self
987 .shared
988 .condvar
989 .wait_timeout(guard, wait)
990 .expect("queue mutex poisoned");
991 guard = result.0;
992 if result.1.timed_out() {
993 return ReadStatus::Timeout;
994 }
995 }
996 None => {
997 guard = self
998 .shared
999 .condvar
1000 .wait(guard)
1001 .expect("queue mutex poisoned");
1002 }
1003 }
1004 }
1005 }
1006
1007 #[inline(never)]
1009 pub fn read_combined(&self, timeout: Option<Duration>) -> ReadStatus<StreamEvent> {
1011 public_symbols::rp_native_process_read_combined_public(self, timeout)
1012 }
1013
1014 fn read_combined_impl(&self, timeout: Option<Duration>) -> ReadStatus<StreamEvent> {
1015 crate::rp_rust_debug_scope!("running_process::NativeProcess::read_combined");
1016 let deadline = timeout.map(|limit| Instant::now() + limit);
1017 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1018
1019 loop {
1020 if let Some(event) = guard.combined_queue.pop_front() {
1021 return ReadStatus::Line(event);
1022 }
1023 if guard.stdout_closed && guard.stderr_closed {
1024 return ReadStatus::Eof;
1025 }
1026
1027 match deadline {
1028 Some(deadline) => {
1029 let now = Instant::now();
1030 if now >= deadline {
1031 return ReadStatus::Timeout;
1032 }
1033 let wait = deadline.saturating_duration_since(now);
1034 let result = self
1035 .shared
1036 .condvar
1037 .wait_timeout(guard, wait)
1038 .expect("queue mutex poisoned");
1039 guard = result.0;
1040 if result.1.timed_out() {
1041 return ReadStatus::Timeout;
1042 }
1043 }
1044 None => {
1045 guard = self
1046 .shared
1047 .condvar
1048 .wait(guard)
1049 .expect("queue mutex poisoned");
1050 }
1051 }
1052 }
1053 }
1054
1055 pub fn captured_stdout(&self) -> Vec<Vec<u8>> {
1057 self.shared
1058 .queues
1059 .lock()
1060 .expect("queue mutex poisoned")
1061 .stdout_history
1062 .clone()
1063 .into_iter()
1064 .collect()
1065 }
1066
1067 fn captured_stdout_raw(&self) -> Vec<u8> {
1068 self.shared
1069 .queues
1070 .lock()
1071 .expect("queue mutex poisoned")
1072 .stdout_raw
1073 .clone()
1074 }
1075
1076 pub fn captured_stderr(&self) -> Vec<Vec<u8>> {
1078 if self.config.stderr_mode == StderrMode::Stdout {
1079 return Vec::new();
1080 }
1081 self.shared
1082 .queues
1083 .lock()
1084 .expect("queue mutex poisoned")
1085 .stderr_history
1086 .clone()
1087 .into_iter()
1088 .collect()
1089 }
1090
1091 fn captured_stderr_raw(&self) -> Vec<u8> {
1092 if self.config.stderr_mode == StderrMode::Stdout {
1093 return Vec::new();
1094 }
1095 self.shared
1096 .queues
1097 .lock()
1098 .expect("queue mutex poisoned")
1099 .stderr_raw
1100 .clone()
1101 }
1102
1103 pub fn captured_combined(&self) -> Vec<StreamEvent> {
1105 self.shared
1106 .queues
1107 .lock()
1108 .expect("queue mutex poisoned")
1109 .combined_history
1110 .clone()
1111 .into_iter()
1112 .collect()
1113 }
1114
1115 pub fn captured_stream_bytes(&self, stream: StreamKind) -> usize {
1117 if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
1118 return 0;
1119 }
1120 let guard = self.shared.queues.lock().expect("queue mutex poisoned");
1121 match stream {
1122 StreamKind::Stdout => guard.stdout_history_bytes,
1123 StreamKind::Stderr => guard.stderr_history_bytes,
1124 }
1125 }
1126
1127 pub fn captured_combined_bytes(&self) -> usize {
1129 self.shared
1130 .queues
1131 .lock()
1132 .expect("queue mutex poisoned")
1133 .combined_history_bytes
1134 }
1135
1136 pub fn clear_captured_stream(&self, stream: StreamKind) -> usize {
1138 if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
1139 return 0;
1140 }
1141 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1142 match stream {
1143 StreamKind::Stdout => {
1144 let released = guard.stdout_history_bytes;
1145 guard.stdout_history.clear();
1146 guard.stdout_raw.clear();
1147 guard.stdout_history_bytes = 0;
1148 released
1149 }
1150 StreamKind::Stderr => {
1151 let released = guard.stderr_history_bytes;
1152 guard.stderr_history.clear();
1153 guard.stderr_raw.clear();
1154 guard.stderr_history_bytes = 0;
1155 released
1156 }
1157 }
1158 }
1159
1160 pub fn clear_captured_combined(&self) -> usize {
1162 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1163 let released = guard.combined_history_bytes;
1164 guard.combined_history.clear();
1165 guard.combined_history_bytes = 0;
1166 released
1167 }
1168
1169 fn build_command(&self) -> Command {
1170 let command_override = self
1171 .command_override
1172 .lock()
1173 .expect("command override mutex poisoned")
1174 .take();
1175 let mut command = match command_override {
1176 Some(command) => command,
1177 None => {
1178 let mut command = match &self.config.command {
1179 CommandSpec::Shell(command) => shell_command(command),
1180 CommandSpec::Argv(argv) => {
1181 let mut command = Command::new(&argv[0]);
1182 if argv.len() > 1 {
1183 command.args(&argv[1..]);
1184 }
1185 command
1186 }
1187 };
1188 if let Some(cwd) = &self.config.cwd {
1189 command.current_dir(cwd);
1190 }
1191 if let Some(env) = &self.config.env {
1192 command.env_clear();
1193 command.envs(env.iter().map(|(k, v)| (k, v)));
1194 }
1195 command
1196 }
1197 };
1198 let windows_creation_flags = {
1199 #[cfg(windows)]
1200 {
1201 windows_creation_flags(
1209 self.config.creationflags,
1210 self.config.create_process_group,
1211 self.config.nice,
1212 running_process_platform_internal::platform::process::parent_has_console(),
1213 )
1214 }
1215 #[cfg(not(windows))]
1216 {
1217 0
1218 }
1219 };
1220 running_process_platform_internal::platform::process::configure_native_command(
1221 &mut command,
1222 windows_creation_flags,
1223 self.config.create_process_group,
1224 self.config.nice,
1225 self.config.address_space_limit_bytes,
1226 );
1227 command
1228 }
1229
1230 fn spawn_reader<R>(
1231 &self,
1232 pipe: R,
1233 source_stream: StreamKind,
1234 visible_stream: StreamKind,
1235 on_pipe_done: Box<dyn FnOnce() + Send>,
1236 ) where
1237 R: Read + Send + 'static,
1238 {
1239 let shared = Arc::clone(&self.shared);
1240 shared.active_capture_readers.fetch_add(1, Ordering::AcqRel);
1241 thread::spawn(move || {
1242 let mut reader = pipe;
1243 let mut chunk = vec![0_u8; 65536];
1244 let mut pending = Vec::new();
1245
1246 loop {
1247 match reader.read(&mut chunk) {
1248 Ok(0) => break,
1249 Ok(n) => {
1250 if append_raw(&shared, visible_stream, &chunk[..n]) {
1251 let lines = feed_chunk(&mut pending, &chunk[..n]);
1252 emit_lines(&shared, visible_stream, lines);
1253 } else {
1254 pending.clear();
1255 }
1256 }
1257 Err(_) => break,
1258 }
1259 }
1260
1261 if !pending.is_empty() && !shared.capture_overflowed.load(Ordering::Acquire) {
1262 emit_lines(&shared, visible_stream, vec![std::mem::take(&mut pending)]);
1263 }
1264
1265 on_pipe_done();
1270 drop(reader);
1271
1272 let mut guard = shared.queues.lock().expect("queue mutex poisoned");
1273 match source_stream {
1274 StreamKind::Stdout => guard.stdout_closed = true,
1275 StreamKind::Stderr => guard.stderr_closed = true,
1276 }
1277 shared.active_capture_readers.fetch_sub(1, Ordering::AcqRel);
1278 shared.condvar.notify_all();
1279 });
1280 }
1281
1282 fn pipe_done_callback(&self, stream: StreamKind) -> Box<dyn FnOnce() + Send> {
1283 let cancellation = Arc::clone(&self.capture_cancellation);
1284 Box::new(move || {
1285 let stream = match stream {
1286 StreamKind::Stdout => {
1287 running_process_platform_internal::platform::process::CaptureStream::Stdout
1288 }
1289 StreamKind::Stderr => {
1290 running_process_platform_internal::platform::process::CaptureStream::Stderr
1291 }
1292 };
1293 running_process_platform_internal::platform::process::capture_reader_done(
1294 &cancellation,
1295 stream,
1296 );
1297 })
1298 }
1299
1300 fn cancel_capture_io(&self) {
1304 crate::rp_rust_debug_scope!("running_process::NativeProcess::cancel_capture_io");
1305 running_process_platform_internal::platform::process::cancel_capture_reader(
1306 &self.capture_cancellation,
1307 );
1308 }
1309
1310 fn set_returncode(&self, code: i32) {
1311 self.shared.returncode.store(code as i64, Ordering::Release);
1312 self.shared.condvar.notify_all();
1313 }
1314
1315 fn finish_capture_drain(&self) {
1325 self.finish_capture_drain_with_deadline(kill_drain_deadline());
1326 }
1327
1328 fn finish_capture_drain_with_deadline(&self, deadline: Instant) {
1329 let drained = self.wait_for_capture_completion_with_deadline_impl(deadline);
1330 if !drained {
1331 self.cancel_capture_io();
1332 }
1333 }
1334
1335 fn wait_for_capture_completion_with_deadline_impl(&self, deadline: Instant) -> bool {
1338 crate::rp_rust_debug_scope!(
1339 "running_process::NativeProcess::wait_for_capture_completion_with_deadline"
1340 );
1341 if !self.config.capture {
1342 return true;
1343 }
1344 finalize_capture_completion(&self.shared, deadline)
1345 }
1346
1347 fn wait_for_capture_readers_with_deadline(&self, deadline: Instant) -> bool {
1348 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
1349 while self.shared.active_capture_readers.load(Ordering::Acquire) != 0 {
1350 let now = Instant::now();
1351 if now >= deadline {
1352 return false;
1353 }
1354 let (next_guard, result) = self
1355 .shared
1356 .condvar
1357 .wait_timeout(guard, deadline - now)
1358 .expect("queue mutex poisoned");
1359 guard = next_guard;
1360 if result.timed_out() && self.shared.active_capture_readers.load(Ordering::Acquire) != 0
1361 {
1362 return false;
1363 }
1364 }
1365 true
1366 }
1367}
1368
1369fn finalize_capture_completion(shared: &SharedState, deadline: Instant) -> bool {
1381 let mut guard = shared.queues.lock().expect("queue mutex poisoned");
1382 while !(guard.stdout_closed && guard.stderr_closed) {
1383 let now = Instant::now();
1384 if now >= deadline {
1385 guard.stdout_closed = true;
1386 guard.stderr_closed = true;
1387 shared.condvar.notify_all();
1388 return false;
1389 }
1390 let (next_guard, result) = shared
1391 .condvar
1392 .wait_timeout(guard, deadline - now)
1393 .expect("queue mutex poisoned");
1394 guard = next_guard;
1395 if result.timed_out() && !(guard.stdout_closed && guard.stderr_closed) {
1396 guard.stdout_closed = true;
1397 guard.stderr_closed = true;
1398 shared.condvar.notify_all();
1399 return false;
1400 }
1401 }
1402 true
1403}
1404
1405fn emit_lines(shared: &Arc<SharedState>, stream: StreamKind, lines: Vec<Vec<u8>>) {
1406 if lines.is_empty() || shared.capture_overflowed.load(Ordering::Acquire) {
1407 return;
1408 }
1409 let mut guard = shared.queues.lock().expect("queue mutex poisoned");
1410 if shared.capture_overflowed.load(Ordering::Acquire) {
1411 return;
1412 }
1413 for line in lines {
1414 let line_len = line.len();
1415 match stream {
1416 StreamKind::Stdout => {
1417 guard.stdout_history_bytes += line_len;
1418 guard.stdout_history.push_back(line.clone());
1419 guard.stdout_queue.push_back(line.clone());
1420 }
1421 StreamKind::Stderr => {
1422 guard.stderr_history_bytes += line_len;
1423 guard.stderr_history.push_back(line.clone());
1424 guard.stderr_queue.push_back(line.clone());
1425 }
1426 }
1427 let event = StreamEvent { stream, line };
1428 guard.combined_history_bytes += line_len;
1429 guard.combined_history.push_back(event.clone());
1430 guard.combined_queue.push_back(event);
1431 }
1432 shared.condvar.notify_all();
1433}
1434
1435fn append_raw(shared: &Arc<SharedState>, stream: StreamKind, chunk: &[u8]) -> bool {
1436 if chunk.is_empty() {
1437 return true;
1438 }
1439 let mut guard = shared.queues.lock().expect("queue mutex poisoned");
1440 let accepted = match shared.capture_limit {
1441 Some(limit) => {
1442 let retained = guard
1443 .stdout_raw
1444 .len()
1445 .saturating_add(guard.stderr_raw.len());
1446 chunk.len().min(limit.saturating_sub(retained))
1447 }
1448 None => chunk.len(),
1449 };
1450 match stream {
1451 StreamKind::Stdout => guard.stdout_raw.extend_from_slice(&chunk[..accepted]),
1452 StreamKind::Stderr => guard.stderr_raw.extend_from_slice(&chunk[..accepted]),
1453 }
1454 if accepted != chunk.len() {
1455 shared.capture_overflowed.store(true, Ordering::Release);
1456 false
1457 } else {
1458 true
1459 }
1460}
1461
1462pub fn run_command(
1468 mut config: ProcessConfig,
1469 timeout: Option<Duration>,
1470) -> Result<RunOutput, ProcessError> {
1471 config.capture = true;
1472 let process = NativeProcess::new(config);
1473 process.start()?;
1474
1475 let exit_code = match process.wait(timeout) {
1476 Ok(code) => code,
1477 Err(ProcessError::Timeout) => {
1478 match process.kill() {
1479 Ok(()) | Err(ProcessError::NotRunning) => {}
1480 Err(error) => return Err(error),
1481 }
1482 return Err(ProcessError::Timeout);
1483 }
1484 Err(error) => return Err(error),
1485 };
1486
1487 Ok(RunOutput {
1488 stdout: process.captured_stdout_raw(),
1489 stderr: process.captured_stderr_raw(),
1490 exit_code,
1491 })
1492}
1493
1494struct BoundedRunCleanup<'a> {
1495 process: &'a NativeProcess,
1496 armed: bool,
1497}
1498
1499impl BoundedRunCleanup<'_> {
1500 fn disarm(&mut self) {
1501 self.armed = false;
1502 }
1503}
1504
1505impl Drop for BoundedRunCleanup<'_> {
1506 fn drop(&mut self) {
1507 if !self.armed {
1508 return;
1509 }
1510
1511 self.process.cancel_capture_io();
1515 let _ = self.process.poll();
1516 if self.process.returncode().is_none() {
1517 let _ = self.process.kill();
1518 } else {
1519 self.process.finish_capture_drain();
1520 }
1521 let _ = self
1522 .process
1523 .wait_for_capture_readers_with_deadline(kill_drain_deadline());
1524 }
1525}
1526
1527fn run_native_process_bounded(
1528 process: NativeProcess,
1529 timeout: Option<Duration>,
1530 output_limit: usize,
1531) -> Result<RunOutput, ProcessError> {
1532 process.start()?;
1533 let mut cleanup = BoundedRunCleanup {
1534 process: &process,
1535 armed: true,
1536 };
1537 let started = Instant::now();
1538
1539 let exit_code = loop {
1540 if process.shared.capture_overflowed.load(Ordering::Acquire) {
1541 return Err(ProcessError::OutputLimitExceeded {
1542 limit: output_limit,
1543 });
1544 }
1545 if let Some(code) = process.poll()? {
1546 process.finish_capture_drain();
1547 break code;
1548 }
1549 if timeout.is_some_and(|limit| started.elapsed() >= limit) {
1550 return Err(ProcessError::Timeout);
1551 }
1552 thread::sleep(Duration::from_millis(5));
1553 };
1554
1555 if !process.wait_for_capture_readers_with_deadline(kill_drain_deadline()) {
1556 return Err(ProcessError::Io(std::io::Error::new(
1557 std::io::ErrorKind::TimedOut,
1558 "capture readers did not stop after process exit",
1559 )));
1560 }
1561 if process.shared.capture_overflowed.load(Ordering::Acquire) {
1562 return Err(ProcessError::OutputLimitExceeded {
1563 limit: output_limit,
1564 });
1565 }
1566
1567 let output = RunOutput {
1568 stdout: process.captured_stdout_raw(),
1569 stderr: process.captured_stderr_raw(),
1570 exit_code,
1571 };
1572 cleanup.disarm();
1573 Ok(output)
1574}
1575
1576pub fn run_command_bounded(
1585 mut config: ProcessConfig,
1586 timeout: Option<Duration>,
1587 output_limit: usize,
1588) -> Result<RunOutput, ProcessError> {
1589 config.capture = true;
1590 config.create_process_group = true;
1591 let process = NativeProcess::new_with_capture_limit(config, output_limit);
1592 run_native_process_bounded(process, timeout, output_limit)
1593}
1594
1595pub fn run_std_command_bounded(
1602 command: Command,
1603 timeout: Option<Duration>,
1604 output_limit: usize,
1605) -> Result<RunOutput, ProcessError> {
1606 let config = ProcessConfig {
1607 command: CommandSpec::Argv(vec!["running-process-command-override".to_string()]),
1611 cwd: None,
1612 env: None,
1613 capture: true,
1614 stderr_mode: StderrMode::Pipe,
1615 creationflags: None,
1616 create_process_group: true,
1617 stdin_mode: StdinMode::Null,
1618 nice: None,
1619 address_space_limit_bytes: None,
1620 };
1621 let process = NativeProcess::new_with_command_capture_limit(command, config, output_limit);
1622 run_native_process_bounded(process, timeout, output_limit)
1623}
1624
1625pub(crate) fn shell_command(command: &str) -> Command {
1626 running_process_platform_internal::platform::process::compat_shell_command(command)
1627}
1628
1629#[cfg(test)]
1630mod tests;