process_tools 0.32.0

Collection of algorithms and structures to handle processes properly.
Documentation
# Extract process management utilities from wplan

## Execution State

- **Executor Type:** any
- **Actor:** null
- **Claimed At:** null
- **Status:** ✅ (Done)
- **Validated By:** Level 3 verification (nextest + doc tests + clippy)
- **Validation Date:** 2026-04-17

## Goal

Three sites in the wplan ecosystem independently implement process lifecycle checks, signal name mapping, and daemonization — all platform-specific code that is duplicated and tested per-project rather than centralized (Motivated: duplicated platform knowledge is a maintenance hazard — a fix in one site is missed in others, and new wTools consumers must re-derive the same platform abstractions; Observable: `process_tools::lifecycle` module exports `is_process_alive`, `signal_name`, and `daemonize` with cross-platform implementations; Scoped: add one new `lifecycle` module tree to `process_tools/src/` with three submodules `check`, `signal`, `daemon`, plus tests in `process_tools/tests/`; Testable: `is_process_alive(std::process::id() as i32)` returns `Ok(true)`, `signal_name(9)` returns `"SIGKILL"`, all tests pass on Unix).

## In Scope

- New `lifecycle` module tree in `process_tools/src/lifecycle/`:
  - `check.rs` — `is_process_alive(pid) -> io::Result<bool>`, `wait_for_exit(pid, timeout) -> io::Result<()>`, `is_pidfile_alive(path) -> io::Result<bool>`
  - `signal.rs` — `signal_name(i32) -> &'static str`, `signal_number(&str) -> Option<i32>`, `all_signals() -> Vec<(i32, &str, &str)>`
  - `daemon.rs` — `DaemonizeOptions`, `daemonize(&opts) -> io::Result<()>`, `write_pidfile(path)`, `read_pidfile(path)`, `remove_pidfile(path)` (Unix only via `#[cfg(unix)]`)
- Register module via `layer lifecycle;` in `process_tools/src/lib.rs` mod_interface block
- Platform-specific implementations: Unix (`kill(pid, 0)`, fork/setsid) and Windows (`OpenProcess`) where applicable
- Tests in `process_tools/tests/lifecycle_test.rs`

## Out of Scope

- Consumer migration (wplan, wplan_client) — covered by follow-up adoption tasks
- Async process management
- Interactive stdin handling
- Signal sending (only mapping names ↔ numbers)
- Windows daemonization (Windows Services API is a different paradigm)
- PTY spawning or terminal management

## Requirements

- All work must strictly adhere to all applicable rulebooks (discover via `kbase .rulebooks`)
- Module follows existing `process_tools` patterns: `mod_interface` with `layer` keyword, `mod private {}` block
- Custom codestyle per `code_style.rulebook.md` — 2-space indents, no `cargo fmt`
- Tests in `process_tools/tests/` directory — no `#[cfg(test)]` in src
- No mocking — test real process behavior
- New dependency: `libc` (workspace) for Unix syscalls
- Conditional dependency: `windows` (workspace, `Win32_System_Threading` feature) for Windows process checks
- **CRITICAL**: This task is INCOMPLETE without follow-up adoption — extraction without migration leaves duplication worse (two copies instead of one)

## Work Procedure

Execute in order. Do not skip or reorder steps.

1. **Read rulebooks** — `kbase .rulebooks`; note code style, mod_interface patterns, test organization constraints
2. **Add dependencies** — add `libc` (workspace) and conditional `windows` dependency to `process_tools/Cargo.toml`
3. **Create check submodule** — `process_tools/src/lifecycle/check.rs` with `mod private {}` containing `is_process_alive`, `wait_for_exit`, `is_pidfile_alive`; use `#[cfg(unix)]` and `#[cfg(windows)]` inside function bodies only
4. **Create signal submodule** — `process_tools/src/lifecycle/signal.rs` with `mod private {}` containing `signal_name`, `signal_number`, `all_signals`
5. **Create daemon submodule** — `process_tools/src/lifecycle/daemon.rs` with `mod private {}` containing `DaemonizeOptions`, `daemonize`, PID file utilities; entire module gated with `#[cfg(unix)]`
6. **Create lifecycle module** — `process_tools/src/lifecycle/mod.rs` using `mod_interface!` to aggregate check, signal, daemon submodules
7. **Register module** — add `layer lifecycle;` to the `mod_interface!` block in `process_tools/src/lib.rs`
8. **Write tests** — create `process_tools/tests/lifecycle_test.rs` covering T01–T12 from Test Matrix
9. **Verify** — `cargo test -p process_tools --all-features` passes in wtools workspace
10. **Walk Validation Checklist** — verify every item answers YES

