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 exec;
26mod fork;
27mod posix;
28
29use exec::ExecContext;
30use fork::spawn_fork_internal;
31use posix::spawn_posix_internal;
32
33unsafe extern "C" {
34 pub(crate) static mut environ: *mut *mut libc::c_char;
35}
36
37const D_STATE_REAP_BOUND: Duration = Duration::from_millis(500);
43
44static ORPHANED: OnceLock<Mutex<HashSet<pid_t>>> = OnceLock::new();
55static REAPER_STARTED: AtomicBool = AtomicBool::new(false);
56
57fn orphan_child(pid: pid_t) {
60 ORPHANED
61 .get_or_init(|| Mutex::new(HashSet::new()))
62 .lock()
63 .unwrap()
64 .insert(pid);
65 start_reaper();
66}
67
68fn start_reaper() {
70 if REAPER_STARTED.load(Ordering::SeqCst) {
71 return;
72 }
73 let r = REAPER_STARTED.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst);
74 if r.is_err() {
75 return;
76 }
77 std::thread::Builder::new()
78 .name("spawn-orphan-reaper".into())
79 .spawn(reap_orphaned)
80 .ok();
81}
82
83fn reap_orphaned() {
87 loop {
88 let pids: Vec<pid_t> = ORPHANED
89 .get_or_init(|| Mutex::new(HashSet::new()))
90 .lock()
91 .unwrap()
92 .iter()
93 .copied()
94 .collect();
95 let mut still_orphaned = Vec::new();
96 for pid in pids {
97 let mut status: libc::c_int = 0;
98 let r = unsafe { waitpid(pid, &mut status, libc::WNOHANG) };
99 if r == pid
100 || (r < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ECHILD))
101 {
102 continue; }
104 still_orphaned.push(pid);
105 }
106 if !still_orphaned.is_empty() {
107 if let Some(set) = ORPHANED.get() {
108 if let Ok(mut guard) = set.lock() {
109 for pid in still_orphaned {
110 guard.insert(pid);
111 }
112 }
113 }
114 }
115 std::thread::sleep(Duration::from_millis(250));
116 }
117}
118
119#[allow(dead_code)]
123fn deorphan_child(pid: pid_t) {
124 if let Some(set) = ORPHANED.get() {
125 if let Ok(mut guard) = set.lock() {
126 guard.remove(&pid);
127 }
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum CancelPolicy {
134 #[default]
136 None,
137 Graceful,
139 Kill,
141}
142
143#[derive(Debug, Clone, Copy, Default)]
145pub struct ProcessGroup {
146 pub leader: Option<pid_t>,
148 pub isolated: bool,
150}
151
152impl ProcessGroup {
153 pub fn new(leader: Option<pid_t>, isolated: bool) -> Self {
155 Self { leader, isolated }
156 }
157}
158
159#[inline(always)]
160fn errno() -> i32 {
161 std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
162}
163
164fn relocate_above_stdio(fd: RawFd, op: &'static str) -> Result<RawFd, CoreError> {
170 if fd >= 3 {
171 return Ok(fd);
172 }
173 let new = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) };
174 syscall_ret(new, op)?;
175 unsafe {
176 libc::close(fd);
177 }
178 Ok(new)
179}
180
181#[inline(always)]
187fn make_pipe() -> Result<(Fd, Fd), CoreError> {
188 let mut fds = [0; 2];
189 let r = unsafe { pipe2(fds.as_mut_ptr(), O_CLOEXEC) };
190 syscall_ret(r, "pipe2")?;
191 let r0 = match relocate_above_stdio(fds[0], "pipe2:relocate") {
192 Ok(fd) => fd,
193 Err(e) => {
194 unsafe {
197 libc::close(fds[0]);
198 }
199 return Err(e);
200 }
201 };
202 let r1 = match relocate_above_stdio(fds[1], "pipe2:relocate") {
203 Ok(fd) => fd,
204 Err(e) => {
205 unsafe {
208 libc::close(r0);
209 libc::close(fds[1]);
210 }
211 return Err(e);
212 }
213 };
214 Ok((Fd::new(r0, "pipe2")?, Fd::new(r1, "pipe2")?))
215}
216
217fn make_cloexec_pipe() -> Result<(RawFd, RawFd), 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 {
225 libc::close(fds[0]);
226 }
227 return Err(e);
228 }
229 };
230 let r1 = match relocate_above_stdio(fds[1], "pipe2:relocate") {
231 Ok(fd) => fd,
232 Err(e) => {
233 unsafe {
234 libc::close(r0);
235 libc::close(fds[1]);
236 }
237 return Err(e);
238 }
239 };
240 Ok((r0, r1))
241}
242
243struct Pipes {
244 stdin_r: Option<Fd>,
245 stdin_w: Option<Fd>,
246 stdout_r: Option<Fd>,
247 stdout_w: Option<Fd>,
248 stderr_r: Option<Fd>,
249 stderr_w: Option<Fd>,
250}
251
252impl Pipes {
253 fn new(in_buf: Option<&[u8]>, out: bool, err: bool) -> Result<Self, CoreError> {
254 let (stdin_r, stdin_w) = if in_buf.is_some() {
255 let (r, w) = make_pipe()?;
256 (Some(r), Some(w))
257 } else {
258 (None, None)
259 };
260
261 let (stdout_r, stdout_w) = if out {
262 let (r, w) = make_pipe()?;
263 (Some(r), Some(w))
264 } else {
265 (None, None)
266 };
267
268 let (stderr_r, stderr_w) = if err {
269 let (r, w) = make_pipe()?;
270 (Some(r), Some(w))
271 } else {
272 (None, None)
273 };
274
275 Ok(Self {
276 stdin_r,
277 stdin_w,
278 stdout_r,
279 stdout_w,
280 stderr_r,
281 stderr_w,
282 })
283 }
284
285 #[inline(always)]
286 fn close_all(&mut self) {
287 self.stdin_r.take();
288 self.stdin_w.take();
289 self.stdout_r.take();
290 self.stdout_w.take();
291 self.stderr_r.take();
292 self.stderr_w.take();
293 }
294}
295
296#[derive(Debug, PartialEq, Eq)]
298pub enum ExitStatus {
299 Exited(i32),
301 Signaled(i32),
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub enum SpawnBackend {
308 PosixSpawn,
310 Fork,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq, Default)]
319pub enum SpawnFdPolicy {
320 #[default]
322 CloexecOnly,
323 CloseFrom3,
326 Allowlist(Vec<RawFd>),
333}
334
335#[inline(always)]
336fn decode_status(status: i32) -> ExitStatus {
337 if WIFEXITED(status) {
338 ExitStatus::Exited(WEXITSTATUS(status))
339 } else if WIFSIGNALED(status) {
340 ExitStatus::Signaled(WTERMSIG(status))
341 } else {
342 ExitStatus::Exited(-1)
343 }
344}
345
346pub struct Process {
354 pid: pid_t,
355}
356
357impl Process {
358 pub fn new(pid: pid_t) -> Self {
360 Self { pid }
361 }
362
363 pub fn pid(&self) -> pid_t {
365 self.pid
366 }
367
368 pub fn wait_step(&self) -> Result<Option<ExitStatus>, CoreError> {
374 loop {
375 let mut status = 0;
376 let r = unsafe { waitpid(self.pid, &mut status, libc::WNOHANG) };
377 if r == 0 {
378 return Ok(None);
379 }
380 if r < 0 {
381 let e = errno();
382 if e == libc::EINTR {
383 continue;
384 }
385 return Err(CoreError::sys(e, "waitpid_step"));
386 }
387 return Ok(Some(decode_status(status)));
388 }
389 }
390
391 pub fn wait_blocking(&self) -> Result<ExitStatus, CoreError> {
396 loop {
397 let mut status = 0;
398 let r = unsafe { waitpid(self.pid, &mut status, 0) };
399 if r < 0 {
400 let e = errno();
401 if e == libc::EINTR {
402 continue;
403 }
404 return Err(CoreError::sys(e, "waitpid_blocking"));
405 }
406 return Ok(decode_status(status));
407 }
408 }
409
410 pub fn kill(&self, sig: i32) -> Result<(), CoreError> {
418 if self.pid <= 0 {
419 return Err(CoreError::sys(libc::EINVAL, "kill: invalid pid"));
420 }
421 let r = unsafe { libc::kill(self.pid, sig) };
422 if r < 0 {
423 let e = errno();
424 if e == libc::ESRCH {
425 return Ok(());
426 }
427 syscall_ret(-1, "kill")?;
428 }
429 Ok(())
430 }
431
432 pub fn kill_pgroup(&self, sig: i32) -> Result<(), CoreError> {
439 self.kill_group(self.pid, sig)
440 }
441
442 pub fn kill_group(&self, pgid: pid_t, sig: i32) -> Result<(), CoreError> {
452 if pgid <= 0 {
453 return Err(CoreError::sys(libc::EINVAL, "kill_group: invalid pgid"));
454 }
455 let r = unsafe { libc::kill(-pgid, sig) };
456 if r < 0 {
457 let e = errno();
458 if e == libc::ESRCH {
459 return Ok(());
460 }
461 syscall_ret(-1, "kill_group")?;
462 }
463 Ok(())
464 }
465}
466
467#[derive(Clone)]
469pub struct SpawnOptions {
470 ctx: ExecContext,
471 stdin: Option<Box<[u8]>>,
472 capture_stdout: bool,
473 capture_stderr: bool,
474 wait: bool,
475 pgroup: ProcessGroup,
476 max_output: usize,
477 timeout_ms: Option<u32>,
478 kill_grace_ms: u32,
479 cancel: CancelPolicy,
480 backend: SpawnBackend,
481 fd_policy: SpawnFdPolicy,
482 early_exit: Option<fn(&[u8]) -> bool>,
483}
484
485impl SpawnOptions {
486 pub fn builder(argv: Vec<String>, backend: SpawnBackend) -> SpawnOptionsBuilder {
488 SpawnOptionsBuilder::new(argv, backend)
489 }
490
491 pub fn run(self) -> Result<Output, CoreError> {
493 spawn(self)
494 }
495}
496
497#[derive(Clone)]
499pub struct SpawnOptionsBuilder {
500 argv: Vec<String>,
501 env: Option<Vec<String>>,
502 cwd: Option<String>,
503 stdin: Option<Box<[u8]>>,
504 capture_stdout: bool,
505 capture_stderr: bool,
506 wait: bool,
507 pgroup: ProcessGroup,
508 max_output: usize,
509 timeout_ms: Option<u32>,
510 kill_grace_ms: u32,
511 cancel: CancelPolicy,
512 backend: SpawnBackend,
513 fd_policy: SpawnFdPolicy,
514 early_exit: Option<fn(&[u8]) -> bool>,
515}
516
517impl SpawnOptionsBuilder {
518 pub fn new(argv: Vec<String>, backend: SpawnBackend) -> Self {
520 Self {
521 argv,
522 env: None,
523 cwd: None,
524 stdin: None,
525 capture_stdout: false,
526 capture_stderr: false,
527 wait: true,
528 pgroup: ProcessGroup::default(),
529 max_output: 1024 * 1024,
530 timeout_ms: None,
531 kill_grace_ms: 2000,
532 cancel: CancelPolicy::Kill,
533 backend,
534 fd_policy: SpawnFdPolicy::default(),
535 early_exit: None,
536 }
537 }
538
539 pub fn env(mut self, env: Vec<String>) -> Self {
541 self.env = Some(env);
542 self
543 }
544
545 pub fn cwd(mut self, cwd: String) -> Self {
547 self.cwd = Some(cwd);
548 self
549 }
550
551 pub fn stdin(mut self, data: impl Into<Box<[u8]>>) -> Self {
553 self.stdin = Some(data.into());
554 self
555 }
556
557 pub fn capture_stdout(mut self) -> Self {
559 self.capture_stdout = true;
560 self
561 }
562
563 pub fn capture_stderr(mut self) -> Self {
565 self.capture_stderr = true;
566 self
567 }
568
569 pub fn wait(mut self, wait: bool) -> Self {
571 self.wait = wait;
572 self
573 }
574
575 pub fn pgroup(mut self, pgroup: ProcessGroup) -> Self {
577 self.pgroup = pgroup;
578 self
579 }
580
581 pub fn max_output(mut self, max: usize) -> Self {
586 self.max_output = max;
587 self
588 }
589
590 pub fn timeout_ms(mut self, ms: u32) -> Self {
592 self.timeout_ms = Some(ms);
593 self
594 }
595
596 pub fn kill_grace_ms(mut self, ms: u32) -> Self {
598 self.kill_grace_ms = ms;
599 self
600 }
601
602 pub fn cancel(mut self, policy: CancelPolicy) -> Self {
604 self.cancel = policy;
605 self
606 }
607
608 pub fn fd_policy(mut self, policy: SpawnFdPolicy) -> Self {
610 self.fd_policy = policy;
611 self
612 }
613
614 pub fn early_exit(mut self, callback: fn(&[u8]) -> bool) -> Self {
616 self.early_exit = Some(callback);
617 self
618 }
619
620 pub fn build(self) -> Result<SpawnOptions, CoreError> {
622 let ctx = ExecContext::new(self.argv, self.env, self.cwd)?;
623 Ok(SpawnOptions {
624 ctx,
625 stdin: self.stdin,
626 capture_stdout: self.capture_stdout,
627 capture_stderr: self.capture_stderr,
628 wait: self.wait,
629 pgroup: self.pgroup,
630 max_output: self.max_output,
631 timeout_ms: self.timeout_ms,
632 kill_grace_ms: self.kill_grace_ms,
633 cancel: self.cancel,
634 backend: self.backend,
635 fd_policy: self.fd_policy,
636 early_exit: self.early_exit,
637 })
638 }
639}
640
641#[derive(Debug)]
643pub struct Output {
644 pub pid: pid_t,
646 pub status: Option<ExitStatus>,
648 pub stdout: Vec<u8>,
650 pub stderr: Vec<u8>,
652 pub timed_out: bool,
654 pub stdout_early_exited: bool,
656}
657
658fn validate_backend(opts: &SpawnOptions) -> Result<(), CoreError> {
659 validate_fd_policy(&opts.fd_policy)?;
660 match opts.backend {
661 SpawnBackend::PosixSpawn => {
662 if opts.ctx.cwd.is_some() {
663 return Err(CoreError::sys(libc::EINVAL, "posix_spawn cwd unsupported"));
664 }
665 if opts.pgroup.isolated {
666 return Err(CoreError::sys(
667 libc::EINVAL,
668 "posix_spawn setsid unsupported",
669 ));
670 }
671 if opts.fd_policy != SpawnFdPolicy::CloexecOnly {
672 return Err(CoreError::sys(
673 libc::EINVAL,
674 "posix_spawn fd policy unsupported",
675 ));
676 }
677 Ok(())
678 }
679 SpawnBackend::Fork => {
680 if opts.pgroup.isolated && opts.pgroup.leader.is_some_and(|l| l != 0) {
685 return Err(CoreError::sys(
686 libc::EINVAL,
687 "fork isolated + custom setpgid leader unsupported",
688 ));
689 }
690 Ok(())
691 }
692 }
693}
694
695fn validate_fd_policy(policy: &SpawnFdPolicy) -> Result<(), CoreError> {
696 if let SpawnFdPolicy::Allowlist(fds) = policy {
697 let mut seen = Vec::with_capacity(fds.len());
698 for &fd in fds {
699 if fd < 0 {
700 return Err(CoreError::sys(libc::EINVAL, "spawn fd allowlist invalid"));
701 }
702 let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
703 if flags < 0 {
704 return Err(CoreError::sys(errno(), "spawn fd allowlist fcntl(F_GETFD)"));
705 }
706 if seen.contains(&fd) {
707 return Err(CoreError::sys(libc::EINVAL, "spawn fd allowlist duplicate"));
708 }
709 seen.push(fd);
710 }
711 }
712 Ok(())
713}
714
715pub type SpawnDrain = DrainState<fn(&[u8]) -> bool>;
717
718pub struct RunningProcess {
725 pub process: Process,
727 drain: SpawnDrain,
728}
729
730pub struct ManagedProcess {
738 running: Option<RunningProcess>,
739 pid: pid_t,
740 timeout_at: Option<Instant>,
741 kill_grace: Duration,
742 cancel: CancelPolicy,
743 pgroup: ProcessGroup,
744 cancel_at: Option<Instant>,
745 kill_state: KillState,
746 status: Option<ExitStatus>,
747 timed_out: bool,
748 kill_sent_at: Option<Instant>,
749}
750
751impl RunningProcess {
752 pub fn register_with_reactor(&mut self, reactor: &mut Reactor) -> Result<(), CoreError> {
758 self.drain.register_with_reactor(reactor)
759 }
760
761 pub fn handle_reactor_event(
767 &mut self,
768 reactor: &mut Reactor,
769 event: &crate::fd::Event,
770 ) -> Result<(), CoreError> {
771 if self.drain.stdout_matches(event.token) {
772 if event.readable || event.hangup {
773 self.drain.handle_stdout_ready(reactor)?;
774 } else if event.error {
775 self.drain.drop_stdout(reactor)?;
776 }
777 } else if self.drain.stderr_matches(event.token) {
778 if event.readable || event.hangup {
779 self.drain.handle_stderr_ready(reactor)?;
780 } else if event.error {
781 self.drain.drop_stderr(reactor)?;
782 }
783 } else if self.drain.stdin_matches(event.token) {
784 if event.writable {
785 self.drain.handle_stdin_writable(reactor)?;
786 } else if event.error || event.hangup {
787 self.drain.drop_stdin(reactor)?;
788 }
789 }
790 Ok(())
791 }
792
793 pub fn io_done(&self) -> bool {
795 self.drain.is_done()
796 }
797
798 pub fn into_output_parts(self) -> (Vec<u8>, Vec<u8>) {
800 self.drain.into_parts()
801 }
802}
803
804impl ManagedProcess {
805 pub fn pid(&self) -> pid_t {
810 self.pid
811 }
812
813 pub fn register_with_reactor(&mut self, reactor: &mut Reactor) -> Result<(), CoreError> {
815 self.running
816 .as_mut()
817 .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?
818 .register_with_reactor(reactor)
819 }
820
821 pub fn handle_reactor_event(
823 &mut self,
824 reactor: &mut Reactor,
825 event: &crate::fd::Event,
826 ) -> Result<(), CoreError> {
827 self.running
828 .as_mut()
829 .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?
830 .handle_reactor_event(reactor, event)
831 }
832
833 pub fn request_cancel(&mut self) {
836 self.cancel_at.get_or_insert_with(Instant::now);
837 }
838
839 pub fn next_deadline(&self) -> Option<Instant> {
845 self.running.as_ref()?;
846 let now = Instant::now();
847 let mut next = now + Duration::from_millis(100);
848 if !self.timed_out
849 && let Some(timeout_at) = self.timeout_at
850 && timeout_at < next
851 {
852 next = timeout_at;
853 }
854 if self.kill_state == KillState::TermSent
855 && let Some(cancel_at) = self.cancel_at
856 {
857 let kill_at = cancel_at + self.kill_grace;
858 if kill_at < next {
859 next = kill_at;
860 }
861 }
862 if let Some(sent_at) = self.kill_sent_at {
865 let bail_at = sent_at + D_STATE_REAP_BOUND;
866 if bail_at < next {
867 next = bail_at;
868 }
869 }
870 Some(next)
871 }
872
873 pub fn poll_completion(&mut self, reactor: &mut Reactor) -> Result<Option<Output>, CoreError> {
882 let now = Instant::now();
883 if !self.timed_out
884 && let Some(timeout_at) = self.timeout_at
885 && now >= timeout_at
886 {
887 self.timed_out = true;
888 self.cancel_at.get_or_insert(timeout_at);
889 }
890
891 self.advance_cancel(now)?;
892
893 let running = self
894 .running
895 .as_ref()
896 .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
897 if self.status.is_none() {
898 self.status = running.process.wait_step()?;
899 }
900
901 let io_done = running.io_done();
902 if self.status.is_some() && (io_done || self.cancel_at.is_some()) {
903 return self.finish(reactor, !io_done).map(Some);
904 }
905 if self.status.is_none()
909 && self
910 .kill_sent_at
911 .is_some_and(|t| now.duration_since(t) >= D_STATE_REAP_BOUND)
912 {
913 return self.finish(reactor, true).map(Some);
914 }
915 Ok(None)
916 }
917
918 fn advance_cancel(&mut self, now: Instant) -> Result<(), CoreError> {
919 let Some(cancel_at) = self.cancel_at else {
920 return Ok(());
921 };
922 if self.status.is_some() {
924 return Ok(());
925 }
926 let running = self
927 .running
928 .as_ref()
929 .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
930 let process = &running.process;
931 let pid = process.pid();
932 let pgid = effective_pgid(pid, self.pgroup);
933 let target_is_group = self.pgroup.isolated || self.pgroup.leader.is_some();
934 match self.kill_state {
935 KillState::None => match self.cancel {
936 CancelPolicy::None => {}
937 CancelPolicy::Graceful => {
938 let result = signal_process(process, target_is_group, pgid, libc::SIGTERM);
939 self.kill_state = if result.is_ok() {
940 KillState::TermSent
941 } else {
942 KillState::KillSent
943 };
944 if self.kill_state == KillState::KillSent {
945 self.kill_sent_at = Some(now);
946 }
947 }
948 CancelPolicy::Kill => {
949 let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
950 self.kill_state = KillState::KillSent;
951 self.kill_sent_at = Some(now);
952 }
953 },
954 KillState::TermSent if now >= cancel_at + self.kill_grace => {
955 let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
956 self.kill_state = KillState::KillSent;
957 self.kill_sent_at = Some(now);
958 }
959 _ => {}
960 }
961 Ok(())
962 }
963
964 fn finish(&mut self, reactor: &mut Reactor, force_close: bool) -> Result<Output, CoreError> {
965 let mut running = self
966 .running
967 .take()
968 .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
969 for slot in running.drain.take_all_slots() {
970 if force_close {
971 let _ = reactor.del(&slot.fd);
972 } else {
973 reactor.del(&slot.fd)?;
974 }
975 }
976 let pid = running.process.pid();
977 let (stdout, stderr, output_limit_exceeded, stdout_early_exited) =
978 running.drain.into_parts_with_state();
979 if self.status.is_none() {
984 orphan_child(pid);
985 }
986 if output_limit_exceeded && !force_close {
991 return Err(CoreError::sys(libc::EOVERFLOW, "spawn output limit"));
992 }
993 Ok(Output {
994 pid,
995 status: self.status.take(),
996 stdout,
997 stderr,
998 timed_out: self.timed_out,
999 stdout_early_exited,
1000 })
1001 }
1002}
1003
1004impl Drop for ManagedProcess {
1005 fn drop(&mut self) {
1006 let Some(running) = self.running.take() else {
1007 return;
1008 };
1009 if self.status.is_some() {
1013 return;
1014 }
1015 let process = &running.process;
1016 let pid = process.pid();
1017 let pgid = effective_pgid(pid, self.pgroup);
1018 let target_is_group = self.pgroup.isolated || self.pgroup.leader.is_some();
1019 let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
1020 let deadline = Instant::now() + Duration::from_millis(100);
1025 while Instant::now() < deadline {
1026 match process.wait_step() {
1027 Ok(Some(_)) => return,
1028 Ok(None) => std::thread::sleep(Duration::from_millis(5)),
1029 Err(_) => return,
1030 }
1031 }
1032 }
1033}
1034
1035fn effective_pgid(pid: pid_t, pgroup: ProcessGroup) -> pid_t {
1036 match pgroup.leader {
1037 Some(0) | None => pid,
1038 Some(leader) => leader,
1039 }
1040}
1041
1042fn signal_process(
1043 process: &Process,
1044 target_is_group: bool,
1045 pgid: pid_t,
1046 signal: i32,
1047) -> Result<(), CoreError> {
1048 if target_is_group {
1049 process.kill_group(pgid, signal)
1050 } else {
1051 process.kill(signal)
1052 }
1053}
1054
1055pub fn spawn_start(opts: SpawnOptions) -> Result<RunningProcess, CoreError> {
1070 if !opts.wait && (opts.stdin.is_some() || opts.capture_stdout || opts.capture_stderr) {
1071 return Err(CoreError::sys(
1072 libc::EINVAL,
1073 "background I/O capture not supported (wait must be true)",
1074 ));
1075 }
1076
1077 validate_backend(&opts)?;
1078
1079 let (pid, drain) = match opts.backend {
1080 SpawnBackend::PosixSpawn => spawn_posix_internal(opts)?,
1081 SpawnBackend::Fork => spawn_fork_internal(opts)?,
1082 };
1083
1084 Ok(RunningProcess {
1085 process: Process::new(pid),
1086 drain,
1087 })
1088}
1089
1090pub fn spawn_managed(opts: SpawnOptions) -> Result<ManagedProcess, CoreError> {
1093 if !opts.wait {
1094 return Err(CoreError::sys(
1095 libc::EINVAL,
1096 "managed process requires wait=true",
1097 ));
1098 }
1099 let timeout_at = opts
1100 .timeout_ms
1101 .map(|ms| Instant::now() + Duration::from_millis(ms as u64));
1102 let kill_grace = Duration::from_millis(opts.kill_grace_ms as u64);
1103 let cancel = opts.cancel;
1104 let pgroup = opts.pgroup;
1105 let running = spawn_start(opts)?;
1106 let pid = running.process.pid();
1107 Ok(ManagedProcess {
1108 running: Some(running),
1109 pid,
1110 timeout_at,
1111 kill_grace,
1112 cancel,
1113 pgroup,
1114 cancel_at: None,
1115 kill_state: KillState::None,
1116 status: None,
1117 timed_out: false,
1118 kill_sent_at: None,
1119 })
1120}
1121
1122pub fn spawn(opts: SpawnOptions) -> Result<Output, CoreError> {
1131 let wait = opts.wait;
1132 let timeout_ms = opts.timeout_ms;
1133 let kill_grace_ms = opts.kill_grace_ms;
1134 let cancel = opts.cancel;
1135 let pgroup = opts.pgroup;
1136
1137 let mut reactor = Reactor::new()?;
1138 let running = spawn_start(opts)?;
1139
1140 let pid = running.process.pid();
1141 let mut drain = running.drain;
1142
1143 drain.register_with_reactor(&mut reactor)?;
1144
1145 if !wait {
1146 let (stdout, stderr) = drain.into_parts();
1147 orphan_child(pid);
1150 return Ok(Output {
1151 pid,
1152 status: None,
1153 stdout,
1154 stderr,
1155 timed_out: false,
1156 stdout_early_exited: false,
1157 });
1158 }
1159
1160 wait_loop(
1161 pid,
1162 drain,
1163 reactor,
1164 timeout_ms,
1165 kill_grace_ms,
1166 cancel,
1167 pgroup,
1168 )
1169}
1170
1171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1172enum KillState {
1173 None,
1174 TermSent,
1175 KillSent,
1176}
1177
1178fn wait_loop(
1179 pid: pid_t,
1180 mut drain: crate::io::DrainState<fn(&[u8]) -> bool>,
1181 mut reactor: Reactor,
1182 timeout_ms: Option<u32>,
1183 kill_grace_ms: u32,
1184 cancel: CancelPolicy,
1185 pgroup: ProcessGroup,
1186) -> Result<Output, CoreError> {
1187 let process = Process::new(pid);
1188 let pgid = effective_pgid(pid, pgroup);
1193 let mut status_raw = process.wait_step()?;
1194 let mut state = KillState::None;
1195 let mut timed_out = false;
1196 let mut kill_sent_at: Option<Instant> = None;
1200 let mut deadline_passed_at: Option<Instant> = None;
1206
1207 let start_time = std::time::Instant::now();
1208 let deadline = timeout_ms.map(|t| std::time::Duration::from_millis(t as u64));
1209
1210 loop {
1211 let mut poll_timeout = -1;
1212
1213 if let Some(dl) = deadline {
1214 let elapsed = start_time.elapsed();
1215 if elapsed >= dl {
1216 timed_out = true;
1217 deadline_passed_at = Some(deadline_passed_at.unwrap_or_else(Instant::now));
1218 let elapsed_over = (elapsed - dl).as_millis();
1219
1220 let target_is_group = pgroup.isolated || pgroup.leader.is_some();
1221
1222 if status_raw.is_none() {
1227 match state {
1228 KillState::None => {
1229 if cancel == CancelPolicy::Graceful {
1230 let r = if target_is_group {
1231 process.kill_group(pgid, libc::SIGTERM)
1232 } else {
1233 process.kill(libc::SIGTERM)
1234 };
1235 if r.is_err() {
1236 state = KillState::KillSent; kill_sent_at = Some(Instant::now());
1238 } else {
1239 state = KillState::TermSent;
1240 }
1241 } else if cancel == CancelPolicy::Kill {
1242 let _ = if target_is_group {
1243 process.kill_group(pgid, libc::SIGKILL)
1244 } else {
1245 process.kill(libc::SIGKILL)
1246 };
1247 state = KillState::KillSent;
1248 kill_sent_at = Some(Instant::now());
1249 } else {
1250 }
1252 }
1253 KillState::TermSent if elapsed_over > kill_grace_ms as u128 => {
1254 let _ = if target_is_group {
1255 process.kill_group(pgid, libc::SIGKILL)
1256 } else {
1257 process.kill(libc::SIGKILL)
1258 };
1259 state = KillState::KillSent;
1260 kill_sent_at = Some(Instant::now());
1261 }
1262 _ => {}
1263 }
1264 }
1265 poll_timeout = 100; } else {
1267 let remaining = dl - elapsed;
1268 poll_timeout = remaining.as_millis().min(i32::MAX as u128) as i32;
1269 }
1270 }
1271
1272 if status_raw.is_none()
1273 && let Some(s) = process.wait_step()?
1274 {
1275 status_raw = Some(s);
1276 }
1277
1278 if drain.is_done() {
1279 let s = if status_raw.is_some() {
1280 status_raw.take()
1281 } else if deadline.is_none() {
1282 Some(process.wait_blocking()?)
1285 } else {
1286 None
1291 };
1292
1293 if let Some(s) = s {
1294 for slot in drain.take_all_slots() {
1295 reactor.del(&slot.fd)?;
1296 }
1297 let (stdout, stderr, output_limit_exceeded, stdout_early_exited) =
1298 drain.into_parts_with_state();
1299 if output_limit_exceeded {
1300 return Err(CoreError::sys(libc::EOVERFLOW, "spawn output limit"));
1301 }
1302 return Ok(Output {
1303 pid,
1304 status: Some(s),
1305 stdout,
1306 stderr,
1307 timed_out,
1308 stdout_early_exited,
1309 });
1310 }
1311 }
1312
1313 if timed_out && status_raw.is_some() {
1318 for slot in drain.take_all_slots() {
1319 let _ = reactor.del(&slot.fd);
1320 }
1321 let (stdout, stderr, _output_limit_exceeded, stdout_early_exited) =
1322 drain.into_parts_with_state();
1323 return Ok(Output {
1324 pid,
1325 status: status_raw,
1326 stdout,
1327 stderr,
1328 timed_out: true,
1329 stdout_early_exited,
1330 });
1331 }
1332
1333 if let Some(sent_at) = kill_sent_at
1339 && sent_at.elapsed() >= D_STATE_REAP_BOUND
1340 && status_raw.is_none()
1341 {
1342 for slot in drain.take_all_slots() {
1343 let _ = reactor.del(&slot.fd);
1344 }
1345 orphan_child(pid);
1349 let (stdout, stderr, _output_limit_exceeded, stdout_early_exited) =
1350 drain.into_parts_with_state();
1351 return Ok(Output {
1352 pid,
1353 status: None,
1354 stdout,
1355 stderr,
1356 timed_out: true,
1357 stdout_early_exited,
1358 });
1359 }
1360
1361 if cancel == CancelPolicy::None
1367 && timed_out
1368 && status_raw.is_none()
1369 && deadline_passed_at.is_some_and(|passed| passed.elapsed() >= D_STATE_REAP_BOUND)
1370 {
1371 for slot in drain.take_all_slots() {
1372 let _ = reactor.del(&slot.fd);
1373 }
1374 orphan_child(pid);
1377 let (stdout, stderr, _output_limit_exceeded, stdout_early_exited) =
1378 drain.into_parts_with_state();
1379 return Ok(Output {
1380 pid,
1381 status: None,
1382 stdout,
1383 stderr,
1384 timed_out: true,
1385 stdout_early_exited,
1386 });
1387 }
1388
1389 let timeout = poll_timeout;
1390
1391 let mut events = Vec::new();
1392 let nevents = reactor.wait(&mut events, 64, timeout)?;
1393
1394 for ev in events.iter().take(nevents) {
1395 if drain.stdout_matches(ev.token) {
1396 if ev.readable || ev.hangup {
1397 drain.handle_stdout_ready(&mut reactor)?;
1398 } else if ev.error {
1399 drain.drop_stdout(&mut reactor)?;
1400 }
1401 } else if drain.stderr_matches(ev.token) {
1402 if ev.readable || ev.hangup {
1403 drain.handle_stderr_ready(&mut reactor)?;
1404 } else if ev.error {
1405 drain.drop_stderr(&mut reactor)?;
1406 }
1407 } else if drain.stdin_matches(ev.token) {
1408 if ev.writable {
1409 drain.handle_stdin_writable(&mut reactor)?;
1410 } else if ev.error || ev.hangup {
1411 drain.drop_stdin(&mut reactor)?;
1412 }
1413 }
1414 }
1415 }
1416}