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 sanitized;
22
23pub use console_detect::{monitor_console_windows, ConsoleWindowInfo};
24pub use containment::{ContainedChild, ContainedProcessGroup, Containment, 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 sanitized::SanitizedChild;
29
30#[macro_export]
31macro_rules! rp_rust_debug_scope {
32 ($label:expr) => {
33 let _running_process_rust_debug_scope =
34 $crate::RustDebugScopeGuard::enter($label, file!(), line!());
35 };
36}
37
38const CHILD_PID_LOG_PATH_ENV: &str = "RUNNING_PROCESS_CHILD_PID_LOG_PATH";
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum StreamKind {
42 Stdout,
43 Stderr,
44}
45
46impl StreamKind {
47 pub fn as_str(self) -> &'static str {
48 match self {
49 Self::Stdout => "stdout",
50 Self::Stderr => "stderr",
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct StreamEvent {
57 pub stream: StreamKind,
58 pub line: Vec<u8>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum ReadStatus<T> {
63 Line(T),
64 Timeout,
65 Eof,
66}
67
68#[derive(Debug, Error)]
69pub enum ProcessError {
70 #[error("process already started")]
71 AlreadyStarted,
72 #[error("process is not running")]
73 NotRunning,
74 #[error("process stdin is not available")]
75 StdinUnavailable,
76 #[error("failed to spawn process: {0}")]
77 Spawn(std::io::Error),
78 #[error("failed to read process output: {0}")]
79 Io(std::io::Error),
80 #[error("process timed out")]
81 Timeout,
82}
83
84#[derive(Debug, Clone)]
85pub enum CommandSpec {
86 Shell(String),
87 Argv(Vec<String>),
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum StdinMode {
92 Inherit,
93 Piped,
94 Null,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum StderrMode {
99 Stdout,
100 Pipe,
101}
102
103#[cfg(unix)]
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum UnixSignal {
106 Interrupt,
107 Terminate,
108 Kill,
109}
110
111#[derive(Debug, Clone)]
112pub struct ProcessConfig {
113 pub command: CommandSpec,
114 pub cwd: Option<PathBuf>,
115 pub env: Option<Vec<(String, String)>>,
116 pub capture: bool,
117 pub stderr_mode: StderrMode,
118 pub creationflags: Option<u32>,
119 pub create_process_group: bool,
120 pub stdin_mode: StdinMode,
121 pub nice: Option<i32>,
122 pub containment: Option<Containment>,
127}
128
129#[derive(Default)]
130struct QueueState {
131 stdout_queue: VecDeque<Vec<u8>>,
132 stderr_queue: VecDeque<Vec<u8>>,
133 combined_queue: VecDeque<StreamEvent>,
134 stdout_history: VecDeque<Vec<u8>>,
135 stderr_history: VecDeque<Vec<u8>>,
136 combined_history: VecDeque<StreamEvent>,
137 stdout_history_bytes: usize,
138 stderr_history_bytes: usize,
139 combined_history_bytes: usize,
140 stdout_closed: bool,
141 stderr_closed: bool,
142}
143
144const RETURNCODE_NOT_SET: i64 = i64::MIN;
146
147struct SharedState {
148 queues: Mutex<QueueState>,
149 condvar: Condvar,
150 returncode: AtomicI64,
153}
154
155#[cfg(windows)]
156struct WindowsJobHandle(usize);
157
158#[cfg(windows)]
159impl Drop for WindowsJobHandle {
160 fn drop(&mut self) {
161 unsafe {
162 winapi::um::handleapi::CloseHandle(self.0 as winapi::shared::ntdef::HANDLE);
163 }
164 }
165}
166
167struct ChildState {
168 child: Child,
169 #[cfg(windows)]
170 _job: WindowsJobHandle,
171}
172
173impl SharedState {
174 fn new(capture: bool) -> Self {
175 let queues = QueueState {
176 stdout_closed: !capture,
177 stderr_closed: !capture,
178 ..QueueState::default()
179 };
180 Self {
181 queues: Mutex::new(queues),
182 condvar: Condvar::new(),
183 returncode: AtomicI64::new(RETURNCODE_NOT_SET),
184 }
185 }
186}
187
188pub struct NativeProcess {
189 config: ProcessConfig,
190 child: Arc<Mutex<Option<ChildState>>>,
191 shared: Arc<SharedState>,
192}
193
194impl NativeProcess {
195 pub fn new(config: ProcessConfig) -> Self {
196 Self {
197 shared: Arc::new(SharedState::new(config.capture)),
198 child: Arc::new(Mutex::new(None)),
199 config,
200 }
201 }
202
203 #[inline(never)]
205 pub fn start(&self) -> Result<(), ProcessError> {
206 public_symbols::rp_native_process_start_public(self)
207 }
208
209 fn start_impl(&self) -> Result<(), ProcessError> {
210 crate::rp_rust_debug_scope!("running_process_core::NativeProcess::start");
211 let mut guard = self.child.lock().expect("child mutex poisoned");
212 if guard.is_some() {
213 return Err(ProcessError::AlreadyStarted);
214 }
215
216 let mut command = self.build_command();
217 match self.config.stdin_mode {
218 StdinMode::Inherit => {}
219 StdinMode::Piped => {
220 command.stdin(Stdio::piped());
221 }
222 StdinMode::Null => {
223 command.stdin(Stdio::null());
224 }
225 }
226 if self.config.capture {
227 command.stdout(Stdio::piped());
228 command.stderr(Stdio::piped());
229 }
230
231 let mut child = command.spawn().map_err(ProcessError::Spawn)?;
232 log_spawned_child_pid(child.id()).map_err(ProcessError::Spawn)?;
233 #[cfg(windows)]
234 let job = public_symbols::rp_assign_child_to_windows_kill_on_close_job_public(&child)
235 .map_err(ProcessError::Spawn)?;
236 if self.config.capture {
237 let stdout = child.stdout.take().expect("stdout pipe missing");
238 let stderr = child.stderr.take().expect("stderr pipe missing");
239 self.spawn_reader(stdout, StreamKind::Stdout, StreamKind::Stdout);
240 self.spawn_reader(
241 stderr,
242 StreamKind::Stderr,
243 match self.config.stderr_mode {
244 StderrMode::Stdout => StreamKind::Stdout,
245 StderrMode::Pipe => StreamKind::Stderr,
246 },
247 );
248 }
249 *guard = Some(ChildState {
250 child,
251 #[cfg(windows)]
252 _job: job,
253 });
254 drop(guard);
255 self.spawn_exit_waiter();
256 Ok(())
257 }
258
259 fn spawn_exit_waiter(&self) {
262 let child = Arc::clone(&self.child);
263 let shared = Arc::clone(&self.shared);
264 thread::spawn(move || loop {
265 if shared.returncode.load(Ordering::Acquire) != RETURNCODE_NOT_SET {
266 return;
267 }
268 {
269 let mut guard = child.lock().expect("child mutex poisoned");
270 if let Some(child_state) = guard.as_mut() {
271 match child_state.child.try_wait() {
272 Ok(Some(status)) => {
273 let code = exit_code(status);
274 shared.returncode.store(code as i64, Ordering::Release);
275 shared.condvar.notify_all();
276 return;
277 }
278 Ok(None) => {}
279 Err(_) => return,
280 }
281 } else {
282 return;
283 }
284 }
285 thread::sleep(Duration::from_millis(10));
286 });
287 }
288
289 pub fn write_stdin(&self, data: &[u8]) -> Result<(), ProcessError> {
290 let mut guard = self.child.lock().expect("child mutex poisoned");
291 let child = &mut guard.as_mut().ok_or(ProcessError::NotRunning)?.child;
292 let stdin = child.stdin.as_mut().ok_or(ProcessError::StdinUnavailable)?;
293 use std::io::Write;
294 stdin.write_all(data).map_err(ProcessError::Io)?;
295 stdin.flush().map_err(ProcessError::Io)?;
296 drop(child.stdin.take());
297 Ok(())
298 }
299
300 pub fn poll(&self) -> Result<Option<i32>, ProcessError> {
301 if let Some(code) = self.returncode() {
303 return Ok(Some(code));
304 }
305 let mut guard = self.child.lock().expect("child mutex poisoned");
306 let Some(child_state) = guard.as_mut() else {
307 return Ok(self.returncode());
308 };
309 let child = &mut child_state.child;
310 let status = child.try_wait().map_err(ProcessError::Io)?;
311 if let Some(status) = status {
312 let code = exit_code(status);
313 self.set_returncode(code);
314 return Ok(Some(code));
315 }
316 Ok(None)
317 }
318
319 #[inline(never)]
321 pub fn wait(&self, timeout: Option<Duration>) -> Result<i32, ProcessError> {
322 public_symbols::rp_native_process_wait_public(self, timeout)
323 }
324
325 fn wait_impl(&self, timeout: Option<Duration>) -> Result<i32, ProcessError> {
326 crate::rp_rust_debug_scope!("running_process_core::NativeProcess::wait");
327 if self.child.lock().expect("child mutex poisoned").is_none() {
328 return self.returncode().ok_or(ProcessError::NotRunning);
329 }
330 if let Some(code) = self.returncode() {
332 public_symbols::rp_native_process_wait_for_capture_completion_public(self);
333 return Ok(code);
334 }
335 let start = Instant::now();
336 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
337 loop {
338 let rc = self.shared.returncode.load(Ordering::Acquire);
340 if rc != RETURNCODE_NOT_SET {
341 drop(guard);
342 let code = rc as i32;
343 public_symbols::rp_native_process_wait_for_capture_completion_public(self);
344 return Ok(code);
345 }
346 if let Some(limit) = timeout {
347 let elapsed = start.elapsed();
348 if elapsed >= limit {
349 return Err(ProcessError::Timeout);
350 }
351 let remaining = limit - elapsed;
352 let wait_time = remaining.min(Duration::from_millis(50));
354 guard = self
355 .shared
356 .condvar
357 .wait_timeout(guard, wait_time)
358 .expect("queue mutex poisoned")
359 .0;
360 } else {
361 guard = self
363 .shared
364 .condvar
365 .wait_timeout(guard, Duration::from_millis(50))
366 .expect("queue mutex poisoned")
367 .0;
368 }
369 }
370 }
371
372 #[inline(never)]
374 pub fn kill(&self) -> Result<(), ProcessError> {
375 public_symbols::rp_native_process_kill_public(self)
376 }
377
378 fn kill_impl(&self) -> Result<(), ProcessError> {
379 crate::rp_rust_debug_scope!("running_process_core::NativeProcess::kill");
380 let mut guard = self.child.lock().expect("child mutex poisoned");
381 let child = &mut guard.as_mut().ok_or(ProcessError::NotRunning)?.child;
382 child.kill().map_err(ProcessError::Io)?;
383 let status = child.wait().map_err(ProcessError::Io)?;
384 self.set_returncode(exit_code(status));
385 Ok(())
386 }
387
388 pub fn terminate(&self) -> Result<(), ProcessError> {
389 self.kill()
390 }
391
392 #[inline(never)]
394 pub fn close(&self) -> Result<(), ProcessError> {
395 public_symbols::rp_native_process_close_public(self)
396 }
397
398 fn close_impl(&self) -> Result<(), ProcessError> {
399 crate::rp_rust_debug_scope!("running_process_core::NativeProcess::close");
400 if self.child.lock().expect("child mutex poisoned").is_none() {
401 return Ok(());
402 }
403 if self.poll()?.is_none() {
404 self.kill()?;
405 } else {
406 public_symbols::rp_native_process_wait_for_capture_completion_public(self);
407 }
408 Ok(())
409 }
410
411 pub fn pid(&self) -> Option<u32> {
412 self.child
413 .lock()
414 .expect("child mutex poisoned")
415 .as_ref()
416 .map(|state| state.child.id())
417 }
418
419 pub fn returncode(&self) -> Option<i32> {
420 let v = self.shared.returncode.load(Ordering::Acquire);
421 if v == RETURNCODE_NOT_SET {
422 None
423 } else {
424 Some(v as i32)
425 }
426 }
427
428 pub fn has_pending_stream(&self, stream: StreamKind) -> bool {
429 if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
430 return false;
431 }
432 let guard = self.shared.queues.lock().expect("queue mutex poisoned");
433 match stream {
434 StreamKind::Stdout => !guard.stdout_queue.is_empty(),
435 StreamKind::Stderr => !guard.stderr_queue.is_empty(),
436 }
437 }
438
439 pub fn has_pending_combined(&self) -> bool {
440 let guard = self.shared.queues.lock().expect("queue mutex poisoned");
441 !guard.combined_queue.is_empty()
442 }
443
444 pub fn drain_stream(&self, stream: StreamKind) -> Vec<Vec<u8>> {
445 if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
446 return Vec::new();
447 }
448 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
449 let queue = match stream {
450 StreamKind::Stdout => &mut guard.stdout_queue,
451 StreamKind::Stderr => &mut guard.stderr_queue,
452 };
453 queue.drain(..).collect()
454 }
455
456 pub fn drain_combined(&self) -> Vec<StreamEvent> {
457 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
458 guard.combined_queue.drain(..).collect()
459 }
460
461 pub fn read_stream(
462 &self,
463 stream: StreamKind,
464 timeout: Option<Duration>,
465 ) -> ReadStatus<Vec<u8>> {
466 let deadline = timeout.map(|limit| Instant::now() + limit);
467 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
468
469 loop {
470 if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
471 return ReadStatus::Eof;
472 }
473
474 let queue = match stream {
475 StreamKind::Stdout => &mut guard.stdout_queue,
476 StreamKind::Stderr => &mut guard.stderr_queue,
477 };
478 if let Some(line) = queue.pop_front() {
479 return ReadStatus::Line(line);
480 }
481
482 let closed = match stream {
483 StreamKind::Stdout => {
484 if self.config.stderr_mode == StderrMode::Stdout {
485 guard.stdout_closed && guard.stderr_closed
486 } else {
487 guard.stdout_closed
488 }
489 }
490 StreamKind::Stderr => guard.stderr_closed,
491 };
492 if closed {
493 return ReadStatus::Eof;
494 }
495
496 match deadline {
497 Some(deadline) => {
498 let now = Instant::now();
499 if now >= deadline {
500 return ReadStatus::Timeout;
501 }
502 let wait = deadline.saturating_duration_since(now);
503 let result = self
504 .shared
505 .condvar
506 .wait_timeout(guard, wait)
507 .expect("queue mutex poisoned");
508 guard = result.0;
509 if result.1.timed_out() {
510 return ReadStatus::Timeout;
511 }
512 }
513 None => {
514 guard = self
515 .shared
516 .condvar
517 .wait(guard)
518 .expect("queue mutex poisoned");
519 }
520 }
521 }
522 }
523
524 #[inline(never)]
526 pub fn read_combined(&self, timeout: Option<Duration>) -> ReadStatus<StreamEvent> {
527 public_symbols::rp_native_process_read_combined_public(self, timeout)
528 }
529
530 fn read_combined_impl(&self, timeout: Option<Duration>) -> ReadStatus<StreamEvent> {
531 crate::rp_rust_debug_scope!("running_process_core::NativeProcess::read_combined");
532 let deadline = timeout.map(|limit| Instant::now() + limit);
533 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
534
535 loop {
536 if let Some(event) = guard.combined_queue.pop_front() {
537 return ReadStatus::Line(event);
538 }
539 if guard.stdout_closed && guard.stderr_closed {
540 return ReadStatus::Eof;
541 }
542
543 match deadline {
544 Some(deadline) => {
545 let now = Instant::now();
546 if now >= deadline {
547 return ReadStatus::Timeout;
548 }
549 let wait = deadline.saturating_duration_since(now);
550 let result = self
551 .shared
552 .condvar
553 .wait_timeout(guard, wait)
554 .expect("queue mutex poisoned");
555 guard = result.0;
556 if result.1.timed_out() {
557 return ReadStatus::Timeout;
558 }
559 }
560 None => {
561 guard = self
562 .shared
563 .condvar
564 .wait(guard)
565 .expect("queue mutex poisoned");
566 }
567 }
568 }
569 }
570
571 pub fn captured_stdout(&self) -> Vec<Vec<u8>> {
572 self.shared
573 .queues
574 .lock()
575 .expect("queue mutex poisoned")
576 .stdout_history
577 .clone()
578 .into_iter()
579 .collect()
580 }
581
582 pub fn captured_stderr(&self) -> Vec<Vec<u8>> {
583 if self.config.stderr_mode == StderrMode::Stdout {
584 return Vec::new();
585 }
586 self.shared
587 .queues
588 .lock()
589 .expect("queue mutex poisoned")
590 .stderr_history
591 .clone()
592 .into_iter()
593 .collect()
594 }
595
596 pub fn captured_combined(&self) -> Vec<StreamEvent> {
597 self.shared
598 .queues
599 .lock()
600 .expect("queue mutex poisoned")
601 .combined_history
602 .clone()
603 .into_iter()
604 .collect()
605 }
606
607 pub fn captured_stream_bytes(&self, stream: StreamKind) -> usize {
608 if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
609 return 0;
610 }
611 let guard = self.shared.queues.lock().expect("queue mutex poisoned");
612 match stream {
613 StreamKind::Stdout => guard.stdout_history_bytes,
614 StreamKind::Stderr => guard.stderr_history_bytes,
615 }
616 }
617
618 pub fn captured_combined_bytes(&self) -> usize {
619 self.shared
620 .queues
621 .lock()
622 .expect("queue mutex poisoned")
623 .combined_history_bytes
624 }
625
626 pub fn clear_captured_stream(&self, stream: StreamKind) -> usize {
627 if stream == StreamKind::Stderr && self.config.stderr_mode == StderrMode::Stdout {
628 return 0;
629 }
630 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
631 match stream {
632 StreamKind::Stdout => {
633 let released = guard.stdout_history_bytes;
634 guard.stdout_history.clear();
635 guard.stdout_history_bytes = 0;
636 released
637 }
638 StreamKind::Stderr => {
639 let released = guard.stderr_history_bytes;
640 guard.stderr_history.clear();
641 guard.stderr_history_bytes = 0;
642 released
643 }
644 }
645 }
646
647 pub fn clear_captured_combined(&self) -> usize {
648 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
649 let released = guard.combined_history_bytes;
650 guard.combined_history.clear();
651 guard.combined_history_bytes = 0;
652 released
653 }
654
655 fn build_command(&self) -> Command {
656 let mut command = match &self.config.command {
657 CommandSpec::Shell(command) => shell_command(command),
658 CommandSpec::Argv(argv) => {
659 let mut command = Command::new(&argv[0]);
660 if argv.len() > 1 {
661 command.args(&argv[1..]);
662 }
663 command
664 }
665 };
666 if let Some(cwd) = &self.config.cwd {
667 command.current_dir(cwd);
668 }
669 if let Some(env) = &self.config.env {
670 command.env_clear();
671 command.envs(env.iter().map(|(k, v)| (k, v)));
672 }
673 #[cfg(windows)]
674 {
675 use std::os::windows::process::CommandExt;
676
677 let flags =
678 self.config.creationflags.unwrap_or(0) | windows_priority_flags(self.config.nice);
679 if flags != 0 {
680 command.creation_flags(flags);
681 }
682 }
683 #[cfg(unix)]
684 {
685 let create_process_group = self.config.create_process_group;
686 let nice = self.config.nice;
687 let containment = self.config.containment;
688
689 let needs_pre_exec = create_process_group || nice.is_some() || containment.is_some();
690
691 if needs_pre_exec {
692 use std::os::unix::process::CommandExt;
693
694 unsafe {
695 command.pre_exec(move || {
696 match containment {
697 Some(Containment::Contained) => {
698 if libc::setpgid(0, 0) == -1 {
700 return Err(std::io::Error::last_os_error());
701 }
702 #[cfg(target_os = "linux")]
708 {
709 if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 {
710 return Err(std::io::Error::last_os_error());
711 }
712 if libc::getppid() == 1 {
713 libc::_exit(1);
714 }
715 }
716 }
717 Some(Containment::Detached) => {
718 if libc::setsid() == -1 {
721 return Err(std::io::Error::last_os_error());
722 }
723 }
724 None => {
725 if create_process_group && libc::setpgid(0, 0) == -1 {
726 return Err(std::io::Error::last_os_error());
727 }
728 }
729 }
730 if let Some(nice) = nice {
731 let result = libc::setpriority(libc::PRIO_PROCESS, 0, nice);
732 if result == -1 {
733 return Err(std::io::Error::last_os_error());
734 }
735 }
736 Ok(())
737 });
738 }
739 }
740 }
741 command
742 }
743
744 fn spawn_reader<R>(&self, pipe: R, source_stream: StreamKind, visible_stream: StreamKind)
745 where
746 R: Read + Send + 'static,
747 {
748 let shared = Arc::clone(&self.shared);
749 thread::spawn(move || {
750 let mut reader = pipe;
751 let mut chunk = vec![0_u8; 65536];
752 let mut pending = Vec::new();
753
754 loop {
755 match reader.read(&mut chunk) {
756 Ok(0) => break,
757 Ok(n) => {
758 let lines = feed_chunk(&mut pending, &chunk[..n]);
759 emit_lines(&shared, visible_stream, lines);
760 }
761 Err(_) => break,
762 }
763 }
764
765 if !pending.is_empty() {
766 emit_lines(&shared, visible_stream, vec![std::mem::take(&mut pending)]);
767 }
768
769 let mut guard = shared.queues.lock().expect("queue mutex poisoned");
770 match source_stream {
771 StreamKind::Stdout => guard.stdout_closed = true,
772 StreamKind::Stderr => guard.stderr_closed = true,
773 }
774 shared.condvar.notify_all();
775 });
776 }
777
778 fn set_returncode(&self, code: i32) {
779 self.shared.returncode.store(code as i64, Ordering::Release);
780 self.shared.condvar.notify_all();
781 }
782
783 fn wait_for_capture_completion_impl(&self) {
784 crate::rp_rust_debug_scope!(
785 "running_process_core::NativeProcess::wait_for_capture_completion"
786 );
787 if !self.config.capture {
788 return;
789 }
790
791 let mut guard = self.shared.queues.lock().expect("queue mutex poisoned");
792 while !(guard.stdout_closed && guard.stderr_closed) {
793 guard = self
794 .shared
795 .condvar
796 .wait(guard)
797 .expect("queue mutex poisoned");
798 }
799 }
800}
801
802#[cfg(unix)]
803pub fn unix_set_priority(pid: u32, nice: i32) -> Result<(), std::io::Error> {
804 let result = unsafe { libc::setpriority(libc::PRIO_PROCESS, pid, nice) };
805 if result == -1 {
806 return Err(std::io::Error::last_os_error());
807 }
808 Ok(())
809}
810
811#[cfg(unix)]
812pub fn unix_signal_process(pid: u32, signal: UnixSignal) -> Result<(), std::io::Error> {
813 let result = unsafe { libc::kill(pid as i32, unix_signal_raw(signal)) };
814 if result == -1 {
815 return Err(std::io::Error::last_os_error());
816 }
817 Ok(())
818}
819
820#[cfg(unix)]
821pub fn unix_signal_process_group(pid: i32, signal: UnixSignal) -> Result<(), std::io::Error> {
822 let result = unsafe { libc::killpg(pid, unix_signal_raw(signal)) };
823 if result == -1 {
824 return Err(std::io::Error::last_os_error());
825 }
826 Ok(())
827}
828
829fn log_spawned_child_pid(pid: u32) -> Result<(), std::io::Error> {
830 let Some(path) = std::env::var_os(CHILD_PID_LOG_PATH_ENV) else {
831 return Ok(());
832 };
833
834 let mut file = OpenOptions::new().create(true).append(true).open(path)?;
835 file.write_all(format!("{pid}\n").as_bytes())?;
836 file.flush()?;
837 Ok(())
838}
839
840#[cfg(windows)]
841fn assign_child_to_windows_kill_on_close_job_impl(
842 child: &Child,
843) -> Result<WindowsJobHandle, std::io::Error> {
844 crate::rp_rust_debug_scope!("running_process_core::assign_child_to_windows_kill_on_close_job");
845 use std::mem::zeroed;
846 use std::os::windows::io::AsRawHandle;
847
848 use winapi::shared::minwindef::FALSE;
849 use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
850 use winapi::um::jobapi2::{
851 AssignProcessToJobObject, CreateJobObjectW, SetInformationJobObject,
852 };
853 use winapi::um::winnt::{
854 JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
855 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
856 };
857
858 let handle = child.as_raw_handle();
859 let job = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };
860 if job.is_null() || job == INVALID_HANDLE_VALUE {
861 return Err(std::io::Error::last_os_error());
862 }
863
864 let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { zeroed() };
865 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
866 let ok = unsafe {
867 SetInformationJobObject(
868 job,
869 JobObjectExtendedLimitInformation,
870 (&mut info as *mut JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
871 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
872 )
873 };
874 if ok == FALSE {
875 let err = std::io::Error::last_os_error();
876 unsafe { CloseHandle(job) };
877 return Err(err);
878 }
879
880 let ok = unsafe { AssignProcessToJobObject(job, handle.cast()) };
881 if ok == FALSE {
882 let err = std::io::Error::last_os_error();
883 unsafe { CloseHandle(job) };
884 return Err(err);
885 }
886
887 Ok(WindowsJobHandle(job as usize))
888}
889
890fn feed_chunk(pending: &mut Vec<u8>, chunk: &[u8]) -> Vec<Vec<u8>> {
891 let mut lines = Vec::new();
892 let mut start = 0;
893 let mut index = 0;
894
895 while index < chunk.len() {
896 if chunk[index] == b'\n' {
897 let end = if index > start && chunk[index - 1] == b'\r' {
898 index - 1
899 } else {
900 index
901 };
902 pending.extend_from_slice(&chunk[start..end]);
903 if !pending.is_empty() {
904 lines.push(std::mem::take(pending));
905 }
906 start = index + 1;
907 }
908 index += 1;
909 }
910
911 pending.extend_from_slice(&chunk[start..]);
912 lines
913}
914
915fn emit_lines(shared: &Arc<SharedState>, stream: StreamKind, lines: Vec<Vec<u8>>) {
916 if lines.is_empty() {
917 return;
918 }
919 let mut guard = shared.queues.lock().expect("queue mutex poisoned");
920 for line in lines {
921 let line_len = line.len();
922 match stream {
923 StreamKind::Stdout => {
924 guard.stdout_history_bytes += line_len;
925 guard.stdout_history.push_back(line.clone());
926 guard.stdout_queue.push_back(line.clone());
927 }
928 StreamKind::Stderr => {
929 guard.stderr_history_bytes += line_len;
930 guard.stderr_history.push_back(line.clone());
931 guard.stderr_queue.push_back(line.clone());
932 }
933 }
934 let event = StreamEvent { stream, line };
935 guard.combined_history_bytes += line_len;
936 guard.combined_history.push_back(event.clone());
937 guard.combined_queue.push_back(event);
938 }
939 shared.condvar.notify_all();
940}
941
942fn shell_command(command: &str) -> Command {
943 #[cfg(windows)]
944 {
945 use std::os::windows::process::CommandExt;
946
947 let mut cmd = Command::new("cmd");
948 cmd.raw_arg("/D /S /C \"");
949 cmd.raw_arg(command);
950 cmd.raw_arg("\"");
951 cmd
952 }
953 #[cfg(not(windows))]
954 {
955 let mut cmd = Command::new("sh");
956 cmd.arg("-lc").arg(command);
957 cmd
958 }
959}
960
961fn exit_code(status: std::process::ExitStatus) -> i32 {
962 #[cfg(unix)]
963 {
964 use std::os::unix::process::ExitStatusExt;
965 status
966 .code()
967 .unwrap_or_else(|| -status.signal().unwrap_or(1))
968 }
969 #[cfg(not(unix))]
970 {
971 status.code().unwrap_or(1)
972 }
973}
974
975#[cfg(unix)]
976fn unix_signal_raw(signal: UnixSignal) -> i32 {
977 match signal {
978 UnixSignal::Interrupt => libc::SIGINT,
979 UnixSignal::Terminate => libc::SIGTERM,
980 UnixSignal::Kill => libc::SIGKILL,
981 }
982}
983
984#[cfg(windows)]
985fn windows_priority_flags(nice: Option<i32>) -> u32 {
986 const IDLE_PRIORITY_CLASS: u32 = 0x0000_0040;
987 const BELOW_NORMAL_PRIORITY_CLASS: u32 = 0x0000_4000;
988 const ABOVE_NORMAL_PRIORITY_CLASS: u32 = 0x0000_8000;
989 const HIGH_PRIORITY_CLASS: u32 = 0x0000_0080;
990
991 match nice {
992 Some(value) if value >= 15 => IDLE_PRIORITY_CLASS,
993 Some(value) if value >= 1 => BELOW_NORMAL_PRIORITY_CLASS,
994 Some(value) if value <= -15 => HIGH_PRIORITY_CLASS,
995 Some(value) if value <= -1 => ABOVE_NORMAL_PRIORITY_CLASS,
996 _ => 0,
997 }
998}
999#[cfg(test)]
1000mod tests {
1001 use super::*;
1002
1003 #[test]
1006 fn stream_kind_as_str_stdout() {
1007 assert_eq!(StreamKind::Stdout.as_str(), "stdout");
1008 }
1009
1010 #[test]
1011 fn stream_kind_as_str_stderr() {
1012 assert_eq!(StreamKind::Stderr.as_str(), "stderr");
1013 }
1014
1015 #[test]
1016 fn stream_kind_equality() {
1017 assert_eq!(StreamKind::Stdout, StreamKind::Stdout);
1018 assert_ne!(StreamKind::Stdout, StreamKind::Stderr);
1019 }
1020
1021 #[test]
1024 fn stream_event_clone() {
1025 let event = StreamEvent {
1026 stream: StreamKind::Stdout,
1027 line: b"hello".to_vec(),
1028 };
1029 let cloned = event.clone();
1030 assert_eq!(event, cloned);
1031 }
1032
1033 #[test]
1036 fn read_status_line_variant() {
1037 let status: ReadStatus<Vec<u8>> = ReadStatus::Line(b"data".to_vec());
1038 assert!(matches!(status, ReadStatus::Line(ref v) if v == b"data"));
1039 }
1040
1041 #[test]
1042 fn read_status_timeout_variant() {
1043 let status: ReadStatus<Vec<u8>> = ReadStatus::Timeout;
1044 assert!(matches!(status, ReadStatus::Timeout));
1045 }
1046
1047 #[test]
1048 fn read_status_eof_variant() {
1049 let status: ReadStatus<Vec<u8>> = ReadStatus::Eof;
1050 assert!(matches!(status, ReadStatus::Eof));
1051 }
1052
1053 #[test]
1056 fn process_error_display_already_started() {
1057 assert_eq!(
1058 ProcessError::AlreadyStarted.to_string(),
1059 "process already started"
1060 );
1061 }
1062
1063 #[test]
1064 fn process_error_display_not_running() {
1065 assert_eq!(
1066 ProcessError::NotRunning.to_string(),
1067 "process is not running"
1068 );
1069 }
1070
1071 #[test]
1072 fn process_error_display_stdin_unavailable() {
1073 assert_eq!(
1074 ProcessError::StdinUnavailable.to_string(),
1075 "process stdin is not available"
1076 );
1077 }
1078
1079 #[test]
1080 fn process_error_display_timeout() {
1081 assert_eq!(ProcessError::Timeout.to_string(), "process timed out");
1082 }
1083
1084 #[test]
1085 fn process_error_display_spawn() {
1086 let err = ProcessError::Spawn(std::io::Error::new(
1087 std::io::ErrorKind::NotFound,
1088 "not found",
1089 ));
1090 assert!(err.to_string().contains("not found"));
1091 }
1092
1093 #[test]
1094 fn process_error_display_io() {
1095 let err = ProcessError::Io(std::io::Error::new(
1096 std::io::ErrorKind::BrokenPipe,
1097 "broken",
1098 ));
1099 assert!(err.to_string().contains("broken"));
1100 }
1101
1102 #[test]
1105 fn command_spec_shell_variant() {
1106 let spec = CommandSpec::Shell("echo hello".to_string());
1107 assert!(matches!(spec, CommandSpec::Shell(ref s) if s == "echo hello"));
1108 }
1109
1110 #[test]
1111 fn command_spec_argv_variant() {
1112 let spec = CommandSpec::Argv(vec!["echo".to_string(), "hello".to_string()]);
1113 assert!(matches!(spec, CommandSpec::Argv(ref v) if v.len() == 2));
1114 }
1115
1116 #[test]
1119 fn stdin_mode_equality() {
1120 assert_eq!(StdinMode::Inherit, StdinMode::Inherit);
1121 assert_ne!(StdinMode::Piped, StdinMode::Null);
1122 }
1123
1124 #[test]
1125 fn stderr_mode_equality() {
1126 assert_eq!(StderrMode::Stdout, StderrMode::Stdout);
1127 assert_ne!(StderrMode::Stdout, StderrMode::Pipe);
1128 }
1129
1130 #[test]
1133 fn shared_state_new_with_capture() {
1134 let state = SharedState::new(true);
1135 let queues = state.queues.lock().unwrap();
1136 assert!(!queues.stdout_closed);
1137 assert!(!queues.stderr_closed);
1138 assert!(queues.stdout_queue.is_empty());
1139 assert!(queues.stderr_queue.is_empty());
1140 }
1141
1142 #[test]
1143 fn shared_state_new_without_capture() {
1144 let state = SharedState::new(false);
1145 let queues = state.queues.lock().unwrap();
1146 assert!(queues.stdout_closed);
1147 assert!(queues.stderr_closed);
1148 }
1149
1150 #[test]
1151 fn shared_state_returncode_initially_not_set() {
1152 let state = SharedState::new(true);
1153 let code = state.returncode.load(Ordering::Acquire);
1154 assert_eq!(code, RETURNCODE_NOT_SET);
1155 }
1156
1157 #[test]
1160 fn feed_chunk_single_line_with_newline() {
1161 let shared = Arc::new(SharedState::new(true));
1162 let mut pending = Vec::new();
1163 let lines = feed_chunk(&mut pending, b"hello\n");
1164 emit_lines(&shared, StreamKind::Stdout, lines);
1165 let queues = shared.queues.lock().unwrap();
1166 assert_eq!(queues.stdout_queue.len(), 1);
1167 assert_eq!(queues.stdout_queue[0], b"hello");
1168 assert!(pending.is_empty());
1169 }
1170
1171 #[test]
1172 fn feed_chunk_crlf_stripping() {
1173 let shared = Arc::new(SharedState::new(true));
1174 let mut pending = Vec::new();
1175 let lines = feed_chunk(&mut pending, b"hello\r\n");
1176 emit_lines(&shared, StreamKind::Stdout, lines);
1177 let queues = shared.queues.lock().unwrap();
1178 assert_eq!(queues.stdout_queue.len(), 1);
1179 assert_eq!(queues.stdout_queue[0], b"hello");
1180 }
1181
1182 #[test]
1183 fn feed_chunk_multiple_lines() {
1184 let shared = Arc::new(SharedState::new(true));
1185 let mut pending = Vec::new();
1186 let lines = feed_chunk(&mut pending, b"a\nb\nc\n");
1187 emit_lines(&shared, StreamKind::Stdout, lines);
1188 let queues = shared.queues.lock().unwrap();
1189 assert_eq!(queues.stdout_queue.len(), 3);
1190 assert_eq!(queues.stdout_queue[0], b"a");
1191 assert_eq!(queues.stdout_queue[1], b"b");
1192 assert_eq!(queues.stdout_queue[2], b"c");
1193 }
1194
1195 #[test]
1196 fn feed_chunk_no_newline_stays_pending() {
1197 let mut pending = Vec::new();
1198 let lines = feed_chunk(&mut pending, b"partial");
1199 assert!(lines.is_empty());
1200 assert_eq!(pending, b"partial");
1201 }
1202
1203 #[test]
1204 fn feed_chunk_accumulates_pending() {
1205 let shared = Arc::new(SharedState::new(true));
1206 let mut pending = Vec::new();
1207 let lines1 = feed_chunk(&mut pending, b"hel");
1208 emit_lines(&shared, StreamKind::Stdout, lines1);
1209 let lines2 = feed_chunk(&mut pending, b"lo\n");
1210 emit_lines(&shared, StreamKind::Stdout, lines2);
1211 let queues = shared.queues.lock().unwrap();
1212 assert_eq!(queues.stdout_queue.len(), 1);
1213 assert_eq!(queues.stdout_queue[0], b"hello");
1214 assert!(pending.is_empty());
1215 }
1216
1217 #[test]
1218 fn feed_chunk_empty_line_not_emitted() {
1219 let shared = Arc::new(SharedState::new(true));
1220 let mut pending = Vec::new();
1221 let lines = feed_chunk(&mut pending, b"\n");
1222 emit_lines(&shared, StreamKind::Stdout, lines);
1223 let queues = shared.queues.lock().unwrap();
1224 assert!(queues.stdout_queue.is_empty());
1225 }
1226
1227 #[test]
1228 fn feed_chunk_stderr_goes_to_stderr_queue() {
1229 let shared = Arc::new(SharedState::new(true));
1230 let mut pending = Vec::new();
1231 let lines = feed_chunk(&mut pending, b"error\n");
1232 emit_lines(&shared, StreamKind::Stderr, lines);
1233 let queues = shared.queues.lock().unwrap();
1234 assert!(queues.stdout_queue.is_empty());
1235 assert_eq!(queues.stderr_queue.len(), 1);
1236 assert_eq!(queues.stderr_queue[0], b"error");
1237 }
1238
1239 #[test]
1242 fn emit_lines_updates_all_queues_and_history() {
1243 let shared = Arc::new(SharedState::new(true));
1244 emit_lines(&shared, StreamKind::Stdout, vec![b"test".to_vec()]);
1245 let queues = shared.queues.lock().unwrap();
1246 assert_eq!(queues.stdout_queue.len(), 1);
1247 assert_eq!(queues.stdout_history.len(), 1);
1248 assert_eq!(queues.stdout_history_bytes, 4);
1249 assert_eq!(queues.combined_queue.len(), 1);
1250 assert_eq!(queues.combined_history.len(), 1);
1251 assert_eq!(queues.combined_history_bytes, 4);
1252 }
1253
1254 #[test]
1255 fn emit_lines_stderr_updates_stderr_queues() {
1256 let shared = Arc::new(SharedState::new(true));
1257 emit_lines(&shared, StreamKind::Stderr, vec![b"err".to_vec()]);
1258 let queues = shared.queues.lock().unwrap();
1259 assert_eq!(queues.stderr_queue.len(), 1);
1260 assert_eq!(queues.stderr_history.len(), 1);
1261 assert_eq!(queues.stderr_history_bytes, 3);
1262 assert_eq!(queues.combined_queue.len(), 1);
1263 assert_eq!(queues.combined_history_bytes, 3);
1264 }
1265
1266 #[test]
1269 fn native_process_returncode_none_before_start() {
1270 let process = NativeProcess::new(ProcessConfig {
1271 command: CommandSpec::Argv(vec!["echo".into()]),
1272 cwd: None,
1273 env: None,
1274 capture: false,
1275 stderr_mode: StderrMode::Stdout,
1276 creationflags: None,
1277 create_process_group: false,
1278 stdin_mode: StdinMode::Inherit,
1279 nice: None,
1280 containment: None,
1281 });
1282 assert!(process.returncode().is_none());
1283 }
1284
1285 #[test]
1286 fn native_process_pid_none_before_start() {
1287 let process = NativeProcess::new(ProcessConfig {
1288 command: CommandSpec::Argv(vec!["echo".into()]),
1289 cwd: None,
1290 env: None,
1291 capture: false,
1292 stderr_mode: StderrMode::Stdout,
1293 creationflags: None,
1294 create_process_group: false,
1295 stdin_mode: StdinMode::Inherit,
1296 nice: None,
1297 containment: None,
1298 });
1299 assert!(process.pid().is_none());
1300 }
1301
1302 #[test]
1303 fn native_process_has_pending_false_when_no_capture() {
1304 let process = NativeProcess::new(ProcessConfig {
1305 command: CommandSpec::Argv(vec!["echo".into()]),
1306 cwd: None,
1307 env: None,
1308 capture: false,
1309 stderr_mode: StderrMode::Stdout,
1310 creationflags: None,
1311 create_process_group: false,
1312 stdin_mode: StdinMode::Inherit,
1313 nice: None,
1314 containment: None,
1315 });
1316 assert!(!process.has_pending_stream(StreamKind::Stdout));
1317 assert!(!process.has_pending_combined());
1318 }
1319
1320 #[test]
1321 fn native_process_drain_empty_when_no_capture() {
1322 let process = NativeProcess::new(ProcessConfig {
1323 command: CommandSpec::Argv(vec!["echo".into()]),
1324 cwd: None,
1325 env: None,
1326 capture: false,
1327 stderr_mode: StderrMode::Stdout,
1328 creationflags: None,
1329 create_process_group: false,
1330 stdin_mode: StdinMode::Inherit,
1331 nice: None,
1332 containment: None,
1333 });
1334 assert!(process.drain_stream(StreamKind::Stdout).is_empty());
1335 assert!(process.drain_combined().is_empty());
1336 }
1337
1338 #[test]
1339 fn native_process_stderr_not_pending_when_merged() {
1340 let process = NativeProcess::new(ProcessConfig {
1341 command: CommandSpec::Argv(vec!["echo".into()]),
1342 cwd: None,
1343 env: None,
1344 capture: true,
1345 stderr_mode: StderrMode::Stdout,
1346 creationflags: None,
1347 create_process_group: false,
1348 stdin_mode: StdinMode::Inherit,
1349 nice: None,
1350 containment: None,
1351 });
1352 assert!(!process.has_pending_stream(StreamKind::Stderr));
1353 }
1354
1355 #[test]
1356 fn native_process_drain_stderr_empty_when_merged() {
1357 let process = NativeProcess::new(ProcessConfig {
1358 command: CommandSpec::Argv(vec!["echo".into()]),
1359 cwd: None,
1360 env: None,
1361 capture: true,
1362 stderr_mode: StderrMode::Stdout,
1363 creationflags: None,
1364 create_process_group: false,
1365 stdin_mode: StdinMode::Inherit,
1366 nice: None,
1367 containment: None,
1368 });
1369 assert!(process.drain_stream(StreamKind::Stderr).is_empty());
1370 }
1371
1372 #[test]
1373 fn native_process_captured_stderr_empty_when_merged() {
1374 let process = NativeProcess::new(ProcessConfig {
1375 command: CommandSpec::Argv(vec!["echo".into()]),
1376 cwd: None,
1377 env: None,
1378 capture: true,
1379 stderr_mode: StderrMode::Stdout,
1380 creationflags: None,
1381 create_process_group: false,
1382 stdin_mode: StdinMode::Inherit,
1383 nice: None,
1384 containment: None,
1385 });
1386 assert!(process.captured_stderr().is_empty());
1387 }
1388
1389 #[test]
1390 fn native_process_captured_stream_bytes_zero_when_merged_stderr() {
1391 let process = NativeProcess::new(ProcessConfig {
1392 command: CommandSpec::Argv(vec!["echo".into()]),
1393 cwd: None,
1394 env: None,
1395 capture: true,
1396 stderr_mode: StderrMode::Stdout,
1397 creationflags: None,
1398 create_process_group: false,
1399 stdin_mode: StdinMode::Inherit,
1400 nice: None,
1401 containment: None,
1402 });
1403 assert_eq!(process.captured_stream_bytes(StreamKind::Stderr), 0);
1404 }
1405
1406 #[test]
1407 fn native_process_clear_captured_stderr_zero_when_merged() {
1408 let process = NativeProcess::new(ProcessConfig {
1409 command: CommandSpec::Argv(vec!["echo".into()]),
1410 cwd: None,
1411 env: None,
1412 capture: true,
1413 stderr_mode: StderrMode::Stdout,
1414 creationflags: None,
1415 create_process_group: false,
1416 stdin_mode: StdinMode::Inherit,
1417 nice: None,
1418 containment: None,
1419 });
1420 assert_eq!(process.clear_captured_stream(StreamKind::Stderr), 0);
1421 }
1422
1423 #[test]
1424 fn native_process_read_stream_eof_when_stderr_merged() {
1425 let process = NativeProcess::new(ProcessConfig {
1426 command: CommandSpec::Argv(vec!["echo".into()]),
1427 cwd: None,
1428 env: None,
1429 capture: true,
1430 stderr_mode: StderrMode::Stdout,
1431 creationflags: None,
1432 create_process_group: false,
1433 stdin_mode: StdinMode::Inherit,
1434 nice: None,
1435 containment: None,
1436 });
1437 assert_eq!(
1438 process.read_stream(StreamKind::Stderr, Some(Duration::from_millis(10))),
1439 ReadStatus::Eof
1440 );
1441 }
1442
1443 #[test]
1446 fn log_spawned_child_pid_noop_without_env() {
1447 std::env::remove_var("RUNNING_PROCESS_CHILD_PID_LOG_PATH");
1448 assert!(log_spawned_child_pid(12345).is_ok());
1449 }
1450
1451 #[test]
1454 fn shell_command_creates_command() {
1455 let cmd = shell_command("echo test");
1456 let _ = format!("{:?}", cmd);
1457 }
1458
1459 #[test]
1462 fn exit_code_from_success() {
1463 let output = std::process::Command::new("python")
1464 .args(["-c", "pass"])
1465 .output()
1466 .unwrap();
1467 assert_eq!(exit_code(output.status), 0);
1468 }
1469
1470 #[test]
1471 fn exit_code_from_nonzero() {
1472 let output = std::process::Command::new("python")
1473 .args(["-c", "import sys; sys.exit(42)"])
1474 .output()
1475 .unwrap();
1476 assert_eq!(exit_code(output.status), 42);
1477 }
1478
1479 #[cfg(windows)]
1482 mod windows_tests {
1483 use super::*;
1484
1485 const IDLE_PRIORITY_CLASS: u32 = 0x0000_0040;
1486 const BELOW_NORMAL_PRIORITY_CLASS: u32 = 0x0000_4000;
1487 const ABOVE_NORMAL_PRIORITY_CLASS: u32 = 0x0000_8000;
1488 const HIGH_PRIORITY_CLASS: u32 = 0x0000_0080;
1489
1490 #[test]
1491 fn priority_flags_none() {
1492 assert_eq!(windows_priority_flags(None), 0);
1493 }
1494
1495 #[test]
1496 fn priority_flags_zero() {
1497 assert_eq!(windows_priority_flags(Some(0)), 0);
1498 }
1499
1500 #[test]
1501 fn priority_flags_high_nice_idle() {
1502 assert_eq!(windows_priority_flags(Some(15)), IDLE_PRIORITY_CLASS);
1503 assert_eq!(windows_priority_flags(Some(20)), IDLE_PRIORITY_CLASS);
1504 }
1505
1506 #[test]
1507 fn priority_flags_low_positive_below_normal() {
1508 assert_eq!(windows_priority_flags(Some(1)), BELOW_NORMAL_PRIORITY_CLASS);
1509 assert_eq!(
1510 windows_priority_flags(Some(14)),
1511 BELOW_NORMAL_PRIORITY_CLASS
1512 );
1513 }
1514
1515 #[test]
1516 fn priority_flags_negative_above_normal() {
1517 assert_eq!(
1518 windows_priority_flags(Some(-1)),
1519 ABOVE_NORMAL_PRIORITY_CLASS
1520 );
1521 assert_eq!(
1522 windows_priority_flags(Some(-14)),
1523 ABOVE_NORMAL_PRIORITY_CLASS
1524 );
1525 }
1526
1527 #[test]
1528 fn priority_flags_very_negative_high() {
1529 assert_eq!(windows_priority_flags(Some(-15)), HIGH_PRIORITY_CLASS);
1530 assert_eq!(windows_priority_flags(Some(-20)), HIGH_PRIORITY_CLASS);
1531 }
1532 }
1533
1534 #[test]
1537 fn process_config_clone() {
1538 let config = ProcessConfig {
1539 command: CommandSpec::Shell("echo".to_string()),
1540 cwd: Some("/tmp".into()),
1541 env: Some(vec![("KEY".to_string(), "VAL".to_string())]),
1542 capture: true,
1543 stderr_mode: StderrMode::Pipe,
1544 creationflags: Some(0x10),
1545 create_process_group: true,
1546 stdin_mode: StdinMode::Piped,
1547 nice: Some(5),
1548 containment: None,
1549 };
1550 let cloned = config.clone();
1551 assert!(cloned.capture);
1552 assert_eq!(cloned.nice, Some(5));
1553 }
1554
1555 #[test]
1558 fn render_rust_debug_traces_returns_string() {
1559 let result = render_rust_debug_traces();
1560 let _ = result.len();
1561 }
1562
1563 #[test]
1566 fn rust_debug_scope_guard_enters_and_drops() {
1567 let _guard = RustDebugScopeGuard::enter("test_scope", file!(), line!());
1568 let traces = render_rust_debug_traces();
1569 assert!(traces.contains("test_scope"));
1570 drop(_guard);
1571 }
1572
1573 #[cfg(unix)]
1576 mod unix_tests {
1577 use super::*;
1578
1579 #[test]
1580 fn unix_signal_raw_values() {
1581 assert_eq!(unix_signal_raw(UnixSignal::Interrupt), libc::SIGINT);
1582 assert_eq!(unix_signal_raw(UnixSignal::Terminate), libc::SIGTERM);
1583 assert_eq!(unix_signal_raw(UnixSignal::Kill), libc::SIGKILL);
1584 }
1585
1586 #[test]
1587 fn unix_signal_enum_equality() {
1588 assert_eq!(UnixSignal::Interrupt, UnixSignal::Interrupt);
1589 assert_ne!(UnixSignal::Interrupt, UnixSignal::Kill);
1590 }
1591 }
1592
1593 #[test]
1596 fn wait_for_capture_completion_noop_without_capture() {
1597 let process = NativeProcess::new(ProcessConfig {
1598 command: CommandSpec::Argv(vec!["echo".into()]),
1599 cwd: None,
1600 env: None,
1601 capture: false,
1602 stderr_mode: StderrMode::Stdout,
1603 creationflags: None,
1604 create_process_group: false,
1605 stdin_mode: StdinMode::Inherit,
1606 nice: None,
1607 containment: None,
1608 });
1609 process.wait_for_capture_completion_impl();
1610 }
1611
1612 #[test]
1615 fn build_command_from_argv() {
1616 let process = NativeProcess::new(ProcessConfig {
1617 command: CommandSpec::Argv(vec!["echo".into(), "hello".into(), "world".into()]),
1618 cwd: None,
1619 env: None,
1620 capture: false,
1621 stderr_mode: StderrMode::Stdout,
1622 creationflags: None,
1623 create_process_group: false,
1624 stdin_mode: StdinMode::Inherit,
1625 nice: None,
1626 containment: None,
1627 });
1628 let cmd = process.build_command();
1629 assert_eq!(cmd.get_program(), "echo");
1630 let args: Vec<_> = cmd.get_args().collect();
1631 assert_eq!(args, vec!["hello", "world"]);
1632 }
1633
1634 #[test]
1635 fn build_command_from_shell() {
1636 let process = NativeProcess::new(ProcessConfig {
1637 command: CommandSpec::Shell("echo test".into()),
1638 cwd: None,
1639 env: None,
1640 capture: false,
1641 stderr_mode: StderrMode::Stdout,
1642 creationflags: None,
1643 create_process_group: false,
1644 stdin_mode: StdinMode::Inherit,
1645 nice: None,
1646 containment: None,
1647 });
1648 let cmd = process.build_command();
1649 let program = cmd.get_program().to_string_lossy().to_string();
1651 #[cfg(windows)]
1652 assert!(
1653 program.contains("cmd"),
1654 "expected cmd shell, got {}",
1655 program
1656 );
1657 #[cfg(not(windows))]
1658 assert!(program.contains("sh"), "expected sh shell, got {}", program);
1659 }
1660
1661 #[test]
1662 fn build_command_with_cwd() {
1663 let tmp = std::env::temp_dir();
1664 let process = NativeProcess::new(ProcessConfig {
1665 command: CommandSpec::Argv(vec!["echo".into()]),
1666 cwd: Some(tmp.clone()),
1667 env: None,
1668 capture: false,
1669 stderr_mode: StderrMode::Stdout,
1670 creationflags: None,
1671 create_process_group: false,
1672 stdin_mode: StdinMode::Inherit,
1673 nice: None,
1674 containment: None,
1675 });
1676 let cmd = process.build_command();
1677 assert_eq!(cmd.get_current_dir().unwrap(), &tmp);
1678 }
1679
1680 #[test]
1681 fn build_command_with_env() {
1682 let process = NativeProcess::new(ProcessConfig {
1683 command: CommandSpec::Argv(vec!["echo".into()]),
1684 cwd: None,
1685 env: Some(vec![
1686 ("FOO".into(), "bar".into()),
1687 ("BAZ".into(), "qux".into()),
1688 ]),
1689 capture: false,
1690 stderr_mode: StderrMode::Stdout,
1691 creationflags: None,
1692 create_process_group: false,
1693 stdin_mode: StdinMode::Inherit,
1694 nice: None,
1695 containment: None,
1696 });
1697 let cmd = process.build_command();
1698 let envs: Vec<_> = cmd.get_envs().collect();
1699 assert!(envs
1700 .iter()
1701 .any(|(k, v)| *k == "FOO" && *v == Some(std::ffi::OsStr::new("bar"))));
1702 assert!(envs
1703 .iter()
1704 .any(|(k, v)| *k == "BAZ" && *v == Some(std::ffi::OsStr::new("qux"))));
1705 }
1706
1707 #[test]
1708 fn build_command_single_argv() {
1709 let process = NativeProcess::new(ProcessConfig {
1710 command: CommandSpec::Argv(vec!["echo".into()]),
1711 cwd: None,
1712 env: None,
1713 capture: false,
1714 stderr_mode: StderrMode::Stdout,
1715 creationflags: None,
1716 create_process_group: false,
1717 stdin_mode: StdinMode::Inherit,
1718 nice: None,
1719 containment: None,
1720 });
1721 let cmd = process.build_command();
1722 assert_eq!(cmd.get_program(), "echo");
1723 assert_eq!(cmd.get_args().count(), 0);
1724 }
1725
1726 #[test]
1729 fn set_returncode_updates_shared_state() {
1730 let process = NativeProcess::new(ProcessConfig {
1731 command: CommandSpec::Argv(vec!["echo".into()]),
1732 cwd: None,
1733 env: None,
1734 capture: false,
1735 stderr_mode: StderrMode::Stdout,
1736 creationflags: None,
1737 create_process_group: false,
1738 stdin_mode: StdinMode::Inherit,
1739 nice: None,
1740 containment: None,
1741 });
1742 assert!(process.returncode().is_none());
1743 process.set_returncode(42);
1744 assert_eq!(process.returncode(), Some(42));
1745 }
1746
1747 #[test]
1748 fn set_returncode_overwrites() {
1749 let process = NativeProcess::new(ProcessConfig {
1750 command: CommandSpec::Argv(vec!["echo".into()]),
1751 cwd: None,
1752 env: None,
1753 capture: false,
1754 stderr_mode: StderrMode::Stdout,
1755 creationflags: None,
1756 create_process_group: false,
1757 stdin_mode: StdinMode::Inherit,
1758 nice: None,
1759 containment: None,
1760 });
1761 process.set_returncode(1);
1762 process.set_returncode(2);
1763 assert_eq!(process.returncode(), Some(2));
1764 }
1765
1766 #[test]
1769 fn shared_state_with_capture_queues_open() {
1770 let state = SharedState::new(true);
1771 let guard = state.queues.lock().unwrap();
1772 assert!(!guard.stdout_closed);
1773 assert!(!guard.stderr_closed);
1774 }
1775
1776 #[test]
1777 fn shared_state_without_capture_queues_closed() {
1778 let state = SharedState::new(false);
1779 let guard = state.queues.lock().unwrap();
1780 assert!(guard.stdout_closed);
1781 assert!(guard.stderr_closed);
1782 }
1783
1784 #[test]
1787 fn process_error_display_io_variant() {
1788 let err = ProcessError::Io(std::io::Error::new(
1789 std::io::ErrorKind::BrokenPipe,
1790 "pipe broken",
1791 ));
1792 let msg = format!("{}", err);
1793 assert!(msg.contains("pipe broken"));
1794 }
1795
1796 #[test]
1797 fn process_error_display_spawn_variant() {
1798 let err = ProcessError::Spawn(std::io::Error::new(
1799 std::io::ErrorKind::NotFound,
1800 "not found",
1801 ));
1802 let msg = format!("{}", err);
1803 assert!(msg.contains("not found"));
1804 }
1805
1806 #[test]
1809 fn shell_command_returns_command_with_shell() {
1810 let cmd = shell_command("echo test");
1811 let program = cmd.get_program().to_string_lossy().to_string();
1812 #[cfg(windows)]
1813 assert!(program.contains("cmd"));
1814 #[cfg(not(windows))]
1815 assert!(program.contains("sh"));
1816 }
1817}