fix: enforce gateway single instance
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
import Subprocess
|
import Subprocess
|
||||||
|
import Network
|
||||||
#if canImport(Darwin)
|
#if canImport(Darwin)
|
||||||
import Darwin
|
import Darwin
|
||||||
#endif
|
#endif
|
||||||
@@ -153,6 +154,10 @@ final class GatewayProcessManager: ObservableObject {
|
|||||||
self.appendLog("[gateway] starting: \(command.joined(separator: " ")) (cwd: \(cwd))\n")
|
self.appendLog("[gateway] starting: \(command.joined(separator: " ")) (cwd: \(cwd))\n")
|
||||||
|
|
||||||
do {
|
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(
|
let result = try await run(
|
||||||
.name(command.first ?? "clawdis"),
|
.name(command.first ?? "clawdis"),
|
||||||
arguments: Arguments(Array(command.dropFirst())),
|
arguments: Arguments(Array(command.dropFirst())),
|
||||||
@@ -168,12 +173,31 @@ final class GatewayProcessManager: ObservableObject {
|
|||||||
await err
|
await err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release the lock after the process exits.
|
||||||
|
listener.cancel()
|
||||||
|
|
||||||
await self.handleTermination(status: result.terminationStatus)
|
await self.handleTermination(status: result.terminationStatus)
|
||||||
} catch {
|
} catch {
|
||||||
await self.handleError(error)
|
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) {
|
private func didStart(_ execution: Execution) {
|
||||||
self.execution = execution
|
self.execution = execution
|
||||||
self.stopping = false
|
self.stopping = false
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { statusCommand } from "../commands/status.js";
|
|||||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||||
import { startGatewayServer } from "../gateway/server.js";
|
import { startGatewayServer } from "../gateway/server.js";
|
||||||
import { danger, info, setVerbose } from "../globals.js";
|
import { danger, info, setVerbose } from "../globals.js";
|
||||||
|
import { GatewayLockError } from "../infra/gateway-lock.js";
|
||||||
import { loginWeb, logoutWeb } from "../provider-web.js";
|
import { loginWeb, logoutWeb } from "../provider-web.js";
|
||||||
import { runRpcLoop } from "../rpc/loop.js";
|
import { runRpcLoop } from "../rpc/loop.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
@@ -325,6 +326,11 @@ Examples:
|
|||||||
try {
|
try {
|
||||||
await startGatewayServer(port);
|
await startGatewayServer(port);
|
||||||
} catch (err) {
|
} 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.error(`Gateway failed to start: ${String(err)}`);
|
||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import { type WebSocket, WebSocketServer } from "ws";
|
import { type WebSocket, WebSocketServer } from "ws";
|
||||||
|
import { GatewayLockError, acquireGatewayLock } from "../infra/gateway-lock.js";
|
||||||
import { createDefaultDeps } from "../cli/deps.js";
|
import { createDefaultDeps } from "../cli/deps.js";
|
||||||
import { agentCommand } from "../commands/agent.js";
|
import { agentCommand } from "../commands/agent.js";
|
||||||
import { getHealthSnapshot } from "../commands/health.js";
|
import { getHealthSnapshot } from "../commands/health.js";
|
||||||
@@ -102,6 +103,12 @@ function formatError(err: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
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({
|
const wss = new WebSocketServer({
|
||||||
port,
|
port,
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
@@ -623,6 +630,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
close: async () => {
|
close: async () => {
|
||||||
|
await releaseLock();
|
||||||
providerAbort.abort();
|
providerAbort.abort();
|
||||||
broadcast("shutdown", {
|
broadcast("shutdown", {
|
||||||
reason: "gateway stopping",
|
reason: "gateway stopping",
|
||||||
|
|||||||
Reference in New Issue
Block a user