fix: enforce gateway single instance
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import Subprocess
|
||||
import Network
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#endif
|
||||
@@ -153,6 +154,10 @@ final class GatewayProcessManager: ObservableObject {
|
||||
self.appendLog("[gateway] starting: \(command.joined(separator: " ")) (cwd: \(cwd))\n")
|
||||
|
||||
do {
|
||||
// Acquire the same UDS lock the CLI uses to guarantee a single instance.
|
||||
let lockPath = FileManager.default.temporaryDirectory.appendingPathComponent("clawdis-gateway.lock").path
|
||||
let listener = try self.acquireGatewayLock(path: lockPath)
|
||||
|
||||
let result = try await run(
|
||||
.name(command.first ?? "clawdis"),
|
||||
arguments: Arguments(Array(command.dropFirst())),
|
||||
@@ -168,12 +173,31 @@ final class GatewayProcessManager: ObservableObject {
|
||||
await err
|
||||
}
|
||||
|
||||
// Release the lock after the process exits.
|
||||
listener.cancel()
|
||||
|
||||
await self.handleTermination(status: result.terminationStatus)
|
||||
} catch {
|
||||
await self.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal clone of the Node gateway lock: bind a UDS and return the listener.
|
||||
private func acquireGatewayLock(path: String) throws -> NWListener {
|
||||
// Remove stale socket if needed
|
||||
try? FileManager.default.removeItem(atPath: path)
|
||||
let params = NWParameters.tcp
|
||||
params.allowLocalEndpointReuse = false
|
||||
let endpoint = NWEndpoint.unix(path: path)
|
||||
let listener = try NWListener(using: params, on: endpoint)
|
||||
listener.newConnectionHandler = { connection in
|
||||
// Any new connection indicates another starter; reject.
|
||||
connection.cancel()
|
||||
}
|
||||
listener.start(queue: .global())
|
||||
return listener
|
||||
}
|
||||
|
||||
private func didStart(_ execution: Execution) {
|
||||
self.execution = execution
|
||||
self.stopping = false
|
||||
|
||||
@@ -10,6 +10,7 @@ import { statusCommand } from "../commands/status.js";
|
||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||
import { startGatewayServer } from "../gateway/server.js";
|
||||
import { danger, info, setVerbose } from "../globals.js";
|
||||
import { GatewayLockError } from "../infra/gateway-lock.js";
|
||||
import { loginWeb, logoutWeb } from "../provider-web.js";
|
||||
import { runRpcLoop } from "../rpc/loop.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@@ -325,6 +326,11 @@ Examples:
|
||||
try {
|
||||
await startGatewayServer(port);
|
||||
} catch (err) {
|
||||
if (err instanceof GatewayLockError) {
|
||||
defaultRuntime.error(`Gateway failed to start: ${err.message}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
defaultRuntime.error(`Gateway failed to start: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import os from "node:os";
|
||||
import { type WebSocket, WebSocketServer } from "ws";
|
||||
import { GatewayLockError, acquireGatewayLock } from "../infra/gateway-lock.js";
|
||||
import { createDefaultDeps } from "../cli/deps.js";
|
||||
import { agentCommand } from "../commands/agent.js";
|
||||
import { getHealthSnapshot } from "../commands/health.js";
|
||||
@@ -102,6 +103,12 @@ function formatError(err: unknown): string {
|
||||
}
|
||||
|
||||
export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
const releaseLock = await acquireGatewayLock().catch((err) => {
|
||||
// Bubble known lock errors so callers can present a nice message.
|
||||
if (err instanceof GatewayLockError) throw err;
|
||||
throw new GatewayLockError(String(err));
|
||||
});
|
||||
|
||||
const wss = new WebSocketServer({
|
||||
port,
|
||||
host: "127.0.0.1",
|
||||
@@ -623,6 +630,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
|
||||
return {
|
||||
close: async () => {
|
||||
await releaseLock();
|
||||
providerAbort.abort();
|
||||
broadcast("shutdown", {
|
||||
reason: "gateway stopping",
|
||||
|
||||
Reference in New Issue
Block a user