mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 16:35:48 +01:00
Fence legacy sandbox dispatch against startup cancellation
Reconcile persisted cancellation with newly registered scopes, check cancellation before dispatch, and preserve external child termination signals. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
066b4f0160
commit
1caad1bf9e
5 files changed
+84
-8
No files matched your search
@@ -7,6 +7,23 @@ import { runAdapterExecutionTargetProcess } from "./execution-target.js";
|
||||
import { beginAdapterRunCancellation, cancelAdapterRunExecution, finishAdapterRunCancellation } from "./adapter-run-cancellation.js";
|
||||
|
||||
describe("sandbox CLI cancellation through the execution target", () => {
|
||||
it.each(["SIGTERM", "SIGKILL"] as const)("preserves a child's external %s termination", async (signal) => {
|
||||
const runId = randomUUID();
|
||||
beginAdapterRunCancellation(runId);
|
||||
const runner: CommandManagedRuntimeRunner = { execute: async (input) => new Promise((resolve, reject) => {
|
||||
const child = spawn(input.command === "node" ? process.execPath : input.command, input.args ?? [], { stdio: "ignore" });
|
||||
child.once("error", reject);
|
||||
child.once("close", (exitCode, signal) => resolve({ exitCode, signal, stdout: "", stderr: "", timedOut: false, pid: child.pid ?? null, startedAt: null }));
|
||||
}) };
|
||||
try {
|
||||
const result = await executeCancellableSandboxCommand(runId, runner, {
|
||||
command: process.execPath, args: ["-e", `process.kill(process.pid, '${signal}')`],
|
||||
}, 100);
|
||||
expect(result.signal).toBe(signal);
|
||||
expect(result.exitCode).toBeNull();
|
||||
} finally { finishAdapterRunCancellation(runId); }
|
||||
});
|
||||
|
||||
it("retains a failed cancellation for an explicit retry", async () => {
|
||||
const runId = randomUUID();
|
||||
beginAdapterRunCancellation(runId);
|
||||
|
||||
@@ -32,11 +32,18 @@ const poll = setInterval(() => { if (cancelled()) stop(); }, 100);
|
||||
for (const name of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(name, stop);
|
||||
const cleanup = () => { clearInterval(poll); clearTimeout(escalation); try { fs.unlinkSync(marker); } catch (e) { if (e.code !== 'ENOENT') throw e; } };
|
||||
child.once('error', () => { exited = true; cleanup(); process.exitCode = 127; });
|
||||
child.once('exit', code => {
|
||||
child.once('exit', (code, childSignal) => {
|
||||
// Do not leave a shell/tool child holding the provider's output pipe after
|
||||
// the CLI exits in response to cancellation. Never retain a PID kill timer.
|
||||
if (stopping) signal('SIGKILL');
|
||||
exited = true; cleanup(); process.exitCode = stopping ? 143 : (code ?? 128);
|
||||
exited = true; cleanup();
|
||||
if (!stopping && childSignal) {
|
||||
// Preserve the real termination signal instead of reporting exit code 128.
|
||||
process.removeAllListeners(childSignal);
|
||||
process.kill(process.pid, childSignal);
|
||||
return;
|
||||
}
|
||||
process.exitCode = stopping ? 143 : (code ?? 1);
|
||||
});
|
||||
process.once('exit', () => { if (!exited) signal('SIGKILL'); });
|
||||
`;
|
||||
|
||||
Reference in new issue
Block a user