Skip to content

Commit 28c19eb

Browse files
Metamoltyandreasjansson
authored andcommitted
fix: prevent gateway double-spawn via port probe safety net
Fixes #289 When the gateway is already running but listProcesses() fails to detect it (e.g. the command string appears in an unexpected form), a second spawn attempt causes 'port already in use' errors. Add a TCP port probe (nc -z) as a safety net before spawning. If port 18789 is already listening, the gateway is definitively running — return null to signal 'gateway is up' without a process handle. All callers only need the gateway to be reachable; none use the returned Process object. This avoids the bug in #293 where the port-check fallback tried to return an arbitrary running process from listProcesses(), which could be a completely unrelated process (rclone sync, shell session, etc.), and fell through to spawn on no match — defeating the safety net.
1 parent 5894364 commit 28c19eb

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

src/gateway/process.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { describe, it, expect, vi } from 'vitest';
22
import { findExistingGatewayProcess } from './process';
3+
import { findExistingGatewayProcess, isGatewayPortOpen } from './process';
34
import type { Sandbox, Process } from '@cloudflare/sandbox';
4-
import { createMockSandbox } from '../test-utils';
5+
import { createMockSandbox, createMockExecResult } from '../test-utils';
56

67
function createFullMockProcess(overrides: Partial<Process> = {}): Process {
78
return {
@@ -143,3 +144,29 @@ describe('findExistingGatewayProcess', () => {
143144
expect(result).toBeNull();
144145
});
145146
});
147+
148+
describe('isGatewayPortOpen', () => {
149+
it('returns true when port is open (nc exits 0)', async () => {
150+
const { sandbox, execMock } = createMockSandbox();
151+
execMock.mockResolvedValue(createMockExecResult('', { exitCode: 0 }));
152+
153+
const result = await isGatewayPortOpen(sandbox);
154+
expect(result).toBe(true);
155+
expect(execMock).toHaveBeenCalledWith('nc -z localhost 18789');
156+
});
157+
158+
it('returns false when port is closed (nc exits non-zero)', async () => {
159+
const { sandbox, execMock } = createMockSandbox();
160+
execMock.mockResolvedValue(createMockExecResult('', { exitCode: 1 }));
161+
162+
const result = await isGatewayPortOpen(sandbox);
163+
expect(result).toBe(false);
164+
});
165+
166+
it('propagates errors from sandbox.exec', async () => {
167+
const { sandbox, execMock } = createMockSandbox();
168+
execMock.mockRejectedValue(new Error('container not ready'));
169+
170+
await expect(isGatewayPortOpen(sandbox)).rejects.toThrow('container not ready');
171+
});
172+
});

src/gateway/process.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import type { OpenClawEnv } from '../types';
33
import { GATEWAY_PORT, STARTUP_TIMEOUT_MS } from '../config';
44
import { buildEnvVars } from './env';
55

6+
/**
7+
* Check if the gateway port is already listening via a TCP probe.
8+
* Used as a safety net when listProcesses() fails to detect the gateway.
9+
*/
10+
export async function isGatewayPortOpen(sandbox: Sandbox): Promise<boolean> {
11+
const result = await sandbox.exec(`nc -z localhost ${GATEWAY_PORT}`);
12+
return result.exitCode === 0;
13+
}
14+
615
/**
716
* Find an existing OpenClaw gateway process
817
*
@@ -50,9 +59,11 @@ export async function findExistingGatewayProcess(sandbox: Sandbox): Promise<Proc
5059
*
5160
* @param sandbox - The sandbox instance
5261
* @param env - Worker environment bindings
53-
* @returns The running gateway process
62+
* @returns The running gateway process, or null if the gateway is up but we
63+
* don't have a process handle (detected via port probe only)
5464
*/
5565
export async function ensureGateway(sandbox: Sandbox, env: OpenClawEnv): Promise<Process> {
66+
export async function ensureGateway(sandbox: Sandbox, env: OpenClawEnv): Promise<Process | null> {
5667
// Check if gateway is already running or starting
5768
const existingProcess = await findExistingGatewayProcess(sandbox);
5869
if (existingProcess) {
@@ -83,6 +94,18 @@ export async function ensureGateway(sandbox: Sandbox, env: OpenClawEnv): Promise
8394
}
8495
}
8596

97+
// Safety net: the process wasn't found by listProcesses() (e.g. the command
98+
// string didn't match any known pattern), but the gateway may still be running.
99+
// Probe the port directly — if it's open, the gateway is up and we're done.
100+
try {
101+
if (await isGatewayPortOpen(sandbox)) {
102+
console.log(`Port ${GATEWAY_PORT} already open — gateway running but undetected by listProcesses(), skipping spawn`);
103+
return null;
104+
}
105+
} catch (e) {
106+
console.log('Port probe failed, proceeding to start gateway:', e);
107+
}
108+
86109
// Start a new OpenClaw gateway
87110
console.log('Starting new OpenClaw gateway...');
88111
const envVars = buildEnvVars(env);

0 commit comments

Comments
 (0)