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/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 d535ca4..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::{Process, 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 3ccff29..e81388b 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,13 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2025 KylinSoft Co., Ltd. https://www.kylinos.cn/ +// Copyright (C) 2025 朝倉水希 asakuramizu111@gmail.com +// See LICENSES for license details. + +//! 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,10 +101,62 @@ pub(crate) struct ThreadGroup { pub(crate) group_exited: bool, } +/// 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 stoppage, 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; + } +} + +/// Information about a zombie (terminated) process. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ZombieInfo { + /// 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. + pub core_dumped: bool, +} + +/// The full process state machine. +#[derive(Debug, Clone, Copy)] +pub struct ProcessState { + kind: ProcessStateKind, + flags: ProcessStateFlags, +} + /// 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, - is_zombie: AtomicBool, + state: SpinNoIrq, pub(crate) tg: SpinNoIrq, // TODO: child subreaper9 @@ -187,11 +319,94 @@ 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) + self.state.lock().is_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) + } else { + None + } + } + + /// 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 { + let state = self.state.lock(); + matches!(state.kind, ProcessStateKind::Stopped { .. }) + } + + /// Check whether the process has continued from the stoppage, + /// 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) + && 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); + } + + /// Attempts to consume the 'continued' event for `waitpid(WCONTINUED)`. + /// + /// 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() + } + + /// 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.kind, ProcessStateKind::Stopped { ptraced: true, .. }) { + return None; + } + state.try_consume_stopped() + } + + /// 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. + // 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(); + 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); + } } /// Terminates the [`Process`], marking it as a zombie process. @@ -199,7 +414,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(); @@ -208,16 +423,43 @@ impl Process { return; } - let mut children = self.children.lock(); // Acquire the lock first - self.is_zombie.store(true, Ordering::Release); + 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: code, + signal: None, + core_dumped: false, + }); - let mut reaper_children = reaper.children.lock(); - let reaper = Arc::downgrade(reaper); + Self::reaper_children(&mut children); + } - for (pid, child) in core::mem::take(&mut *children) { - *child.parent.lock() = reaper.clone(); - reaper_children.insert(pid, child); + /// 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 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(); + + 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: code, + signal: Some(signal), + core_dumped, + }); + + Self::reaper_children(&mut children); } /// Frees a zombie [`Process`]. Removes it from the parent. @@ -230,6 +472,54 @@ impl Process { parent.children.lock().remove(&self.pid); } } + + /// 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().transition_to_stopped(stop_signal, false); + } + + /// 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().transition_to_stopped(signal, true); + } + + /// 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 { + let state = self.state.lock(); + matches!(state.kind, ProcessStateKind::Stopped { ptraced: 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 { + let state = self.state.lock(); + matches!(state.kind, ProcessStateKind::Stopped { ptraced: 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) { + self.state.lock().transition_to_running(false); + } } impl fmt::Debug for Process { @@ -266,7 +556,7 @@ impl Process { let process = Arc::new(Process { pid, - is_zombie: AtomicBool::new(false), + 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()), @@ -293,7 +583,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())) } } @@ -306,3 +596,157 @@ 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. + 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`. + /// + /// 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`. + /// + /// 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; + if with_continued_flag { + 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 { + match self.kind { + ProcessStateKind::Stopped { signal, .. } + if self.flags.contains(ProcessStateFlags::STOPPED_UNACKED) => + { + self.flags.remove(ProcessStateFlags::STOPPED_UNACKED); + 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 + } + + /// 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 b91eb0d..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,5 +71,96 @@ fn thread_exit() { let last2 = child.exit_thread(102, 3); assert!(last2); + child.exit(); assert_eq!(child.exit_code(), 7); -} \ No newline at end of file + // Exit code 7 -> status (7 << 8) = 0x0700 + assert_eq!(child.wait_status(), Some(0x0700)); +} + +#[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 + // 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) + 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 + // POSIX continued status: 0xffff + assert_eq!(child.wait_status(), Some(0xffff)); + 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, 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 +} 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 +}