mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 07:41:10 +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 originalPlatform = process.platform;
|
||||||
const originalGetUid = process.getuid;
|
const originalGetUid = process.getuid;
|
||||||
|
|
||||||
async function prepareAndReadScript(env: Record<string, string>) {
|
async function prepareAndReadScript(env: Record<string, string>, gatewayPort = 18789) {
|
||||||
const scriptPath = await prepareRestartScript(env);
|
const scriptPath = await prepareRestartScript(env, gatewayPort);
|
||||||
expect(scriptPath).toBeTruthy();
|
expect(scriptPath).toBeTruthy();
|
||||||
const content = await fs.readFile(scriptPath!, "utf-8");
|
const content = await fs.readFile(scriptPath!, "utf-8");
|
||||||
return { scriptPath: scriptPath!, content };
|
return { scriptPath: scriptPath!, content };
|
||||||
@@ -22,6 +22,39 @@ describe("restart-helper", () => {
|
|||||||
await fs.unlink(scriptPath);
|
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(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
@@ -91,6 +124,7 @@ describe("restart-helper", () => {
|
|||||||
expect(content).toContain("@echo off");
|
expect(content).toContain("@echo off");
|
||||||
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway"');
|
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway"');
|
||||||
expect(content).toContain('schtasks /Run /TN "OpenClaw Gateway"');
|
expect(content).toContain('schtasks /Run /TN "OpenClaw Gateway"');
|
||||||
|
expectWindowsRestartWaitOrdering(content);
|
||||||
// Batch self-cleanup
|
// Batch self-cleanup
|
||||||
expect(content).toContain('del "%~f0"');
|
expect(content).toContain('del "%~f0"');
|
||||||
await cleanupScript(scriptPath);
|
await cleanupScript(scriptPath);
|
||||||
@@ -105,6 +139,25 @@ describe("restart-helper", () => {
|
|||||||
});
|
});
|
||||||
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway (custom)"');
|
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway (custom)"');
|
||||||
expect(content).toContain('schtasks /Run /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);
|
await cleanupScript(scriptPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,6 +188,7 @@ describe("restart-helper", () => {
|
|||||||
OPENCLAW_PROFILE: "production",
|
OPENCLAW_PROFILE: "production",
|
||||||
});
|
});
|
||||||
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway (production)"');
|
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway (production)"');
|
||||||
|
expectWindowsRestartWaitOrdering(content);
|
||||||
await cleanupScript(scriptPath);
|
await cleanupScript(scriptPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { DEFAULT_GATEWAY_PORT } from "../../config/paths.js";
|
||||||
import {
|
import {
|
||||||
resolveGatewayLaunchAgentLabel,
|
resolveGatewayLaunchAgentLabel,
|
||||||
resolveGatewaySystemdServiceName,
|
resolveGatewaySystemdServiceName,
|
||||||
@@ -55,6 +56,7 @@ function resolveWindowsTaskName(env: NodeJS.ProcessEnv): string {
|
|||||||
*/
|
*/
|
||||||
export async function prepareRestartScript(
|
export async function prepareRestartScript(
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
gatewayPort: number = DEFAULT_GATEWAY_PORT,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const tmpDir = os.tmpdir();
|
const tmpDir = os.tmpdir();
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
@@ -95,12 +97,29 @@ rm -f "$0"
|
|||||||
if (!isBatchSafe(taskName)) {
|
if (!isBatchSafe(taskName)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const port =
|
||||||
|
Number.isFinite(gatewayPort) && gatewayPort > 0 ? gatewayPort : DEFAULT_GATEWAY_PORT;
|
||||||
filename = `openclaw-restart-${timestamp}.bat`;
|
filename = `openclaw-restart-${timestamp}.bat`;
|
||||||
scriptContent = `@echo off
|
scriptContent = `@echo off
|
||||||
REM Standalone restart script — survives parent process termination.
|
REM Standalone restart script — survives parent process termination.
|
||||||
REM Wait briefly to ensure file locks are released after update.
|
REM Wait briefly to ensure file locks are released after update.
|
||||||
timeout /t 2 /nobreak >nul
|
timeout /t 2 /nobreak >nul
|
||||||
schtasks /End /TN "${taskName}"
|
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}"
|
schtasks /Run /TN "${taskName}"
|
||||||
REM Self-cleanup
|
REM Self-cleanup
|
||||||
del "%~f0"
|
del "%~f0"
|
||||||
|
|||||||
@@ -818,11 +818,15 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
|||||||
|
|
||||||
let restartScriptPath: string | null = null;
|
let restartScriptPath: string | null = null;
|
||||||
let refreshGatewayServiceEnv = false;
|
let refreshGatewayServiceEnv = false;
|
||||||
|
const gatewayPort = resolveGatewayPort(
|
||||||
|
configSnapshot.valid ? configSnapshot.config : undefined,
|
||||||
|
process.env,
|
||||||
|
);
|
||||||
if (shouldRestart) {
|
if (shouldRestart) {
|
||||||
try {
|
try {
|
||||||
const loaded = await resolveGatewayService().isLoaded({ env: process.env });
|
const loaded = await resolveGatewayService().isLoaded({ env: process.env });
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
restartScriptPath = await prepareRestartScript(process.env);
|
restartScriptPath = await prepareRestartScript(process.env, gatewayPort);
|
||||||
refreshGatewayServiceEnv = true;
|
refreshGatewayServiceEnv = true;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -903,7 +907,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
|||||||
result,
|
result,
|
||||||
opts,
|
opts,
|
||||||
refreshServiceEnv: refreshGatewayServiceEnv,
|
refreshServiceEnv: refreshGatewayServiceEnv,
|
||||||
gatewayPort: resolveGatewayPort(configSnapshot.valid ? configSnapshot.config : undefined),
|
gatewayPort,
|
||||||
restartScriptPath,
|
restartScriptPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user