diff --git a/src/macos/gateway-daemon.ts b/src/macos/gateway-daemon.ts deleted file mode 100644 index 46fa9b41984..00000000000 --- a/src/macos/gateway-daemon.ts +++ /dev/null @@ -1,279 +0,0 @@ -#!/usr/bin/env node -import process from "node:process"; -import type { GatewayLockHandle } from "../infra/gateway-lock.js"; -import { restartGatewayProcessWithFreshPid } from "../infra/process-respawn.js"; - -declare const __OPENCLAW_VERSION__: string | undefined; - -const BUNDLED_VERSION = - (typeof __OPENCLAW_VERSION__ === "string" && __OPENCLAW_VERSION__) || - process.env.OPENCLAW_BUNDLED_VERSION || - "0.0.0"; - -function argValue(args: string[], flag: string): string | undefined { - const idx = args.indexOf(flag); - if (idx < 0) { - return undefined; - } - const value = args[idx + 1]; - return value && !value.startsWith("-") ? value : undefined; -} - -function hasFlag(args: string[], flag: string): boolean { - return args.includes(flag); -} - -const args = process.argv.slice(2); - -type GatewayWsLogStyle = "auto" | "full" | "compact"; - -async function main() { - if (hasFlag(args, "--version") || hasFlag(args, "-v")) { - // Match `openclaw --version` behavior for Swift env/version checks. - // Keep output a single line. - console.log(BUNDLED_VERSION); - process.exit(0); - } - - // Bun runtime ships a global `Long` that protobufjs detects, but it does not - // implement the long.js API that Baileys/WAProto expects (fromBits, ...). - // Ensure we use long.js so the embedded gateway doesn't crash at startup. - if (typeof process.versions.bun === "string") { - const mod = await import("long"); - const Long = (mod as unknown as { default?: unknown }).default ?? mod; - (globalThis as unknown as { Long?: unknown }).Long = Long; - } - - const [ - { loadConfig }, - { startGatewayServer }, - { setGatewayWsLogStyle }, - { setVerbose }, - { acquireGatewayLock, GatewayLockError }, - { - consumeGatewaySigusr1RestartAuthorization, - isGatewaySigusr1RestartExternallyAllowed, - markGatewaySigusr1RestartHandled, - }, - { defaultRuntime }, - { enableConsoleCapture, setConsoleTimestampPrefix }, - commandQueueMod, - { createRestartIterationHook }, - ] = await Promise.all([ - import("../config/config.js"), - import("../gateway/server.js"), - import("../gateway/ws-logging.js"), - import("../globals.js"), - import("../infra/gateway-lock.js"), - import("../infra/restart.js"), - import("../runtime.js"), - import("../logging.js"), - import("../process/command-queue.js"), - import("../process/restart-recovery.js"), - ] as const); - - enableConsoleCapture(); - setConsoleTimestampPrefix(true); - setVerbose(hasFlag(args, "--verbose")); - - const wsLogRaw = hasFlag(args, "--compact") ? "compact" : argValue(args, "--ws-log"); - const wsLogStyle: GatewayWsLogStyle = - wsLogRaw === "compact" ? "compact" : wsLogRaw === "full" ? "full" : "auto"; - setGatewayWsLogStyle(wsLogStyle); - - const cfg = loadConfig(); - const portRaw = - argValue(args, "--port") ?? - process.env.OPENCLAW_GATEWAY_PORT ?? - process.env.CLAWDBOT_GATEWAY_PORT ?? - (typeof cfg.gateway?.port === "number" ? String(cfg.gateway.port) : "") ?? - "18789"; - const port = Number.parseInt(portRaw, 10); - if (Number.isNaN(port) || port <= 0) { - defaultRuntime.error(`Invalid --port (${portRaw})`); - process.exit(1); - } - - const bindRaw = - argValue(args, "--bind") ?? - process.env.OPENCLAW_GATEWAY_BIND ?? - process.env.CLAWDBOT_GATEWAY_BIND ?? - cfg.gateway?.bind ?? - "loopback"; - const bind = - bindRaw === "loopback" || - bindRaw === "lan" || - bindRaw === "auto" || - bindRaw === "custom" || - bindRaw === "tailnet" - ? bindRaw - : null; - if (!bind) { - defaultRuntime.error('Invalid --bind (use "loopback", "lan", "tailnet", "auto", or "custom")'); - process.exit(1); - } - - const token = argValue(args, "--token"); - if (token) { - process.env.OPENCLAW_GATEWAY_TOKEN = token; - } - - let server: Awaited> | null = null; - let lock: GatewayLockHandle | null = null; - let shuttingDown = false; - let forceExitTimer: ReturnType | null = null; - let restartResolver: (() => void) | null = null; - - const cleanupSignals = () => { - process.removeListener("SIGTERM", onSigterm); - process.removeListener("SIGINT", onSigint); - process.removeListener("SIGUSR1", onSigusr1); - }; - - const request = (action: "stop" | "restart", signal: string) => { - if (shuttingDown) { - defaultRuntime.log(`gateway: received ${signal} during shutdown; ignoring`); - return; - } - shuttingDown = true; - const isRestart = action === "restart"; - defaultRuntime.log( - `gateway: received ${signal}; ${isRestart ? "restarting" : "shutting down"}`, - ); - - const DRAIN_TIMEOUT_MS = 30_000; - const SHUTDOWN_TIMEOUT_MS = 5_000; - const forceExitMs = isRestart ? DRAIN_TIMEOUT_MS + SHUTDOWN_TIMEOUT_MS : SHUTDOWN_TIMEOUT_MS; - forceExitTimer = setTimeout(() => { - defaultRuntime.error("gateway: shutdown timed out; exiting without full cleanup"); - cleanupSignals(); - process.exit(0); - }, forceExitMs); - - void (async () => { - try { - if (isRestart) { - const activeTasks = commandQueueMod.getActiveTaskCount(); - if (activeTasks > 0) { - defaultRuntime.log( - `gateway: draining ${activeTasks} active task(s) before restart (timeout ${DRAIN_TIMEOUT_MS}ms)`, - ); - const { drained } = await commandQueueMod.waitForActiveTasks(DRAIN_TIMEOUT_MS); - if (drained) { - defaultRuntime.log("gateway: all active tasks drained"); - } else { - defaultRuntime.log("gateway: drain timeout reached; proceeding with restart"); - } - } - } - - await server?.close({ - reason: isRestart ? "gateway restarting" : "gateway stopping", - restartExpectedMs: isRestart ? 1500 : null, - }); - } catch (err) { - defaultRuntime.error(`gateway: shutdown error: ${String(err)}`); - } finally { - if (forceExitTimer) { - clearTimeout(forceExitTimer); - } - server = null; - if (isRestart) { - const respawn = restartGatewayProcessWithFreshPid(); - if (respawn.mode === "spawned" || respawn.mode === "supervised") { - const modeLabel = - respawn.mode === "spawned" - ? `spawned pid ${respawn.pid ?? "unknown"}` - : "supervisor restart"; - defaultRuntime.log(`gateway: restart mode full process restart (${modeLabel})`); - cleanupSignals(); - process.exit(0); - } else { - if (respawn.mode === "failed") { - defaultRuntime.log( - `gateway: full process restart failed (${respawn.detail ?? "unknown error"}); falling back to in-process restart`, - ); - } else { - defaultRuntime.log("gateway: restart mode in-process restart (OPENCLAW_NO_RESPAWN)"); - } - shuttingDown = false; - restartResolver?.(); - } - } else { - cleanupSignals(); - process.exit(0); - } - } - })(); - }; - - const onSigterm = () => { - defaultRuntime.log("gateway: signal SIGTERM received"); - request("stop", "SIGTERM"); - }; - const onSigint = () => { - defaultRuntime.log("gateway: signal SIGINT received"); - request("stop", "SIGINT"); - }; - const onSigusr1 = () => { - defaultRuntime.log("gateway: signal SIGUSR1 received"); - const authorized = consumeGatewaySigusr1RestartAuthorization(); - if (!authorized && !isGatewaySigusr1RestartExternallyAllowed()) { - defaultRuntime.log( - "gateway: SIGUSR1 restart ignored (not authorized; commands.restart=false or use gateway tool).", - ); - return; - } - markGatewaySigusr1RestartHandled(); - request("restart", "SIGUSR1"); - }; - - process.on("SIGTERM", onSigterm); - process.on("SIGINT", onSigint); - process.on("SIGUSR1", onSigusr1); - - try { - try { - lock = await acquireGatewayLock(); - } catch (err) { - if (err instanceof GatewayLockError) { - defaultRuntime.error(`Gateway start blocked: ${err.message}`); - process.exit(1); - } - throw err; - } - const onIteration = createRestartIterationHook(() => { - // After an in-process restart (SIGUSR1), reset command-queue lane state. - // Interrupted tasks from the previous lifecycle may have left `active` - // counts elevated (their finally blocks never ran), permanently blocking - // new work from draining. - commandQueueMod.resetAllLanes(); - }); - - // eslint-disable-next-line no-constant-condition - while (true) { - onIteration(); - try { - server = await startGatewayServer(port, { bind }); - } catch (err) { - cleanupSignals(); - defaultRuntime.error(`Gateway failed to start: ${String(err)}`); - process.exit(1); - } - await new Promise((resolve) => { - restartResolver = resolve; - }); - } - } finally { - await lock?.release(); - cleanupSignals(); - } -} - -void main().catch((err) => { - console.error( - "[openclaw] Gateway daemon failed:", - err instanceof Error ? (err.stack ?? err.message) : err, - ); - process.exit(1); -}); diff --git a/src/macos/relay-smoke.test.ts b/src/macos/relay-smoke.test.ts deleted file mode 100644 index 891efd67676..00000000000 --- a/src/macos/relay-smoke.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { parseRelaySmokeTest, runRelaySmokeTest } from "./relay-smoke.js"; - -vi.mock("../web/qr-image.js", () => ({ - renderQrPngBase64: vi.fn(async () => "base64"), -})); - -describe("parseRelaySmokeTest", () => { - it("parses --smoke qr", () => { - expect(parseRelaySmokeTest(["--smoke", "qr"], {})).toBe("qr"); - }); - - it("rejects --smoke without a value", () => { - expect(() => parseRelaySmokeTest(["--smoke"], {})).toThrow( - "Missing value for --smoke (expected: qr)", - ); - }); - - it("rejects --smoke when the next arg is another flag", () => { - expect(() => parseRelaySmokeTest(["--smoke", "--smoke-qr"], {})).toThrow( - "Missing value for --smoke (expected: qr)", - ); - }); - - it("parses --smoke-qr", () => { - expect(parseRelaySmokeTest(["--smoke-qr"], {})).toBe("qr"); - }); - - it("parses env var smoke mode only when no args", () => { - expect(parseRelaySmokeTest([], { OPENCLAW_SMOKE_QR: "1" })).toBe("qr"); - expect(parseRelaySmokeTest(["send"], { OPENCLAW_SMOKE_QR: "1" })).toBe(null); - }); - - it("supports OPENCLAW_SMOKE=qr only when no args", () => { - expect(parseRelaySmokeTest([], { OPENCLAW_SMOKE: "qr" })).toBe("qr"); - expect(parseRelaySmokeTest(["send"], { OPENCLAW_SMOKE: "qr" })).toBe(null); - }); - - it("rejects unknown smoke values", () => { - expect(() => parseRelaySmokeTest(["--smoke", "nope"], {})).toThrow("Unknown smoke test"); - }); - - it("prefers explicit --smoke over env vars", () => { - expect(parseRelaySmokeTest(["--smoke", "qr"], { OPENCLAW_SMOKE: "nope" })).toBe("qr"); - }); -}); - -describe("runRelaySmokeTest", () => { - it("runs qr smoke test", async () => { - await runRelaySmokeTest("qr"); - const mod = await import("../web/qr-image.js"); - expect(mod.renderQrPngBase64).toHaveBeenCalledWith("smoke-test"); - }); -}); diff --git a/src/macos/relay-smoke.ts b/src/macos/relay-smoke.ts deleted file mode 100644 index 3dac2015849..00000000000 --- a/src/macos/relay-smoke.ts +++ /dev/null @@ -1,37 +0,0 @@ -export type RelaySmokeTest = "qr"; - -export function parseRelaySmokeTest(args: string[], env: NodeJS.ProcessEnv): RelaySmokeTest | null { - const smokeIdx = args.indexOf("--smoke"); - if (smokeIdx !== -1) { - const value = args[smokeIdx + 1]; - if (!value || value.startsWith("-")) { - throw new Error("Missing value for --smoke (expected: qr)"); - } - if (value === "qr") { - return "qr"; - } - throw new Error(`Unknown smoke test: ${value}`); - } - - if (args.includes("--smoke-qr")) { - return "qr"; - } - - // Back-compat: only run env-based smoke mode when no CLI args are present, - // to avoid surprising early-exit when users set env vars globally. - if (args.length === 0 && (env.OPENCLAW_SMOKE_QR === "1" || env.OPENCLAW_SMOKE === "qr")) { - return "qr"; - } - - return null; -} - -export async function runRelaySmokeTest(test: RelaySmokeTest): Promise { - switch (test) { - case "qr": { - const { renderQrPngBase64 } = await import("../web/qr-image.js"); - await renderQrPngBase64("smoke-test"); - return; - } - } -} diff --git a/src/macos/relay.ts b/src/macos/relay.ts deleted file mode 100644 index c39a4f02a34..00000000000 --- a/src/macos/relay.ts +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env node -import process from "node:process"; - -declare const __OPENCLAW_VERSION__: string | undefined; - -const BUNDLED_VERSION = - (typeof __OPENCLAW_VERSION__ === "string" && __OPENCLAW_VERSION__) || - process.env.OPENCLAW_BUNDLED_VERSION || - "0.0.0"; - -function hasFlag(args: string[], flag: string): boolean { - return args.includes(flag); -} - -async function patchBunLongForProtobuf(): Promise { - // Bun ships a global `Long` that protobufjs detects, but it is not long.js and - // misses critical APIs (fromBits, ...). Baileys WAProto expects long.js. - if (typeof process.versions.bun !== "string") { - return; - } - const mod = await import("long"); - const Long = (mod as unknown as { default?: unknown }).default ?? mod; - (globalThis as unknown as { Long?: unknown }).Long = Long; -} - -async function main() { - const args = process.argv.slice(2); - - // Swift side expects `--version` to return a plain semver string. - if (hasFlag(args, "--version") || hasFlag(args, "-V") || hasFlag(args, "-v")) { - console.log(BUNDLED_VERSION); - process.exit(0); - } - - const { parseRelaySmokeTest, runRelaySmokeTest } = await import("./relay-smoke.js"); - const smokeTest = parseRelaySmokeTest(args, process.env); - if (smokeTest) { - try { - await runRelaySmokeTest(smokeTest); - process.exit(0); - } catch (err) { - console.error(`Relay smoke test failed (${smokeTest}):`, err); - process.exit(1); - } - } - - await patchBunLongForProtobuf(); - - const { loadDotEnv } = await import("../infra/dotenv.js"); - loadDotEnv({ quiet: true }); - - const { ensureOpenClawCliOnPath } = await import("../infra/path-env.js"); - ensureOpenClawCliOnPath(); - - const { enableConsoleCapture } = await import("../logging.js"); - enableConsoleCapture(); - - const { assertSupportedRuntime } = await import("../infra/runtime-guard.js"); - assertSupportedRuntime(); - const { formatUncaughtError } = await import("../infra/errors.js"); - const { installUnhandledRejectionHandler } = await import("../infra/unhandled-rejections.js"); - - const { buildProgram } = await import("../cli/program.js"); - const program = buildProgram(); - - installUnhandledRejectionHandler(); - - process.on("uncaughtException", (error) => { - console.error("[openclaw] Uncaught exception:", formatUncaughtError(error)); - process.exit(1); - }); - - await program.parseAsync(process.argv); -} - -void main().catch((err) => { - console.error( - "[openclaw] Relay failed:", - err instanceof Error ? (err.stack ?? err.message) : err, - ); - process.exit(1); -});