From 2ad93da755fbdb893ce4b9b58a565f733d443c30 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Tue, 18 Aug 2026 16:32:02 +0800 Subject: [PATCH] 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//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. --- .../code-runtime/code-runtime-python/src/index.ts | 15 ++++++++++++--- .../code-runtime-python/tests/runtime.spec.ts | 8 +++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/code-runtime/code-runtime-python/src/index.ts b/packages/code-runtime/code-runtime-python/src/index.ts index 1ab308a163..604cc420b9 100644 --- a/packages/code-runtime/code-runtime-python/src/index.ts +++ b/packages/code-runtime/code-runtime-python/src/index.ts @@ -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//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. diff --git a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts index b3a46cee21..a674056ece 100644 --- a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts @@ -399,7 +399,13 @@ describe('PythonCodeRuntime — process identity', () => { expect(own).toBeDefined() expect(readProcessStart(process.pid)).toBe(own) // Pid 0 is never a readable /proc entry, so the guard degrades to - // undefined rather than throwing on a teardown path. + // undefined rather than throwing on a teardown path. This is also the + // reading a REAPED leader produces -- its /proc entry is gone while the + // group it led can still hold survivors -- so `undefined` must NOT be + // treated as an identity mismatch. Reading it as one refused the SIGKILL + // that the same-group survivor tests depend on, which is why they went red + // on Linux while passing on Darwin (where the reader always returns + // undefined and the guard is inert). expect(readProcessStart(0)).toBeUndefined() } else { // Darwin has no /proc: the reader reports undefined, and `killGroup` then