1use std::os::unix::io::RawFd;
13use std::time::{Duration, Instant};
14
15use crate::CoreError;
16use crate::error::syscall_ret;
17use crate::fd::Fd;
18use crate::io::DrainState;
19use crate::reactor::Reactor;
20use libc::{O_CLOEXEC, WEXITSTATUS, WIFEXITED, WIFSIGNALED, WTERMSIG, pid_t, pipe2, waitpid};
21use std::collections::HashSet;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::{Mutex, OnceLock};
24
25mod clone3;
26mod exec;
27mod fork;
28mod posix;
29
30use clone3::spawn_clone3_internal;
31use exec::ExecContext;
32use fork::{spawn_fork_internal, spawn_vfork_internal};
33use posix::spawn_posix_internal;
34
35unsafe extern "C" {
36 pub(crate) static mut environ: *mut *mut libc::c_char;
37}
38
39#[cfg(any(
43 target_arch = "x86_64",
44 target_arch = "aarch64",
45 target_arch = "arm",
46 target_arch = "riscv64",
47 target_arch = "loongarch64",
48 target_arch = "powerpc64",
49 target_arch = "s390x"
50))]
51const SYS_CLONE3: libc::c_long = 435;
52#[cfg(any(
53 target_arch = "x86_64",
54 target_arch = "aarch64",
55 target_arch = "arm",
56 target_arch = "riscv64",
57 target_arch = "loongarch64",
58 target_arch = "powerpc64",
59 target_arch = "s390x"
60))]
61const SYS_PIDFD_SEND_SIGNAL: libc::c_long = 424;
62
63const CLONE_PIDFD: u64 = 0x0000_1000;
66
67const D_STATE_REAP_BOUND: Duration = Duration::from_millis(500);
73
74static ORPHANED: OnceLock<Mutex<HashSet<pid_t>>> = OnceLock::new();
85static REAPER_STARTED: AtomicBool = AtomicBool::new(false);
86
87fn orphan_child(pid: pid_t) {
90 ORPHANED
91 .get_or_init(|| Mutex::new(HashSet::new()))
92 .lock()
93 .unwrap()
94 .insert(pid);
95 start_reaper();
96}
97
98fn start_reaper() {
100 if REAPER_STARTED.load(Ordering::SeqCst) {
101 return;
102 }
103 let r = REAPER_STARTED.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst);
104 if r.is_err() {
105 return;
106 }
107 std::thread::Builder::new()
108 .name("spawn-orphan-reaper".into())
109 .spawn(reap_orphaned)
110 .ok();
111}
112
113fn reap_orphaned() {
117 loop {
118 let pids: Vec<pid_t> = ORPHANED
119 .get_or_init(|| Mutex::new(HashSet::new()))
120 .lock()
121 .unwrap()
122 .iter()
123 .copied()
124 .collect();
125 let mut still_orphaned = Vec::new();
126 for pid in pids {
127 let mut status: libc::c_int = 0;
128 let r = unsafe { waitpid(pid, &mut status, libc::WNOHANG) };
129 if r == pid
130 || (r < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ECHILD))
131 {
132 continue; }
134 still_orphaned.push(pid);
135 }
136 if !still_orphaned.is_empty() {
137 if let Some(set) = ORPHANED.get() {
138 if let Ok(mut guard) = set.lock() {
139 for pid in still_orphaned {
140 guard.insert(pid);
141 }
142 }
143 }
144 }
145 std::thread::sleep(Duration::from_millis(250));
146 }
147}
148
149#[allow(dead_code)]
153fn deorphan_child(pid: pid_t) {
154 if let Some(set) = ORPHANED.get() {
155 if let Ok(mut guard) = set.lock() {
156 guard.remove(&pid);
157 }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
163pub enum CancelPolicy {
164 #[default]
166 None,
167 Graceful,
169 Kill,
171}
172
173#[derive(Debug, Clone, Copy, Default)]
175pub struct ProcessGroup {
176 pub leader: Option<pid_t>,
178 pub isolated: bool,
180}
181
182impl ProcessGroup {
183 pub fn new(leader: Option<pid_t>, isolated: bool) -> Self {
185 Self { leader, isolated }
186 }
187}
188
189#[inline(always)]
190fn errno() -> i32 {
191 std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
192}
193
194fn relocate_above_stdio(fd: RawFd, op: &'static str) -> Result<RawFd, CoreError> {
200 if fd >= 3 {
201 return Ok(fd);
202 }
203 let new = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) };
204 syscall_ret(new, op)?;
205 unsafe {
206 libc::close(fd);
207 }
208 Ok(new)
209}
210
211#[inline(always)]
217fn make_pipe() -> Result<(Fd, Fd), CoreError> {
218 let mut fds = [0; 2];
219 let r = unsafe { pipe2(fds.as_mut_ptr(), O_CLOEXEC) };
220 syscall_ret(r, "pipe2")?;
221 let r0 = match relocate_above_stdio(fds[0], "pipe2:relocate") {
222 Ok(fd) => fd,
223 Err(e) => {
224 unsafe {
227 libc::close(fds[0]);
228 }
229 return Err(e);
230 }
231 };
232 let r1 = match relocate_above_stdio(fds[1], "pipe2:relocate") {
233 Ok(fd) => fd,
234 Err(e) => {
235 unsafe {
238 libc::close(r0);
239 libc::close(fds[1]);
240 }
241 return Err(e);
242 }
243 };
244 Ok((Fd::new(r0, "pipe2")?, Fd::new(r1, "pipe2")?))
245}
246
247fn make_cloexec_pipe() -> Result<(RawFd, RawFd), CoreError> {
248 let mut fds = [0; 2];
249 let r = unsafe { pipe2(fds.as_mut_ptr(), O_CLOEXEC) };
250 syscall_ret(r, "pipe2")?;
251 let r0 = match relocate_above_stdio(fds[0], "pipe2:relocate") {
252 Ok(fd) => fd,
253 Err(e) => {
254 unsafe {
255 libc::close(fds[0]);
256 }
257 return Err(e);
258 }
259 };
260 let r1 = match relocate_above_stdio(fds[1], "pipe2:relocate") {
261 Ok(fd) => fd,
262 Err(e) => {
263 unsafe {
264 libc::close(r0);
265 libc::close(fds[1]);
266 }
267 return Err(e);
268 }
269 };
270 Ok((r0, r1))
271}
272
273struct Pipes {
274 stdin_r: Option<Fd>,
275 stdin_w: Option<Fd>,
276 stdout_r: Option<Fd>,
277 stdout_w: Option<Fd>,
278 stderr_r: Option<Fd>,
279 stderr_w: Option<Fd>,
280}
281
282impl Pipes {
283 fn new(in_buf: Option<&[u8]>, out: bool, err: bool) -> Result<Self, CoreError> {
284 let (stdin_r, stdin_w) = if in_buf.is_some() {
285 let (r, w) = make_pipe()?;
286 (Some(r), Some(w))
287 } else {
288 (None, None)
289 };
290
291 let (stdout_r, stdout_w) = if out {
292 let (r, w) = make_pipe()?;
293 (Some(r), Some(w))
294 } else {
295 (None, None)
296 };
297
298 let (stderr_r, stderr_w) = if err {
299 let (r, w) = make_pipe()?;
300 (Some(r), Some(w))
301 } else {
302 (None, None)
303 };
304
305 Ok(Self {
306 stdin_r,
307 stdin_w,
308 stdout_r,
309 stdout_w,
310 stderr_r,
311 stderr_w,
312 })
313 }
314
315 #[inline(always)]
316 fn close_all(&mut self) {
317 self.stdin_r.take();
318 self.stdin_w.take();
319 self.stdout_r.take();
320 self.stdout_w.take();
321 self.stderr_r.take();
322 self.stderr_w.take();
323 }
324}
325
326#[derive(Debug, PartialEq, Eq)]
328pub enum ExitStatus {
329 Exited(i32),
331 Signaled(i32),
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
337pub enum SpawnBackend {
338 PosixSpawn,
340 Fork,
345 Vfork,
358 Clone3,
365 Clone3Pidfd,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Default)]
377pub enum SpawnFdPolicy {
378 #[default]
380 CloexecOnly,
381 CloseFrom3,
384 Allowlist(Vec<RawFd>),
391}
392
393#[inline(always)]
394fn decode_status(status: i32) -> ExitStatus {
395 if WIFEXITED(status) {
396 ExitStatus::Exited(WEXITSTATUS(status))
397 } else if WIFSIGNALED(status) {
398 ExitStatus::Signaled(WTERMSIG(status))
399 } else {
400 ExitStatus::Exited(-1)
401 }
402}
403
404pub struct Process {
417 pid: pid_t,
418 pidfd: Option<RawFd>,
419}
420
421impl Process {
422 pub fn new(pid: pid_t) -> Self {
424 Self { pid, pidfd: None }
425 }
426
427 pub(crate) fn with_pidfd(pid: pid_t, pidfd: RawFd) -> Self {
429 Self {
430 pid,
431 pidfd: Some(pidfd),
432 }
433 }
434
435 pub fn pid(&self) -> pid_t {
437 self.pid
438 }
439
440 pub fn pidfd(&self) -> Option<RawFd> {
442 self.pidfd
443 }
444
445 pub fn wait_step(&self) -> Result<Option<ExitStatus>, CoreError> {
455 if let Some(pidfd) = self.pidfd {
456 return wait_step_pidfd(pidfd, self.pid);
457 }
458 loop {
459 let mut status = 0;
460 let r = unsafe { waitpid(self.pid, &mut status, libc::WNOHANG) };
461 if r == 0 {
462 return Ok(None);
463 }
464 if r < 0 {
465 let e = errno();
466 if e == libc::EINTR {
467 continue;
468 }
469 return Err(CoreError::sys(e, "waitpid_step"));
470 }
471 return Ok(Some(decode_status(status)));
472 }
473 }
474
475 pub fn wait_blocking(&self) -> Result<ExitStatus, CoreError> {
480 loop {
481 let mut status = 0;
482 let r = unsafe { waitpid(self.pid, &mut status, 0) };
483 if r < 0 {
484 let e = errno();
485 if e == libc::EINTR {
486 continue;
487 }
488 return Err(CoreError::sys(e, "waitpid_blocking"));
489 }
490 return Ok(decode_status(status));
491 }
492 }
493
494 pub fn kill(&self, sig: i32) -> Result<(), CoreError> {
506 if let Some(pidfd) = self.pidfd {
507 let r = unsafe {
508 libc::syscall(
509 SYS_PIDFD_SEND_SIGNAL,
510 pidfd,
511 sig,
512 std::ptr::null_mut::<libc::siginfo_t>(),
513 0,
514 )
515 };
516 if r < 0 {
517 let e = errno();
518 if e == libc::ESRCH {
519 return Ok(());
520 }
521 if e != libc::ENOSYS && e != libc::EINVAL {
522 return Err(CoreError::sys(e, "pidfd_send_signal"));
523 }
524 } else {
526 return Ok(());
527 }
528 }
529 if self.pid <= 0 {
530 return Err(CoreError::sys(libc::EINVAL, "kill: invalid pid"));
531 }
532 let r = unsafe { libc::kill(self.pid, sig) };
533 if r < 0 {
534 let e = errno();
535 if e == libc::ESRCH {
536 return Ok(());
537 }
538 syscall_ret(-1, "kill")?;
539 }
540 Ok(())
541 }
542
543 pub fn kill_pgroup(&self, sig: i32) -> Result<(), CoreError> {
550 self.kill_group(self.pid, sig)
551 }
552
553 pub fn kill_group(&self, pgid: pid_t, sig: i32) -> Result<(), CoreError> {
563 if pgid <= 0 {
564 return Err(CoreError::sys(libc::EINVAL, "kill_group: invalid pgid"));
565 }
566 let r = unsafe { libc::kill(-pgid, sig) };
567 if r < 0 {
568 let e = errno();
569 if e == libc::ESRCH {
570 return Ok(());
571 }
572 syscall_ret(-1, "kill_group")?;
573 }
574 Ok(())
575 }
576}
577
578impl Drop for Process {
579 fn drop(&mut self) {
580 if let Some(pidfd) = self.pidfd.take() {
581 unsafe {
582 libc::close(pidfd);
583 }
584 }
585 }
586}
587
588fn wait_step_pidfd(pidfd: RawFd, pid: pid_t) -> Result<Option<ExitStatus>, CoreError> {
593 let mut pfd = libc::pollfd {
594 fd: pidfd,
595 events: libc::POLLIN,
596 revents: 0,
597 };
598 loop {
599 let r = unsafe { libc::poll(&mut pfd, 1, 0) };
600 if r < 0 {
601 let e = errno();
602 if e == libc::EINTR {
603 continue;
604 }
605 return Err(CoreError::sys(e, "poll(pidfd)"));
606 }
607 break;
608 }
609 if pfd.revents & libc::POLLIN == 0 {
610 return Ok(None);
611 }
612 loop {
613 let mut status = 0;
614 let r = unsafe { waitpid(pid, &mut status, libc::WNOHANG) };
615 if r == pid {
616 return Ok(Some(decode_status(status)));
617 }
618 if r < 0 {
619 let e = errno();
620 if e == libc::EINTR {
621 continue;
622 }
623 if e == libc::ECHILD {
624 return Ok(None);
626 }
627 return Err(CoreError::sys(e, "waitpid(pidfd step)"));
628 }
629 return Ok(None);
631 }
632}
633
634#[derive(Clone)]
636pub struct SpawnOptions {
637 ctx: ExecContext,
638 stdin: Option<Box<[u8]>>,
639 capture_stdout: bool,
640 capture_stderr: bool,
641 wait: bool,
642 pgroup: ProcessGroup,
643 max_output: usize,
644 timeout_ms: Option<u32>,
645 kill_grace_ms: u32,
646 cancel: CancelPolicy,
647 backend: SpawnBackend,
648 fd_policy: SpawnFdPolicy,
649 early_exit: Option<fn(&[u8]) -> bool>,
650}
651
652impl SpawnOptions {
653 pub fn builder(argv: Vec<String>, backend: SpawnBackend) -> SpawnOptionsBuilder {
655 SpawnOptionsBuilder::new(argv, backend)
656 }
657
658 pub fn run(self) -> Result<Output, CoreError> {
660 spawn(self)
661 }
662}
663
664#[derive(Clone)]
666pub struct SpawnOptionsBuilder {
667 argv: Vec<String>,
668 env: Option<Vec<String>>,
669 cwd: Option<String>,
670 stdin: Option<Box<[u8]>>,
671 capture_stdout: bool,
672 capture_stderr: bool,
673 wait: bool,
674 pgroup: ProcessGroup,
675 max_output: usize,
676 timeout_ms: Option<u32>,
677 kill_grace_ms: u32,
678 cancel: CancelPolicy,
679 backend: SpawnBackend,
680 fd_policy: SpawnFdPolicy,
681 early_exit: Option<fn(&[u8]) -> bool>,
682}
683
684impl SpawnOptionsBuilder {
685 pub fn new(argv: Vec<String>, backend: SpawnBackend) -> Self {
687 Self {
688 argv,
689 env: None,
690 cwd: None,
691 stdin: None,
692 capture_stdout: false,
693 capture_stderr: false,
694 wait: true,
695 pgroup: ProcessGroup::default(),
696 max_output: 1024 * 1024,
697 timeout_ms: None,
698 kill_grace_ms: 2000,
699 cancel: CancelPolicy::Kill,
700 backend,
701 fd_policy: SpawnFdPolicy::default(),
702 early_exit: None,
703 }
704 }
705
706 pub fn env(mut self, env: Vec<String>) -> Self {
708 self.env = Some(env);
709 self
710 }
711
712 pub fn cwd(mut self, cwd: String) -> Self {
714 self.cwd = Some(cwd);
715 self
716 }
717
718 pub fn stdin(mut self, data: impl Into<Box<[u8]>>) -> Self {
720 self.stdin = Some(data.into());
721 self
722 }
723
724 pub fn capture_stdout(mut self) -> Self {
726 self.capture_stdout = true;
727 self
728 }
729
730 pub fn capture_stderr(mut self) -> Self {
732 self.capture_stderr = true;
733 self
734 }
735
736 pub fn wait(mut self, wait: bool) -> Self {
738 self.wait = wait;
739 self
740 }
741
742 pub fn pgroup(mut self, pgroup: ProcessGroup) -> Self {
744 self.pgroup = pgroup;
745 self
746 }
747
748 pub fn max_output(mut self, max: usize) -> Self {
753 self.max_output = max;
754 self
755 }
756
757 pub fn timeout_ms(mut self, ms: u32) -> Self {
759 self.timeout_ms = Some(ms);
760 self
761 }
762
763 pub fn kill_grace_ms(mut self, ms: u32) -> Self {
765 self.kill_grace_ms = ms;
766 self
767 }
768
769 pub fn cancel(mut self, policy: CancelPolicy) -> Self {
771 self.cancel = policy;
772 self
773 }
774
775 pub fn fd_policy(mut self, policy: SpawnFdPolicy) -> Self {
777 self.fd_policy = policy;
778 self
779 }
780
781 pub fn early_exit(mut self, callback: fn(&[u8]) -> bool) -> Self {
783 self.early_exit = Some(callback);
784 self
785 }
786
787 pub fn build(self) -> Result<SpawnOptions, CoreError> {
789 let ctx = ExecContext::new(self.argv, self.env, self.cwd)?;
790 Ok(SpawnOptions {
791 ctx,
792 stdin: self.stdin,
793 capture_stdout: self.capture_stdout,
794 capture_stderr: self.capture_stderr,
795 wait: self.wait,
796 pgroup: self.pgroup,
797 max_output: self.max_output,
798 timeout_ms: self.timeout_ms,
799 kill_grace_ms: self.kill_grace_ms,
800 cancel: self.cancel,
801 backend: self.backend,
802 fd_policy: self.fd_policy,
803 early_exit: self.early_exit,
804 })
805 }
806}
807
808#[derive(Debug)]
810pub struct Output {
811 pub pid: pid_t,
813 pub status: Option<ExitStatus>,
815 pub stdout: Vec<u8>,
817 pub stderr: Vec<u8>,
819 pub timed_out: bool,
821 pub stdout_early_exited: bool,
823}
824
825fn validate_backend(opts: &SpawnOptions) -> Result<(), CoreError> {
826 validate_fd_policy(&opts.fd_policy)?;
827 match opts.backend {
828 SpawnBackend::PosixSpawn => {
829 if opts.ctx.cwd.is_some() {
830 return Err(CoreError::sys(libc::EINVAL, "posix_spawn cwd unsupported"));
831 }
832 if opts.pgroup.isolated {
833 return Err(CoreError::sys(
834 libc::EINVAL,
835 "posix_spawn setsid unsupported",
836 ));
837 }
838 if opts.fd_policy != SpawnFdPolicy::CloexecOnly {
839 return Err(CoreError::sys(
840 libc::EINVAL,
841 "posix_spawn fd policy unsupported",
842 ));
843 }
844 Ok(())
845 }
846 SpawnBackend::Fork
847 | SpawnBackend::Vfork
848 | SpawnBackend::Clone3
849 | SpawnBackend::Clone3Pidfd => {
850 if opts.pgroup.isolated && opts.pgroup.leader.is_some_and(|l| l != 0) {
856 return Err(CoreError::sys(
857 libc::EINVAL,
858 "exec isolated + custom setpgid leader unsupported",
859 ));
860 }
861 Ok(())
862 }
863 }
864}
865
866fn validate_fd_policy(policy: &SpawnFdPolicy) -> Result<(), CoreError> {
867 if let SpawnFdPolicy::Allowlist(fds) = policy {
868 let mut seen = Vec::with_capacity(fds.len());
869 for &fd in fds {
870 if fd < 0 {
871 return Err(CoreError::sys(libc::EINVAL, "spawn fd allowlist invalid"));
872 }
873 let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
874 if flags < 0 {
875 return Err(CoreError::sys(errno(), "spawn fd allowlist fcntl(F_GETFD)"));
876 }
877 if seen.contains(&fd) {
878 return Err(CoreError::sys(libc::EINVAL, "spawn fd allowlist duplicate"));
879 }
880 seen.push(fd);
881 }
882 }
883 Ok(())
884}
885
886pub type SpawnDrain = DrainState<fn(&[u8]) -> bool>;
888
889pub struct RunningProcess {
896 pub process: Process,
898 drain: SpawnDrain,
899}
900
901pub struct ManagedProcess {
909 running: Option<RunningProcess>,
910 pid: pid_t,
911 timeout_at: Option<Instant>,
912 kill_grace: Duration,
913 cancel: CancelPolicy,
914 pgroup: ProcessGroup,
915 cancel_at: Option<Instant>,
916 kill_state: KillState,
917 status: Option<ExitStatus>,
918 timed_out: bool,
919 kill_sent_at: Option<Instant>,
920}
921
922impl RunningProcess {
923 pub fn register_with_reactor(&mut self, reactor: &mut Reactor) -> Result<(), CoreError> {
929 self.drain.register_with_reactor(reactor)
930 }
931
932 pub fn handle_reactor_event(
938 &mut self,
939 reactor: &mut Reactor,
940 event: &crate::fd::Event,
941 ) -> Result<(), CoreError> {
942 if self.drain.stdout_matches(event.token) {
943 if event.readable || event.hangup {
944 self.drain.handle_stdout_ready(reactor)?;
945 } else if event.error {
946 self.drain.drop_stdout(reactor)?;
947 }
948 } else if self.drain.stderr_matches(event.token) {
949 if event.readable || event.hangup {
950 self.drain.handle_stderr_ready(reactor)?;
951 } else if event.error {
952 self.drain.drop_stderr(reactor)?;
953 }
954 } else if self.drain.stdin_matches(event.token) {
955 if event.writable {
956 self.drain.handle_stdin_writable(reactor)?;
957 } else if event.error || event.hangup {
958 self.drain.drop_stdin(reactor)?;
959 }
960 }
961 Ok(())
962 }
963
964 pub fn io_done(&self) -> bool {
966 self.drain.is_done()
967 }
968
969 pub fn into_output_parts(self) -> (Vec<u8>, Vec<u8>) {
971 self.drain.into_parts()
972 }
973}
974
975impl ManagedProcess {
976 pub fn pid(&self) -> pid_t {
981 self.pid
982 }
983
984 pub fn register_with_reactor(&mut self, reactor: &mut Reactor) -> Result<(), CoreError> {
986 self.running
987 .as_mut()
988 .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?
989 .register_with_reactor(reactor)
990 }
991
992 pub fn handle_reactor_event(
994 &mut self,
995 reactor: &mut Reactor,
996 event: &crate::fd::Event,
997 ) -> Result<(), CoreError> {
998 self.running
999 .as_mut()
1000 .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?
1001 .handle_reactor_event(reactor, event)
1002 }
1003
1004 pub fn request_cancel(&mut self) {
1007 self.cancel_at.get_or_insert_with(Instant::now);
1008 }
1009
1010 pub fn next_deadline(&self) -> Option<Instant> {
1016 self.running.as_ref()?;
1017 let now = Instant::now();
1018 let mut next = now + Duration::from_millis(100);
1019 if !self.timed_out
1020 && let Some(timeout_at) = self.timeout_at
1021 && timeout_at < next
1022 {
1023 next = timeout_at;
1024 }
1025 if self.kill_state == KillState::TermSent
1026 && let Some(cancel_at) = self.cancel_at
1027 {
1028 let kill_at = cancel_at + self.kill_grace;
1029 if kill_at < next {
1030 next = kill_at;
1031 }
1032 }
1033 if let Some(sent_at) = self.kill_sent_at {
1036 let bail_at = sent_at + D_STATE_REAP_BOUND;
1037 if bail_at < next {
1038 next = bail_at;
1039 }
1040 }
1041 Some(next)
1042 }
1043
1044 pub fn poll_completion(&mut self, reactor: &mut Reactor) -> Result<Option<Output>, CoreError> {
1053 let now = Instant::now();
1054 if !self.timed_out
1055 && let Some(timeout_at) = self.timeout_at
1056 && now >= timeout_at
1057 {
1058 self.timed_out = true;
1059 self.cancel_at.get_or_insert(timeout_at);
1060 }
1061
1062 self.advance_cancel(now)?;
1063
1064 let running = self
1065 .running
1066 .as_ref()
1067 .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
1068 if self.status.is_none() {
1069 self.status = running.process.wait_step()?;
1070 }
1071
1072 let io_done = running.io_done();
1073 if self.status.is_some() && (io_done || self.cancel_at.is_some()) {
1074 return self.finish(reactor, !io_done).map(Some);
1075 }
1076 if self.status.is_none()
1080 && self
1081 .kill_sent_at
1082 .is_some_and(|t| now.duration_since(t) >= D_STATE_REAP_BOUND)
1083 {
1084 return self.finish(reactor, true).map(Some);
1085 }
1086 Ok(None)
1087 }
1088
1089 fn advance_cancel(&mut self, now: Instant) -> Result<(), CoreError> {
1090 let Some(cancel_at) = self.cancel_at else {
1091 return Ok(());
1092 };
1093 if self.status.is_some() {
1095 return Ok(());
1096 }
1097 let running = self
1098 .running
1099 .as_ref()
1100 .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
1101 let process = &running.process;
1102 let pid = process.pid();
1103 let pgid = effective_pgid(pid, self.pgroup);
1104 let target_is_group = self.pgroup.isolated || self.pgroup.leader.is_some();
1105 match self.kill_state {
1106 KillState::None => match self.cancel {
1107 CancelPolicy::None => {}
1108 CancelPolicy::Graceful => {
1109 let result = signal_process(process, target_is_group, pgid, libc::SIGTERM);
1110 self.kill_state = if result.is_ok() {
1111 KillState::TermSent
1112 } else {
1113 KillState::KillSent
1114 };
1115 if self.kill_state == KillState::KillSent {
1116 self.kill_sent_at = Some(now);
1117 }
1118 }
1119 CancelPolicy::Kill => {
1120 let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
1121 self.kill_state = KillState::KillSent;
1122 self.kill_sent_at = Some(now);
1123 }
1124 },
1125 KillState::TermSent if now >= cancel_at + self.kill_grace => {
1126 let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
1127 self.kill_state = KillState::KillSent;
1128 self.kill_sent_at = Some(now);
1129 }
1130 _ => {}
1131 }
1132 Ok(())
1133 }
1134
1135 fn finish(&mut self, reactor: &mut Reactor, force_close: bool) -> Result<Output, CoreError> {
1136 let mut running = self
1137 .running
1138 .take()
1139 .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
1140 for slot in running.drain.take_all_slots() {
1141 if force_close {
1142 let _ = reactor.del(&slot.fd);
1143 } else {
1144 reactor.del(&slot.fd)?;
1145 }
1146 }
1147 let pid = running.process.pid();
1148 let (stdout, stderr, output_limit_exceeded, stdout_early_exited) =
1149 running.drain.into_parts_with_state();
1150 if self.status.is_none() {
1155 orphan_child(pid);
1156 }
1157 if output_limit_exceeded && !force_close {
1162 return Err(CoreError::sys(libc::EOVERFLOW, "spawn output limit"));
1163 }
1164 Ok(Output {
1165 pid,
1166 status: self.status.take(),
1167 stdout,
1168 stderr,
1169 timed_out: self.timed_out,
1170 stdout_early_exited,
1171 })
1172 }
1173}
1174
1175impl Drop for ManagedProcess {
1176 fn drop(&mut self) {
1177 let Some(running) = self.running.take() else {
1178 return;
1179 };
1180 if self.status.is_some() {
1184 return;
1185 }
1186 let process = &running.process;
1187 let pid = process.pid();
1188 let pgid = effective_pgid(pid, self.pgroup);
1189 let target_is_group = self.pgroup.isolated || self.pgroup.leader.is_some();
1190 let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
1191 let deadline = Instant::now() + Duration::from_millis(100);
1196 while Instant::now() < deadline {
1197 match process.wait_step() {
1198 Ok(Some(_)) => return,
1199 Ok(None) => std::thread::sleep(Duration::from_millis(5)),
1200 Err(_) => return,
1201 }
1202 }
1203 }
1204}
1205
1206fn effective_pgid(pid: pid_t, pgroup: ProcessGroup) -> pid_t {
1207 match pgroup.leader {
1208 Some(0) | None => pid,
1209 Some(leader) => leader,
1210 }
1211}
1212
1213fn signal_process(
1214 process: &Process,
1215 target_is_group: bool,
1216 pgid: pid_t,
1217 signal: i32,
1218) -> Result<(), CoreError> {
1219 if target_is_group {
1220 process.kill_group(pgid, signal)
1221 } else {
1222 process.kill(signal)
1223 }
1224}
1225
1226pub fn spawn_start(opts: SpawnOptions) -> Result<RunningProcess, CoreError> {
1241 if !opts.wait && (opts.stdin.is_some() || opts.capture_stdout || opts.capture_stderr) {
1242 return Err(CoreError::sys(
1243 libc::EINVAL,
1244 "background I/O capture not supported (wait must be true)",
1245 ));
1246 }
1247
1248 validate_backend(&opts)?;
1249
1250 let (process, drain) = match opts.backend {
1251 SpawnBackend::PosixSpawn => spawn_posix_internal(opts)?,
1252 SpawnBackend::Fork => spawn_fork_internal(opts)?,
1253 SpawnBackend::Vfork => spawn_vfork_internal(opts)?,
1254 SpawnBackend::Clone3 => spawn_clone3_internal(opts, false)?,
1255 SpawnBackend::Clone3Pidfd => spawn_clone3_internal(opts, true)?,
1256 };
1257
1258 Ok(RunningProcess { process, drain })
1259}
1260
1261pub fn spawn_managed(opts: SpawnOptions) -> Result<ManagedProcess, CoreError> {
1264 if !opts.wait {
1265 return Err(CoreError::sys(
1266 libc::EINVAL,
1267 "managed process requires wait=true",
1268 ));
1269 }
1270 let timeout_at = opts
1271 .timeout_ms
1272 .map(|ms| Instant::now() + Duration::from_millis(ms as u64));
1273 let kill_grace = Duration::from_millis(opts.kill_grace_ms as u64);
1274 let cancel = opts.cancel;
1275 let pgroup = opts.pgroup;
1276 let running = spawn_start(opts)?;
1277 let pid = running.process.pid();
1278 Ok(ManagedProcess {
1279 running: Some(running),
1280 pid,
1281 timeout_at,
1282 kill_grace,
1283 cancel,
1284 pgroup,
1285 cancel_at: None,
1286 kill_state: KillState::None,
1287 status: None,
1288 timed_out: false,
1289 kill_sent_at: None,
1290 })
1291}
1292
1293pub fn spawn(opts: SpawnOptions) -> Result<Output, CoreError> {
1302 let wait = opts.wait;
1303 let timeout_ms = opts.timeout_ms;
1304 let kill_grace_ms = opts.kill_grace_ms;
1305 let cancel = opts.cancel;
1306 let pgroup = opts.pgroup;
1307
1308 let mut reactor = Reactor::new()?;
1309 let running = spawn_start(opts)?;
1310
1311 let pid = running.process.pid();
1312 let mut drain = running.drain;
1313
1314 drain.register_with_reactor(&mut reactor)?;
1315
1316 if !wait {
1317 let (stdout, stderr) = drain.into_parts();
1318 orphan_child(pid);
1321 return Ok(Output {
1322 pid,
1323 status: None,
1324 stdout,
1325 stderr,
1326 timed_out: false,
1327 stdout_early_exited: false,
1328 });
1329 }
1330
1331 wait_loop(
1332 running.process,
1333 drain,
1334 reactor,
1335 timeout_ms,
1336 kill_grace_ms,
1337 cancel,
1338 pgroup,
1339 )
1340}
1341
1342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1343enum KillState {
1344 None,
1345 TermSent,
1346 KillSent,
1347}
1348
1349fn wait_loop(
1350 process: Process,
1351 mut drain: crate::io::DrainState<fn(&[u8]) -> bool>,
1352 mut reactor: Reactor,
1353 timeout_ms: Option<u32>,
1354 kill_grace_ms: u32,
1355 cancel: CancelPolicy,
1356 pgroup: ProcessGroup,
1357) -> Result<Output, CoreError> {
1358 let pid = process.pid();
1359 let pgid = effective_pgid(pid, pgroup);
1364 let mut status_raw = process.wait_step()?;
1365 let mut state = KillState::None;
1366 let mut timed_out = false;
1367 let mut kill_sent_at: Option<Instant> = None;
1371 let mut deadline_passed_at: Option<Instant> = None;
1377
1378 let start_time = std::time::Instant::now();
1379 let deadline = timeout_ms.map(|t| std::time::Duration::from_millis(t as u64));
1380
1381 loop {
1382 let mut poll_timeout = -1;
1383
1384 if let Some(dl) = deadline {
1385 let elapsed = start_time.elapsed();
1386 if elapsed >= dl {
1387 timed_out = true;
1388 deadline_passed_at = Some(deadline_passed_at.unwrap_or_else(Instant::now));
1389 let elapsed_over = (elapsed - dl).as_millis();
1390
1391 let target_is_group = pgroup.isolated || pgroup.leader.is_some();
1392
1393 if status_raw.is_none() {
1398 match state {
1399 KillState::None => {
1400 if cancel == CancelPolicy::Graceful {
1401 let r = if target_is_group {
1402 process.kill_group(pgid, libc::SIGTERM)
1403 } else {
1404 process.kill(libc::SIGTERM)
1405 };
1406 if r.is_err() {
1407 state = KillState::KillSent; kill_sent_at = Some(Instant::now());
1409 } else {
1410 state = KillState::TermSent;
1411 }
1412 } else if cancel == CancelPolicy::Kill {
1413 let _ = if target_is_group {
1414 process.kill_group(pgid, libc::SIGKILL)
1415 } else {
1416 process.kill(libc::SIGKILL)
1417 };
1418 state = KillState::KillSent;
1419 kill_sent_at = Some(Instant::now());
1420 } else {
1421 }
1423 }
1424 KillState::TermSent if elapsed_over > kill_grace_ms as u128 => {
1425 let _ = if target_is_group {
1426 process.kill_group(pgid, libc::SIGKILL)
1427 } else {
1428 process.kill(libc::SIGKILL)
1429 };
1430 state = KillState::KillSent;
1431 kill_sent_at = Some(Instant::now());
1432 }
1433 _ => {}
1434 }
1435 }
1436 poll_timeout = 100; } else {
1438 let remaining = dl - elapsed;
1439 poll_timeout = remaining.as_millis().min(i32::MAX as u128) as i32;
1440 }
1441 }
1442
1443 if status_raw.is_none()
1444 && let Some(s) = process.wait_step()?
1445 {
1446 status_raw = Some(s);
1447 }
1448
1449 if drain.is_done() {
1450 let s = if status_raw.is_some() {
1451 status_raw.take()
1452 } else if deadline.is_none() {
1453 Some(process.wait_blocking()?)
1456 } else {
1457 None
1462 };
1463
1464 if let Some(s) = s {
1465 for slot in drain.take_all_slots() {
1466 reactor.del(&slot.fd)?;
1467 }
1468 let (stdout, stderr, output_limit_exceeded, stdout_early_exited) =
1469 drain.into_parts_with_state();
1470 if output_limit_exceeded {
1471 return Err(CoreError::sys(libc::EOVERFLOW, "spawn output limit"));
1472 }
1473 return Ok(Output {
1474 pid,
1475 status: Some(s),
1476 stdout,
1477 stderr,
1478 timed_out,
1479 stdout_early_exited,
1480 });
1481 }
1482 }
1483
1484 if timed_out && status_raw.is_some() {
1489 for slot in drain.take_all_slots() {
1490 let _ = reactor.del(&slot.fd);
1491 }
1492 let (stdout, stderr, _output_limit_exceeded, stdout_early_exited) =
1493 drain.into_parts_with_state();
1494 return Ok(Output {
1495 pid,
1496 status: status_raw,
1497 stdout,
1498 stderr,
1499 timed_out: true,
1500 stdout_early_exited,
1501 });
1502 }
1503
1504 if let Some(sent_at) = kill_sent_at
1510 && sent_at.elapsed() >= D_STATE_REAP_BOUND
1511 && status_raw.is_none()
1512 {
1513 for slot in drain.take_all_slots() {
1514 let _ = reactor.del(&slot.fd);
1515 }
1516 orphan_child(pid);
1520 let (stdout, stderr, _output_limit_exceeded, stdout_early_exited) =
1521 drain.into_parts_with_state();
1522 return Ok(Output {
1523 pid,
1524 status: None,
1525 stdout,
1526 stderr,
1527 timed_out: true,
1528 stdout_early_exited,
1529 });
1530 }
1531
1532 if cancel == CancelPolicy::None
1538 && timed_out
1539 && status_raw.is_none()
1540 && deadline_passed_at.is_some_and(|passed| passed.elapsed() >= D_STATE_REAP_BOUND)
1541 {
1542 for slot in drain.take_all_slots() {
1543 let _ = reactor.del(&slot.fd);
1544 }
1545 orphan_child(pid);
1548 let (stdout, stderr, _output_limit_exceeded, stdout_early_exited) =
1549 drain.into_parts_with_state();
1550 return Ok(Output {
1551 pid,
1552 status: None,
1553 stdout,
1554 stderr,
1555 timed_out: true,
1556 stdout_early_exited,
1557 });
1558 }
1559
1560 let timeout = poll_timeout;
1561
1562 let mut events = Vec::new();
1563 let nevents = reactor.wait(&mut events, 64, timeout)?;
1564
1565 for ev in events.iter().take(nevents) {
1566 if drain.stdout_matches(ev.token) {
1567 if ev.readable || ev.hangup {
1568 drain.handle_stdout_ready(&mut reactor)?;
1569 } else if ev.error {
1570 drain.drop_stdout(&mut reactor)?;
1571 }
1572 } else if drain.stderr_matches(ev.token) {
1573 if ev.readable || ev.hangup {
1574 drain.handle_stderr_ready(&mut reactor)?;
1575 } else if ev.error {
1576 drain.drop_stderr(&mut reactor)?;
1577 }
1578 } else if drain.stdin_matches(ev.token) {
1579 if ev.writable {
1580 drain.handle_stdin_writable(&mut reactor)?;
1581 } else if ev.error || ev.hangup {
1582 drain.drop_stdin(&mut reactor)?;
1583 }
1584 }
1585 }
1586 }
1587}