Skip to content

Commit fa95f44

Browse files
author
Thatgfsj
committed
fix: use triggerOpenClawRestart for Windows unmanaged gateway restart
On Windows, neither SIGUSR1 nor SIGTERM are safe for cross-process restart signaling: SIGUSR1 is unsupported, and Node.js unconditionally terminates the target process on SIGTERM, bypassing any JS-level handler. Instead, use the existing triggerOpenClawRestart() path (schtasks-based handoff) which is the established Windows restart mechanism. If it fails (e.g. no scheduled task installed), the error message directs the user to run "openclaw gateway install". Per ClawSweeper P1 review feedback — the SIGTERM substitution was incorrect because: "Node v26 docs state that process.kill() with SIGTERM on Windows causes unconditional termination of the target process" so the gateway's SIGTERM handler would never run.
1 parent 90fdf32 commit fa95f44

2 files changed

Lines changed: 36 additions & 14 deletions

File tree

src/cli/daemon-cli/lifecycle.test.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const renderRestartDiagnostics = vi.fn(() => ["diag: unhealthy runtime"]);
3838
const resolveGatewayPort = vi.hoisted(() => vi.fn((_cfg?: unknown, _env?: unknown) => 18789));
3939
const findVerifiedGatewayListenerPidsOnPortSync = vi.fn<(port: number) => number[]>(() => []);
4040
const signalVerifiedGatewayPidSync = vi.fn<(pid: number, signal: "SIGTERM" | "SIGUSR1") => void>();
41+
const triggerOpenClawRestart = vi.fn<() => { ok: boolean; method: string; detail?: string; tried?: string[] }>(() => ({
42+
ok: true,
43+
method: "schtasks",
44+
}));
4145
const formatGatewayPidList = vi.fn<(pids: number[]) => string>((pids) => pids.join(", "));
4246
const probeGateway = vi.fn<
4347
(opts: {
@@ -93,6 +97,11 @@ vi.mock("../../infra/gateway-processes.js", () => ({
9397
formatGatewayPidList: (pids: number[]) => formatGatewayPidList(pids),
9498
}));
9599

100+
vi.mock("../../infra/restart.js", () => ({
101+
writeGatewayRestartIntentSync: vi.fn(),
102+
triggerOpenClawRestart: () => triggerOpenClawRestart(),
103+
}));
104+
96105
vi.mock("../../gateway/probe.js", () => ({
97106
probeGateway: (opts: {
98107
url: string;
@@ -535,19 +544,18 @@ describe("runDaemonRestart health checks", () => {
535544
expect(service.restart).not.toHaveBeenCalled();
536545
});
537546

538-
it("uses SIGTERM for unmanaged gateway restart on Windows", async () => {
547+
it("uses scheduled task restart for unmanaged gateway restart on Windows", async () => {
539548
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
540549
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4200]);
541550
mockUnmanagedRestart({ runPostRestartCheck: true });
542551

543552
await runDaemonRestart({ json: true });
544553

545554
expect(findVerifiedGatewayListenerPidsOnPortSync).toHaveBeenCalledWith(18789);
546-
// SIGUSR1 is unsupported on Windows; SIGTERM is sent instead.
547-
// The gateway's SIGTERM handler reads the restart intent file (written
548-
// before signaling) and triggers a restart rather than a stop.
549-
expect(signalVerifiedGatewayPidSync).toHaveBeenCalledWith(4200, "SIGTERM");
550-
expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalledWith(4200, "SIGUSR1");
555+
// On Windows, SIGUSR1 is unsupported for cross-process signaling.
556+
// triggerOpenClawRestart() uses the scheduled-task restart handoff.
557+
expect(triggerOpenClawRestart).toHaveBeenCalledOnce();
558+
expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalled();
551559
expect(probeGateway).toHaveBeenCalledTimes(1);
552560
expect(waitForGatewayHealthyListener).toHaveBeenCalledTimes(1);
553561
expect(waitForGatewayHealthyRestart).not.toHaveBeenCalled();

src/cli/daemon-cli/lifecycle.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
signalVerifiedGatewayPidSync,
1010
} from "../../infra/gateway-processes.js";
1111
import type { SafeGatewayRestartRequestResult } from "../../infra/restart-coordinator.js";
12-
import { type GatewayRestartIntent, writeGatewayRestartIntentSync } from "../../infra/restart.js";
12+
import {
13+
type GatewayRestartIntent,
14+
triggerOpenClawRestart,
15+
writeGatewayRestartIntentSync,
16+
} from "../../infra/restart.js";
1317
import { defaultRuntime } from "../../runtime.js";
1418
import { normalizeOptionalString } from "../../shared/string-coerce.js";
1519
import { theme } from "../../terminal/theme.js";
@@ -211,13 +215,23 @@ async function restartGatewayWithoutServiceManager(
211215
reason: "gateway.restart",
212216
...(restartIntent ? { intent: restartIntent } : {}),
213217
});
214-
// On Windows, SIGUSR1 is not supported; use SIGTERM instead.
215-
// The gateway's SIGTERM handler reads the restart intent file written above
216-
// and triggers a draining restart (instead of stopping) when the intent is present.
217-
signalVerifiedGatewayPidSync(
218-
pids[0],
219-
process.platform === "win32" ? "SIGTERM" : "SIGUSR1",
220-
);
218+
// On Windows, SIGUSR1 is not supported for cross-process signaling.
219+
// Node.js unconditionally terminates the target on Windows for SIGTERM too,
220+
// so we must use the scheduled-task restart handoff instead.
221+
if (process.platform === "win32") {
222+
const result = triggerOpenClawRestart();
223+
if (!result.ok) {
224+
throw new Error(
225+
`Gateway restart failed on Windows: ${result.detail ?? "unknown error"}. ` +
226+
`Run "openclaw gateway install" to set up the Windows scheduled task.`,
227+
);
228+
}
229+
return {
230+
result: "restarted" as const,
231+
message: `Gateway restart triggered via ${result.method} on port ${port}: ${pids[0]}.`,
232+
};
233+
}
234+
signalVerifiedGatewayPidSync(pids[0], "SIGUSR1");
221235
return {
222236
result: "restarted" as const,
223237
message: `Gateway restart signal sent to unmanaged process on port ${port}: ${pids[0]}.`,

0 commit comments

Comments
 (0)