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::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    /// Cooperatively reap any zombies whose TTL deadline has elapsed.
506    ///
507    /// On native this is a cheap no-op fast path (the background thread does the
508    /// reaping); on wasm32 there is no reaper thread, so process-table
509    /// operations drive reaping through this instead.
510    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(&registered) {
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// On native, the zombie reaper runs on a background thread that blocks on a
1290// condvar until the next TTL deadline. wasm32 is single-threaded with no
1291// blocking primitives, so there the reaper is driven cooperatively via
1292// `ProcessTable::reap_due_zombies` from process-table operations instead.
1293#[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
1312/// Reap a single due zombie pid (shared by the native reaper thread and the
1313/// cooperative wasm drain). Removes the entry if it is an unparented zombie,
1314/// otherwise reschedules it for a later TTL pass.
1315fn 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    // Blocking variant used only by the native reaper thread.
1421    #[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    /// Non-blocking variant of [`take_next_due_pid`]: returns a pid whose TTL
1452    /// deadline has already elapsed, or `None` immediately. Used by the
1453    /// cooperative wasm reaper, which cannot block.
1454    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}