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 type { ServerWebSocket } from 'bun';
|
||||||
import { join, isAbsolute } from 'node:path';
|
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 { getTaskByDirName } from './task-files';
|
||||||
import { getHomeDirForRole, DATA_PATH } from '../../data-path';
|
import { getHomeDirForRole, DATA_PATH } from '../../data-path';
|
||||||
import { buildSandboxPrefix, buildRunuserSuffix } from '../../sidecar/sandbox';
|
import { buildSandboxPrefix, buildRunuserSuffix } from '../../sidecar/sandbox';
|
||||||
@@ -36,6 +36,58 @@ type OutMessage =
|
|||||||
// Active processes per WebSocket
|
// Active processes per WebSocket
|
||||||
const activeProcs = new WeakMap<ServerWebSocket<WSData>, { proc: ReturnType<typeof Bun.spawn>; kill: () => void }>();
|
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';
|
import { tmpdir } from 'node:os';
|
||||||
|
|
||||||
function send(ws: ServerWebSocket<WSData>, msg: OutMessage) {
|
function send(ws: ServerWebSocket<WSData>, msg: OutMessage) {
|
||||||
@@ -179,7 +231,7 @@ async function handleRun(ws: ServerWebSocket<WSData>, msg: RunMessage) {
|
|||||||
activeProcs.set(ws, {
|
activeProcs.set(ws, {
|
||||||
proc,
|
proc,
|
||||||
kill: () => {
|
kill: () => {
|
||||||
try { proc.kill(); } catch { /* already dead */ }
|
try { killTree(proc.pid); } catch { /* already dead */ }
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user