## Test Matrix

| # | Input Scenario | Config Under Test | Expected Behavior |
|---|---------------|-------------------|-------------------|
| T01 | `is_process_alive(std::process::id() as i32)` | Unix and Windows | `Ok(true)` — current process is alive |
| T02 | `is_process_alive(-1)` | Unix and Windows | `Ok(false)` or `Err(_)` — invalid PID |
| T03 | `is_process_alive(999999)` | Unix | `Ok(false)` — nonexistent PID |
| T04 | `signal_name(9)` | Unix | `"SIGKILL"` |
| T05 | `signal_name(15)` | Unix | `"SIGTERM"` |
| T06 | `signal_name(2)` | Unix | `"SIGINT"` |
| T07 | `signal_name(999)` | Unix | `"UNKNOWN"` |
| T08 | `signal_number("SIGKILL")` | Unix | `Some(9)` |
| T09 | `signal_number("UNKNOWN")` | Unix | `None` |
| T10 | `all_signals()` | Unix | Non-empty vec with at least SIGHUP, SIGINT, SIGTERM, SIGKILL |
| T11 | `write_pidfile` + `read_pidfile` + `remove_pidfile` | Unix | Round-trip PID file lifecycle succeeds |
| T12 | `wait_for_exit` with already-dead PID | Unix | `Ok(())` — returns immediately |

## Acceptance Criteria

- `process_tools` crate exports `lifecycle` module with `is_process_alive`, `wait_for_exit`, `signal_name`, `signal_number`, `all_signals`
- `is_process_alive(std::process::id() as i32)` returns `Ok(true)` on current platform
- `signal_name(9)` returns `"SIGKILL"` on Unix
- PID file round-trip (write/read/remove) works correctly
- Daemon submodule compiles and is gated to Unix only
- `cargo test -p process_tools --all-features` passes with zero failures

## Validation

### Checklist

Desired answer for every question is YES.

**API Surface**
- [x] C1 — Does `process_tools/src/lifecycle/` directory exist with `check.rs`, `signal.rs`, `daemon.rs`, `mod.rs`?
- [x] C2 — Does `is_process_alive(pid: i32) -> io::Result<bool>` exist with cfg-gated internals?
- [x] C3 — Does `wait_for_exit(pid, timeout)` exist with polling implementation?
- [x] C4 — Do `signal_name` and `signal_number` exist with bidirectional mapping?
- [x] C5 — Does `daemonize(&DaemonizeOptions) -> io::Result<()>` exist gated to `#[cfg(unix)]`?
- [x] C6 — Do PID file utilities (`write_pidfile`, `read_pidfile`, `remove_pidfile`) exist?
- [x] C7 — Is the module registered as `layer lifecycle;` in `lib.rs` mod_interface block?

**Tests**
- [x] C8 — Does `process_tools/tests/lifecycle_check_test.rs` (and sibling files) exist?
- [x] C9 — Does it cover all 12 test matrix scenarios (T01–T12) plus 45 corner cases?
- [x] C10 — Does `cargo test -p process_tools --all-features` pass with zero failures?

**Code Quality**
- [x] C11 — Does the module use 2-space indents (custom codestyle)?
- [x] C12 — Are there zero `#[cfg(test)]` blocks in `src/lifecycle/*.rs`?
- [x] C13 — Is platform-specific code confined to `#[cfg]` blocks inside function bodies (check, signal) or at module level (daemon)?

### Measurements

- Test count: 102 nextest tests + 17 doc tests (far exceeds minimum 12)
- Platform coverage: Unix fully tested, Windows compilation verified
- API surface: 3 submodules, 11 public functions + 1 struct

### Invariants

- `is_process_alive(std::process::id() as i32)` always returns `Ok(true)` regardless of platform
- `signal_name(signal_number(name).unwrap())` round-trips for all standard signal names
- PID file write + read returns the same PID value

### Anti-Faking

- Tests must call real OS syscalls (no mocking `kill` or `OpenProcess`)
- `is_process_alive` test uses actual current process PID, not a hardcoded value
- Signal mapping tests verify specific known POSIX signal numbers, not just "returns something"
- PID file tests use real filesystem operations with temp directories

## Outcomes

