Skip to main content

Module process

Module process 

Source
Expand description

Best-effort child-process termination, by PID.

Before this module, the same two operations were implemented five times across four crates — agentmux-srv (twice), agentmux-cef, agentmux-bashwrap — one of which described itself as a “Mirror of the cef-side helper” (docs/reports/REPORT_DRY_AND_MODULARITY_AUDIT_2026_09_06.md §2.2). Only one of the five suppressed the console flash that taskkill otherwise causes from a GUI-subsystem parent; here every Windows path does.

This is not process_tracker. That module owns tracked agent process trees (Job Object / cgroup / pgid) and is the right tool when the child was assigned to a tracker at spawn. These helpers are for the untracked cases — a one-shot login CLI, a shell node, a pager left behind by a hook — where all the caller has is a PID.

Every function is scoped strictly to the given PID (and, on Windows, its tree via /T) — never by image name. Killing by image name is the cross-instance hazard CLAUDE.md bans outright.

§The kill(2) argument ranges, and why the guards are what they are

Callers hand us a u32; POSIX kill(2) takes a signed pid_t, and its argument is not simply “a process id”. Every range means something different:

pid argumentkill(2) signals…
> 0exactly that one process
== 0every process in the caller’s own process group
== -1every process the caller may signal — a system-wide broadcast
< -1every process in process group -pid

Three successive ReAgent P1s on PR #3033 each caught this module hitting one of the non-literal rows through an innocent-looking u32:

  1. u32::MAX - 1 cast with as wraps to -2 → row four, group 2.
  2. 0 → row two, the caller’s own group (the test runner).
  3. 1, negated for the group call → -1 → row three, the broadcast.

So the guards are derived from the table, not added one hole at a time:

  • [checked_pid] refuses 0 and anything that does not fit pid_t, which is exactly the set of inputs that could reach rows two and four from the single-PID path. Every pid_t that reaches kill_pid is strictly positive → row one only.
  • signal_process_group (and kill_process_group and force_kill_tree, which go through it on Unix) additionally requires pgid >= 2, because it negates: -1 would be row three, and -0 is 0, row two. Every value it passes to kill(2) is <= -2 → row four only, and never the broadcast.

Real PIDs are never 0 or 1 for anything this module should touch (1 is init/systemd), and never approach i32::MAX on any supported platform (Linux pid_max caps at 4194304; Windows PIDs are small multiples of 4) — so the guards cost nothing in practice and close the whole class, not just the three instances that were reported.

Constants§

SRV_APP_EXIT_CAP
The cap on srv’s app-exit teardown (agent_teardown::app_exit: every agent closed, then leftover shells swept). Shared so the launcher’s wait for srv (SRV_EXIT_WAIT) stays above it.
SRV_EXIT_WAIT
How long the launcher waits for srv to exit on its own after telling it to (stdin closed; on Unix also SIGTERM) before force-killing it: on every quit and on an upgrade (upgrade::quiesce_srv). Above SRV_APP_EXIT_CAP with room for srv’s own exit, so a force-kill never lands mid-teardown. srv usually exits well before it.

Functions§

force_kill_tree
Kill a process and everything it spawned at once, with no grace: SIGKILL to the process group led by pid on Unix (the child must be a group or session leader), taskkill /F /T /PID on Windows. For a caller that has already asked nicely, or never can (bashwrap’s idle kill of a wrapped command). The guards are signal_process_group’s on Unix and kill_pid’s on Windows; the result is for logging, since “already gone” is an error too.
kill_pid
Terminate one process by PID — SIGTERM on Unix; taskkill /F /T /PID on Windows (which also takes the tree, since Windows has no process-group signal).
kill_process_group
Terminate a process group led by pid, with escalation: SIGTERM to the group, a short grace, then SIGKILL to the group. On Windows the same taskkill /F /T /PID as kill_pid — there is no group signal, and /T already takes the tree.
signal_process_group
Send one signal to the process group led by pid, with no grace and no escalation: the building block of kill_process_group, for callers that pick their own signal and timing (the launcher’s teardown backstop). Same guards: 0, 1 and anything out of range for pid_t are refused with InvalidInput and nothing is sent. An already-empty group is an ESRCH error, which callers usually ignore.