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