- Created `src/lifecycle/` module tree with three submodules: `signal.rs`, `check.rs`, `daemon.rs`
- `signal.rs`: 25 POSIX signals bidirectional mapping via single `SIGNALS` const array (3 functions)
- `check.rs`: `is_process_alive` via `libc::kill(pid, 0)` with correct ESRCH/EPERM handling, `wait_for_exit` with 50ms polling, `is_pidfile_alive` combining file read + alive check (3 functions)
- `daemon.rs`: double-fork daemonization with all 5 wplan pitfalls addressed (flock singleton, lock-before-truncate, fd redirect not close, inherited fd cleanup), `DaemonizeOptions` with `#[derive(Former)]`, PID file utilities (5 functions + 1 struct, Unix-only)
- 16 automated tests covering T01-T12 matrix + 45 additional corner case tests (total 102 nextest + 17 doc)
- 8 manual test scenarios documented for daemon and process check verification
- `unsafe-code` override via per-function `#[allow(unsafe_code)]` (Cargo.toml `[lints.rust]` override incompatible with `workspace = true`)
- Level 3 verification: 102 nextest + 17 doc tests + clippy clean (0 errors)
- `/test_manual` completed 2026-04-17: 45 corner cases added, 1 doc issue fixed (exit_status range pitfall)
- `/test_clean` completed 2026-04-17: all 3 impacted crates verified clean (process_tools, test_tools, willbe)

## Source Analysis

### Process Alive Check

**Current Location**: `wplan/src/daemon_routines.rs:99-107`

```rust
pub fn is_process_alive( pid : i32 ) -> bool
{
  unsafe
  {
    // kill( pid, 0 ) doesn't send a signal, just checks if process exists
    libc::kill( pid, 0 ) == 0
  }
}
```

**Why This Works**:
- `kill(pid, 0)` is a Unix idiom for "check if process exists"
- Returns 0 if process alive and we have permission
- Returns -1 with `ESRCH` if process doesn't exist
- Returns -1 with `EPERM` if process exists but we lack permission

**Use Cases**: Daemon management, test teardown, PID file validation, service health checks

### Signal Name Mapping

**Current Location**: `wplan_client/src/cli/formatting.rs:981-1011`

```rust
pub fn signal_name( signal : i32 ) -> &'static str
{
  match signal
  {
    1 => "SIGHUP",
    2 => "SIGINT",
    3 => "SIGQUIT",
    6 => "SIGABRT",
    9 => "SIGKILL",
    14 => "SIGALRM",
    15 => "SIGTERM",
    _ => "UNKNOWN",
  }
}
```

**Use Cases**: Displaying process exit status, log messages about killed processes, error reporting, test output

### Daemon Management (Fork/Setsid)

**Current Location**: `wplan/src/daemon_routines.rs:150-200`

**Functionality**: Fork to background, call `setsid()` for new session, redirect stdio to /dev/null, write PID file

**Use Cases**: Server/daemon applications, background task runners, service management

## Platform Considerations

### Unix
- `kill(pid, 0)` for process checks
- Standard POSIX signals (full mapping)
- Full daemonization support (fork + setsid + chdir + stdio redirect)

### Windows
- `OpenProcess` + `CloseHandle` for process checks
- Limited signal support (SIGINT, SIGTERM via Ctrl+C/Break)
- No daemonization (Windows Services API is a different paradigm)

### Cross-Platform Strategy
- Use `#[cfg(unix)]` and `#[cfg(windows)]` inside function bodies for check/signal modules
- Gate entire daemon module with `#[cfg(unix)]`
- Document platform limitations clearly in function-level docs

## Dependencies

```toml
# process_tools/Cargo.toml additions
[dependencies]
libc = { workspace = true }

[target.'cfg(windows)'.dependencies]
windows = { workspace = true, features = ["Win32_System_Threading"] }
```

## Cross-References

- **Follow-up adoption** (CRITICAL — extraction without migration leaves duplication):
  - `wplan_client/task/006_adopt_process_utilities_from_process_tools.md`
  - `wplan/task/086_adopt_process_utilities_from_process_tools.md`
- **Source files**:
  - `/home/user1/pro/lib/willbe/module/wplan/src/daemon_routines.rs:99-107` (is_process_alive)
  - `/home/user1/pro/lib/willbe/module/wplan/src/daemon_routines.rs:150-200` (daemonize)
  - `/home/user1/pro/lib/willbe/module/wplan_client/src/cli/formatting.rs:981-1011` (signal_name)
- **Related consumers**: wtest (test harness), benchkit (benchmark isolation), willbe (background builds)