feat(worker): in-box process-group marker + kill helpers for agent turns
This commit is contained in:
@@ -391,6 +391,42 @@ export const ensureUserContainer = async (args: {
|
|||||||
return name;
|
return name;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shellQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
export const buildMarkedCommand = (
|
||||||
|
marker: string,
|
||||||
|
command: string[],
|
||||||
|
): string[] => {
|
||||||
|
const script = `# ${marker}\nexec 0</dev/null\n${command.map(shellQuote).join(' ')}`;
|
||||||
|
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 =>
|
||||||
|
[
|
||||||
|
`pids=$(pgrep -f ${shellQuote(marker)} || true)`,
|
||||||
|
`for pid in $pids; do kill -TERM -"$pid" 2>/dev/null || true; done`,
|
||||||
|
`sleep 2`,
|
||||||
|
`for pid in $pids; do kill -KILL -"$pid" 2>/dev/null || true; done`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
export const killBoxProcessesByMarker = async (args: {
|
||||||
|
containerName: string;
|
||||||
|
marker: string;
|
||||||
|
}): Promise<void> => {
|
||||||
|
await execa(
|
||||||
|
containerRuntime(),
|
||||||
|
['exec', args.containerName, 'bash', '-lc', buildKillScript(args.marker)],
|
||||||
|
{ reject: false, stdin: 'ignore' },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const streamExecInContainer = async (args: {
|
export const streamExecInContainer = async (args: {
|
||||||
containerName: string;
|
containerName: string;
|
||||||
command: string[];
|
command: string[];
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
const loadDocker = async () => {
|
||||||
|
process.env.SPOON_WORKER_TOKEN = 'test-worker-token';
|
||||||
|
process.env.GITHUB_APP_ID = '123';
|
||||||
|
process.env.GITHUB_APP_PRIVATE_KEY =
|
||||||
|
'-----BEGIN PRIVATE KEY-----\\ntest\\n-----END PRIVATE KEY-----';
|
||||||
|
return await import('../../src/runtime/docker');
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('in-box process-group marker + kill helpers', () => {
|
||||||
|
test('buildMarkedCommand wraps argv as a setsid bash session carrying the marker', async () => {
|
||||||
|
const { buildMarkedCommand } = await loadDocker();
|
||||||
|
const argv = buildMarkedCommand('spoon-turn-abc', [
|
||||||
|
'codex',
|
||||||
|
'exec',
|
||||||
|
'--json',
|
||||||
|
'hi',
|
||||||
|
]);
|
||||||
|
expect(argv[0]).toBe('setsid');
|
||||||
|
expect(argv[1]).toBe('bash');
|
||||||
|
expect(argv[2]).toBe('-lc');
|
||||||
|
const script = argv[3] ?? '';
|
||||||
|
expect(script).toContain('# spoon-turn-abc');
|
||||||
|
expect(script.split('\n').at(-1)).toBe("'codex' 'exec' '--json' 'hi'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildKillScript TERM-then-KILLs every group matching the marker', async () => {
|
||||||
|
const { buildKillScript } = await loadDocker();
|
||||||
|
const script = buildKillScript('spoon-turn-abc');
|
||||||
|
expect(script).toContain("pgrep -f 'spoon-turn-abc'");
|
||||||
|
expect(script).toContain('kill -TERM -"$pid"');
|
||||||
|
expect(script).toContain('kill -KILL -"$pid"');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user