fix(e2e): forward sighup in node watchdogs

This commit is contained in:
Vincent Koc
2026-06-02 00:59:04 +02:00
parent 4bd7421182
commit 32f98d7fe8
4 changed files with 72 additions and 59 deletions

View File

@@ -99,6 +99,7 @@ const forwardSignal = (signal) => {
};
process.once("SIGINT", forwardSignal);
process.once("SIGTERM", forwardSignal);
process.once("SIGHUP", forwardSignal);
child.on("exit", (code, signal) => {
clearTimeout(timer);
if (parentSignalTimer) {

View File

@@ -137,6 +137,7 @@ const forwardSignal = (signal) => {
};
process.once("SIGINT", forwardSignal);
process.once("SIGTERM", forwardSignal);
process.once("SIGHUP", forwardSignal);
child.on("close", (code, signal) => {
clearTimeout(timer);
if (parentSignalTimer) {

View File

@@ -517,29 +517,33 @@ stderr="$(<"$TMPDIR/stderr")"
}
});
it("escalates Docker watchdog children that ignore parent termination", () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-node-signal-"));
for (const [shellSignal, expectedStatus] of [
["TERM", "143"],
["HUP", "129"],
] as const) {
it(`escalates Docker watchdog children that ignore parent SIG${shellSignal}`, () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-node-signal-"));
try {
const binDir = join(workDir, "bin");
mkdirSync(binDir);
writeFileSync(
join(binDir, "node"),
`#!/bin/bash\nexec ${shellQuote(process.execPath)} "$@"\n`,
);
writeFileSync(
join(binDir, "docker"),
`#!/bin/bash
try {
const binDir = join(workDir, "bin");
mkdirSync(binDir);
writeFileSync(
join(binDir, "node"),
`#!/bin/bash\nexec ${shellQuote(process.execPath)} "$@"\n`,
);
writeFileSync(
join(binDir, "docker"),
`#!/bin/bash
printf "%s\\n" "$$" >"$TMPDIR/docker-pid"
printf "%s\\n" "$PPID" >"$TMPDIR/watchdog-pid"
trap "" TERM
trap "" TERM HUP
while true; do /bin/sleep 1; done
`,
);
chmodSync(join(binDir, "node"), 0o755);
chmodSync(join(binDir, "docker"), 0o755);
const rootDir = process.cwd();
const script = `
);
chmodSync(join(binDir, "node"), 0o755);
chmodSync(join(binDir, "docker"), 0o755);
const rootDir = process.cwd();
const script = `
set -euo pipefail
ROOT_DIR=${shellQuote(rootDir)}
TMPDIR=${shellQuote(workDir)}
@@ -558,12 +562,12 @@ for ((i = 0; i < 100; i += 1)); do
done
[ -s "$TMPDIR/docker-pid" ]
[ -s "$TMPDIR/watchdog-pid" ]
kill -TERM "$(/bin/cat "$TMPDIR/watchdog-pid")"
kill -${shellSignal} "$(/bin/cat "$TMPDIR/watchdog-pid")"
set +e
wait "$watchdog_pid"
status="$?"
set -e
[ "$status" = "143" ]
[ "$status" = "${expectedStatus}" ]
docker_pid="$(/bin/cat "$TMPDIR/docker-pid")"
for ((i = 0; i < 100; i += 1)); do
kill -0 "$docker_pid" 2>/dev/null || exit 0
@@ -573,11 +577,12 @@ echo "docker child still alive after watchdog termination" >&2
exit 1
`;
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
} finally {
rmSync(workDir, { recursive: true, force: true });
}
});
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
} finally {
rmSync(workDir, { recursive: true, force: true });
}
});
}
it("uses plain timeout when kill-after is unsupported", () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-plain-timeout-"));

View File

@@ -374,28 +374,33 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
}
});
it("escalates Node watchdog children that ignore parent termination", () => {
const tempDir = fs.mkdtempSync(
path.join(os.tmpdir(), "openclaw-e2e-instance-node-watchdog-signal-"),
);
try {
writeNodeShim(tempDir);
const childPath = path.join(tempDir, "ignore-term.cjs");
const pidPath = path.join(tempDir, "child.pid");
const watchdogPidPath = path.join(tempDir, "watchdog.pid");
fs.writeFileSync(
childPath,
[
"const fs = require('node:fs');",
"fs.writeFileSync(process.argv[2], String(process.pid));",
"fs.writeFileSync(process.argv[3], String(process.ppid));",
"process.on('SIGTERM', () => {});",
"setInterval(() => {}, 1000);",
"",
].join("\n"),
for (const [shellSignal, expectedStatus] of [
["TERM", "143"],
["HUP", "129"],
] as const) {
it(`escalates Node watchdog children that ignore parent SIG${shellSignal}`, () => {
const tempDir = fs.mkdtempSync(
path.join(os.tmpdir(), "openclaw-e2e-instance-node-watchdog-signal-"),
);
try {
writeNodeShim(tempDir);
const childPath = path.join(tempDir, "ignore-term.cjs");
const pidPath = path.join(tempDir, "child.pid");
const watchdogPidPath = path.join(tempDir, "watchdog.pid");
fs.writeFileSync(
childPath,
[
"const fs = require('node:fs');",
"fs.writeFileSync(process.argv[2], String(process.pid));",
"fs.writeFileSync(process.argv[3], String(process.ppid));",
"process.on('SIGTERM', () => {});",
"process.on('SIGHUP', () => {});",
"setInterval(() => {}, 1000);",
"",
].join("\n"),
);
const script = `
const script = `
set -euo pipefail
source ${shellQuote(helperPath)}
export OPENCLAW_E2E_TIMEOUT_KILL_GRACE_MS=100
@@ -407,12 +412,12 @@ for ((i = 0; i < 100; i += 1)); do
done
[ -s ${shellQuote(pidPath)} ]
[ -s ${shellQuote(watchdogPidPath)} ]
kill -TERM "$(/bin/cat ${shellQuote(watchdogPidPath)})"
kill -${shellSignal} "$(/bin/cat ${shellQuote(watchdogPidPath)})"
set +e
wait "$wrapper_pid"
status="$?"
set -e
[ "$status" = "143" ]
[ "$status" = "${expectedStatus}" ]
child_pid="$(/bin/cat ${shellQuote(pidPath)})"
for ((i = 0; i < 100; i += 1)); do
kill -0 "$child_pid" 2>/dev/null || exit 0
@@ -422,19 +427,20 @@ echo "child still alive after watchdog termination" >&2
exit 1
`;
const result = spawnSync("/bin/bash", ["-c", script], {
encoding: "utf8",
env: shellTestEnv({
PATH: tempDir,
}),
timeout: 5_000,
});
const result = spawnSync("/bin/bash", ["-c", script], {
encoding: "utf8",
env: shellTestEnv({
PATH: tempDir,
}),
timeout: 5_000,
});
expectShellSuccess(result);
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
expectShellSuccess(result);
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
}
it("terminates only the tracked gateway process", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-gateway-terminate-"));