fix(code-runtime-python): treat an absent start-time reading as reaped, not recycled

The PID-reuse guard refused to signal whenever the current reading differed
from the one taken at spawn, including when it was ABSENT. On Linux a reaped
leader has no /proc/<pid>/stat, so every teardown after the leader exited
skipped SIGTERM/SIGKILL while the group it led still held survivors -- the
exact case the process-group teardown exists to reap. Three same-group survivor
tests went red on the coverage lane; they pass on Darwin because the reader
always returns undefined there, leaving the guard inert.

Only a present-and-different reading now blocks the signal. Verified on the
self-hosted Linux box: a reaped leader with live survivors allows the signal, a
pid whose start time differs still blocks it, and a live matching process is
signalled.
This commit is contained in:
Chinesezjc
2026-08-31 14:30:02 +08:00
committed by Tianyi Cui
parent 33318a5767
commit 2ad93da755
2 changed files with 19 additions and 4 deletions
@@ -1416,9 +1416,18 @@ export class PythonCodeRuntime extends CodeRuntime {
if (child.pid === undefined) return
// A pid alone cannot answer this: `process.kill(pid, 0)` succeeds just
// as well for a REPLACEMENT process holding the recycled number. Only
// the start time distinguishes the two, so a reading that no longer
// matches means the group is not this run's and must not be signalled.
if (leaderStarted !== undefined && readProcessStart(child.pid) !== leaderStarted) return
// the start time distinguishes the two, so a reading that DISAGREES
// means the number now belongs to another process and must not be
// signalled.
//
// An ABSENT reading is the ordinary case, not a mismatch: once the
// leader is reaped its `/proc/<pid>/stat` is gone, while the group it
// led can still hold survivors that this teardown exists to reap. So
// only a present-and-different reading blocks the signal; undefined
// falls through, which is also the behavior on platforms with no
// `/proc` to read.
const nowStarted = readProcessStart(child.pid)
if (leaderStarted !== undefined && nowStarted !== undefined && nowStarted !== leaderStarted) return
process.kill(-child.pid, sig)
} catch {
// ESRCH — the process already died. Nothing to do.