task runner: kill the whole process tree on Stop
proc.kill() only signalled the direct child (bash), leaving a running ffmpeg grandchild orphaned and still encoding. now killTree() walks /proc, SIGTERMs the whole subtree (bash + ffmpeg), then SIGKILLs any straggler after 2s. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { ServerWebSocket } from 'bun';
|
||||
import { join, isAbsolute } from 'node:path';
|
||||
import { mkdirSync, writeFileSync, chmodSync, rmSync } from 'node:fs';
|
||||
import { mkdirSync, writeFileSync, chmodSync, rmSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import { getTaskByDirName } from './task-files';
|
||||
import { getHomeDirForRole, DATA_PATH } from '../../data-path';
|
||||
import { buildSandboxPrefix, buildRunuserSuffix } from '../../sidecar/sandbox';
|
||||
@@ -36,6 +36,58 @@ type OutMessage =
|
||||
// Active processes per WebSocket
|
||||
const activeProcs = new WeakMap<ServerWebSocket<WSData>, { proc: ReturnType<typeof Bun.spawn>; kill: () => void }>();
|
||||
|
||||
// Every descendant PID of `root`, from a single /proc walk (Linux). Bun's proc.kill() signals only
|
||||
// the direct child (bash), so a running grandchild like ffmpeg is left orphaned and keeps going.
|
||||
function descendantPids(root: number): number[] {
|
||||
const childrenOf = new Map<number, number[]>();
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = readdirSync('/proc');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const pid = Number(entry);
|
||||
if (!Number.isInteger(pid)) continue;
|
||||
try {
|
||||
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
||||
// "pid (comm) state ppid …" — comm may contain spaces/parens, so parse after the last ')'.
|
||||
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
||||
const ppid = Number(fields[1]);
|
||||
if (!Number.isInteger(ppid)) continue;
|
||||
const list = childrenOf.get(ppid);
|
||||
if (list) list.push(pid);
|
||||
else childrenOf.set(ppid, [pid]);
|
||||
} catch {
|
||||
// process vanished mid-scan
|
||||
}
|
||||
}
|
||||
const out: number[] = [];
|
||||
const stack = [root];
|
||||
while (stack.length > 0) {
|
||||
const parent = stack.pop()!;
|
||||
for (const child of childrenOf.get(parent) ?? []) {
|
||||
out.push(child);
|
||||
stack.push(child);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Terminate a process and its whole subtree: SIGTERM everything, then SIGKILL stragglers after a
|
||||
// grace period. Killing the root (bash) too stops it from spawning the next file mid-batch.
|
||||
function killTree(root: number) {
|
||||
const pids = [root, ...descendantPids(root)];
|
||||
for (const pid of pids) {
|
||||
try { process.kill(pid, 'SIGTERM'); } catch { /* already gone */ }
|
||||
}
|
||||
setTimeout(() => {
|
||||
for (const pid of pids) {
|
||||
try { process.kill(pid, 'SIGKILL'); } catch { /* gone */ }
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
function send(ws: ServerWebSocket<WSData>, msg: OutMessage) {
|
||||
@@ -179,7 +231,7 @@ async function handleRun(ws: ServerWebSocket<WSData>, msg: RunMessage) {
|
||||
activeProcs.set(ws, {
|
||||
proc,
|
||||
kill: () => {
|
||||
try { proc.kill(); } catch { /* already dead */ }
|
||||
try { killTree(proc.pid); } catch { /* already dead */ }
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user