From ddeaad31fd380dcee14cdf78bb7791149a2c101d Mon Sep 17 00:00:00 2001 From: Haoze Wu Date: Mon, 17 Nov 2025 16:06:10 +0800 Subject: [PATCH 01/10] feat: add process state management in support of SIGSTOP, SIGCONT, and waitpid --- src/lib.rs | 2 +- src/process.rs | 122 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d535ca4..c89a2c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,6 @@ mod session; /// A process ID, also used as session ID, process group ID, and thread ID. pub type Pid = u32; -pub use process::{Process, init_proc}; +pub use process::{Process, ZombieInfo, init_proc, ProcessState}; pub use process_group::ProcessGroup; pub use session::Session; diff --git a/src/process.rs b/src/process.rs index 3ccff29..b13a3ec 100644 --- a/src/process.rs +++ b/src/process.rs @@ -21,10 +21,25 @@ pub(crate) struct ThreadGroup { pub(crate) group_exited: bool, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ZombieInfo { + pub exit_code: i32, + pub signal: Option, + pub core_dumped: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcessState { + Running, + Stopped { signal: i32 }, + Continued, + Zombie { info: ZombieInfo }, +} + /// A process. pub struct Process { pid: Pid, - is_zombie: AtomicBool, + state: SpinNoIrq, pub(crate) tg: SpinNoIrq, // TODO: child subreaper9 @@ -32,6 +47,8 @@ pub struct Process { parent: SpinNoIrq>, group: SpinNoIrq>, + continued_unacked: AtomicBool, + stopped_unacked: AtomicBool, } impl Process { @@ -187,13 +204,68 @@ impl Process { } } -/// Status & exit +/// Status, exit, stop & cont impl Process { /// Returns `true` if the [`Process`] is a zombie process. pub fn is_zombie(&self) -> bool { - self.is_zombie.load(Ordering::Acquire) + matches!(*self.state.lock(), ProcessState::Zombie { .. }) + } + + pub fn get_zombie_info(&self) -> Option { + if let ProcessState::Zombie { info } = *self.state.lock() { + Some(info) + } else { + None + } + } + + pub fn is_stopped(&self) -> bool { + matches!(*self.state.lock(), ProcessState::Stopped { .. }) || self.stopped_unacked.load(Ordering::Acquire) + } + + pub fn state_snapshot(&self) -> ProcessState { + *self.state.lock() + } + + pub fn stopped_unacked(&self) -> bool { + self.stopped_unacked.load(Ordering::Acquire) + } + + pub fn set_stopped_by_signal(&self, signal: i32) { + *self.state.lock() = ProcessState::Stopped { signal }; + } + + pub fn get_stop_signal(&self) -> Option { + if let ProcessState::Stopped { signal } = *self.state.lock() { + Some(signal) + } else { + None + } + } + + pub fn ack_stopped(&self) { + self.stopped_unacked.store(false, Ordering::Release); + } + + pub fn is_continued(&self) -> bool { + matches!(*self.state.lock(), ProcessState::Continued) || self.continued_unacked.load(Ordering::Acquire) + } + + pub fn continue_from_stop(&self) { + let mut state = self.state.lock(); + if matches!(*state, ProcessState::Stopped { .. }) { + *state = ProcessState::Continued; + self.continued_unacked.store(true, Ordering::Release); + } } + pub fn ack_continued(&self) { + let mut state = self.state.lock(); + if matches!(*state, ProcessState::Continued) { + *state = ProcessState::Running; + } + self.continued_unacked.store(false, Ordering::Release); + } /// Terminates the [`Process`], marking it as a zombie process. /// /// Child processes are inherited by the init process or by the nearest @@ -209,7 +281,40 @@ impl Process { } let mut children = self.children.lock(); // Acquire the lock first - self.is_zombie.store(true, Ordering::Release); + let exit_code = self.tg.lock().exit_code; + *self.state.lock() = ProcessState::Zombie { + info: ZombieInfo { + exit_code, + signal: None, + core_dumped: false, + }, + }; + + let mut reaper_children = reaper.children.lock(); + let reaper = Arc::downgrade(reaper); + + for (pid, child) in core::mem::take(&mut *children) { + *child.parent.lock() = reaper.clone(); + reaper_children.insert(pid, child); + } + } + + pub fn exit_with_signal(self: &Arc, signal: i32, core_dumped: bool) { + let reaper = INIT_PROC.get().unwrap(); + + if Arc::ptr_eq(self, reaper) { + return; + } + + let mut children = self.children.lock(); // Acquire the lock first + let exit_code = 128 + signal; + *self.state.lock() = ProcessState::Zombie { + info: ZombieInfo { + exit_code, + signal: Some(signal), + core_dumped, + }, + }; let mut reaper_children = reaper.children.lock(); let reaper = Arc::downgrade(reaper); @@ -230,6 +335,11 @@ impl Process { parent.children.lock().remove(&self.pid); } } + + pub fn stop_by_signal(&self, stop_signal: i32) { + *self.state.lock() = ProcessState::Stopped { signal: stop_signal }; + self.stopped_unacked.store(true, Ordering::Release); + } } impl fmt::Debug for Process { @@ -266,11 +376,13 @@ impl Process { let process = Arc::new(Process { pid, - is_zombie: AtomicBool::new(false), + state: SpinNoIrq::new(ProcessState::Running), tg: SpinNoIrq::new(ThreadGroup::default()), children: SpinNoIrq::new(StrongMap::new()), parent: SpinNoIrq::new(parent.as_ref().map(Arc::downgrade).unwrap_or_default()), group: SpinNoIrq::new(group.clone()), + continued_unacked: AtomicBool::new(false), + stopped_unacked: AtomicBool::new(false), }); group.processes.lock().insert(pid, &process); From e1080ddc8d7dbb4234c03e07929d817726c6fc84 Mon Sep 17 00:00:00 2001 From: Haoze Wu Date: Mon, 17 Nov 2025 16:54:43 +0800 Subject: [PATCH 02/10] ci: fix clippy errors --- src/process.rs | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/process.rs b/src/process.rs index b13a3ec..48a7854 100644 --- a/src/process.rs +++ b/src/process.rs @@ -21,19 +21,35 @@ pub(crate) struct ThreadGroup { pub(crate) group_exited: bool, } +/// Store basic information about a zombie process #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ZombieInfo { + /// exit_code of the process pub exit_code: i32, + /// signal that terminates the process if any, an option field pub signal: Option, + /// indicator of any core_dump + /// currently a placeholder, unimplemented pub core_dumped: bool, } +/// Process State during the entire lifecycle #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProcessState { + /// State for process just inited or actually executing Running, - Stopped { signal: i32 }, + /// State for signal-stopped process + Stopped { + /// Corresponding signal of the stopped process + signal: i32 + }, + /// State for process just continued but whose parent has not been notified Continued, - Zombie { info: ZombieInfo }, + /// State for process just finished executing + Zombie { + /// Relative info of the zombie process + info: ZombieInfo + }, } /// A process. @@ -211,6 +227,7 @@ impl Process { matches!(*self.state.lock(), ProcessState::Zombie { .. }) } + /// Get the information of the process ig it is a zombie pub fn get_zombie_info(&self) -> Option { if let ProcessState::Zombie { info } = *self.state.lock() { Some(info) @@ -219,22 +236,29 @@ impl Process { } } + /// Check whether the process has stopped, + /// including both the case which the process just stopped without acked by its parent + /// and already acked by its parent pub fn is_stopped(&self) -> bool { matches!(*self.state.lock(), ProcessState::Stopped { .. }) || self.stopped_unacked.load(Ordering::Acquire) } + /// A real-time quick snapshot of the process's state pub fn state_snapshot(&self) -> ProcessState { *self.state.lock() } + /// Check whether the process's Stoppage has been acked by its parent pub fn stopped_unacked(&self) -> bool { self.stopped_unacked.load(Ordering::Acquire) } + /// Set the process to be stopped with the corresponding signal pub fn set_stopped_by_signal(&self, signal: i32) { *self.state.lock() = ProcessState::Stopped { signal }; } + /// Get the corresponding signal of a stopped process if it is stopped pub fn get_stop_signal(&self) -> Option { if let ProcessState::Stopped { signal } = *self.state.lock() { Some(signal) @@ -243,14 +267,19 @@ impl Process { } } + /// Set the stoppage of the process has been acked by its parent pub fn ack_stopped(&self) { self.stopped_unacked.store(false, Ordering::Release); } + /// Check whether the process has continued from the stoppage, + /// including both the case which the process just continued without acked by its parent + /// and already acked by its parent pub fn is_continued(&self) -> bool { matches!(*self.state.lock(), ProcessState::Continued) || self.continued_unacked.load(Ordering::Acquire) } + /// Updating the status of a process continued from stoppage pub fn continue_from_stop(&self) { let mut state = self.state.lock(); if matches!(*state, ProcessState::Stopped { .. }) { @@ -259,6 +288,7 @@ impl Process { } } + /// Update the statue of the process after its continuation has been acked by its parent pub fn ack_continued(&self) { let mut state = self.state.lock(); if matches!(*state, ProcessState::Continued) { @@ -266,6 +296,7 @@ impl Process { } self.continued_unacked.store(false, Ordering::Release); } + /// Terminates the [`Process`], marking it as a zombie process. /// /// Child processes are inherited by the init process or by the nearest @@ -299,6 +330,12 @@ impl Process { } } + /// Terminates the [`Process`], marking it as a zombie process ONLY when the termination is due to a signal + /// + /// Child processes are inherited by the init process or by the nearest + /// subreaper process. + /// + /// This method panics if the [`Process`] is the init process. pub fn exit_with_signal(self: &Arc, signal: i32, core_dumped: bool) { let reaper = INIT_PROC.get().unwrap(); @@ -336,6 +373,7 @@ impl Process { } } + /// Stops the [`Process`], marking it as stopped when a signal stops it(majorly SIGSTOP) pub fn stop_by_signal(&self, stop_signal: i32) { *self.state.lock() = ProcessState::Stopped { signal: stop_signal }; self.stopped_unacked.store(true, Ordering::Release); From 239d21aa0abd3fe42ee2c320531b8ae993d12882 Mon Sep 17 00:00:00 2001 From: Haoze Wu Date: Wed, 19 Nov 2025 08:55:33 +0800 Subject: [PATCH 03/10] feat: Update daat structure support for ptrace --- src/process.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/src/process.rs b/src/process.rs index 48a7854..0bfafb9 100644 --- a/src/process.rs +++ b/src/process.rs @@ -38,10 +38,18 @@ pub struct ZombieInfo { pub enum ProcessState { /// State for process just inited or actually executing Running, - /// State for signal-stopped process + /// State for signal-stopped process, including both signal-stops and ptrace-stops Stopped { - /// Corresponding signal of the stopped process - signal: i32 + /// Signal number for reporting to parent/tracer. + /// For signal-stops: actual signal (SIGSTOP=19, SIGTSTP=20, etc.) + /// For ptrace-stops: SIGTRAP=5 (or with 0x80 for syscall-stops) + signal: i32, + + /// Whether this is a ptrace-stop (vs signal-stop). + /// - signal-stops: visible to parent via waitpid(WUNTRACED) + /// - ptrace-stops: only visible to tracer + /// - SIGCONT resumes signal-stops but NOT ptrace-stops + is_ptrace_stopped: bool, }, /// State for process just continued but whose parent has not been notified Continued, @@ -255,15 +263,19 @@ impl Process { /// Set the process to be stopped with the corresponding signal pub fn set_stopped_by_signal(&self, signal: i32) { - *self.state.lock() = ProcessState::Stopped { signal }; + *self.state.lock() = ProcessState::Stopped { signal, is_ptrace_stopped: false }; + self.stopped_unacked.store(true, Ordering::Release); } - /// Get the corresponding signal of a stopped process if it is stopped + /// Get the stop signal if the process is in any stopped state. + /// + /// # Returns + /// * `Some(signal)` if process is stopped (signal-stop or ptrace-stop) + /// * `None` if process is running, continued, or zombie pub fn get_stop_signal(&self) -> Option { - if let ProcessState::Stopped { signal } = *self.state.lock() { - Some(signal) - } else { - None + match *self.state.lock() { + ProcessState::Stopped { signal, .. } => Some(signal), + _ => None, } } @@ -375,9 +387,63 @@ impl Process { /// Stops the [`Process`], marking it as stopped when a signal stops it(majorly SIGSTOP) pub fn stop_by_signal(&self, stop_signal: i32) { - *self.state.lock() = ProcessState::Stopped { signal: stop_signal }; + *self.state.lock() = ProcessState::Stopped { signal: stop_signal, is_ptrace_stopped: false }; self.stopped_unacked.store(true, Ordering::Release); } + + /// Set the process to be stopped due to a ptrace event. + /// + /// This is similar to signal-stops but is only visible to the tracer, + /// not the parent. The signal parameter is typically SIGTRAP (5) for + /// syscall-stops and exec-stops, or the actual signal number for + /// signal-delivery-stops. + /// + /// # Arguments + /// * `signal` - Signal to report via waitpid (SIGTRAP or actual signal) + pub fn set_ptrace_stopped(&self, signal: i32) { + *self.state.lock() = ProcessState::Stopped { + signal, + is_ptrace_stopped: true, + }; + self.stopped_unacked.store(true, Ordering::Release); + } + + /// Check if the process is in a ptrace-stop state. + /// + /// # Returns + /// * `true` if process is stopped due to ptrace + /// * `false` if running, signal-stopped, continued, or zombie + pub fn is_ptrace_stopped(&self) -> bool { + matches!( + *self.state.lock(), + ProcessState::Stopped { is_ptrace_stopped: true, .. } + ) + } + + /// Check if the process is in a signal-stop state (not ptrace). + /// + /// # Returns + /// * `true` if process is stopped due to signal (SIGSTOP, etc.) + /// * `false` if running, ptrace-stopped, continued, or zombie + pub fn is_signal_stopped(&self) -> bool { + matches!( + *self.state.lock(), + ProcessState::Stopped { is_ptrace_stopped: false, .. } + ) + } + + /// Resume from ptrace-stop by transitioning back to Running state. + /// + /// This is called by the tracer via PTRACE_CONT/SYSCALL/DETACH. + /// Unlike signal-stops which go through Continued state, ptrace + /// resumes directly to Running. + pub fn resume_from_ptrace_stop(&self) { + let mut state = self.state.lock(); + if matches!(*state, ProcessState::Stopped { is_ptrace_stopped: true, .. }) { + *state = ProcessState::Running; + self.stopped_unacked.store(false, Ordering::Release); + } + } } impl fmt::Debug for Process { From 8391dc500d2628f36d7ddafd909dc224588030b7 Mon Sep 17 00:00:00 2001 From: Haoze Wu Date: Mon, 24 Nov 2025 14:01:45 +0800 Subject: [PATCH 04/10] feat: Refactor process state machine for better encapsulation Improved process state management with composite state model and type safety. Key changes: - Introduced ProcessStateKind + ProcessStateFlags composite state - Encapsulated all transitions in ProcessState methods - Added atomic try_consume_* methods for waitpid event consumption - Introduced ExitCode type to replace magic number (128 + signal) - Extracted reaper_children helper to eliminate code duplication Addresses code review feedback from PR #4. --- Cargo.toml | 1 + src/process.rs | 486 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 344 insertions(+), 143 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 90e1744..0cd78f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/Starry-OS/starry-process" [dependencies] +bitflags = "2.10.0" kspin = "0.1" lazyinit = "0.2.1" weak-map = "0.1" diff --git a/src/process.rs b/src/process.rs index 0bfafb9..aea6840 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,13 +1,88 @@ +//! Process state management implementation. +//! +//! # Architecture Overview +//! +//! This module implements a composite process state machine that tracks both +//! the primary lifecycle state of a process and auxiliary event flags for +//! parent notification via `waitpid`. +//! +//! ## State Model +//! +//! The process state is represented by two components: +//! +//! 1. **`ProcessStateKind`** - The primary lifecycle state: +//! - `Running`: Process is executing or ready to execute. +//! - `Stopped`: Process is stopped by a signal or ptrace. +//! - `Zombie`: Process has terminated but not yet been reaped. +//! +//! 2. **`ProcessStateFlags`** - Event acknowledgement flags: +//! - `STOPPED_UNACKED`: A stop event has occurred but not yet reported to +//! parent. +//! - `CONTINUED_UNACKED`: A continuation event has occurred but not yet +//! reported to parent. +//! +//! These are combined in the `ProcessState` struct, which encapsulates all +//! state transitions and ensures invariants are maintained. +//! +//! ## State Transitions +//! +//! State transitions are managed through explicit methods on `ProcessState`: +//! +//! - `transition_to_stopped(signal, ptraced)`: `Running` → `Stopped` + +//! `STOPPED_UNACKED` +//! - `transition_to_running()`: `Stopped` → `Running` + `CONTINUED_UNACKED` +//! - `transition_to_zombie(info)`: Any state → `Zombie` (clears all flags) +//! +//! ### Important Invariants +//! +//! - A `Zombie` process cannot transition to any other state. +//! - The `CONTINUED_UNACKED` flag is only valid when `kind` is `Running`. +//! - The `STOPPED_UNACKED` flag is only valid when `kind` is `Stopped`. +//! - Flags are automatically set during transitions and consumed atomically by +//! `waitpid`. +//! +//! ## Interaction with `waitpid` +//! +//! Parent processes use `waitpid` with options like `WUNTRACED` and +//! `WCONTINUED` to wait for child state changes. The event consumption flow is: +//! +//! 1. Child transitions (e.g., `Running` → `Stopped`), setting the +//! corresponding flag. +//! 2. Parent calls `waitpid(WUNTRACED)`, which internally calls +//! `try_consume_stopped()`. +//! 3. `try_consume_stopped()` atomically checks and clears the +//! `STOPPED_UNACKED` flag. +//! 4. If successful, the event is reported exactly once to the parent. +//! +//! This ensures that each state change event is reported exactly once, +//! preventing duplicate notifications and race conditions. +//! +//! ## Thread Safety +//! +//! The `ProcessState` is protected by a `SpinNoIrq` lock within the `Process` +//! struct. All state queries and transitions must acquire this lock, ensuring +//! atomic updates even in concurrent scenarios (e.g., signal delivery while +//! parent is waiting). +//! +//! ## Ptrace Integration +//! +//! Ptrace stops are represented as `Stopped { ptraced: true, signal }`. They +//! differ from signal-stops in key ways: +//! +//! - Ptrace stops are NOT consumed by standard `waitpid(WUNTRACED)` calls. +//! - They are handled separately via `check_ptrace_stop()` in the ptrace +//! subsystem. +//! - Resuming from ptrace stops goes directly to `Running` (no `CONTINUED` +//! event). + use alloc::{ collections::btree_set::BTreeSet, sync::{Arc, Weak}, vec::Vec, }; -use core::{ - fmt, - sync::atomic::{AtomicBool, Ordering}, -}; +use core::fmt; +use bitflags::bitflags; use kspin::SpinNoIrq; use lazyinit::LazyInit; use weak_map::StrongMap; @@ -21,43 +96,80 @@ pub(crate) struct ThreadGroup { pub(crate) group_exited: bool, } -/// Store basic information about a zombie process +/// The primary lifecycle state of the process. +/// +/// We create three states for process, `Running`, `Stopped`, and `Zombie`. +/// +/// For a `Running` process, it can be actually running(if the +/// `ProcessStateFlags` is empty) or it can be just `Continued` from a stoppage +/// but not acked by its parent(if the `ProcessStateFlags` contain +/// `CONTINUED_UNACKED`). +/// +/// For a `Stopped` process, if its stoppage has not been acked by its parent, +/// i.e., the parent has not been notified for the child's stoppade, the +/// corresponding `ProcessStateFlags` will be marked as `STOPPED_UNACKED`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcessStateKind { + Running, + Stopped { signal: i32, ptraced: bool }, + Zombie { info: ZombieInfo }, +} + +bitflags! { + /// Composite flags for process state (e.g., reporting status). + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + pub struct ProcessStateFlags: u8 { + // A status of a process who has stopped but its stoppage + // has not been acked by its parent + const STOPPED_UNACKED = 1 << 0; + // A status of a process who has just continued but its continuation + // has not been acked by its parent + const CONTINUED_UNACKED = 1 << 1; + } +} + +/// The exit code value following POSIX conventions. +/// +/// This type encapsulates the numeric exit code that is reported to the parent +/// process via `waitpid`. The encoding follows POSIX standards: +/// - Normal exit: Exit code 0-255 directly +/// - Signal termination: Exit code = 128 + signal number +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ExitCode(i32); + +impl ExitCode { + /// Creates an exit code from a normal exit. + pub fn from_code(code: i32) -> Self { + Self(code) + } + + /// Creates an exit code from signal termination (128 + signal). + pub fn from_signal(signal: i32) -> Self { + Self(128 + signal) + } + + /// Returns the raw exit code value. + pub fn as_raw(self) -> i32 { + self.0 + } +} + +/// Information about a zombie (terminated) process. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ZombieInfo { - /// exit_code of the process - pub exit_code: i32, - /// signal that terminates the process if any, an option field + /// The exit code value. + pub exit_code: ExitCode, + /// The signal that terminated the process, if any. pub signal: Option, - /// indicator of any core_dump - /// currently a placeholder, unimplemented + /// Whether a core dump was produced. pub core_dumped: bool, } -/// Process State during the entire lifecycle -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ProcessState { - /// State for process just inited or actually executing - Running, - /// State for signal-stopped process, including both signal-stops and ptrace-stops - Stopped { - /// Signal number for reporting to parent/tracer. - /// For signal-stops: actual signal (SIGSTOP=19, SIGTSTP=20, etc.) - /// For ptrace-stops: SIGTRAP=5 (or with 0x80 for syscall-stops) - signal: i32, - - /// Whether this is a ptrace-stop (vs signal-stop). - /// - signal-stops: visible to parent via waitpid(WUNTRACED) - /// - ptrace-stops: only visible to tracer - /// - SIGCONT resumes signal-stops but NOT ptrace-stops - is_ptrace_stopped: bool, - }, - /// State for process just continued but whose parent has not been notified - Continued, - /// State for process just finished executing - Zombie { - /// Relative info of the zombie process - info: ZombieInfo - }, +/// The full process state machine. +#[derive(Debug, Clone, Copy)] +pub struct ProcessState { + kind: ProcessStateKind, + flags: ProcessStateFlags, } /// A process. @@ -71,8 +183,6 @@ pub struct Process { parent: SpinNoIrq>, group: SpinNoIrq>, - continued_unacked: AtomicBool, - stopped_unacked: AtomicBool, } impl Process { @@ -232,12 +342,12 @@ impl Process { impl Process { /// Returns `true` if the [`Process`] is a zombie process. pub fn is_zombie(&self) -> bool { - matches!(*self.state.lock(), ProcessState::Zombie { .. }) + self.state.lock().is_zombie() } /// Get the information of the process ig it is a zombie pub fn get_zombie_info(&self) -> Option { - if let ProcessState::Zombie { info } = *self.state.lock() { + if let ProcessStateKind::Zombie { info } = self.state.lock().kind { Some(info) } else { None @@ -245,68 +355,72 @@ impl Process { } /// Check whether the process has stopped, - /// including both the case which the process just stopped without acked by its parent - /// and already acked by its parent + /// including both the case which the process just stopped without acked by + /// its parent and already acked by its parent pub fn is_stopped(&self) -> bool { - matches!(*self.state.lock(), ProcessState::Stopped { .. }) || self.stopped_unacked.load(Ordering::Acquire) - } - - /// A real-time quick snapshot of the process's state - pub fn state_snapshot(&self) -> ProcessState { - *self.state.lock() + let state = self.state.lock(); + matches!(state.kind, ProcessStateKind::Stopped { .. }) + || state.flags.contains(ProcessStateFlags::STOPPED_UNACKED) } - /// Check whether the process's Stoppage has been acked by its parent - pub fn stopped_unacked(&self) -> bool { - self.stopped_unacked.load(Ordering::Acquire) + /// Check whether the process has continued from the stoppage, + /// including both the case which the process just continued without acked + /// by its parent and already acked by its parent + pub fn is_continued(&self) -> bool { + let state = self.state.lock(); + matches!(state.kind, ProcessStateKind::Running) + && state.flags.contains(ProcessStateFlags::CONTINUED_UNACKED) } - /// Set the process to be stopped with the corresponding signal - pub fn set_stopped_by_signal(&self, signal: i32) { - *self.state.lock() = ProcessState::Stopped { signal, is_ptrace_stopped: false }; - self.stopped_unacked.store(true, Ordering::Release); + /// Updating the status of a process continued from stoppage + pub fn continue_from_stop(&self) { + self.state.lock().transition_to_running(); } - /// Get the stop signal if the process is in any stopped state. + /// Attempts to consume the 'continued' event for `waitpid(WCONTINUED)`. /// - /// # Returns - /// * `Some(signal)` if process is stopped (signal-stop or ptrace-stop) - /// * `None` if process is running, continued, or zombie - pub fn get_stop_signal(&self) -> Option { - match *self.state.lock() { - ProcessState::Stopped { signal, .. } => Some(signal), - _ => None, - } - } - - /// Set the stoppage of the process has been acked by its parent - pub fn ack_stopped(&self) { - self.stopped_unacked.store(false, Ordering::Release); - } - - /// Check whether the process has continued from the stoppage, - /// including both the case which the process just continued without acked by its parent - /// and already acked by its parent - pub fn is_continued(&self) -> bool { - matches!(*self.state.lock(), ProcessState::Continued) || self.continued_unacked.load(Ordering::Acquire) + /// This is a thread-safe wrapper around + /// `ProcessState::try_consume_continued`. It acquires the state lock + /// and checks if the process has a pending continuation event. + /// + /// Returns `true` if the event was successfully consumed. + pub fn try_consume_continued(&self) -> bool { + self.state.lock().try_consume_continued() } - /// Updating the status of a process continued from stoppage - pub fn continue_from_stop(&self) { + /// Attempts to consume the 'stopped' event for `waitpid(WUNTRACED)`. + /// + /// This is a thread-safe wrapper around + /// `ProcessState::try_consume_stopped`. It acquires the state lock and + /// checks if the process has a pending stop event. + /// + /// IMPORTANT: This method explicitly ignores ptrace-stops. Ptrace stops are + /// handled separately via `check_ptrace_stop` and are not consumed by + /// standard `waitpid(WUNTRACED)` calls. + /// + /// Returns `Some(signal)` if the event was successfully consumed. + pub fn try_consume_stopped(&self) -> Option { + // Only consume if it's NOT a ptrace stop (for WUNTRACED) let mut state = self.state.lock(); - if matches!(*state, ProcessState::Stopped { .. }) { - *state = ProcessState::Continued; - self.continued_unacked.store(true, Ordering::Release); + if matches!(state.kind, ProcessStateKind::Stopped { ptraced: true, .. }) { + return None; } + state.try_consume_stopped() } - /// Update the statue of the process after its continuation has been acked by its parent - pub fn ack_continued(&self) { - let mut state = self.state.lock(); - if matches!(*state, ProcessState::Continued) { - *state = ProcessState::Running; + /// Transfers all children of this process to the init process (reaper). + /// + /// This is called when a process exits to ensure orphaned children are + /// reparented to init. + fn reaper_children(children: &mut StrongMap>) { + let reaper = INIT_PROC.get().unwrap(); + let mut reaper_children = reaper.children.lock(); + let reaper_weak = Arc::downgrade(reaper); + + for (pid, child) in core::mem::take(children) { + *child.parent.lock() = reaper_weak.clone(); + reaper_children.insert(pid, child); } - self.continued_unacked.store(false, Ordering::Release); } /// Terminates the [`Process`], marking it as a zombie process. @@ -323,26 +437,19 @@ impl Process { return; } - let mut children = self.children.lock(); // Acquire the lock first - let exit_code = self.tg.lock().exit_code; - *self.state.lock() = ProcessState::Zombie { - info: ZombieInfo { - exit_code, - signal: None, - core_dumped: false, - }, - }; - - let mut reaper_children = reaper.children.lock(); - let reaper = Arc::downgrade(reaper); + let mut children = self.children.lock(); + let code = self.tg.lock().exit_code; + self.state.lock().transition_to_zombie(ZombieInfo { + exit_code: ExitCode::from_code(code), + signal: None, + core_dumped: false, + }); - for (pid, child) in core::mem::take(&mut *children) { - *child.parent.lock() = reaper.clone(); - reaper_children.insert(pid, child); - } + Self::reaper_children(&mut children); } - /// Terminates the [`Process`], marking it as a zombie process ONLY when the termination is due to a signal + /// Terminates the [`Process`], marking it as a zombie process ONLY when the + /// termination is due to a signal /// /// Child processes are inherited by the init process or by the nearest /// subreaper process. @@ -355,23 +462,14 @@ impl Process { return; } - let mut children = self.children.lock(); // Acquire the lock first - let exit_code = 128 + signal; - *self.state.lock() = ProcessState::Zombie { - info: ZombieInfo { - exit_code, - signal: Some(signal), - core_dumped, - }, - }; - - let mut reaper_children = reaper.children.lock(); - let reaper = Arc::downgrade(reaper); + let mut children = self.children.lock(); + self.state.lock().transition_to_zombie(ZombieInfo { + exit_code: ExitCode::from_signal(signal), + signal: Some(signal), + core_dumped, + }); - for (pid, child) in core::mem::take(&mut *children) { - *child.parent.lock() = reaper.clone(); - reaper_children.insert(pid, child); - } + Self::reaper_children(&mut children); } /// Frees a zombie [`Process`]. Removes it from the parent. @@ -385,10 +483,10 @@ impl Process { } } - /// Stops the [`Process`], marking it as stopped when a signal stops it(majorly SIGSTOP) + /// Stops the [`Process`], marking it as stopped when a signal stops + /// it(majorly SIGSTOP) pub fn stop_by_signal(&self, stop_signal: i32) { - *self.state.lock() = ProcessState::Stopped { signal: stop_signal, is_ptrace_stopped: false }; - self.stopped_unacked.store(true, Ordering::Release); + self.state.lock().transition_to_stopped(stop_signal, false); } /// Set the process to be stopped due to a ptrace event. @@ -401,11 +499,7 @@ impl Process { /// # Arguments /// * `signal` - Signal to report via waitpid (SIGTRAP or actual signal) pub fn set_ptrace_stopped(&self, signal: i32) { - *self.state.lock() = ProcessState::Stopped { - signal, - is_ptrace_stopped: true, - }; - self.stopped_unacked.store(true, Ordering::Release); + self.state.lock().transition_to_stopped(signal, true); } /// Check if the process is in a ptrace-stop state. @@ -414,10 +508,8 @@ impl Process { /// * `true` if process is stopped due to ptrace /// * `false` if running, signal-stopped, continued, or zombie pub fn is_ptrace_stopped(&self) -> bool { - matches!( - *self.state.lock(), - ProcessState::Stopped { is_ptrace_stopped: true, .. } - ) + let state = self.state.lock(); + matches!(state.kind, ProcessStateKind::Stopped { ptraced: true, .. }) } /// Check if the process is in a signal-stop state (not ptrace). @@ -426,10 +518,8 @@ impl Process { /// * `true` if process is stopped due to signal (SIGSTOP, etc.) /// * `false` if running, ptrace-stopped, continued, or zombie pub fn is_signal_stopped(&self) -> bool { - matches!( - *self.state.lock(), - ProcessState::Stopped { is_ptrace_stopped: false, .. } - ) + let state = self.state.lock(); + matches!(state.kind, ProcessStateKind::Stopped { ptraced: false, .. }) } /// Resume from ptrace-stop by transitioning back to Running state. @@ -438,11 +528,7 @@ impl Process { /// Unlike signal-stops which go through Continued state, ptrace /// resumes directly to Running. pub fn resume_from_ptrace_stop(&self) { - let mut state = self.state.lock(); - if matches!(*state, ProcessState::Stopped { is_ptrace_stopped: true, .. }) { - *state = ProcessState::Running; - self.stopped_unacked.store(false, Ordering::Release); - } + self.state.lock().transition_to_running(); } } @@ -480,13 +566,11 @@ impl Process { let process = Arc::new(Process { pid, - state: SpinNoIrq::new(ProcessState::Running), + state: SpinNoIrq::new(ProcessState::new_running()), tg: SpinNoIrq::new(ThreadGroup::default()), children: SpinNoIrq::new(StrongMap::new()), parent: SpinNoIrq::new(parent.as_ref().map(Arc::downgrade).unwrap_or_default()), group: SpinNoIrq::new(group.clone()), - continued_unacked: AtomicBool::new(false), - stopped_unacked: AtomicBool::new(false), }); group.processes.lock().insert(pid, &process); @@ -522,3 +606,119 @@ static INIT_PROC: LazyInit> = LazyInit::new(); pub fn init_proc() -> Arc { INIT_PROC.get().unwrap().clone() } + +impl ProcessState { + /// Creates a new `ProcessState` in the Running state, + /// with its `kind` to be `ProcessStateKind::Running`, + /// and its `flags` to be empty. + pub fn new_running() -> Self { + Self { + kind: ProcessStateKind::Running, + flags: ProcessStateFlags::empty(), + } + } + + /// Returns `true` if the state is Zombie. + pub fn is_zombie(&self) -> bool { + matches!(self.kind, ProcessStateKind::Zombie { .. }) + } + + /// Transitions the state to Stopped. + /// + /// This method updates the process state kind to `Stopped` with the + /// given signal and ptrace status. It also sets the `STOPPED_UNACKED` + /// flag, indicating that the parent has not yet acknowledged this stop + /// event (via `waitpid` with `WUNTRACED`). + /// + /// If the process is already a `Zombie`, this transition is ignored. + /// + /// # Arguments + /// * `signal` - The signal that caused the stop. + /// * `ptraced` - Whether the stop is due to ptrace.s + pub fn transition_to_stopped(&mut self, signal: i32, ptraced: bool) { + if self.is_zombie() { + return; + } + + self.kind = ProcessStateKind::Stopped { signal, ptraced }; + self.flags.insert(ProcessStateFlags::STOPPED_UNACKED); + } + + /// Transitions the process state from `Stopped` to `Running`. + /// + /// This method is called when a stopped process is resumed (e.g., via + /// `SIGCONT` or `PTRACE_CONT`). It updates the state kind to `Running` + /// and sets the `CONTINUED_UNACKED` flag, indicating that the parent + /// has not yet acknowledged this continuation (via `waitpid` with + /// `WCONTINUED`). + /// + /// It also clears the `STOPPED_UNACKED` flag, as the process is no longer + /// stopped, even if the parent may not be aware that there is a + /// stop-continue event happened. + /// + /// If the process is not currently `Stopped`, this method does nothing. + pub fn transition_to_running(&mut self) { + if let ProcessStateKind::Stopped { .. } = self.kind { + self.kind = ProcessStateKind::Running; + self.flags.insert(ProcessStateFlags::CONTINUED_UNACKED); + self.flags.remove(ProcessStateFlags::STOPPED_UNACKED); + } + } + + /// Transitions the process state to `Zombie`. + /// + /// This method is called when the process terminates. It updates the state + /// kind to `Zombie`, no matter what the previous state of the target + /// process is at. + /// + /// All state flags (e.g., `STOPPED_UNACKED`, `CONTINUED_UNACKED`) are + /// cleared, as they are no longer relevant for a dead process. + pub fn transition_to_zombie(&mut self, info: ZombieInfo) { + self.kind = ProcessStateKind::Zombie { info }; + self.flags = ProcessStateFlags::empty(); + } + + /// Attempts to consume the 'stopped' event for `waitpid(WUNTRACED)`. + /// + /// This method checks if the process is in the `Stopped` state and if the + /// `STOPPED_UNACKED` flag is set. If both are true, it: + /// 1. Clears the `STOPPED_UNACKED` flag (atomically consuming the event). + /// 2. Returns `Some(signal)` where `signal` is the signal that caused the + /// stop. + /// + /// If the process is not stopped, or if the event has already been consumed + /// (flag is clear), it returns `None`. + /// + /// This ensures that a stop event is reported exactly once to a parent + /// calling `waitpid`. + pub fn try_consume_stopped(&mut self) -> Option { + if self.flags.contains(ProcessStateFlags::STOPPED_UNACKED) { + if let ProcessStateKind::Stopped { signal, .. } = self.kind { + self.flags.remove(ProcessStateFlags::STOPPED_UNACKED); + return Some(signal); + } + } + None + } + + /// Attempts to consume the 'continued' event for `waitpid(WCONTINUED)`. + /// + /// This method checks if the process is in the `Running` state (which + /// implies it might have been continued) and if the `CONTINUED_UNACKED` + /// flag is set. If both are true, it: + /// 1. Clears the `CONTINUED_UNACKED` flag (atomically consuming the event). + /// 2. Returns `true`. + /// + /// If the process is not running, or if the event has already been consumed + /// (flag is clear), it returns `false`. + /// + /// This ensures that a continuation event is reported exactly once to a + /// parent calling `waitpid`. + pub fn try_consume_continued(&mut self) -> bool { + if self.flags.contains(ProcessStateFlags::CONTINUED_UNACKED) { + self.flags.remove(ProcessStateFlags::CONTINUED_UNACKED); + return true; + } + false + } +} From 4bc75de6c2c8c90d9ac2a46108b37f746fdba29e Mon Sep 17 00:00:00 2001 From: Haoze Wu Date: Tue, 25 Nov 2025 10:18:13 +0800 Subject: [PATCH 05/10] license: Updated licenses for starry-process. Format with cargo fmt --- LICENSES | 202 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- src/process.rs | 6 ++ tests/process.rs | 2 +- tests/session.rs | 5 +- 5 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 LICENSES diff --git a/LICENSES b/LICENSES new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSES @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c89a2c0..283137b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,6 @@ mod session; /// A process ID, also used as session ID, process group ID, and thread ID. pub type Pid = u32; -pub use process::{Process, ZombieInfo, init_proc, ProcessState}; +pub use process::{Process, ProcessState, ZombieInfo, init_proc}; pub use process_group::ProcessGroup; pub use session::Session; diff --git a/src/process.rs b/src/process.rs index aea6840..997b512 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,3 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2025 KylinSoft Co., Ltd. https://www.kylinos.cn/ +// Copyright (C) 2025 Azure-stars Azure_stars@126.com +// ...[and other authors] +// See LICENSES for license details. + //! Process state management implementation. //! //! # Architecture Overview diff --git a/tests/process.rs b/tests/process.rs index b91eb0d..c60af2d 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -70,4 +70,4 @@ fn thread_exit() { let last2 = child.exit_thread(102, 3); assert!(last2); assert_eq!(child.exit_code(), 7); -} \ No newline at end of file +} diff --git a/tests/session.rs b/tests/session.rs index 71269ee..73d1fa6 100644 --- a/tests/session.rs +++ b/tests/session.rs @@ -1,5 +1,4 @@ -use std::sync::Arc; -use std::any::Any; +use std::{any::Any, sync::Arc}; use starry_process::init_proc; @@ -129,4 +128,4 @@ fn terminal_set_unset() { assert!(session.unset_terminal(&term)); assert!(session.terminal().is_none()); -} \ No newline at end of file +} From 3a45c9157d5566aea855813ee1e01ffbbcfcbe32 Mon Sep 17 00:00:00 2001 From: Haoze Wu Date: Tue, 25 Nov 2025 11:16:03 +0800 Subject: [PATCH 06/10] fix(process): enhance process state transitions and tests \- Fix license header in by removing placeholder authors. \- Fix to accept to handle both signal and ptrace continuations correctly without code duplication. \- Fix lint in by using a match expression. \- Add comprehensive integration tests in for: \- Stop/Continue lifecycle (SIGSTOP/SIGCONT). \- Ptrace stop/resume (ensuring no continued event is generated). \- Exit with signal (verifying exit code and signal info). --- src/process.rs | 40 +++++++++++++----------- tests/process.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 18 deletions(-) diff --git a/src/process.rs b/src/process.rs index 997b512..8345e2a 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2025 KylinSoft Co., Ltd. https://www.kylinos.cn/ // Copyright (C) 2025 Azure-stars Azure_stars@126.com -// ...[and other authors] // See LICENSES for license details. //! Process state management implementation. @@ -380,7 +379,7 @@ impl Process { /// Updating the status of a process continued from stoppage pub fn continue_from_stop(&self) { - self.state.lock().transition_to_running(); + self.state.lock().transition_to_running(true); } /// Attempts to consume the 'continued' event for `waitpid(WCONTINUED)`. @@ -534,7 +533,7 @@ impl Process { /// Unlike signal-stops which go through Continued state, ptrace /// resumes directly to Running. pub fn resume_from_ptrace_stop(&self) { - self.state.lock().transition_to_running(); + self.state.lock().transition_to_running(false); } } @@ -599,7 +598,7 @@ impl Process { } /// Creates a child [`Process`]. - pub fn fork(self: &Arc, pid: Pid) -> Arc { + pub fn fork(self: &Arc, pid: Pid) -> Arc { Self::new(pid, Some(self.clone())) } } @@ -653,20 +652,23 @@ impl ProcessState { /// Transitions the process state from `Stopped` to `Running`. /// /// This method is called when a stopped process is resumed (e.g., via - /// `SIGCONT` or `PTRACE_CONT`). It updates the state kind to `Running` - /// and sets the `CONTINUED_UNACKED` flag, indicating that the parent - /// has not yet acknowledged this continuation (via `waitpid` with - /// `WCONTINUED`). + /// `SIGCONT` or `PTRACE_CONT`). It updates the state kind to `Running`. /// - /// It also clears the `STOPPED_UNACKED` flag, as the process is no longer - /// stopped, even if the parent may not be aware that there is a - /// stop-continue event happened. + /// If `with_continued_flag` is true, it sets the `CONTINUED_UNACKED` flag, + /// indicating that the parent has not yet acknowledged this continuation + /// (via `waitpid` with `WCONTINUED`). Ptrace resumes typically pass + /// `false`. /// - /// If the process is not currently `Stopped`, this method does nothing. - pub fn transition_to_running(&mut self) { + /// It also clears the `STOPPED_UNACKED` flag. + /// + /// # Arguments + /// * `with_continued_flag` - Whether to set the `CONTINUED_UNACKED` flag. + pub fn transition_to_running(&mut self, with_continued_flag: bool) { if let ProcessStateKind::Stopped { .. } = self.kind { self.kind = ProcessStateKind::Running; - self.flags.insert(ProcessStateFlags::CONTINUED_UNACKED); + if with_continued_flag { + self.flags.insert(ProcessStateFlags::CONTINUED_UNACKED); + } self.flags.remove(ProcessStateFlags::STOPPED_UNACKED); } } @@ -698,13 +700,15 @@ impl ProcessState { /// This ensures that a stop event is reported exactly once to a parent /// calling `waitpid`. pub fn try_consume_stopped(&mut self) -> Option { - if self.flags.contains(ProcessStateFlags::STOPPED_UNACKED) { - if let ProcessStateKind::Stopped { signal, .. } = self.kind { + match self.kind { + ProcessStateKind::Stopped { signal, .. } + if self.flags.contains(ProcessStateFlags::STOPPED_UNACKED) => + { self.flags.remove(ProcessStateFlags::STOPPED_UNACKED); - return Some(signal); + Some(signal) } + _ => None, } - None } /// Attempts to consume the 'continued' event for `waitpid(WCONTINUED)`. diff --git a/tests/process.rs b/tests/process.rs index c60af2d..9bb6390 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -71,3 +71,82 @@ fn thread_exit() { assert!(last2); assert_eq!(child.exit_code(), 7); } + +#[test] +fn test_stop_continue_integration() { + let parent = init_proc(); + let child = parent.new_child(); + + // Initial state + assert!(!child.is_stopped()); + assert!(!child.is_continued()); + + // Stop the process + let sig_stop = 19; // SIGSTOP + child.stop_by_signal(sig_stop); + + assert!(child.is_stopped()); + assert!(child.is_signal_stopped()); + assert!(!child.is_ptrace_stopped()); + + // Consume stopped event + assert_eq!(child.try_consume_stopped(), Some(sig_stop)); + + // Should be consumed now (still stopped, but event consumed) + assert!(child.is_stopped()); + assert_eq!(child.try_consume_stopped(), None); + + // Continue the process + child.continue_from_stop(); + + assert!(!child.is_stopped()); + assert!(child.is_continued()); + + // Consume continued event + assert!(child.try_consume_continued()); + + // Should be consumed now + assert!(!child.try_consume_continued()); +} + +#[test] +fn test_ptrace_integration() { + let parent = init_proc(); + let child = parent.new_child(); + + // Ptrace stop + let sig_trap = 5; // SIGTRAP + child.set_ptrace_stopped(sig_trap); + + assert!(child.is_stopped()); + assert!(child.is_ptrace_stopped()); + assert!(!child.is_signal_stopped()); + + // Ptrace stops are NOT consumed by standard waitpid (try_consume_stopped) + assert_eq!(child.try_consume_stopped(), None); + + // Resume from ptrace + child.resume_from_ptrace_stop(); + + assert!(!child.is_stopped()); + assert!(!child.is_ptrace_stopped()); + + // Resume from ptrace does NOT set continued flag + assert!(!child.is_continued()); +} + +#[test] +fn test_exit_signal_integration() { + let parent = init_proc(); + let child = parent.new_child(); + + let sig_kill = 9; // SIGKILL + child.exit_with_signal(sig_kill, false); + + assert!(child.is_zombie()); + + let info = child.get_zombie_info().expect("Should have zombie info"); + assert_eq!(info.signal, Some(sig_kill)); + assert_eq!(info.exit_code.as_raw(), 128 + sig_kill); + assert!(!info.core_dumped); +} From b9ee11d035a370674d37ec7b58433242dbc622e8 Mon Sep 17 00:00:00 2001 From: Haoze Wu Date: Tue, 25 Nov 2025 14:25:46 +0800 Subject: [PATCH 07/10] fix: fix incorrect author info, typos, and comments, removed redundant code \- Correct license header in with correct author information. \- Export in . \- Remove redundant check in as it is implied by the state. \- Fix various typos in documentation comments (e.g., stoppade, ptrace.s). \- Correct documentation for to accurately reflect that it checks for unacked continuations. --- src/lib.rs | 2 +- src/process.rs | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 283137b..720cdda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,6 @@ mod session; /// A process ID, also used as session ID, process group ID, and thread ID. pub type Pid = u32; -pub use process::{Process, ProcessState, ZombieInfo, init_proc}; +pub use process::{ExitCode, Process, ZombieInfo, init_proc}; pub use process_group::ProcessGroup; pub use session::Session; diff --git a/src/process.rs b/src/process.rs index 8345e2a..aa22bd6 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2025 KylinSoft Co., Ltd. https://www.kylinos.cn/ -// Copyright (C) 2025 Azure-stars Azure_stars@126.com +// Copyright (C) 2025 朝倉水希 asakuramizu111@gmail.com // See LICENSES for license details. //! Process state management implementation. @@ -105,13 +105,13 @@ pub(crate) struct ThreadGroup { /// /// We create three states for process, `Running`, `Stopped`, and `Zombie`. /// -/// For a `Running` process, it can be actually running(if the +/// For a `Running` process, it can be actually running (if the /// `ProcessStateFlags` is empty) or it can be just `Continued` from a stoppage -/// but not acked by its parent(if the `ProcessStateFlags` contain +/// but not acked by its parent (if the `ProcessStateFlags` contain /// `CONTINUED_UNACKED`). /// /// For a `Stopped` process, if its stoppage has not been acked by its parent, -/// i.e., the parent has not been notified for the child's stoppade, the +/// i.e., the parent has not been notified for the child's stoppage, the /// corresponding `ProcessStateFlags` will be marked as `STOPPED_UNACKED`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProcessStateKind { @@ -350,7 +350,7 @@ impl Process { self.state.lock().is_zombie() } - /// Get the information of the process ig it is a zombie + /// Get the information of the process if it is a zombie pub fn get_zombie_info(&self) -> Option { if let ProcessStateKind::Zombie { info } = self.state.lock().kind { Some(info) @@ -365,12 +365,10 @@ impl Process { pub fn is_stopped(&self) -> bool { let state = self.state.lock(); matches!(state.kind, ProcessStateKind::Stopped { .. }) - || state.flags.contains(ProcessStateFlags::STOPPED_UNACKED) } /// Check whether the process has continued from the stoppage, - /// including both the case which the process just continued without acked - /// by its parent and already acked by its parent + /// but its continuation has not been acked by its parent pub fn is_continued(&self) -> bool { let state = self.state.lock(); matches!(state.kind, ProcessStateKind::Running) @@ -639,7 +637,7 @@ impl ProcessState { /// /// # Arguments /// * `signal` - The signal that caused the stop. - /// * `ptraced` - Whether the stop is due to ptrace.s + /// * `ptraced` - Whether the stop is due to ptrace. pub fn transition_to_stopped(&mut self, signal: i32, ptraced: bool) { if self.is_zombie() { return; From 4c371b60435f2b7bd84122e4c55009e7e8c2fa61 Mon Sep 17 00:00:00 2001 From: Haoze Wu Date: Tue, 25 Nov 2025 14:37:27 +0800 Subject: [PATCH 08/10] fix: update comments --- src/process.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/process.rs b/src/process.rs index aa22bd6..b17e2c4 100644 --- a/src/process.rs +++ b/src/process.rs @@ -457,7 +457,7 @@ impl Process { /// Child processes are inherited by the init process or by the nearest /// subreaper process. /// - /// This method panics if the [`Process`] is the init process. + /// This method silently returns if the [`Process`] is the init process. pub fn exit_with_signal(self: &Arc, signal: i32, core_dumped: bool) { let reaper = INIT_PROC.get().unwrap(); @@ -487,7 +487,7 @@ impl Process { } /// Stops the [`Process`], marking it as stopped when a signal stops - /// it(majorly SIGSTOP) + /// it (majorly SIGSTOP) pub fn stop_by_signal(&self, stop_signal: i32) { self.state.lock().transition_to_stopped(stop_signal, false); } From e1f70e96864d4326a3d0d113d5862b40eef84830 Mon Sep 17 00:00:00 2001 From: Haoze Wu Date: Tue, 25 Nov 2025 14:38:30 +0800 Subject: [PATCH 09/10] fix: update comments --- src/process.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/process.rs b/src/process.rs index b17e2c4..4decb4e 100644 --- a/src/process.rs +++ b/src/process.rs @@ -431,7 +431,7 @@ impl Process { /// Child processes are inherited by the init process or by the nearest /// subreaper process. /// - /// This method panics if the [`Process`] is the init process. + /// This method silently returns if the [`Process`] is the init process. pub fn exit(self: &Arc) { // TODO: child subreaper let reaper = INIT_PROC.get().unwrap(); From ad525db6e473120d0f2df9292e9bbcf525481696 Mon Sep 17 00:00:00 2001 From: Haoze Wu Date: Wed, 26 Nov 2025 09:43:01 +0800 Subject: [PATCH 10/10] feat: updated Process State management to preserve raw info and todo for future improvements. --- src/lib.rs | 2 +- src/process.rs | 82 ++++++++++++++++++++++++++++++------------------ tests/process.rs | 16 +++++++++- 3 files changed, 67 insertions(+), 33 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 720cdda..4826ca7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,6 @@ mod session; /// A process ID, also used as session ID, process group ID, and thread ID. pub type Pid = u32; -pub use process::{ExitCode, Process, ZombieInfo, init_proc}; +pub use process::{Process, ZombieInfo, init_proc}; pub use process_group::ProcessGroup; pub use session::Session; diff --git a/src/process.rs b/src/process.rs index 4decb4e..e81388b 100644 --- a/src/process.rs +++ b/src/process.rs @@ -133,37 +133,11 @@ bitflags! { } } -/// The exit code value following POSIX conventions. -/// -/// This type encapsulates the numeric exit code that is reported to the parent -/// process via `waitpid`. The encoding follows POSIX standards: -/// - Normal exit: Exit code 0-255 directly -/// - Signal termination: Exit code = 128 + signal number -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ExitCode(i32); - -impl ExitCode { - /// Creates an exit code from a normal exit. - pub fn from_code(code: i32) -> Self { - Self(code) - } - - /// Creates an exit code from signal termination (128 + signal). - pub fn from_signal(signal: i32) -> Self { - Self(128 + signal) - } - - /// Returns the raw exit code value. - pub fn as_raw(self) -> i32 { - self.0 - } -} - /// Information about a zombie (terminated) process. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ZombieInfo { - /// The exit code value. - pub exit_code: ExitCode, + /// The exit code value passed to exit(). + pub exit_code: i32, /// The signal that terminated the process, if any. pub signal: Option, /// Whether a core dump was produced. @@ -178,6 +152,8 @@ pub struct ProcessState { } /// A process. +// TODO: Optimize Process struct for dead processes (fat zombie now). +// TODO: O(N) wait: Polling children is inefficient. May Use WaitQueue. pub struct Process { pid: Pid, state: SpinNoIrq, @@ -375,6 +351,12 @@ impl Process { && state.flags.contains(ProcessStateFlags::CONTINUED_UNACKED) } + /// Returns the POSIX wait status word for the current state if there is a + /// reportable event. + pub fn wait_status(&self) -> Option { + self.state.lock().wait_status() + } + /// Updating the status of a process continued from stoppage pub fn continue_from_stop(&self) { self.state.lock().transition_to_running(true); @@ -415,6 +397,7 @@ impl Process { /// /// This is called when a process exits to ensure orphaned children are /// reparented to init. + // TODO: Reparenting is O(N) and locks global init. fn reaper_children(children: &mut StrongMap>) { let reaper = INIT_PROC.get().unwrap(); let mut reaper_children = reaper.children.lock(); @@ -441,9 +424,11 @@ impl Process { } let mut children = self.children.lock(); + // We now simply save the exit code and signal, + // and let the wait system call handle the rest let code = self.tg.lock().exit_code; self.state.lock().transition_to_zombie(ZombieInfo { - exit_code: ExitCode::from_code(code), + exit_code: code, signal: None, core_dumped: false, }); @@ -464,10 +449,12 @@ impl Process { if Arc::ptr_eq(self, reaper) { return; } - + // We now simply save the exit code and signal, + // and let the wait system call handle the rest + let code = self.tg.lock().exit_code; let mut children = self.children.lock(); self.state.lock().transition_to_zombie(ZombieInfo { - exit_code: ExitCode::from_signal(signal), + exit_code: code, signal: Some(signal), core_dumped, }); @@ -729,4 +716,37 @@ impl ProcessState { } false } + + /// Returns the POSIX wait status word for the current state if there is a + /// reportable event. + /// + /// - Stopped + STOPPED_UNACKED: Returns `(signal << 8) | 0x7f` + /// - Running + CONTINUED_UNACKED: Returns `0xffff` + /// - Zombie: Returns encoded exit status per POSIX + /// - Otherwise: Returns `None` + pub fn wait_status(&self) -> Option { + match self.kind { + ProcessStateKind::Stopped { signal, .. } + if self.flags.contains(ProcessStateFlags::STOPPED_UNACKED) => + { + Some((signal << 8) | 0x7f) + } + ProcessStateKind::Running + if self.flags.contains(ProcessStateFlags::CONTINUED_UNACKED) => + { + Some(0xffff) + } + ProcessStateKind::Zombie { info } => { + if let Some(sig) = info.signal { + // WIFSIGNALED: Bits 0-6 are signal, Bit 7 is core dump. + let core_bit = if info.core_dumped { 0x80 } else { 0 }; + Some((sig & 0x7f) | core_bit) + } else { + // WIFEXITED: Bits 8-15 are exit code. + Some((info.exit_code & 0xff) << 8) + } + } + _ => None, + } + } } diff --git a/tests/process.rs b/tests/process.rs index 9bb6390..c378233 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -19,6 +19,8 @@ fn exit() { let child = parent.new_child(); child.exit(); assert!(child.is_zombie()); + // Normal exit with code 0 -> status 0 + assert_eq!(child.wait_status(), Some(0)); assert!(parent.children().iter().any(|c| Arc::ptr_eq(c, &child))); } @@ -69,7 +71,10 @@ fn thread_exit() { let last2 = child.exit_thread(102, 3); assert!(last2); + child.exit(); assert_eq!(child.exit_code(), 7); + // Exit code 7 -> status (7 << 8) = 0x0700 + assert_eq!(child.wait_status(), Some(0x0700)); } #[test] @@ -90,6 +95,8 @@ fn test_stop_continue_integration() { assert!(!child.is_ptrace_stopped()); // Consume stopped event + // POSIX stopped status: (sig << 8) | 0x7f + assert_eq!(child.wait_status(), Some((sig_stop << 8) | 0x7f)); assert_eq!(child.try_consume_stopped(), Some(sig_stop)); // Should be consumed now (still stopped, but event consumed) @@ -103,6 +110,8 @@ fn test_stop_continue_integration() { assert!(child.is_continued()); // Consume continued event + // POSIX continued status: 0xffff + assert_eq!(child.wait_status(), Some(0xffff)); assert!(child.try_consume_continued()); // Should be consumed now @@ -147,6 +156,11 @@ fn test_exit_signal_integration() { let info = child.get_zombie_info().expect("Should have zombie info"); assert_eq!(info.signal, Some(sig_kill)); - assert_eq!(info.exit_code.as_raw(), 128 + sig_kill); + assert_eq!(info.exit_code, 0); // Exit code is 0 for signal termination assert!(!info.core_dumped); + + // Verify POSIX wait status manufacturing + let status = child.wait_status().expect("Should have wait status"); + assert_eq!(status & 0x7f, sig_kill); + assert_eq!(status & 0x80, 0); // No core dump }