Skip to main content

agentos_kernel/
process_table.rs

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    // Registration keeps the process image, context, driver, and requested
437    // group explicit so ownership validation happens at one boundary.
438    #[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    /// Replace the userspace image metadata while retaining Linux process
557    /// identity (PID/PPID/PGID/SID), wait relationships, signal mask, pending
558    /// signals, and the driver process used to report the eventual exit.
559    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    /// Earliest cooperative zombie-reap deadline. Runtime adapters compare the
594    /// exact instant when deciding whether their one process-level timer must
595    /// be replaced; deriving a fresh duration on every pump would make the same
596    /// deadline appear to move and cause cancellation churn.
597    pub fn next_zombie_reap_deadline(&self) -> Option<Instant> {
598        self.inner.reaper.next_deadline()
599    }
600
601    /// Cooperatively reap any zombies whose TTL deadline has elapsed.
602    ///
603    /// The kernel owns deadlines but no scheduler or worker. Runtime adapters
604    /// call this from their bounded timer/event turn.
605    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    /// Consume one waitable stopped/continued transition without observing or
702    /// reaping terminal state. Sidecar child-process bridges use this while
703    /// terminal reaping remains coupled to stdout/stderr EOF delivery.
704    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(&registered) {
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
1421/// Reap a single due zombie pid. The kernel remains runtime-neutral: the
1422/// sidecar drives this cooperatively from its process event turn.
1423fn 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    /// Return one due pid without blocking. Runtime adapters drain this method
1511    /// through `ProcessTable::reap_due_zombies`.
1512    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}