fix(worker): marked-command survives tail-exec; kill script excludes itself
This commit is contained in:
@@ -393,28 +393,75 @@ export const ensureUserContainer = async (args: {
|
||||
|
||||
const shellQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
||||
|
||||
// The marker is embedded literally into a `# comment` line and into a `pgrep`
|
||||
// pattern, so it must not contain shell metacharacters or a newline (either could
|
||||
// break out of the comment or the pattern). Callers generate it, but validate
|
||||
// defensively so a malformed marker fails loudly instead of silently corrupting
|
||||
// the script.
|
||||
const MARKER_PATTERN = /^[A-Za-z0-9_-]+$/;
|
||||
const assertMarker = (marker: string) => {
|
||||
if (!MARKER_PATTERN.test(marker)) {
|
||||
throw new Error(
|
||||
`Invalid process marker ${JSON.stringify(marker)}: must match ${String(MARKER_PATTERN)}.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 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, which would replace the
|
||||
// marker-carrying bash with the CLI and make `pgrep -f <marker>` miss the running
|
||||
// turn. Keeping a trailing `rc=$?; exit "$rc"` after the CLI means the CLI is no
|
||||
// longer the script's final statement, so bash survives as the group leader while
|
||||
// still propagating the CLI's exit code (downstream `normalizeRunResult` relies on
|
||||
// it — a bare trailing `wait` would mask a non-zero CLI failure as 0). The CLI runs
|
||||
// in the same process group as this bash, so the kill helper's group signal reaches
|
||||
// it. stdout/stderr fds are inherited by the CLI so streaming still works.
|
||||
export const buildMarkedCommand = (
|
||||
marker: string,
|
||||
command: string[],
|
||||
): string[] => {
|
||||
const script = `# ${marker}\nexec 0</dev/null\n${command.map(shellQuote).join(' ')}`;
|
||||
assertMarker(marker);
|
||||
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.
|
||||
// The negative pid (`-"$pid"`) targets the whole process group, so the CLI and any
|
||||
// children it spawned die together.
|
||||
export const buildKillScript = (marker: string): string =>
|
||||
[
|
||||
// The negative pid (`kill -TERM -"$pgid"`) targets the whole process group, so the
|
||||
// CLI and any children it spawned die together.
|
||||
//
|
||||
// pgrep self-match: this kill script runs via `bash -lc <script>`, whose OWN argv
|
||||
// contains the marker literal, so `pgrep -f <marker>` matches the kill script (and
|
||||
// its command-substitution subshells) too. Left unguarded it would `kill -TERM` its
|
||||
// own process group and die on iteration one, leaking the real target. So we compute
|
||||
// this script's pgid up front and skip any match sharing it, killing only the
|
||||
// target's process group.
|
||||
export const buildKillScript = (marker: string): string => {
|
||||
assertMarker(marker);
|
||||
return [
|
||||
`self_pgid=$(ps -o pgid= -p $$ | tr -d ' ')`,
|
||||
`pids=$(pgrep -f ${shellQuote(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');
|
||||
};
|
||||
|
||||
export const killBoxProcessesByMarker = async (args: {
|
||||
containerName: string;
|
||||
|
||||
Reference in New Issue
Block a user