Skip to content

Commit 6fa0568

Browse files
committed
fix(check): clean managed child groups after forwarded signals
1 parent 6585cb3 commit 6fa0568

2 files changed

Lines changed: 78 additions & 56 deletions

File tree

scripts/lib/managed-child-process.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ export async function runManagedCommand({
103103
if (managedChild.forceKillTimer) {
104104
clearTimeout(managedChild.forceKillTimer);
105105
}
106+
if (managedChild.receivedSignal) {
107+
terminateManagedChild(child, "SIGKILL");
108+
}
106109
resolve(
107110
managedChild.receivedSignal
108111
? signalExitCode(managedChild.receivedSignal)

test/scripts/managed-child-process.test.ts

Lines changed: 75 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -140,70 +140,89 @@ describe("managed-child-process", () => {
140140
}
141141
});
142142

143-
posixIt("kills the managed child process group when the runner is terminated", async () => {
144-
const dir = createTempDir("openclaw-managed-child-");
145-
const childPath = path.join(dir, "child.mjs");
146-
const runnerPath = path.join(dir, "runner.mjs");
147-
const childPidPath = path.join(dir, "child.pid");
148-
const runnerReadyPath = path.join(dir, "runner.ready");
149-
const helperUrl = pathToFileURL(path.resolve("scripts/lib/managed-child-process.mjs")).href;
150-
151-
fs.writeFileSync(
152-
childPath,
153-
`
154-
import fs from "node:fs";
155-
156-
fs.writeFileSync(process.argv[2], String(process.pid));
157-
for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"]) {
158-
process.on(signal, () => process.exit(0));
159-
}
143+
posixIt(
144+
"kills managed child process group descendants when the runner is terminated",
145+
async () => {
146+
const dir = createTempDir("openclaw-managed-child-");
147+
const childPath = path.join(dir, "child.mjs");
148+
const runnerPath = path.join(dir, "runner.mjs");
149+
const childPidPath = path.join(dir, "child.pid");
150+
const descendantPidPath = path.join(dir, "descendant.pid");
151+
const runnerReadyPath = path.join(dir, "runner.ready");
152+
const helperUrl = pathToFileURL(path.resolve("scripts/lib/managed-child-process.mjs")).href;
153+
154+
fs.writeFileSync(
155+
childPath,
156+
`
157+
import { spawn } from "node:child_process";
158+
import fs from "node:fs";
159+
160+
const descendant = spawn(process.execPath, [
161+
"-e",
162+
"process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);",
163+
], { stdio: "ignore" });
164+
fs.writeFileSync(process.argv[2], String(process.pid));
165+
fs.writeFileSync(process.argv[3], String(descendant.pid));
166+
for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"]) {
167+
process.on(signal, () => process.exit(0));
168+
}
160169
setInterval(() => {}, 1_000);
161170
`,
162-
"utf8",
163-
);
164-
fs.writeFileSync(
165-
runnerPath,
166-
`
171+
"utf8",
172+
);
173+
fs.writeFileSync(
174+
runnerPath,
175+
`
167176
import fs from "node:fs";
168177
import { runManagedCommand } from ${JSON.stringify(helperUrl)};
169178
170-
process.exitCode = await runManagedCommand({
171-
bin: process.execPath,
172-
args: [${JSON.stringify(childPath)}, ${JSON.stringify(childPidPath)}],
173-
stdio: "ignore",
174-
onReady: () => fs.writeFileSync(${JSON.stringify(runnerReadyPath)}, "1"),
175-
});
179+
process.exitCode = await runManagedCommand({
180+
bin: process.execPath,
181+
args: [${JSON.stringify(childPath)}, ${JSON.stringify(childPidPath)}, ${JSON.stringify(descendantPidPath)}],
182+
stdio: "ignore",
183+
onReady: () => fs.writeFileSync(${JSON.stringify(runnerReadyPath)}, "1"),
184+
});
176185
`,
177-
"utf8",
178-
);
179-
180-
const runner = spawn(process.execPath, [runnerPath], {
181-
stdio: "ignore",
182-
});
183-
const runnerPid = expectProcessPid(runner.pid);
184-
let childPid = 0;
186+
"utf8",
187+
);
185188

186-
try {
187-
await waitFor(() => fs.existsSync(runnerReadyPath));
188-
await waitFor(() => fs.existsSync(childPidPath));
189-
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
190-
expect(Number.isInteger(childPid)).toBe(true);
191-
expect(isProcessAlive(childPid)).toBe(true);
192-
193-
process.kill(runnerPid, "SIGTERM");
194-
const result = await waitForClose(runner);
195-
196-
expect(result).toEqual({ code: 143, signal: null });
197-
await waitFor(() => !isProcessAlive(childPid), 1_500);
198-
} finally {
199-
if (isProcessAlive(runnerPid)) {
200-
process.kill(runnerPid, "SIGKILL");
201-
}
202-
if (childPid && isProcessAlive(childPid)) {
203-
process.kill(childPid, "SIGKILL");
189+
const runner = spawn(process.execPath, [runnerPath], {
190+
stdio: "ignore",
191+
});
192+
const runnerPid = expectProcessPid(runner.pid);
193+
let childPid = 0;
194+
let descendantPid = 0;
195+
196+
try {
197+
await waitFor(() => fs.existsSync(runnerReadyPath));
198+
await waitFor(() => fs.existsSync(childPidPath));
199+
await waitFor(() => fs.existsSync(descendantPidPath));
200+
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
201+
descendantPid = Number(fs.readFileSync(descendantPidPath, "utf8"));
202+
expect(Number.isInteger(childPid)).toBe(true);
203+
expect(Number.isInteger(descendantPid)).toBe(true);
204+
expect(isProcessAlive(childPid)).toBe(true);
205+
expect(isProcessAlive(descendantPid)).toBe(true);
206+
207+
process.kill(runnerPid, "SIGTERM");
208+
const result = await waitForClose(runner);
209+
210+
expect(result).toEqual({ code: 143, signal: null });
211+
await waitFor(() => !isProcessAlive(childPid), 1_500);
212+
await waitFor(() => !isProcessAlive(descendantPid), 1_500);
213+
} finally {
214+
if (isProcessAlive(runnerPid)) {
215+
process.kill(runnerPid, "SIGKILL");
216+
}
217+
if (childPid && isProcessAlive(childPid)) {
218+
process.kill(childPid, "SIGKILL");
219+
}
220+
if (descendantPid && isProcessAlive(descendantPid)) {
221+
process.kill(descendantPid, "SIGKILL");
222+
}
204223
}
205-
}
206-
});
224+
},
225+
);
207226
});
208227

209228
async function waitFor(condition: () => boolean, timeoutMs = 3_000) {

0 commit comments

Comments
 (0)