1use crate::user::ProcessIdentity;
2use std::collections::{BTreeMap, BTreeSet, VecDeque};
3use std::error::Error;
4use std::fmt;
5use std::ops::{BitOr, BitOrAssign};
6use std::sync::{Arc, Condvar, Mutex, MutexGuard};
7use std::time::Duration;
8use web_time::{Instant, SystemTime, UNIX_EPOCH};
9
10const ZOMBIE_TTL: Duration = Duration::from_secs(60);
11const INIT_PID: u32 = 1;
12const MAX_ALLOCATED_PID: u32 = i32::MAX as u32;
13pub const DEFAULT_PROCESS_UMASK: u32 = 0o022;
14pub const SIGHUP: i32 = 1;
15pub const SIGCHLD: i32 = 17;
16pub const SIGCONT: i32 = 18;
17pub const SIGSTOP: i32 = 19;
18pub const SIGTSTP: i32 = 20;
19pub const SIGTERM: i32 = 15;
20pub const SIGKILL: i32 = 9;
21pub const SIGPIPE: i32 = 13;
22pub const SIGWINCH: i32 = 28;
23const MAX_SIGNAL: i32 = 64;
24
25pub type ProcessResult<T> = Result<T, ProcessTableError>;
26pub type ProcessExitCallback = Arc<dyn Fn(i32) + Send + Sync + 'static>;
27
28pub trait DriverProcess: Send + Sync {
29 fn kill(&self, signal: i32);
30 fn wait(&self, timeout: Duration) -> Option<i32>;
31 fn set_on_exit(&self, callback: ProcessExitCallback);
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ProcessTableError {
36 code: &'static str,
37 message: String,
38}
39
40impl ProcessTableError {
41 pub fn code(&self) -> &'static str {
42 self.code
43 }
44
45 fn invalid_signal(signal: i32) -> Self {
46 Self {
47 code: "EINVAL",
48 message: format!("invalid signal {signal}"),
49 }
50 }
51
52 fn no_such_process(pid: u32) -> Self {
53 Self {
54 code: "ESRCH",
55 message: format!("no such process {pid}"),
56 }
57 }
58
59 fn no_such_process_group(pgid: u32) -> Self {
60 Self {
61 code: "ESRCH",
62 message: format!("no such process group {pgid}"),
63 }
64 }
65
66 fn no_matching_child(waiter_pid: u32, pid: i32) -> Self {
67 Self {
68 code: "ECHILD",
69 message: format!("process {waiter_pid} has no matching child for waitpid({pid})"),
70 }
71 }
72
73 fn pid_space_exhausted() -> Self {
74 Self {
75 code: "EAGAIN",
76 message: String::from("process id space exhausted"),
77 }
78 }
79
80 fn permission_denied(message: impl Into<String>) -> Self {
81 Self {
82 code: "EPERM",
83 message: message.into(),
84 }
85 }
86}
87
88impl fmt::Display for ProcessTableError {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 write!(f, "{}: {}", self.code, self.message)
91 }
92}
93
94impl Error for ProcessTableError {}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum ProcessStatus {
98 Running,
99 Stopped,
100 Exited,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
104pub struct SignalSet {
105 bits: u64,
106}
107
108impl SignalSet {
109 pub const fn empty() -> Self {
110 Self { bits: 0 }
111 }
112
113 pub const fn is_empty(self) -> bool {
114 self.bits == 0
115 }
116
117 pub fn from_signal(signal: i32) -> ProcessResult<Self> {
118 Ok(Self {
119 bits: signal_bit(signal)?,
120 })
121 }
122
123 pub fn from_signals(signals: impl IntoIterator<Item = i32>) -> ProcessResult<Self> {
124 let mut set = Self::empty();
125 for signal in signals {
126 set.insert(signal)?;
127 }
128 Ok(set)
129 }
130
131 pub fn contains(self, signal: i32) -> bool {
132 signal_bit(signal)
133 .map(|bit| self.bits & bit != 0)
134 .unwrap_or(false)
135 }
136
137 pub fn insert(&mut self, signal: i32) -> ProcessResult<()> {
138 self.bits |= signal_bit(signal)?;
139 Ok(())
140 }
141
142 pub fn remove(&mut self, signal: i32) -> ProcessResult<()> {
143 self.bits &= !signal_bit(signal)?;
144 Ok(())
145 }
146
147 pub fn union(self, other: Self) -> Self {
148 Self {
149 bits: self.bits | other.bits,
150 }
151 }
152
153 pub fn difference(self, other: Self) -> Self {
154 Self {
155 bits: self.bits & !other.bits,
156 }
157 }
158
159 pub fn signals(self) -> Vec<i32> {
160 let mut signals = Vec::new();
161 for signal in 1..=MAX_SIGNAL {
162 if self.contains(signal) {
163 signals.push(signal);
164 }
165 }
166 signals
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum SigmaskHow {
172 Block,
173 Unblock,
174 SetMask,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct WaitPidFlags {
179 bits: u32,
180}
181
182impl WaitPidFlags {
183 pub const WNOHANG: Self = Self { bits: 1 << 0 };
184 pub const WUNTRACED: Self = Self { bits: 1 << 1 };
185 pub const WCONTINUED: Self = Self { bits: 1 << 2 };
186
187 pub const fn empty() -> Self {
188 Self { bits: 0 }
189 }
190
191 pub const fn contains(self, other: Self) -> bool {
192 (self.bits & other.bits) == other.bits
193 }
194}
195
196impl Default for WaitPidFlags {
197 fn default() -> Self {
198 Self::empty()
199 }
200}
201
202impl BitOr for WaitPidFlags {
203 type Output = Self;
204
205 fn bitor(self, rhs: Self) -> Self::Output {
206 Self {
207 bits: self.bits | rhs.bits,
208 }
209 }
210}
211
212impl BitOrAssign for WaitPidFlags {
213 fn bitor_assign(&mut self, rhs: Self) {
214 self.bits |= rhs.bits;
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum ProcessWaitEvent {
220 Exited,
221 Stopped,
222 Continued,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct ProcessWaitResult {
227 pub pid: u32,
228 pub status: i32,
229 pub event: ProcessWaitEvent,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct ProcessFileDescriptors {
234 pub stdin: u32,
235 pub stdout: u32,
236 pub stderr: u32,
237}
238
239impl Default for ProcessFileDescriptors {
240 fn default() -> Self {
241 Self {
242 stdin: 0,
243 stdout: 1,
244 stderr: 2,
245 }
246 }
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct ProcessContext {
251 pub pid: u32,
252 pub ppid: u32,
253 pub env: BTreeMap<String, String>,
254 pub cwd: String,
255 pub umask: u32,
256 pub fds: ProcessFileDescriptors,
257 pub identity: ProcessIdentity,
258 pub blocked_signals: SignalSet,
259 pub pending_signals: SignalSet,
260}
261
262impl Default for ProcessContext {
263 fn default() -> Self {
264 Self {
265 pid: 0,
266 ppid: 0,
267 env: BTreeMap::new(),
268 cwd: String::from("/"),
269 umask: DEFAULT_PROCESS_UMASK,
270 fds: ProcessFileDescriptors::default(),
271 identity: ProcessIdentity::default(),
272 blocked_signals: SignalSet::empty(),
273 pending_signals: SignalSet::empty(),
274 }
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct ProcessEntry {
280 pub pid: u32,
281 pub ppid: u32,
282 pub pgid: u32,
283 pub sid: u32,
284 pub driver: String,
285 pub command: String,
286 pub args: Vec<String>,
287 pub status: ProcessStatus,
288 pub exit_code: Option<i32>,
289 pub exit_time_ms: Option<u64>,
290 pub env: BTreeMap<String, String>,
291 pub cwd: String,
292 pub umask: u32,
293 pub identity: ProcessIdentity,
294}
295
296#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct ProcessInfo {
298 pub pid: u32,
299 pub ppid: u32,
300 pub pgid: u32,
301 pub sid: u32,
302 pub driver: String,
303 pub command: String,
304 pub status: ProcessStatus,
305 pub exit_code: Option<i32>,
306 pub identity: ProcessIdentity,
307}
308
309#[derive(Clone)]
310pub struct ProcessTable {
311 inner: Arc<ProcessTableInner>,
312}
313
314struct ProcessTableInner {
315 state: Mutex<ProcessTableState>,
316 waiters: Condvar,
317 reaper: Arc<ZombieReaper>,
318}
319
320struct ProcessRecord {
321 entry: ProcessEntry,
322 driver_process: Arc<dyn DriverProcess>,
323 pending_wait_events: VecDeque<PendingWaitEvent>,
324 blocked_signals: SignalSet,
325 pending_signals: SignalSet,
326}
327
328struct ScheduledSignalDelivery {
329 pid: u32,
330 signal: i32,
331 status: ProcessStatus,
332 driver_process: Arc<dyn DriverProcess>,
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336struct PendingWaitEvent {
337 status: i32,
338 event: ProcessWaitEvent,
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342enum WaitSelector {
343 AnyChild,
344 ChildPid(u32),
345 ProcessGroup(u32),
346}
347
348struct ZombieReaper {
349 state: Mutex<ZombieReaperState>,
350}
351
352#[derive(Default)]
353struct ZombieReaperState {
354 deadlines: BTreeMap<u32, Instant>,
355}
356
357struct ProcessTableState {
358 entries: BTreeMap<u32, ProcessRecord>,
359 next_pid: u32,
360 zombie_ttl: Duration,
361 on_process_exit: Option<Arc<dyn Fn(u32) + Send + Sync + 'static>>,
362 terminating_all: bool,
363}
364
365impl Default for ProcessTableState {
366 fn default() -> Self {
367 Self {
368 entries: BTreeMap::new(),
369 next_pid: 1,
370 zombie_ttl: ZOMBIE_TTL,
371 on_process_exit: None,
372 terminating_all: false,
373 }
374 }
375}
376
377impl Default for ProcessTable {
378 fn default() -> Self {
379 let reaper = Arc::new(ZombieReaper::default());
380 Self {
381 inner: Arc::new(ProcessTableInner {
382 state: Mutex::new(ProcessTableState::default()),
383 waiters: Condvar::new(),
384 reaper,
385 }),
386 }
387 }
388}
389
390impl ProcessTable {
391 pub fn new() -> Self {
392 Self::default()
393 }
394
395 pub fn with_zombie_ttl(zombie_ttl: Duration) -> Self {
396 let table = Self::new();
397 table.inner.lock_state().zombie_ttl = zombie_ttl;
398 table
399 }
400
401 pub fn allocate_pid(&self) -> ProcessResult<u32> {
402 let mut state = self.inner.lock_state();
403 let start = normalize_next_pid(state.next_pid);
404 let mut pid = start;
405
406 loop {
407 if !state.entries.contains_key(&pid) {
408 state.next_pid = next_allocated_pid_after(pid);
409 return Ok(pid);
410 }
411
412 pid = next_allocated_pid_after(pid);
413 if pid == start {
414 return Err(ProcessTableError::pid_space_exhausted());
415 }
416 }
417 }
418
419 pub fn set_on_process_exit(&self, callback: Option<Arc<dyn Fn(u32) + Send + Sync + 'static>>) {
420 self.inner.lock_state().on_process_exit = callback;
421 }
422
423 pub fn register(
424 &self,
425 pid: u32,
426 driver: impl Into<String>,
427 command: impl Into<String>,
428 args: Vec<String>,
429 ctx: ProcessContext,
430 driver_process: Arc<dyn DriverProcess>,
431 ) -> ProcessEntry {
432 self.register_with_process_group(pid, driver, command, args, ctx, driver_process, None)
433 .expect("inheriting a process group cannot fail")
434 }
435
436 #[allow(clippy::too_many_arguments)]
439 pub fn register_with_process_group(
440 &self,
441 pid: u32,
442 driver: impl Into<String>,
443 command: impl Into<String>,
444 args: Vec<String>,
445 ctx: ProcessContext,
446 driver_process: Arc<dyn DriverProcess>,
447 requested_pgid: Option<u32>,
448 ) -> ProcessResult<ProcessEntry> {
449 let driver = driver.into();
450 let command = command.into();
451 let mut state = self.inner.lock_state();
452 let (inherited_pgid, sid) = match state.entries.get(&ctx.ppid) {
453 Some(parent) => (parent.entry.pgid, parent.entry.sid),
454 None => (pid, pid),
455 };
456 let pgid = requested_pgid.map_or(inherited_pgid, |pgid| if pgid == 0 { pid } else { pgid });
457 if requested_pgid.is_some() && pgid != pid {
458 let mut group_exists = false;
459 for record in state.entries.values() {
460 if record.entry.pgid != pgid || record.entry.status == ProcessStatus::Exited {
461 continue;
462 }
463 if record.entry.sid != sid {
464 return Err(ProcessTableError::permission_denied(
465 "cannot join process group in different session",
466 ));
467 }
468 group_exists = true;
469 break;
470 }
471 if !group_exists {
472 return Err(ProcessTableError::permission_denied(format!(
473 "no such process group {pgid}"
474 )));
475 }
476 }
477
478 let entry = ProcessEntry {
479 pid,
480 ppid: ctx.ppid,
481 pgid,
482 sid,
483 driver,
484 command,
485 args,
486 status: ProcessStatus::Running,
487 exit_code: None,
488 exit_time_ms: None,
489 env: ctx.env,
490 cwd: ctx.cwd,
491 umask: ctx.umask & 0o777,
492 identity: ctx.identity,
493 };
494
495 state.next_pid = next_pid_after_registered(state.next_pid, pid);
496 state.entries.insert(
497 pid,
498 ProcessRecord {
499 entry: entry.clone(),
500 driver_process: driver_process.clone(),
501 pending_wait_events: VecDeque::new(),
502 blocked_signals: ctx.blocked_signals,
503 pending_signals: ctx.pending_signals,
504 },
505 );
506 drop(state);
507
508 let weak = Arc::downgrade(&self.inner);
509 driver_process.set_on_exit(Arc::new(move |code| {
510 if let Some(inner) = weak.upgrade() {
511 mark_exited_inner(&inner, pid, code);
512 }
513 }));
514
515 Ok(entry)
516 }
517
518 pub fn get(&self, pid: u32) -> Option<ProcessEntry> {
519 self.reap_due_zombies();
520 self.inner
521 .lock_state()
522 .entries
523 .get(&pid)
524 .map(|record| record.entry.clone())
525 }
526
527 pub fn set_identity(&self, pid: u32, identity: ProcessIdentity) -> ProcessResult<()> {
528 let mut state = self.inner.lock_state();
529 let record = state
530 .entries
531 .get_mut(&pid)
532 .ok_or_else(|| ProcessTableError::no_such_process(pid))?;
533 record.entry.identity = identity;
534 Ok(())
535 }
536
537 pub fn inherited_context(&self, parent_pid: u32) -> ProcessResult<ProcessContext> {
538 let state = self.inner.lock_state();
539 let parent = state
540 .entries
541 .get(&parent_pid)
542 .ok_or_else(|| ProcessTableError::no_such_process(parent_pid))?;
543 Ok(ProcessContext {
544 pid: 0,
545 ppid: parent_pid,
546 env: parent.entry.env.clone(),
547 cwd: parent.entry.cwd.clone(),
548 umask: parent.entry.umask,
549 fds: ProcessFileDescriptors::default(),
550 identity: parent.entry.identity.clone(),
551 blocked_signals: parent.blocked_signals,
552 pending_signals: SignalSet::empty(),
553 })
554 }
555
556 pub fn exec(
560 &self,
561 pid: u32,
562 driver: impl Into<String>,
563 command: impl Into<String>,
564 args: Vec<String>,
565 env: BTreeMap<String, String>,
566 cwd: String,
567 ) -> ProcessResult<()> {
568 let mut state = self.inner.lock_state();
569 let record = state
570 .entries
571 .get_mut(&pid)
572 .ok_or_else(|| ProcessTableError::no_such_process(pid))?;
573 if record.entry.status == ProcessStatus::Exited {
574 return Err(ProcessTableError::no_such_process(pid));
575 }
576 record.entry.driver = driver.into();
577 record.entry.command = command.into();
578 record.entry.args = args;
579 record.entry.env = env;
580 record.entry.cwd = cwd;
581 record.entry.status = ProcessStatus::Running;
582 record.entry.exit_code = None;
583 record.entry.exit_time_ms = None;
584 self.inner.waiters.notify_all();
585 Ok(())
586 }
587
588 pub fn zombie_timer_count(&self) -> usize {
589 self.reap_due_zombies();
590 self.inner.reaper.scheduled_count()
591 }
592
593 pub fn next_zombie_reap_deadline(&self) -> Option<Instant> {
598 self.inner.reaper.next_deadline()
599 }
600
601 pub fn reap_due_zombies(&self) {
606 while let Some(pid) = self.inner.reaper.take_due_pid_now() {
607 reap_due_pid(&self.inner, &self.inner.reaper, pid);
608 }
609 }
610
611 pub fn running_count(&self) -> usize {
612 self.reap_due_zombies();
613 self.inner
614 .lock_state()
615 .entries
616 .values()
617 .filter(|record| record.entry.status == ProcessStatus::Running)
618 .count()
619 }
620
621 pub fn mark_exited(&self, pid: u32, exit_code: i32) {
622 mark_exited_inner(&self.inner, pid, exit_code);
623 }
624
625 pub fn mark_stopped(&self, pid: u32, signal: i32) {
626 mark_wait_event_inner(
627 &self.inner,
628 pid,
629 ProcessStatus::Stopped,
630 PendingWaitEvent {
631 status: signal,
632 event: ProcessWaitEvent::Stopped,
633 },
634 );
635 }
636
637 pub fn mark_continued(&self, pid: u32) {
638 mark_wait_event_inner(
639 &self.inner,
640 pid,
641 ProcessStatus::Running,
642 PendingWaitEvent {
643 status: SIGCONT,
644 event: ProcessWaitEvent::Continued,
645 },
646 );
647 }
648
649 pub fn waitpid(&self, pid: u32) -> ProcessResult<(u32, i32)> {
650 let mut state = self.inner.lock_state();
651 loop {
652 let Some(record) = state.entries.get(&pid) else {
653 return Err(ProcessTableError::no_such_process(pid));
654 };
655
656 if record.entry.status == ProcessStatus::Exited {
657 let status = record.entry.exit_code.unwrap_or_default();
658 state.entries.remove(&pid);
659 drop(state);
660 self.inner.reaper.cancel(pid);
661 self.inner.waiters.notify_all();
662 return Ok((pid, status));
663 }
664
665 state = self.inner.wait_for_state(state);
666 }
667 }
668
669 pub fn waitpid_for(
670 &self,
671 waiter_pid: u32,
672 pid: i32,
673 flags: WaitPidFlags,
674 ) -> ProcessResult<Option<ProcessWaitResult>> {
675 let mut state = self.inner.lock_state();
676 loop {
677 let selector = resolve_wait_selector(&state, waiter_pid, pid)?;
678 let matching_children = matching_child_pids(&state, waiter_pid, selector);
679 if matching_children.is_empty() {
680 return Err(ProcessTableError::no_matching_child(waiter_pid, pid));
681 }
682
683 if let Some(result) = take_waitable_event(&mut state, &matching_children, flags) {
684 let should_reap = result.event == ProcessWaitEvent::Exited;
685 drop(state);
686 if should_reap {
687 self.inner.reaper.cancel(result.pid);
688 self.inner.waiters.notify_all();
689 }
690 return Ok(Some(result));
691 }
692
693 if flags.contains(WaitPidFlags::WNOHANG) {
694 return Ok(None);
695 }
696
697 state = self.inner.wait_for_state(state);
698 }
699 }
700
701 pub fn take_nonterminal_wait_event_for(
705 &self,
706 waiter_pid: u32,
707 pid: i32,
708 flags: WaitPidFlags,
709 ) -> ProcessResult<Option<ProcessWaitResult>> {
710 let mut state = self.inner.lock_state();
711 let selector = resolve_wait_selector(&state, waiter_pid, pid)?;
712 let matching_children = matching_child_pids(&state, waiter_pid, selector);
713 if matching_children.is_empty() {
714 return Err(ProcessTableError::no_matching_child(waiter_pid, pid));
715 }
716
717 for child_pid in matching_children {
718 let Some(record) = state.entries.get_mut(&child_pid) else {
719 continue;
720 };
721 let Some(index) = record.pending_wait_events.iter().position(|event| {
722 event.event != ProcessWaitEvent::Exited && is_waitable_event(event.event, flags)
723 }) else {
724 continue;
725 };
726 let event = record
727 .pending_wait_events
728 .remove(index)
729 .expect("pending nonterminal wait event should exist");
730 return Ok(Some(ProcessWaitResult {
731 pid: child_pid,
732 status: event.status,
733 event: event.event,
734 }));
735 }
736
737 Ok(None)
738 }
739
740 pub fn kill(&self, pid: i32, signal: i32) -> ProcessResult<()> {
741 if !(0..=MAX_SIGNAL).contains(&signal) {
742 return Err(ProcessTableError::invalid_signal(signal));
743 }
744
745 let deliveries = {
746 let mut state = self.inner.lock_state();
747 if pid < 0 {
748 let pgid = pid.unsigned_abs();
749 let grouped = state
750 .entries
751 .values()
752 .filter(|record| record.entry.pgid == pgid)
753 .map(|record| record.entry.pid)
754 .collect::<Vec<_>>();
755 if grouped.is_empty() {
756 return Err(ProcessTableError::no_such_process_group(pgid));
757 }
758 if signal == 0 {
759 return Ok(());
760 }
761 collect_signal_deliveries(&mut state, &grouped, signal)?
762 } else {
763 let pid = pid as u32;
764 let Some(record) = state.entries.get(&pid) else {
765 return Err(ProcessTableError::no_such_process(pid));
766 };
767 if record.entry.status == ProcessStatus::Exited || signal == 0 {
768 return Ok(());
769 }
770 collect_signal_deliveries(&mut state, &[pid], signal)?
771 }
772 };
773
774 if signal == 0 {
775 return Ok(());
776 }
777
778 deliver_signals(&self.inner, deliveries);
779 Ok(())
780 }
781
782 pub fn setpgid(&self, pid: u32, pgid: u32) -> ProcessResult<()> {
783 let mut state = self.inner.lock_state();
784 let (current_sid, target_pgid) = {
785 let Some(record) = state.entries.get(&pid) else {
786 return Err(ProcessTableError::no_such_process(pid));
787 };
788 (record.entry.sid, if pgid == 0 { pid } else { pgid })
789 };
790
791 if target_pgid != pid {
792 let mut group_exists = false;
793 for record in state.entries.values() {
794 if record.entry.pgid != target_pgid || record.entry.status == ProcessStatus::Exited
795 {
796 continue;
797 }
798 if record.entry.sid != current_sid {
799 return Err(ProcessTableError::permission_denied(
800 "cannot join process group in different session",
801 ));
802 }
803 group_exists = true;
804 break;
805 }
806 if !group_exists {
807 return Err(ProcessTableError::permission_denied(format!(
808 "no such process group {target_pgid}"
809 )));
810 }
811 }
812
813 if let Some(record) = state.entries.get_mut(&pid) {
814 record.entry.pgid = target_pgid;
815 }
816 Ok(())
817 }
818
819 pub fn getpgid(&self, pid: u32) -> ProcessResult<u32> {
820 self.get(pid)
821 .map(|entry| entry.pgid)
822 .ok_or_else(|| ProcessTableError::no_such_process(pid))
823 }
824
825 pub fn setsid(&self, pid: u32) -> ProcessResult<u32> {
826 let mut state = self.inner.lock_state();
827 let Some(record) = state.entries.get_mut(&pid) else {
828 return Err(ProcessTableError::no_such_process(pid));
829 };
830
831 if record.entry.pgid == pid {
832 return Err(ProcessTableError::permission_denied(format!(
833 "process {pid} is already a process group leader"
834 )));
835 }
836
837 record.entry.sid = pid;
838 record.entry.pgid = pid;
839 Ok(pid)
840 }
841
842 pub fn getsid(&self, pid: u32) -> ProcessResult<u32> {
843 self.get(pid)
844 .map(|entry| entry.sid)
845 .ok_or_else(|| ProcessTableError::no_such_process(pid))
846 }
847
848 pub fn getppid(&self, pid: u32) -> ProcessResult<u32> {
849 self.get(pid)
850 .map(|entry| entry.ppid)
851 .ok_or_else(|| ProcessTableError::no_such_process(pid))
852 }
853
854 pub fn get_umask(&self, pid: u32) -> ProcessResult<u32> {
855 self.get(pid)
856 .map(|entry| entry.umask)
857 .ok_or_else(|| ProcessTableError::no_such_process(pid))
858 }
859
860 pub fn set_umask(&self, pid: u32, umask: u32) -> ProcessResult<u32> {
861 let mut state = self.inner.lock_state();
862 let record = state
863 .entries
864 .get_mut(&pid)
865 .ok_or_else(|| ProcessTableError::no_such_process(pid))?;
866 let previous = record.entry.umask;
867 record.entry.umask = umask & 0o777;
868 Ok(previous)
869 }
870
871 pub fn has_process_group(&self, pgid: u32) -> bool {
872 self.inner
873 .lock_state()
874 .entries
875 .values()
876 .any(|record| record.entry.pgid == pgid && record.entry.status != ProcessStatus::Exited)
877 }
878
879 pub fn list_processes(&self) -> BTreeMap<u32, ProcessInfo> {
880 self.reap_due_zombies();
881 self.inner
882 .lock_state()
883 .entries
884 .values()
885 .map(|record| (record.entry.pid, to_process_info(&record.entry)))
886 .collect()
887 }
888
889 pub fn terminate_all(&self) {
890 let running = {
891 let mut state = self.inner.lock_state();
892 state.terminating_all = true;
893 self.inner.reaper.clear();
894 state
895 .entries
896 .values()
897 .filter(|record| record.entry.status == ProcessStatus::Running)
898 .map(|record| (record.entry.pid, Arc::clone(&record.driver_process)))
899 .collect::<Vec<_>>()
900 };
901
902 for (_, driver) in &running {
903 driver.kill(SIGTERM);
904 }
905 for (pid, driver) in &running {
906 if let Some(exit_code) = driver.wait(Duration::from_secs(1)) {
907 self.mark_exited(*pid, exit_code);
908 }
909 }
910
911 let survivors = {
912 let state = self.inner.lock_state();
913 running
914 .iter()
915 .filter(|(pid, _)| {
916 state
917 .entries
918 .get(pid)
919 .map(|record| record.entry.status == ProcessStatus::Running)
920 .unwrap_or(false)
921 })
922 .cloned()
923 .collect::<Vec<_>>()
924 };
925
926 for (_, driver) in &survivors {
927 driver.kill(SIGKILL);
928 }
929 for (pid, driver) in &survivors {
930 if let Some(exit_code) = driver.wait(Duration::from_millis(500)) {
931 self.mark_exited(*pid, exit_code);
932 }
933 }
934
935 self.inner.lock_state().terminating_all = false;
936 }
937
938 pub fn sigprocmask(
939 &self,
940 pid: u32,
941 how: SigmaskHow,
942 set: SignalSet,
943 ) -> ProcessResult<SignalSet> {
944 let (previous, deliveries) = {
945 let mut state = self.inner.lock_state();
946 let record = state
947 .entries
948 .get_mut(&pid)
949 .ok_or_else(|| ProcessTableError::no_such_process(pid))?;
950 let previous = record.blocked_signals;
951 record.blocked_signals = match how {
952 SigmaskHow::Block => previous.union(set),
953 SigmaskHow::Unblock => previous.difference(set),
954 SigmaskHow::SetMask => set,
955 };
956
957 let unblocked_pending = record.pending_signals.difference(record.blocked_signals);
958 let deliveries = collect_pending_signal_deliveries(record, unblocked_pending)?;
959 (previous, deliveries)
960 };
961
962 deliver_signals(&self.inner, deliveries);
963 Ok(previous)
964 }
965
966 pub fn sigpending(&self, pid: u32) -> ProcessResult<SignalSet> {
967 self.inner
968 .lock_state()
969 .entries
970 .get(&pid)
971 .map(|record| record.pending_signals)
972 .ok_or_else(|| ProcessTableError::no_such_process(pid))
973 }
974}
975
976fn to_process_info(entry: &ProcessEntry) -> ProcessInfo {
977 ProcessInfo {
978 pid: entry.pid,
979 ppid: entry.ppid,
980 pgid: entry.pgid,
981 sid: entry.sid,
982 driver: entry.driver.clone(),
983 command: entry.command.clone(),
984 status: entry.status,
985 exit_code: entry.exit_code,
986 identity: entry.identity.clone(),
987 }
988}
989
990fn mark_exited_inner(inner: &Arc<ProcessTableInner>, pid: u32, exit_code: i32) {
991 let (callback, zombie_ttl, should_schedule, deliveries) = {
992 let mut state = inner.lock_state();
993 let (ppid, pgid) = {
994 let Some(record) = state.entries.get_mut(&pid) else {
995 return;
996 };
997
998 if record.entry.status == ProcessStatus::Exited {
999 return;
1000 }
1001
1002 record.entry.status = ProcessStatus::Exited;
1003 record.entry.exit_code = Some(exit_code);
1004 record.entry.exit_time_ms = Some(now_ms());
1005 let ppid = record.entry.ppid;
1006 let pgid = record.entry.pgid;
1007 (ppid, pgid)
1008 };
1009 let mut affected_pgids = BTreeSet::from([pgid]);
1010 reparent_children_to_init(&mut state, pid, &mut affected_pgids);
1011
1012 let orphaned_group_targets = collect_orphaned_group_signal_targets(&state, &affected_pgids);
1013
1014 let should_schedule = !state.terminating_all;
1015 let mut deliveries = Vec::new();
1016 if should_schedule {
1017 if let Some(parent) = state
1018 .entries
1019 .get_mut(&ppid)
1020 .filter(|parent| parent.entry.status == ProcessStatus::Running)
1021 {
1022 if let Some(delivery) =
1023 queue_or_schedule_signal(parent, SIGCHLD).expect("SIGCHLD should be valid")
1024 {
1025 deliveries.push(delivery);
1026 }
1027 }
1028 }
1029
1030 for target_pid in orphaned_group_targets {
1031 if let Some(record) = state.entries.get_mut(&target_pid) {
1032 if let Some(delivery) =
1033 queue_or_schedule_signal(record, SIGHUP).expect("SIGHUP should be valid")
1034 {
1035 deliveries.push(delivery);
1036 }
1037 if let Some(delivery) =
1038 queue_or_schedule_signal(record, SIGCONT).expect("SIGCONT should be valid")
1039 {
1040 deliveries.push(delivery);
1041 }
1042 }
1043 }
1044
1045 (
1046 state.on_process_exit.clone(),
1047 state.zombie_ttl,
1048 should_schedule,
1049 deliveries,
1050 )
1051 };
1052
1053 if should_schedule {
1054 inner.reaper.schedule(pid, zombie_ttl);
1055 } else {
1056 inner.reaper.cancel(pid);
1057 }
1058
1059 deliver_signals(inner, deliveries);
1060
1061 if let Some(on_process_exit) = callback {
1062 on_process_exit(pid);
1063 }
1064
1065 inner.waiters.notify_all();
1066}
1067
1068fn reparent_children_to_init(
1069 state: &mut ProcessTableState,
1070 exiting_pid: u32,
1071 affected_pgids: &mut BTreeSet<u32>,
1072) {
1073 let new_parent = reparent_target_pid(state, exiting_pid);
1074 for record in state.entries.values_mut() {
1075 if record.entry.ppid != exiting_pid {
1076 continue;
1077 }
1078 record.entry.ppid = new_parent;
1079 affected_pgids.insert(record.entry.pgid);
1080 }
1081}
1082
1083fn reparent_target_pid(state: &ProcessTableState, exiting_pid: u32) -> u32 {
1084 if exiting_pid != INIT_PID
1085 && state
1086 .entries
1087 .get(&INIT_PID)
1088 .map(|record| record.entry.status != ProcessStatus::Exited)
1089 .unwrap_or(false)
1090 {
1091 INIT_PID
1092 } else {
1093 0
1094 }
1095}
1096
1097fn collect_orphaned_group_signal_targets(
1098 state: &ProcessTableState,
1099 candidate_pgids: &BTreeSet<u32>,
1100) -> Vec<u32> {
1101 let mut targets = Vec::new();
1102 for &pgid in candidate_pgids {
1103 if !process_group_is_orphaned(state, pgid) || !process_group_has_stopped_member(state, pgid)
1104 {
1105 continue;
1106 }
1107
1108 for record in state.entries.values() {
1109 if record.entry.pgid == pgid && record.entry.status != ProcessStatus::Exited {
1110 targets.push(record.entry.pid);
1111 }
1112 }
1113 }
1114 targets
1115}
1116
1117fn process_group_is_orphaned(state: &ProcessTableState, pgid: u32) -> bool {
1118 let mut has_member = false;
1119 for record in state.entries.values() {
1120 if record.entry.pgid != pgid || record.entry.status == ProcessStatus::Exited {
1121 continue;
1122 }
1123 has_member = true;
1124 if has_parent_outside_group_in_same_session(state, &record.entry) {
1125 return false;
1126 }
1127 }
1128
1129 has_member
1130}
1131
1132fn has_parent_outside_group_in_same_session(
1133 state: &ProcessTableState,
1134 entry: &ProcessEntry,
1135) -> bool {
1136 match entry.ppid {
1137 0 | INIT_PID => false,
1138 ppid => state
1139 .entries
1140 .get(&ppid)
1141 .map(|parent| {
1142 parent.entry.status != ProcessStatus::Exited
1143 && parent.entry.sid == entry.sid
1144 && parent.entry.pgid != entry.pgid
1145 })
1146 .unwrap_or(false),
1147 }
1148}
1149
1150fn process_group_has_stopped_member(state: &ProcessTableState, pgid: u32) -> bool {
1151 state
1152 .entries
1153 .values()
1154 .any(|record| record.entry.pgid == pgid && record.entry.status == ProcessStatus::Stopped)
1155}
1156
1157fn mark_wait_event_inner(
1158 inner: &Arc<ProcessTableInner>,
1159 pid: u32,
1160 next_status: ProcessStatus,
1161 event: PendingWaitEvent,
1162) {
1163 let deliveries = {
1164 let mut state = inner.lock_state();
1165 let ppid = {
1166 let Some(record) = state.entries.get_mut(&pid) else {
1167 return;
1168 };
1169
1170 if record.entry.status == ProcessStatus::Exited || record.entry.status == next_status {
1171 return;
1172 }
1173
1174 record.entry.status = next_status;
1175 record.pending_wait_events.push_back(event);
1176 record.entry.ppid
1177 };
1178
1179 state
1180 .entries
1181 .get_mut(&ppid)
1182 .filter(|parent| parent.entry.status == ProcessStatus::Running)
1183 .and_then(|parent| {
1184 queue_or_schedule_signal(parent, SIGCHLD)
1185 .expect("SIGCHLD should be valid")
1186 .into_iter()
1187 .next()
1188 })
1189 .into_iter()
1190 .collect::<Vec<_>>()
1191 };
1192
1193 deliver_signals(inner, deliveries);
1194
1195 inner.waiters.notify_all();
1196}
1197
1198fn signal_bit(signal: i32) -> ProcessResult<u64> {
1199 if !(1..=MAX_SIGNAL).contains(&signal) {
1200 return Err(ProcessTableError::invalid_signal(signal));
1201 }
1202 Ok(1u64 << (signal - 1))
1203}
1204
1205fn normalize_next_pid(pid: u32) -> u32 {
1206 if (INIT_PID..=MAX_ALLOCATED_PID).contains(&pid) {
1207 pid
1208 } else {
1209 INIT_PID
1210 }
1211}
1212
1213fn next_allocated_pid_after(pid: u32) -> u32 {
1214 if pid >= MAX_ALLOCATED_PID {
1215 INIT_PID
1216 } else {
1217 pid + 1
1218 }
1219}
1220
1221fn next_pid_after_registered(current: u32, registered: u32) -> u32 {
1222 let current = normalize_next_pid(current);
1223 if !(INIT_PID..=MAX_ALLOCATED_PID).contains(®istered) {
1224 return current;
1225 }
1226
1227 if current <= registered {
1228 next_allocated_pid_after(registered)
1229 } else {
1230 current
1231 }
1232}
1233
1234fn signal_can_be_blocked(signal: i32) -> bool {
1235 !matches!(signal, SIGKILL | SIGSTOP | SIGCONT)
1236}
1237
1238fn queue_or_schedule_signal(
1239 record: &mut ProcessRecord,
1240 signal: i32,
1241) -> ProcessResult<Option<ScheduledSignalDelivery>> {
1242 if signal_can_be_blocked(signal) && record.blocked_signals.contains(signal) {
1243 record.pending_signals.insert(signal)?;
1244 return Ok(None);
1245 }
1246
1247 Ok(Some(ScheduledSignalDelivery {
1248 pid: record.entry.pid,
1249 signal,
1250 status: record.entry.status,
1251 driver_process: Arc::clone(&record.driver_process),
1252 }))
1253}
1254
1255fn collect_signal_deliveries(
1256 state: &mut ProcessTableState,
1257 target_pids: &[u32],
1258 signal: i32,
1259) -> ProcessResult<Vec<ScheduledSignalDelivery>> {
1260 let mut deliveries = Vec::new();
1261 for pid in target_pids {
1262 let Some(record) = state.entries.get_mut(pid) else {
1263 continue;
1264 };
1265 if let Some(delivery) = queue_or_schedule_signal(record, signal)? {
1266 deliveries.push(delivery);
1267 }
1268 }
1269 Ok(deliveries)
1270}
1271
1272fn collect_pending_signal_deliveries(
1273 record: &mut ProcessRecord,
1274 signals: SignalSet,
1275) -> ProcessResult<Vec<ScheduledSignalDelivery>> {
1276 let mut deliveries = Vec::new();
1277 for signal in signals.signals() {
1278 record.pending_signals.remove(signal)?;
1279 deliveries.push(ScheduledSignalDelivery {
1280 pid: record.entry.pid,
1281 signal,
1282 status: record.entry.status,
1283 driver_process: Arc::clone(&record.driver_process),
1284 });
1285 }
1286 Ok(deliveries)
1287}
1288
1289fn deliver_signals(inner: &Arc<ProcessTableInner>, deliveries: Vec<ScheduledSignalDelivery>) {
1290 let mut stopped = Vec::new();
1291 let mut continued = Vec::new();
1292
1293 for delivery in &deliveries {
1294 match delivery.signal {
1295 SIGSTOP | SIGTSTP if delivery.status == ProcessStatus::Running => {
1296 stopped.push((delivery.pid, delivery.signal))
1297 }
1298 SIGCONT if delivery.status == ProcessStatus::Stopped => continued.push(delivery.pid),
1299 _ => {}
1300 }
1301 delivery.driver_process.kill(delivery.signal);
1302 }
1303
1304 for (pid, signal) in stopped {
1305 mark_wait_event_inner(
1306 inner,
1307 pid,
1308 ProcessStatus::Stopped,
1309 PendingWaitEvent {
1310 status: signal,
1311 event: ProcessWaitEvent::Stopped,
1312 },
1313 );
1314 }
1315 for pid in continued {
1316 mark_wait_event_inner(
1317 inner,
1318 pid,
1319 ProcessStatus::Running,
1320 PendingWaitEvent {
1321 status: SIGCONT,
1322 event: ProcessWaitEvent::Continued,
1323 },
1324 );
1325 }
1326}
1327
1328fn resolve_wait_selector(
1329 state: &ProcessTableState,
1330 waiter_pid: u32,
1331 pid: i32,
1332) -> ProcessResult<WaitSelector> {
1333 let waiter = state
1334 .entries
1335 .get(&waiter_pid)
1336 .ok_or_else(|| ProcessTableError::no_such_process(waiter_pid))?;
1337
1338 Ok(match pid {
1339 -1 => WaitSelector::AnyChild,
1340 0 => WaitSelector::ProcessGroup(waiter.entry.pgid),
1341 p if p < -1 => WaitSelector::ProcessGroup(p.unsigned_abs()),
1342 p => WaitSelector::ChildPid(p as u32),
1343 })
1344}
1345
1346fn matching_child_pids(
1347 state: &ProcessTableState,
1348 waiter_pid: u32,
1349 selector: WaitSelector,
1350) -> Vec<u32> {
1351 state
1352 .entries
1353 .values()
1354 .filter(|record| record.entry.ppid == waiter_pid)
1355 .filter(|record| match selector {
1356 WaitSelector::AnyChild => true,
1357 WaitSelector::ChildPid(pid) => record.entry.pid == pid,
1358 WaitSelector::ProcessGroup(pgid) => record.entry.pgid == pgid,
1359 })
1360 .map(|record| record.entry.pid)
1361 .collect()
1362}
1363
1364fn take_waitable_event(
1365 state: &mut ProcessTableState,
1366 matching_children: &[u32],
1367 flags: WaitPidFlags,
1368) -> Option<ProcessWaitResult> {
1369 for child_pid in matching_children {
1370 let mut non_exit_result = None;
1371 let mut should_reap = false;
1372 {
1373 let record = state.entries.get_mut(child_pid)?;
1374 if let Some(index) = record
1375 .pending_wait_events
1376 .iter()
1377 .position(|event| is_waitable_event(event.event, flags))
1378 {
1379 let event = record
1380 .pending_wait_events
1381 .remove(index)
1382 .expect("pending wait event should exist");
1383 non_exit_result = Some(ProcessWaitResult {
1384 pid: *child_pid,
1385 status: event.status,
1386 event: event.event,
1387 });
1388 } else if record.entry.status == ProcessStatus::Exited {
1389 should_reap = true;
1390 }
1391 }
1392
1393 if let Some(result) = non_exit_result {
1394 return Some(result);
1395 }
1396
1397 if should_reap {
1398 let record = state
1399 .entries
1400 .remove(child_pid)
1401 .expect("exited child should still exist");
1402 return Some(ProcessWaitResult {
1403 pid: *child_pid,
1404 status: record.entry.exit_code.unwrap_or_default(),
1405 event: ProcessWaitEvent::Exited,
1406 });
1407 }
1408 }
1409
1410 None
1411}
1412
1413fn is_waitable_event(event: ProcessWaitEvent, flags: WaitPidFlags) -> bool {
1414 match event {
1415 ProcessWaitEvent::Exited => true,
1416 ProcessWaitEvent::Stopped => flags.contains(WaitPidFlags::WUNTRACED),
1417 ProcessWaitEvent::Continued => flags.contains(WaitPidFlags::WCONTINUED),
1418 }
1419}
1420
1421fn reap_due_pid(inner: &ProcessTableInner, reaper: &ZombieReaper, pid: u32) {
1424 let mut state = inner.lock_state();
1425 let should_reap = state
1426 .entries
1427 .get(&pid)
1428 .map(|record| {
1429 record.entry.status == ProcessStatus::Exited
1430 && !has_living_parent(&state, record.entry.ppid)
1431 })
1432 .unwrap_or(false);
1433 if should_reap {
1434 state.entries.remove(&pid);
1435 } else if state
1436 .entries
1437 .get(&pid)
1438 .map(|record| record.entry.status == ProcessStatus::Exited)
1439 .unwrap_or(false)
1440 {
1441 reaper.schedule(pid, state.zombie_ttl);
1442 }
1443 drop(state);
1444 inner.waiters.notify_all();
1445}
1446
1447fn has_living_parent(state: &ProcessTableState, ppid: u32) -> bool {
1448 ppid != 0
1449 && state
1450 .entries
1451 .get(&ppid)
1452 .map(|record| record.entry.status != ProcessStatus::Exited)
1453 .unwrap_or(false)
1454}
1455
1456impl ProcessTableInner {
1457 fn lock_state(&self) -> MutexGuard<'_, ProcessTableState> {
1458 lock_or_recover(&self.state)
1459 }
1460
1461 fn wait_for_state<'a>(
1462 &self,
1463 guard: MutexGuard<'a, ProcessTableState>,
1464 ) -> MutexGuard<'a, ProcessTableState> {
1465 wait_or_recover(&self.waiters, guard)
1466 }
1467}
1468
1469fn now_ms() -> u64 {
1470 SystemTime::now()
1471 .duration_since(UNIX_EPOCH)
1472 .unwrap_or_default()
1473 .as_millis() as u64
1474}
1475
1476impl Default for ZombieReaper {
1477 fn default() -> Self {
1478 Self {
1479 state: Mutex::new(ZombieReaperState::default()),
1480 }
1481 }
1482}
1483
1484impl ZombieReaper {
1485 fn schedule(&self, pid: u32, ttl: Duration) {
1486 let mut state = lock_or_recover(&self.state);
1487 state.deadlines.insert(pid, Instant::now() + ttl);
1488 }
1489
1490 fn cancel(&self, pid: u32) {
1491 lock_or_recover(&self.state).deadlines.remove(&pid);
1492 }
1493
1494 fn clear(&self) {
1495 lock_or_recover(&self.state).deadlines.clear();
1496 }
1497
1498 fn scheduled_count(&self) -> usize {
1499 lock_or_recover(&self.state).deadlines.len()
1500 }
1501
1502 fn next_deadline(&self) -> Option<Instant> {
1503 lock_or_recover(&self.state)
1504 .deadlines
1505 .values()
1506 .min()
1507 .copied()
1508 }
1509
1510 fn take_due_pid_now(&self) -> Option<u32> {
1513 let mut state = lock_or_recover(&self.state);
1514 let now = Instant::now();
1515 let due = state
1516 .deadlines
1517 .iter()
1518 .filter(|(_, deadline)| **deadline <= now)
1519 .min_by_key(|(_, deadline)| **deadline)
1520 .map(|(&pid, _)| pid);
1521 if let Some(pid) = due {
1522 state.deadlines.remove(&pid);
1523 }
1524 due
1525 }
1526}
1527
1528fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>) -> MutexGuard<'a, T> {
1529 match mutex.lock() {
1530 Ok(guard) => guard,
1531 Err(poisoned) => poisoned.into_inner(),
1532 }
1533}
1534
1535fn wait_or_recover<'a, T>(condvar: &Condvar, guard: MutexGuard<'a, T>) -> MutexGuard<'a, T> {
1536 match condvar.wait(guard) {
1537 Ok(guard) => guard,
1538 Err(poisoned) => poisoned.into_inner(),
1539 }
1540}
1541
1542#[cfg(test)]
1543mod tests {
1544 use super::*;
1545
1546 #[derive(Default)]
1547 struct TestDriverProcess {
1548 on_exit: Mutex<Option<ProcessExitCallback>>,
1549 }
1550
1551 impl TestDriverProcess {
1552 fn exit(&self, exit_code: i32) {
1553 let callback = self
1554 .on_exit
1555 .lock()
1556 .expect("test driver lock poisoned")
1557 .clone();
1558 if let Some(callback) = callback {
1559 callback(exit_code);
1560 }
1561 }
1562 }
1563
1564 impl DriverProcess for TestDriverProcess {
1565 fn kill(&self, _signal: i32) {}
1566
1567 fn wait(&self, _timeout: Duration) -> Option<i32> {
1568 None
1569 }
1570
1571 fn set_on_exit(&self, callback: ProcessExitCallback) {
1572 *self.on_exit.lock().expect("test driver lock poisoned") = Some(callback);
1573 }
1574 }
1575
1576 struct AlreadyExitedDriverProcess(i32);
1577
1578 impl DriverProcess for AlreadyExitedDriverProcess {
1579 fn kill(&self, _signal: i32) {}
1580
1581 fn wait(&self, _timeout: Duration) -> Option<i32> {
1582 Some(self.0)
1583 }
1584
1585 fn set_on_exit(&self, callback: ProcessExitCallback) {
1586 callback(self.0);
1587 }
1588 }
1589
1590 fn context(ppid: u32) -> ProcessContext {
1591 ProcessContext {
1592 ppid,
1593 ..ProcessContext::default()
1594 }
1595 }
1596
1597 #[test]
1598 fn register_accepts_synchronous_already_exited_callback() {
1599 let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600));
1600 table.register(
1601 10,
1602 "test",
1603 "already-exited",
1604 Vec::new(),
1605 context(0),
1606 Arc::new(AlreadyExitedDriverProcess(27)),
1607 );
1608
1609 let entry = table.get(10).expect("registered process remains a zombie");
1610 assert_eq!(entry.status, ProcessStatus::Exited);
1611 assert_eq!(entry.exit_code, Some(27));
1612 }
1613
1614 #[test]
1615 fn spawn_process_group_is_applied_atomically() {
1616 let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600));
1617 table.register(
1618 10,
1619 "test",
1620 "parent",
1621 Vec::new(),
1622 context(0),
1623 Arc::new(TestDriverProcess::default()),
1624 );
1625
1626 let leader = table
1627 .register_with_process_group(
1628 11,
1629 "test",
1630 "leader",
1631 Vec::new(),
1632 context(10),
1633 Arc::new(TestDriverProcess::default()),
1634 Some(0),
1635 )
1636 .expect("spawn should create a new process group");
1637 assert_eq!(leader.pgid, 11);
1638
1639 let peer = table
1640 .register_with_process_group(
1641 12,
1642 "test",
1643 "peer",
1644 Vec::new(),
1645 context(10),
1646 Arc::new(TestDriverProcess::default()),
1647 Some(11),
1648 )
1649 .expect("spawn should join an existing group in the same session");
1650 assert_eq!(peer.pgid, 11);
1651
1652 let error = table
1653 .register_with_process_group(
1654 13,
1655 "test",
1656 "invalid",
1657 Vec::new(),
1658 context(10),
1659 Arc::new(TestDriverProcess::default()),
1660 Some(999),
1661 )
1662 .expect_err("spawn must reject a nonexistent process group");
1663 assert_eq!(error.code(), "EPERM");
1664 assert!(
1665 table.get(13).is_none(),
1666 "failed spawn must not register a child"
1667 );
1668
1669 table.register(
1670 20,
1671 "test",
1672 "other-session",
1673 Vec::new(),
1674 context(0),
1675 Arc::new(TestDriverProcess::default()),
1676 );
1677 let error = table
1678 .register_with_process_group(
1679 14,
1680 "test",
1681 "cross-session",
1682 Vec::new(),
1683 context(10),
1684 Arc::new(TestDriverProcess::default()),
1685 Some(20),
1686 )
1687 .expect_err("spawn must reject a process group in another session");
1688 assert_eq!(error.code(), "EPERM");
1689 assert!(table.get(14).is_none(), "failed spawn must remain atomic");
1690 }
1691
1692 #[test]
1693 fn allocate_pid_wraps_without_reusing_live_or_zombie_processes() {
1694 let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600));
1695 let live_high = Arc::new(TestDriverProcess::default());
1696 let zombie_high = Arc::new(TestDriverProcess::default());
1697 let live_one = Arc::new(TestDriverProcess::default());
1698 let max_pid = MAX_ALLOCATED_PID;
1699
1700 table.register(
1701 max_pid - 1,
1702 "test",
1703 "live-high",
1704 Vec::new(),
1705 context(0),
1706 live_high,
1707 );
1708 table.register(
1709 max_pid,
1710 "test",
1711 "zombie-high",
1712 Vec::new(),
1713 context(0),
1714 zombie_high.clone(),
1715 );
1716 table.register(1, "test", "live-one", Vec::new(), context(0), live_one);
1717 zombie_high.exit(0);
1718
1719 table.inner.lock_state().next_pid = max_pid - 1;
1720
1721 assert_eq!(table.allocate_pid().expect("allocate pid"), 2);
1722 assert_eq!(table.allocate_pid().expect("allocate pid"), 3);
1723 }
1724}