mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-23 23:22:32 +00:00
fix: prevent duplicate gateway watchers
This commit is contained in:
4
src/infra/scripts-modules.d.ts
vendored
4
src/infra/scripts-modules.d.ts
vendored
@@ -1,4 +1,5 @@
|
||||
declare module "../../scripts/watch-node.mjs" {
|
||||
export function resolveWatchLockPath(cwd: string, args?: string[]): string;
|
||||
export function runWatchMain(params?: {
|
||||
spawn?: (
|
||||
cmd: string,
|
||||
@@ -10,6 +11,9 @@ declare module "../../scripts/watch-node.mjs" {
|
||||
args?: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
now?: () => number;
|
||||
sleep?: (ms: number) => Promise<void>;
|
||||
signalProcess?: (pid: number, signal: NodeJS.Signals | 0) => void;
|
||||
lockDisabled?: boolean;
|
||||
}): Promise<number>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { EventEmitter } from "node:events";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
@@ -12,6 +13,20 @@ const VOICE_CALL_MANIFEST = bundledPluginFile("voice-call", "openclaw.plugin.jso
|
||||
const VOICE_CALL_PACKAGE = bundledPluginFile("voice-call", "package.json");
|
||||
const VOICE_CALL_INDEX = bundledPluginFile("voice-call", "index.ts");
|
||||
const VOICE_CALL_RUNTIME = bundledPluginFile("voice-call", "src/runtime.ts");
|
||||
type WatchRunParams = NonNullable<Parameters<typeof runWatchMain>[0]> & {
|
||||
lockDisabled?: boolean;
|
||||
signalProcess?: (pid: number, signal: NodeJS.Signals | 0) => void;
|
||||
sleep?: (ms: number) => Promise<void>;
|
||||
};
|
||||
|
||||
const runWatch = (params: WatchRunParams) => runWatchMain(params);
|
||||
const resolveTestWatchLockPath = (cwd: string, args: string[]) =>
|
||||
path.join(
|
||||
cwd,
|
||||
".local",
|
||||
"watch-node",
|
||||
`${createHash("sha256").update(cwd).update("\0").update(args.join("\0")).digest("hex").slice(0, 12)}.json`,
|
||||
);
|
||||
|
||||
const createFakeProcess = () =>
|
||||
Object.assign(new EventEmitter(), {
|
||||
@@ -39,11 +54,12 @@ describe("watch-node script", () => {
|
||||
fs.mkdirSync(path.join(cwd, "src", "infra"), { recursive: true });
|
||||
fs.mkdirSync(path.join(cwd, "extensions", "voice-call"), { recursive: true });
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
cwd,
|
||||
createWatcher,
|
||||
env: { PATH: "/usr/bin" },
|
||||
lockDisabled: true,
|
||||
now: () => 1700000000000,
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
@@ -104,9 +120,10 @@ describe("watch-node script", () => {
|
||||
it("terminates child on SIGINT and returns shell interrupt code", async () => {
|
||||
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
createWatcher,
|
||||
lockDisabled: true,
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
});
|
||||
@@ -124,9 +141,10 @@ describe("watch-node script", () => {
|
||||
it("terminates child on SIGTERM and returns shell terminate code", async () => {
|
||||
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
createWatcher,
|
||||
lockDisabled: true,
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
});
|
||||
@@ -144,9 +162,10 @@ describe("watch-node script", () => {
|
||||
it("returns the child exit code when the runner exits on its own", async () => {
|
||||
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force", "--help"],
|
||||
createWatcher,
|
||||
lockDisabled: true,
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
});
|
||||
@@ -174,9 +193,10 @@ describe("watch-node script", () => {
|
||||
const createWatcher = vi.fn(() => watcher);
|
||||
const fakeProcess = createFakeProcess();
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
createWatcher,
|
||||
lockDisabled: true,
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
});
|
||||
@@ -195,13 +215,14 @@ describe("watch-node script", () => {
|
||||
it("forces no-respawn for watch children even when supervisor hints are inherited", async () => {
|
||||
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
createWatcher,
|
||||
env: {
|
||||
LAUNCH_JOB_LABEL: "ai.openclaw.gateway",
|
||||
PATH: "/usr/bin",
|
||||
},
|
||||
lockDisabled: true,
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
});
|
||||
@@ -255,9 +276,10 @@ describe("watch-node script", () => {
|
||||
const createWatcher = vi.fn(() => watcher);
|
||||
const fakeProcess = createFakeProcess();
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
createWatcher,
|
||||
lockDisabled: true,
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
});
|
||||
@@ -305,9 +327,10 @@ describe("watch-node script", () => {
|
||||
it("kills child and exits when watcher emits an error", async () => {
|
||||
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
||||
|
||||
const runPromise = runWatchMain({
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
createWatcher,
|
||||
lockDisabled: true,
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
});
|
||||
@@ -319,4 +342,66 @@ describe("watch-node script", () => {
|
||||
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
expect(watcher.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("replaces an existing watcher lock holder before starting", async () => {
|
||||
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-watch-node-lock-"));
|
||||
const lockPath = resolveTestWatchLockPath(cwd, ["gateway", "--force"]);
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
lockPath,
|
||||
`${JSON.stringify({
|
||||
pid: 2121,
|
||||
command: "gateway --force",
|
||||
createdAt: new Date(1_700_000_000_000).toISOString(),
|
||||
cwd,
|
||||
watchSession: "existing-session",
|
||||
})}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
let existingWatcherAlive = true;
|
||||
const signalProcess = vi.fn((pid: number, signal: NodeJS.Signals | 0) => {
|
||||
if (signal === 0) {
|
||||
if (pid === 2121 && existingWatcherAlive) {
|
||||
return;
|
||||
}
|
||||
throw Object.assign(new Error("ESRCH"), { code: "ESRCH" });
|
||||
}
|
||||
if (pid === 2121 && signal === "SIGTERM") {
|
||||
existingWatcherAlive = false;
|
||||
return;
|
||||
}
|
||||
throw new Error(`unexpected signal ${signal} for pid ${pid}`);
|
||||
});
|
||||
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
createWatcher,
|
||||
cwd,
|
||||
now: () => 1_700_000_000_000,
|
||||
process: fakeProcess,
|
||||
signalProcess,
|
||||
sleep: async () => {},
|
||||
spawn,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(signalProcess).toHaveBeenCalledWith(2121, "SIGTERM");
|
||||
expect(spawn).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(fs.readFileSync(lockPath, "utf8"))).toMatchObject({
|
||||
pid: 4242,
|
||||
command: "gateway --force",
|
||||
watchSession: "1700000000000-4242",
|
||||
});
|
||||
|
||||
fakeProcess.emit("SIGINT");
|
||||
const exitCode = await runPromise;
|
||||
|
||||
expect(exitCode).toBe(130);
|
||||
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
expect(fs.existsSync(lockPath)).toBe(false);
|
||||
expect(watcher.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user