mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
fix(code-runtime-python): guard teardown, log prefix, and settlement flush
Four independent corrections in the run lifecycle.
`killGroup` signalled `-child.pid` with a raw `process.kill`. Node keeps the
numeric `child.pid` after the leader is reaped and only clears its internal
handle, so `child.kill()` refuses while the raw call does not; `close` can
trail `exit` by seconds when a pipe-holding descendant keeps the streams open.
A recycled pgid could therefore receive this run's SIGTERM and armed SIGKILL.
`groupEmpty()` does not cover it: it reports whether the group has members, not
whether they are ours, and it first runs after the signal. The leader's start
time is now read at spawn and re-checked before each signal, matching the
position packages/subprocess/subprocess-local already states
("ProcessIdentity ... preventing teardown escalation after PID reuse"). Kept
local rather than depending on that package, which would add an architectural
edge. Linux reads /proc; Darwin has no /proc, so the reader reports undefined
and the guard degrades to the previous behavior instead of forking `ps` on a
teardown path.
`_push_bounded_prefix` built `(*self._pending, extra)`, copying every pending
reference into a same-size tuple before the bounded loop. For a
single-character drip that is a second pointer array as large as the list:
measured +80 MiB of tuple over a 40 MiB list for 5.2M chunks, the allocation
the bounded prefix exists to avoid. It now iterates the list in place and
handles `extra` in the loop's `else`; 4000 randomized inputs produce byte-identical
prefixes.
The settlement `flush_out()`/`flush_err()` ran outside any guard while `done`
was already decided, so a flush raising under memory pressure skipped
`send_done` and downgraded a child-classified `exception` into a host-side
`worker-exit`. Both are now wrapped, swallowing only the log tail.
The boot re-check's `if effective_soft != RLIM_INFINITY` was dead: `_clamped`
is asked for a finite `addr_bytes` on both sides and each branch returns that
value or a `min` with an inherited bound, so RLIM_INFINITY is unreachable. The
guard could only ever have skipped the re-check it claimed to protect.
This commit is contained in:
@@ -3,8 +3,8 @@ import { mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { PythonCodeRuntime } from '../src/index.ts'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { PythonCodeRuntime, readProcessStart } from '../src/index.ts'
|
||||
import { logTruncationMarker } from '../src/protocol.ts'
|
||||
import type { Config } from '../src/index.ts'
|
||||
import type { CodeBindingFunction, CodeJsonValue, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
|
||||
@@ -383,6 +383,32 @@ describe('PythonCodeRuntime — seam descriptors and misuse', () => {
|
||||
}, 15_000)
|
||||
})
|
||||
|
||||
describe('PythonCodeRuntime — process identity', () => {
|
||||
it('reads a live process start time and distinguishes it from an absent pid', () => {
|
||||
// The teardown guard signals `-child.pid` with a RAW `process.kill`, which
|
||||
// (unlike `child.kill()`) has no handle check, so it would reach a recycled
|
||||
// pgid during the window between the leader being reaped and `close` firing.
|
||||
// A pid alone cannot separate the original from its replacement -- both
|
||||
// answer `kill(pid, 0)` -- so the guard compares START TIME, and this pins
|
||||
// that the reading is stable for one process and absent for a pid that
|
||||
// cannot be read.
|
||||
const own = readProcessStart(process.pid)
|
||||
if (process.platform === 'linux') {
|
||||
// Same process, two reads: the identity must be stable, or the guard would
|
||||
// refuse to signal its own live group.
|
||||
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.
|
||||
expect(readProcessStart(0)).toBeUndefined()
|
||||
} else {
|
||||
// Darwin has no /proc: the reader reports undefined, and `killGroup` then
|
||||
// keeps its pre-existing behavior instead of paying a `ps` fork per signal.
|
||||
expect(own).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('PythonCodeRuntime — inherited resource limits', () => {
|
||||
it('runs under an inherited hard limit tighter than addressSpaceMb', async () => {
|
||||
// An unprivileged process may lower a hard rlimit but never raise it. Under
|
||||
|
||||
Reference in New Issue
Block a user