mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix(deadcode): clean Knip child trees on parent signal
This commit is contained in:
parent
ad049ef083
commit
aaf335af04
2 changed files with 104 additions and 0 deletions
|
|
@ -228,6 +228,28 @@ export async function runKnipUnusedFiles(params = {}) {
|
|||
detached: process.platform !== "win32",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
const parentSignalHandlers = [];
|
||||
const cleanupParentSignalHandlers = () => {
|
||||
for (const { signal, handler } of parentSignalHandlers) {
|
||||
process.off(signal, handler);
|
||||
}
|
||||
parentSignalHandlers.length = 0;
|
||||
};
|
||||
const relayParentSignal = (signal) => {
|
||||
const handler = () => {
|
||||
signalProcessTree(child, signal);
|
||||
signalProcessTree(child, "SIGKILL");
|
||||
cleanupParentSignalHandlers();
|
||||
process.kill(process.pid, signal);
|
||||
};
|
||||
parentSignalHandlers.push({ signal, handler });
|
||||
process.once(signal, handler);
|
||||
};
|
||||
if (process.platform !== "win32") {
|
||||
relayParentSignal("SIGINT");
|
||||
relayParentSignal("SIGTERM");
|
||||
relayParentSignal("SIGHUP");
|
||||
}
|
||||
|
||||
const heartbeatTimer = setInterval(() => {
|
||||
writeStatus(
|
||||
|
|
@ -255,6 +277,7 @@ export async function runKnipUnusedFiles(params = {}) {
|
|||
clearTimeout(timeoutTimer);
|
||||
clearInterval(heartbeatTimer);
|
||||
clearTimeout(killTimer);
|
||||
cleanupParentSignalHandlers();
|
||||
resolve({
|
||||
...result,
|
||||
output: output.join(""),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { EventEmitter } from "node:events";
|
|||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
checkUnusedFiles,
|
||||
|
|
@ -65,6 +66,21 @@ async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
|
|||
throw new Error(`process still alive: ${pid}`);
|
||||
}
|
||||
|
||||
function waitForChildClose(
|
||||
child: ReturnType<typeof spawn>,
|
||||
timeoutMs = 5_000,
|
||||
): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("child did not close before timeout"));
|
||||
}, timeoutMs);
|
||||
child.once("close", (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ code, signal });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("check-deadcode-unused-files", () => {
|
||||
it("parses the compact Knip unused-file section", () => {
|
||||
expect(
|
||||
|
|
@ -366,6 +382,71 @@ src/a.ts: src/a.ts
|
|||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"cleans active Knip descendants before forwarding parent SIGTERM",
|
||||
async () => {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "openclaw-knip-parent-signal-"));
|
||||
const childPidPath = path.join(root, "child.pid");
|
||||
const readyPath = path.join(root, "child.ready");
|
||||
const scriptUrl = pathToFileURL(path.resolve("scripts/check-deadcode-unused-files.mjs")).href;
|
||||
let childPid = 0;
|
||||
let runner: ReturnType<typeof spawn> | undefined;
|
||||
|
||||
try {
|
||||
const childScript = [
|
||||
"const fs = require('node:fs');",
|
||||
"process.on('SIGTERM', () => {});",
|
||||
`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("");
|
||||
const parentScript = [
|
||||
"const { spawn } = require('node:child_process');",
|
||||
`spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
|
||||
`require('node:fs').writeFileSync(${JSON.stringify(readyPath)}, 'ready');`,
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("");
|
||||
const runnerScript = [
|
||||
"import { spawn } from 'node:child_process';",
|
||||
`import { runKnipUnusedFiles } from ${JSON.stringify(scriptUrl)};`,
|
||||
"await runKnipUnusedFiles({",
|
||||
" spawnCommand(_command, _args, options) {",
|
||||
` return spawn(process.execPath, ['-e', ${JSON.stringify(parentScript)}], options);`,
|
||||
" },",
|
||||
" timeoutMs: 60_000,",
|
||||
" writeStatus: () => {},",
|
||||
"});",
|
||||
].join("\n");
|
||||
|
||||
runner = spawn(process.execPath, ["--input-type=module", "-e", runnerScript], {
|
||||
cwd: process.cwd(),
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
|
||||
await waitForFile(readyPath, 2_000);
|
||||
await waitForFile(childPidPath, 2_000);
|
||||
childPid = Number.parseInt(readFileSync(childPidPath, "utf8"), 10);
|
||||
expect(isProcessAlive(childPid)).toBe(true);
|
||||
|
||||
runner.kill("SIGTERM");
|
||||
|
||||
await expect(waitForChildClose(runner)).resolves.toEqual({
|
||||
code: null,
|
||||
signal: "SIGTERM",
|
||||
});
|
||||
await waitForDead(childPid, 2_000);
|
||||
} finally {
|
||||
if (runner?.pid && isProcessAlive(runner.pid)) {
|
||||
runner.kill("SIGKILL");
|
||||
}
|
||||
if (childPid && isProcessAlive(childPid)) {
|
||||
process.kill(childPid, "SIGKILL");
|
||||
}
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps output delivered after process exit but before stdio close", async () => {
|
||||
const child = new FakeKnipProcess();
|
||||
const resultPromise = runKnipUnusedFiles({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue