mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-08 18:33:39 +02:00
fix(dev): forward run-node wrapper signals
This commit is contained in:
@@ -19,6 +19,7 @@ export function runNodeMain(params?: {
|
||||
args: string[],
|
||||
options: unknown,
|
||||
) => {
|
||||
kill?: (signal?: string) => boolean | void;
|
||||
on: (
|
||||
event: "exit",
|
||||
cb: (code: number | null, signal: string | null) => void,
|
||||
@@ -27,6 +28,7 @@ export function runNodeMain(params?: {
|
||||
spawnSync?: unknown;
|
||||
fs?: unknown;
|
||||
stderr?: { write: (value: string) => void };
|
||||
process?: NodeJS.Process;
|
||||
execPath?: string;
|
||||
cwd?: string;
|
||||
args?: string[];
|
||||
|
||||
@@ -250,6 +250,15 @@ const BUILD_REASON_LABELS = {
|
||||
|
||||
const formatBuildReason = (reason) => BUILD_REASON_LABELS[reason] ?? reason;
|
||||
|
||||
const SIGNAL_EXIT_CODES = {
|
||||
SIGINT: 130,
|
||||
SIGTERM: 143,
|
||||
};
|
||||
|
||||
const isSignalKey = (signal) => Object.hasOwn(SIGNAL_EXIT_CODES, signal);
|
||||
|
||||
const getSignalExitCode = (signal) => (isSignalKey(signal) ? SIGNAL_EXIT_CODES[signal] : 1);
|
||||
|
||||
const logRunner = (message, deps) => {
|
||||
if (deps.env.OPENCLAW_RUNNER_LOG === "0") {
|
||||
return;
|
||||
@@ -257,19 +266,65 @@ const logRunner = (message, deps) => {
|
||||
deps.stderr.write(`[openclaw] ${message}\n`);
|
||||
};
|
||||
|
||||
const waitForSpawnedProcess = async (childProcess, deps) => {
|
||||
let forwardedSignal = null;
|
||||
let onSigInt;
|
||||
let onSigTerm;
|
||||
|
||||
const cleanupSignals = () => {
|
||||
if (onSigInt) {
|
||||
deps.process.off("SIGINT", onSigInt);
|
||||
}
|
||||
if (onSigTerm) {
|
||||
deps.process.off("SIGTERM", onSigTerm);
|
||||
}
|
||||
};
|
||||
|
||||
const forwardSignal = (signal) => {
|
||||
if (forwardedSignal) {
|
||||
return;
|
||||
}
|
||||
forwardedSignal = signal;
|
||||
try {
|
||||
childProcess.kill?.(signal);
|
||||
} catch {
|
||||
// Best-effort only. Exit handling still happens via the child "exit" event.
|
||||
}
|
||||
};
|
||||
|
||||
onSigInt = () => {
|
||||
forwardSignal("SIGINT");
|
||||
};
|
||||
onSigTerm = () => {
|
||||
forwardSignal("SIGTERM");
|
||||
};
|
||||
|
||||
deps.process.on("SIGINT", onSigInt);
|
||||
deps.process.on("SIGTERM", onSigTerm);
|
||||
|
||||
try {
|
||||
return await new Promise((resolve) => {
|
||||
childProcess.on("exit", (exitCode, exitSignal) => {
|
||||
resolve({ exitCode, exitSignal, forwardedSignal });
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
cleanupSignals();
|
||||
}
|
||||
};
|
||||
|
||||
const runOpenClaw = async (deps) => {
|
||||
const nodeProcess = deps.spawn(deps.execPath, ["openclaw.mjs", ...deps.args], {
|
||||
cwd: deps.cwd,
|
||||
env: deps.env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
const res = await new Promise((resolve) => {
|
||||
nodeProcess.on("exit", (exitCode, exitSignal) => {
|
||||
resolve({ exitCode, exitSignal });
|
||||
});
|
||||
});
|
||||
const res = await waitForSpawnedProcess(nodeProcess, deps);
|
||||
if (res.exitSignal) {
|
||||
return 1;
|
||||
return getSignalExitCode(res.exitSignal);
|
||||
}
|
||||
if (res.forwardedSignal) {
|
||||
return getSignalExitCode(res.forwardedSignal);
|
||||
}
|
||||
return res.exitCode ?? 1;
|
||||
};
|
||||
@@ -306,6 +361,7 @@ export async function runNodeMain(params = {}) {
|
||||
spawnSync: params.spawnSync ?? spawnSync,
|
||||
fs: params.fs ?? fs,
|
||||
stderr: params.stderr ?? process.stderr,
|
||||
process: params.process ?? process,
|
||||
execPath: params.execPath ?? process.execPath,
|
||||
cwd: params.cwd ?? process.cwd(),
|
||||
args: params.args ?? process.argv.slice(2),
|
||||
@@ -341,11 +397,12 @@ export async function runNodeMain(params = {}) {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
const buildRes = await new Promise((resolve) => {
|
||||
build.on("exit", (exitCode, exitSignal) => resolve({ exitCode, exitSignal }));
|
||||
});
|
||||
const buildRes = await waitForSpawnedProcess(build, deps);
|
||||
if (buildRes.exitSignal) {
|
||||
return 1;
|
||||
return getSignalExitCode(buildRes.exitSignal);
|
||||
}
|
||||
if (buildRes.forwardedSignal) {
|
||||
return getSignalExitCode(buildRes.forwardedSignal);
|
||||
}
|
||||
if (buildRes.exitCode !== 0 && buildRes.exitCode !== null) {
|
||||
return buildRes.exitCode;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveBuildRequirement, runNodeMain } from "../../scripts/run-node.mjs";
|
||||
import {
|
||||
bundledDistPluginFile,
|
||||
@@ -54,6 +55,13 @@ function createExitedProcess(code: number | null, signal: string | null = null)
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeProcess() {
|
||||
return Object.assign(new EventEmitter(), {
|
||||
pid: 4242,
|
||||
execPath: process.execPath,
|
||||
}) as unknown as NodeJS.Process;
|
||||
}
|
||||
|
||||
async function writeRuntimePostBuildScaffold(tmp: string): Promise<void> {
|
||||
const pluginSdkAliasPath = path.join(tmp, "src", "plugin-sdk", "root-alias.cjs");
|
||||
await fs.mkdir(path.dirname(pluginSdkAliasPath), { recursive: true });
|
||||
@@ -341,6 +349,66 @@ describe("run-node script", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards wrapper SIGTERM to the active openclaw child and returns 143", async () => {
|
||||
await withTempDir(async (tmp) => {
|
||||
await setupTrackedProject(tmp, {
|
||||
files: {
|
||||
[ROOT_SRC]: "export const value = 1;\n",
|
||||
},
|
||||
oldPaths: [ROOT_SRC, ROOT_TSCONFIG, ROOT_PACKAGE],
|
||||
buildPaths: [DIST_ENTRY, BUILD_STAMP],
|
||||
});
|
||||
|
||||
const fakeProcess = createFakeProcess();
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
kill: vi.fn((signal: string) => {
|
||||
queueMicrotask(() => child.emit("exit", 0, null));
|
||||
return signal;
|
||||
}),
|
||||
});
|
||||
const spawn = vi.fn<
|
||||
(
|
||||
cmd: string,
|
||||
args: string[],
|
||||
options: unknown,
|
||||
) => {
|
||||
kill: (signal?: string) => string;
|
||||
on: (event: "exit", cb: (code: number | null, signal: string | null) => void) => void;
|
||||
}
|
||||
>(() => ({
|
||||
kill: (signal) => child.kill(String(signal ?? "SIGTERM")),
|
||||
on: (event, cb) => {
|
||||
child.on(event, cb);
|
||||
},
|
||||
}));
|
||||
|
||||
const exitCodePromise = runNodeMain({
|
||||
cwd: tmp,
|
||||
args: ["status"],
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_RUNNER_LOG: "0",
|
||||
},
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
execPath: process.execPath,
|
||||
});
|
||||
|
||||
fakeProcess.emit("SIGTERM");
|
||||
const exitCode = await exitCodePromise;
|
||||
|
||||
expect(exitCode).toBe(143);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
process.execPath,
|
||||
["openclaw.mjs", "status"],
|
||||
expect.objectContaining({ stdio: "inherit" }),
|
||||
);
|
||||
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
expect(fakeProcess.listenerCount("SIGINT")).toBe(0);
|
||||
expect(fakeProcess.listenerCount("SIGTERM")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("rebuilds when extension sources are newer than the build stamp", async () => {
|
||||
await withTempDir(async (tmp) => {
|
||||
await setupTrackedProject(tmp, {
|
||||
|
||||
Reference in New Issue
Block a user