mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(scripts): preserve boundary abort grace
This commit is contained in:
@@ -444,9 +444,43 @@ export function runNodeStep(label, args, timeoutMs, params = {}) {
|
||||
let settled = false;
|
||||
let canceled = false;
|
||||
let killTimer;
|
||||
let killDeadlineAt = 0;
|
||||
const stdoutWriter = createPrefixedOutputWriter(label, process.stdout);
|
||||
const stderrWriter = createPrefixedOutputWriter(label, process.stderr);
|
||||
const killNodeStep = (signal) => signalNodeStep(child, signal);
|
||||
const processGroupAlive = () => {
|
||||
if (process.platform === "win32" || !child.pid) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
process.kill(-child.pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return Boolean(error && error.code === "EPERM");
|
||||
}
|
||||
};
|
||||
const waitForProcessGroupExit = async (waitMs) => {
|
||||
const deadlineAt = Date.now() + waitMs;
|
||||
while (Date.now() < deadlineAt) {
|
||||
if (!processGroupAlive()) {
|
||||
return true;
|
||||
}
|
||||
await new Promise((resolvePoll) => {
|
||||
setTimeout(resolvePoll, 25);
|
||||
});
|
||||
}
|
||||
return !processGroupAlive();
|
||||
};
|
||||
const waitForCanceledStepTeardown = async () => {
|
||||
const remainingGraceMs = Math.max(0, killDeadlineAt - Date.now());
|
||||
if (remainingGraceMs > 0) {
|
||||
await waitForProcessGroupExit(remainingGraceMs);
|
||||
}
|
||||
if (processGroupAlive()) {
|
||||
killNodeStep("SIGKILL");
|
||||
await waitForProcessGroupExit(100);
|
||||
}
|
||||
};
|
||||
ACTIVE_NODE_STEP_KILLERS.add(killNodeStep);
|
||||
const abortStep = () => {
|
||||
if (settled || canceled) {
|
||||
@@ -454,6 +488,7 @@ export function runNodeStep(label, args, timeoutMs, params = {}) {
|
||||
}
|
||||
canceled = true;
|
||||
killNodeStep("SIGTERM");
|
||||
killDeadlineAt = Date.now() + NODE_STEP_ABORT_KILL_GRACE_MS;
|
||||
killTimer = setTimeout(() => {
|
||||
killTimer = undefined;
|
||||
killNodeStep("SIGKILL");
|
||||
@@ -508,27 +543,29 @@ export function runNodeStep(label, args, timeoutMs, params = {}) {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
stdoutWriter.flush();
|
||||
stderrWriter.flush();
|
||||
if (exitingAfterParentSignal) {
|
||||
killNodeStep("SIGKILL");
|
||||
void (async () => {
|
||||
settled = true;
|
||||
stdoutWriter.flush();
|
||||
stderrWriter.flush();
|
||||
if (exitingAfterParentSignal) {
|
||||
killNodeStep("SIGKILL");
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
if (canceled) {
|
||||
await waitForCanceledStepTeardown();
|
||||
cleanup();
|
||||
rejectPromise(new Error(`${label} canceled after sibling failure`));
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
if (canceled) {
|
||||
killNodeStep("SIGKILL");
|
||||
cleanup();
|
||||
rejectPromise(new Error(`${label} canceled after sibling failure`));
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
if (code === 0) {
|
||||
resolvePromise();
|
||||
return;
|
||||
}
|
||||
abortSiblingSteps(abortController);
|
||||
rejectPromise(new Error(`${label} failed with exit code ${code ?? 1}`));
|
||||
if (code === 0) {
|
||||
resolvePromise();
|
||||
return;
|
||||
}
|
||||
abortSiblingSteps(abortController);
|
||||
rejectPromise(new Error(`${label} failed with exit code ${code ?? 1}`));
|
||||
})();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
runNodeSteps,
|
||||
runNodeStepsInParallel,
|
||||
} from "../../scripts/prepare-extension-package-boundary-artifacts.mjs";
|
||||
import { makeTempDir } from "../helpers/temp-dir.js";
|
||||
|
||||
const tempRoots = new Set<string>();
|
||||
|
||||
@@ -170,6 +171,49 @@ describe("prepare-extension-package-boundary-artifacts", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"lets aborted sibling descendants drain during kill grace",
|
||||
async () => {
|
||||
const rootDir = makeTempDir(tempRoots, "openclaw-boundary-abort-drain-");
|
||||
const readyPath = path.join(rootDir, "descendant.ready");
|
||||
const drainedPath = path.join(rootDir, "descendant.drained");
|
||||
const descendantScript = [
|
||||
"const fs = require('node:fs');",
|
||||
"process.on('SIGTERM', () => {",
|
||||
" setTimeout(() => {",
|
||||
` fs.writeFileSync(${JSON.stringify(drainedPath)}, 'drained');`,
|
||||
" process.exit(0);",
|
||||
" }, 50);",
|
||||
"});",
|
||||
`fs.writeFileSync(${JSON.stringify(readyPath)}, 'ready');`,
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n");
|
||||
const parentScript = [
|
||||
"const { spawn } = require('node:child_process');",
|
||||
`spawn(process.execPath, ["--eval", ${JSON.stringify(descendantScript)}], { stdio: "ignore" });`,
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n");
|
||||
|
||||
const command = runNodeStepsInParallel([
|
||||
{
|
||||
label: "delayed-fail",
|
||||
args: ["--eval", "setTimeout(() => process.exit(2), 150)"],
|
||||
timeoutMs: 5_000,
|
||||
},
|
||||
{
|
||||
label: "abort-group-drain",
|
||||
args: ["--eval", parentScript],
|
||||
timeoutMs: 60_000,
|
||||
},
|
||||
]);
|
||||
|
||||
await waitForFile(readyPath, 1_000);
|
||||
await expect(command).rejects.toThrow("delayed-fail failed with exit code 2");
|
||||
expect(fs.readFileSync(drainedPath, "utf8")).toBe("drained");
|
||||
},
|
||||
);
|
||||
|
||||
it("hard-kills timed out prep steps", async () => {
|
||||
const signals: Array<NodeJS.Signals | number | undefined> = [];
|
||||
const child = new EventEmitter() as EventEmitter & {
|
||||
|
||||
Reference in New Issue
Block a user