mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(dev): release run-node build lock on SIGINT/SIGTERM/exit (#69785)
This commit is contained in:
@@ -13,6 +13,15 @@ export function resolveBuildRequirement(deps: {
|
||||
configFiles: string[];
|
||||
}): { shouldBuild: boolean; reason: string };
|
||||
|
||||
export function acquireRunNodeBuildLock(deps: {
|
||||
cwd: string;
|
||||
args: readonly string[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
fs: unknown;
|
||||
process: NodeJS.Process;
|
||||
stderr: { write: (value: string) => void };
|
||||
}): Promise<() => void>;
|
||||
|
||||
export function runNodeMain(params?: {
|
||||
spawn?: (
|
||||
cmd: string,
|
||||
|
||||
+23
-2
@@ -459,7 +459,7 @@ const removeStaleBuildLock = (deps, lockDir, staleMs) => {
|
||||
}
|
||||
};
|
||||
|
||||
const acquireRunNodeBuildLock = async (deps) => {
|
||||
export const acquireRunNodeBuildLock = async (deps) => {
|
||||
const lockRoot = path.join(deps.cwd, ".artifacts");
|
||||
const lockDir = path.join(lockRoot, "run-node-build.lock");
|
||||
const timeoutMs = parsePositiveIntegerEnv(
|
||||
@@ -501,8 +501,29 @@ const acquireRunNodeBuildLock = async (deps) => {
|
||||
} catch {
|
||||
// Owner metadata is diagnostic only; the directory itself is the lock.
|
||||
}
|
||||
let released = false;
|
||||
const removeLockDir = () => {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
released = true;
|
||||
try {
|
||||
deps.fs.rmSync(lockDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Best-effort cleanup; a follow-up waiter will fall back to staleness
|
||||
// detection if the directory is still present.
|
||||
}
|
||||
};
|
||||
const onSignal = () => removeLockDir();
|
||||
const onExit = () => removeLockDir();
|
||||
deps.process.on("SIGINT", onSignal);
|
||||
deps.process.on("SIGTERM", onSignal);
|
||||
deps.process.on("exit", onExit);
|
||||
return () => {
|
||||
deps.fs.rmSync(lockDir, { recursive: true, force: true });
|
||||
deps.process.off("SIGINT", onSignal);
|
||||
deps.process.off("SIGTERM", onSignal);
|
||||
deps.process.off("exit", onExit);
|
||||
removeLockDir();
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.code !== "EEXIST") {
|
||||
|
||||
@@ -3,7 +3,11 @@ import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveBuildRequirement, runNodeMain } from "../../scripts/run-node.mjs";
|
||||
import {
|
||||
acquireRunNodeBuildLock,
|
||||
resolveBuildRequirement,
|
||||
runNodeMain,
|
||||
} from "../../scripts/run-node.mjs";
|
||||
import {
|
||||
bundledDistPluginFile,
|
||||
bundledPluginFile,
|
||||
@@ -1071,4 +1075,80 @@ describe("run-node script", () => {
|
||||
expect(spawnCalls).toEqual([expectedBuildSpawn(), statusCommandSpawn()]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("acquireRunNodeBuildLock", () => {
|
||||
const lockDeps = (tmp: string, fakeProcess: NodeJS.Process) => ({
|
||||
cwd: tmp,
|
||||
args: ["status"],
|
||||
env: { OPENCLAW_RUNNER_LOG: "0" },
|
||||
fs: fsSync,
|
||||
process: fakeProcess,
|
||||
stderr: { write: () => true } as unknown as NodeJS.WriteStream,
|
||||
});
|
||||
|
||||
it("releases the lock directory when the wrapper receives SIGINT", async () => {
|
||||
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
|
||||
const fakeProcess = createFakeProcess();
|
||||
const lockDir = path.join(tmp, ".artifacts", "run-node-build.lock");
|
||||
|
||||
const release = await acquireRunNodeBuildLock(lockDeps(tmp, fakeProcess));
|
||||
expect(fsSync.existsSync(lockDir)).toBe(true);
|
||||
|
||||
fakeProcess.emit("SIGINT");
|
||||
expect(fsSync.existsSync(lockDir)).toBe(false);
|
||||
|
||||
// Normal release after signal must be a no-op, not throw.
|
||||
expect(() => release()).not.toThrow();
|
||||
expect(fakeProcess.listenerCount("SIGINT")).toBe(0);
|
||||
expect(fakeProcess.listenerCount("SIGTERM")).toBe(0);
|
||||
expect(fakeProcess.listenerCount("exit")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("releases the lock directory when the wrapper receives SIGTERM", async () => {
|
||||
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
|
||||
const fakeProcess = createFakeProcess();
|
||||
const lockDir = path.join(tmp, ".artifacts", "run-node-build.lock");
|
||||
|
||||
const release = await acquireRunNodeBuildLock(lockDeps(tmp, fakeProcess));
|
||||
expect(fsSync.existsSync(lockDir)).toBe(true);
|
||||
|
||||
fakeProcess.emit("SIGTERM");
|
||||
expect(fsSync.existsSync(lockDir)).toBe(false);
|
||||
expect(() => release()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it("releases the lock directory on process exit", async () => {
|
||||
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
|
||||
const fakeProcess = createFakeProcess();
|
||||
const lockDir = path.join(tmp, ".artifacts", "run-node-build.lock");
|
||||
|
||||
const release = await acquireRunNodeBuildLock(lockDeps(tmp, fakeProcess));
|
||||
expect(fsSync.existsSync(lockDir)).toBe(true);
|
||||
|
||||
fakeProcess.emit("exit");
|
||||
expect(fsSync.existsSync(lockDir)).toBe(false);
|
||||
expect(() => release()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it("detaches signal listeners after a normal release", async () => {
|
||||
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
|
||||
const fakeProcess = createFakeProcess();
|
||||
const lockDir = path.join(tmp, ".artifacts", "run-node-build.lock");
|
||||
|
||||
const release = await acquireRunNodeBuildLock(lockDeps(tmp, fakeProcess));
|
||||
expect(fakeProcess.listenerCount("SIGINT")).toBe(1);
|
||||
expect(fakeProcess.listenerCount("SIGTERM")).toBe(1);
|
||||
expect(fakeProcess.listenerCount("exit")).toBe(1);
|
||||
|
||||
release();
|
||||
expect(fsSync.existsSync(lockDir)).toBe(false);
|
||||
expect(fakeProcess.listenerCount("SIGINT")).toBe(0);
|
||||
expect(fakeProcess.listenerCount("SIGTERM")).toBe(0);
|
||||
expect(fakeProcess.listenerCount("exit")).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user