fix(worker): marked-command survives tail-exec; kill script excludes itself
This commit is contained in:
@@ -131,23 +131,50 @@ export const getAdapter = (name: AgentRuntimeName): AgentRuntime => {
|
||||
```ts
|
||||
// Wraps a CLI argv so the process runs as a new session/process-group leader whose
|
||||
// bash parent carries the marker in its argv (matchable by `pgrep -f`). We do NOT
|
||||
// `exec` (bash must survive so the marker stays visible); stdout/stderr fds are
|
||||
// inherited by the CLI so streaming still works.
|
||||
// `exec` the CLI: bash tail-exec's a final simple command, so an `exec`-or-trailing
|
||||
// CLI would replace the marker-carrying bash and `pgrep -f <marker>` would miss the
|
||||
// running turn. Keep a trailing `rc=$?; exit "$rc"` so the CLI is no longer the final
|
||||
// statement (bash survives as group leader) while still propagating its exit code
|
||||
// (downstream `normalizeRunResult` relies on it; a bare `wait` would mask failures).
|
||||
export const buildMarkedCommand = (marker: string, command: string[]): string[] => {
|
||||
const script = `# ${marker}\nexec 0</dev/null\n${command.map(shellQuote).join(' ')}`;
|
||||
// assertMarker(marker) — reject anything but /^[A-Za-z0-9_-]+$/ so a newline can't
|
||||
// break out of the `# comment` line.
|
||||
const script = [
|
||||
`# ${marker}`,
|
||||
'exec 0</dev/null',
|
||||
command.map(shellQuote).join(' '),
|
||||
'rc=$?',
|
||||
'exit "$rc"',
|
||||
].join('\n');
|
||||
return ['setsid', 'bash', '-lc', script];
|
||||
};
|
||||
|
||||
// Kills every process group whose bash parent matches the marker, TERM then KILL.
|
||||
// pgrep self-match: the kill script runs via `bash -lc <script>` whose own argv holds
|
||||
// the marker literal, so pgrep matches the kill script (and its command-substitution
|
||||
// subshells) too. Exclude its own pgid or it SIGTERMs itself on iteration one and
|
||||
// leaks the target; kill the TARGET's pgid instead.
|
||||
export const killBoxProcessesByMarker = async (args: {
|
||||
containerName: string;
|
||||
marker: string;
|
||||
}): Promise<void> => {
|
||||
// assertMarker(args.marker) — same /^[A-Za-z0-9_-]+$/ guard for the pgrep pattern.
|
||||
const script = [
|
||||
`self_pgid=$(ps -o pgid= -p $$ | tr -d ' ')`,
|
||||
`pids=$(pgrep -f ${shellQuote(args.marker)} || true)`,
|
||||
`for pid in $pids; do kill -TERM -"$pid" 2>/dev/null || true; done`,
|
||||
`for pid in $pids; do`,
|
||||
` pgid=$(ps -o pgid= -p "$pid" | tr -d ' ')`,
|
||||
` [ -z "$pgid" ] && continue`,
|
||||
` [ "$pgid" = "$self_pgid" ] && continue`,
|
||||
` kill -TERM -"$pgid" 2>/dev/null || true`,
|
||||
`done`,
|
||||
`sleep 2`,
|
||||
`for pid in $pids; do kill -KILL -"$pid" 2>/dev/null || true; done`,
|
||||
`for pid in $pids; do`,
|
||||
` pgid=$(ps -o pgid= -p "$pid" | tr -d ' ')`,
|
||||
` [ -z "$pgid" ] && continue`,
|
||||
` [ "$pgid" = "$self_pgid" ] && continue`,
|
||||
` kill -KILL -"$pgid" 2>/dev/null || true`,
|
||||
`done`,
|
||||
].join('\n');
|
||||
await execa(containerRuntime(), ['exec', args.containerName, 'bash', '-lc', script], {
|
||||
reject: false, stdin: 'ignore',
|
||||
@@ -159,7 +186,7 @@ Add a local `const shellQuote = (v: string) => `'${v.replaceAll("'", "'\\''")}'`
|
||||
*Consumes:* `execa`, `containerRuntime()` (already in file).
|
||||
|
||||
**Steps:**
|
||||
- [ ] Write failing `apps/agent-worker/tests/unit/box-process-kill.test.ts`: import `buildMarkedCommand`. Assert `buildMarkedCommand('spoon-turn-abc', ['codex', 'exec', '--json', 'hi'])` returns `['setsid', 'bash', '-lc', expect.stringContaining('# spoon-turn-abc')]` and the script's last line contains `codex exec --json 'hi'`. (Do not unit-test `killBoxProcessesByMarker` end-to-end — it shells out; assert only the argv via a spy if you refactor it to a pure `buildKillScript(marker)` helper. **Recommended:** extract `export const buildKillScript = (marker: string): string` and test it contains `pgrep -f 'spoon-turn-abc'`, `kill -TERM -"$pid"`, and `kill -KILL -"$pid"`.)
|
||||
- [ ] Write failing `apps/agent-worker/tests/unit/box-process-kill.test.ts`: import `buildMarkedCommand`. Assert `buildMarkedCommand('spoon-turn-abc', ['codex', 'exec', '--json', 'hi'])` returns `['setsid', 'bash', '-lc', expect.stringContaining('# spoon-turn-abc')]` and the script's last line contains `codex exec --json 'hi'`. (Do not unit-test `killBoxProcessesByMarker` end-to-end — it shells out; assert only the argv via a spy if you refactor it to a pure `buildKillScript(marker)` helper. **Recommended:** extract `export const buildKillScript = (marker: string): string` and test it contains `pgrep -f 'spoon-turn-abc'`, `kill -TERM -"$pgid"`, and `kill -KILL -"$pgid"`, plus the self-pgid exclusion (`[ "$pgid" = "$self_pgid" ] && continue`).)
|
||||
- [ ] Run `bun run test:unit` → FAIL.
|
||||
- [ ] Implement `buildMarkedCommand`, `buildKillScript`, and `killBoxProcessesByMarker` in `docker.ts`.
|
||||
- [ ] Run `bun run test:unit` → PASS. `bun run typecheck` → PASS.
|
||||
|
||||
Reference in New Issue
Block a user