mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 00:33:43 +00:00
fix(cli): wait for process exit before restarting gateway on Windows (openclaw#27913) thanks @tda1017
Verified: - pnpm vitest src/cli/update-cli/restart-helper.test.ts - pnpm check - pnpm build Co-authored-by: tda1017 <95275462+tda1017@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -11,8 +11,8 @@ describe("restart-helper", () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalGetUid = process.getuid;
|
||||
|
||||
async function prepareAndReadScript(env: Record<string, string>) {
|
||||
const scriptPath = await prepareRestartScript(env);
|
||||
async function prepareAndReadScript(env: Record<string, string>, gatewayPort = 18789) {
|
||||
const scriptPath = await prepareRestartScript(env, gatewayPort);
|
||||
expect(scriptPath).toBeTruthy();
|
||||
const content = await fs.readFile(scriptPath!, "utf-8");
|
||||
return { scriptPath: scriptPath!, content };
|
||||
@@ -22,6 +22,39 @@ describe("restart-helper", () => {
|
||||
await fs.unlink(scriptPath);
|
||||
}
|
||||
|
||||
function expectWindowsRestartWaitOrdering(content: string, port = 18789) {
|
||||
const endCommand = 'schtasks /End /TN "';
|
||||
const pollAttemptsInit = "set /a attempts=0";
|
||||
const pollLabel = ":wait_for_port_release";
|
||||
const pollAttemptIncrement = "set /a attempts+=1";
|
||||
const pollNetstatCheck = `netstat -ano | findstr /R /C:":${port} .*LISTENING" >nul`;
|
||||
const forceKillLabel = ":force_kill_listener";
|
||||
const forceKillCommand = "taskkill /F /PID %%P >nul 2>&1";
|
||||
const portReleasedLabel = ":port_released";
|
||||
const runCommand = 'schtasks /Run /TN "';
|
||||
const endIndex = content.indexOf(endCommand);
|
||||
const attemptsInitIndex = content.indexOf(pollAttemptsInit, endIndex);
|
||||
const pollLabelIndex = content.indexOf(pollLabel, attemptsInitIndex);
|
||||
const pollAttemptIncrementIndex = content.indexOf(pollAttemptIncrement, pollLabelIndex);
|
||||
const pollNetstatCheckIndex = content.indexOf(pollNetstatCheck, pollAttemptIncrementIndex);
|
||||
const forceKillLabelIndex = content.indexOf(forceKillLabel, pollNetstatCheckIndex);
|
||||
const forceKillCommandIndex = content.indexOf(forceKillCommand, forceKillLabelIndex);
|
||||
const portReleasedLabelIndex = content.indexOf(portReleasedLabel, forceKillCommandIndex);
|
||||
const runIndex = content.indexOf(runCommand, portReleasedLabelIndex);
|
||||
|
||||
expect(endIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(attemptsInitIndex).toBeGreaterThan(endIndex);
|
||||
expect(pollLabelIndex).toBeGreaterThan(attemptsInitIndex);
|
||||
expect(pollAttemptIncrementIndex).toBeGreaterThan(pollLabelIndex);
|
||||
expect(pollNetstatCheckIndex).toBeGreaterThan(pollAttemptIncrementIndex);
|
||||
expect(forceKillLabelIndex).toBeGreaterThan(pollNetstatCheckIndex);
|
||||
expect(forceKillCommandIndex).toBeGreaterThan(forceKillLabelIndex);
|
||||
expect(portReleasedLabelIndex).toBeGreaterThan(forceKillCommandIndex);
|
||||
expect(runIndex).toBeGreaterThan(portReleasedLabelIndex);
|
||||
|
||||
expect(content).not.toContain("timeout /t 3 /nobreak >nul");
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
@@ -91,6 +124,7 @@ describe("restart-helper", () => {
|
||||
expect(content).toContain("@echo off");
|
||||
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway"');
|
||||
expect(content).toContain('schtasks /Run /TN "OpenClaw Gateway"');
|
||||
expectWindowsRestartWaitOrdering(content);
|
||||
// Batch self-cleanup
|
||||
expect(content).toContain('del "%~f0"');
|
||||
await cleanupScript(scriptPath);
|
||||
@@ -105,6 +139,25 @@ describe("restart-helper", () => {
|
||||
});
|
||||
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway (custom)"');
|
||||
expect(content).toContain('schtasks /Run /TN "OpenClaw Gateway (custom)"');
|
||||
expectWindowsRestartWaitOrdering(content);
|
||||
await cleanupScript(scriptPath);
|
||||
});
|
||||
|
||||
it("uses passed gateway port for port polling on Windows", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
const customPort = 9999;
|
||||
|
||||
const { scriptPath, content } = await prepareAndReadScript(
|
||||
{
|
||||
OPENCLAW_PROFILE: "default",
|
||||
},
|
||||
customPort,
|
||||
);
|
||||
expect(content).toContain(`netstat -ano | findstr /R /C:":${customPort} .*LISTENING" >nul`);
|
||||
expect(content).toContain(
|
||||
`for /f "tokens=5" %%P in ('netstat -ano ^| findstr /R /C:":${customPort} .*LISTENING"') do (`,
|
||||
);
|
||||
expectWindowsRestartWaitOrdering(content, customPort);
|
||||
await cleanupScript(scriptPath);
|
||||
});
|
||||
|
||||
@@ -135,6 +188,7 @@ describe("restart-helper", () => {
|
||||
OPENCLAW_PROFILE: "production",
|
||||
});
|
||||
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway (production)"');
|
||||
expectWindowsRestartWaitOrdering(content);
|
||||
await cleanupScript(scriptPath);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { DEFAULT_GATEWAY_PORT } from "../../config/paths.js";
|
||||
import {
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
@@ -55,6 +56,7 @@ function resolveWindowsTaskName(env: NodeJS.ProcessEnv): string {
|
||||
*/
|
||||
export async function prepareRestartScript(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
gatewayPort: number = DEFAULT_GATEWAY_PORT,
|
||||
): Promise<string | null> {
|
||||
const tmpDir = os.tmpdir();
|
||||
const timestamp = Date.now();
|
||||
@@ -95,12 +97,29 @@ rm -f "$0"
|
||||
if (!isBatchSafe(taskName)) {
|
||||
return null;
|
||||
}
|
||||
const port =
|
||||
Number.isFinite(gatewayPort) && gatewayPort > 0 ? gatewayPort : DEFAULT_GATEWAY_PORT;
|
||||
filename = `openclaw-restart-${timestamp}.bat`;
|
||||
scriptContent = `@echo off
|
||||
REM Standalone restart script — survives parent process termination.
|
||||
REM Wait briefly to ensure file locks are released after update.
|
||||
timeout /t 2 /nobreak >nul
|
||||
schtasks /End /TN "${taskName}"
|
||||
REM Poll for gateway port release before rerun; force-kill listener if stuck.
|
||||
set /a attempts=0
|
||||
:wait_for_port_release
|
||||
set /a attempts+=1
|
||||
netstat -ano | findstr /R /C:":${port} .*LISTENING" >nul
|
||||
if errorlevel 1 goto port_released
|
||||
if %attempts% GEQ 10 goto force_kill_listener
|
||||
timeout /t 1 /nobreak >nul
|
||||
goto wait_for_port_release
|
||||
:force_kill_listener
|
||||
for /f "tokens=5" %%P in ('netstat -ano ^| findstr /R /C:":${port} .*LISTENING"') do (
|
||||
taskkill /F /PID %%P >nul 2>&1
|
||||
goto port_released
|
||||
)
|
||||
:port_released
|
||||
schtasks /Run /TN "${taskName}"
|
||||
REM Self-cleanup
|
||||
del "%~f0"
|
||||
|
||||
@@ -818,11 +818,15 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
|
||||
let restartScriptPath: string | null = null;
|
||||
let refreshGatewayServiceEnv = false;
|
||||
const gatewayPort = resolveGatewayPort(
|
||||
configSnapshot.valid ? configSnapshot.config : undefined,
|
||||
process.env,
|
||||
);
|
||||
if (shouldRestart) {
|
||||
try {
|
||||
const loaded = await resolveGatewayService().isLoaded({ env: process.env });
|
||||
if (loaded) {
|
||||
restartScriptPath = await prepareRestartScript(process.env);
|
||||
restartScriptPath = await prepareRestartScript(process.env, gatewayPort);
|
||||
refreshGatewayServiceEnv = true;
|
||||
}
|
||||
} catch {
|
||||
@@ -903,7 +907,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
result,
|
||||
opts,
|
||||
refreshServiceEnv: refreshGatewayServiceEnv,
|
||||
gatewayPort: resolveGatewayPort(configSnapshot.valid ? configSnapshot.config : undefined),
|
||||
gatewayPort,
|
||||
restartScriptPath,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user