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
142const RETURNCODE_NOT_SET: i64 = i64::MIN;
144
145struct SharedState {
146 queues: Mutex<QueueState>,
147 condvar: Condvar,
148 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 #[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 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 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 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 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 #[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 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 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 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 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 #[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 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 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 return Ok(());
464 }
465 let pid = match self.pid() {
466 Some(p) => p,
467 None => return Err(ProcessError::NotRunning),
468 };
469 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 if err.raw_os_error() != Some(6) {
486 return Err(ProcessError::Io(err));
487 }
488 }
489 Ok(())
490 }
491 }
492
493 #[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 #[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 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;