From fb47f06228cdcadec6fa972adf81d1fe8c4b352a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 5 Aug 2026 17:46:19 -0400 Subject: [PATCH] refactor(desktop): remove superseded local sidecar (#40743) --- packages/desktop/src/main/env.d.ts | 12 -- packages/desktop/src/main/server.ts | 181 --------------------------- packages/desktop/src/main/sidecar.ts | 157 ----------------------- 3 files changed, 350 deletions(-) delete mode 100644 packages/desktop/src/main/sidecar.ts diff --git a/packages/desktop/src/main/env.d.ts b/packages/desktop/src/main/env.d.ts index d69e6feec25..0ee0c551df8 100644 --- a/packages/desktop/src/main/env.d.ts +++ b/packages/desktop/src/main/env.d.ts @@ -5,15 +5,3 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv } - -declare module "virtual:opencode-server" { - export namespace Server { - export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen - export type Listener = import("../../../opencode/dist/types/src/node").Server.Listener - } - export namespace Config { - export const get: typeof import("../../../opencode/dist/types/src/node").Config.get - export type Info = import("../../../opencode/dist/types/src/node").Config.Info - } - export const bootstrap: typeof import("../../../opencode/dist/types/src/node").bootstrap -} diff --git a/packages/desktop/src/main/server.ts b/packages/desktop/src/main/server.ts index ae1a98efdfa..36f58772b46 100644 --- a/packages/desktop/src/main/server.ts +++ b/packages/desktop/src/main/server.ts @@ -1,32 +1,8 @@ -import { dirname, join } from "node:path" -import { fileURLToPath } from "node:url" -import { app, utilityProcess } from "electron" -import type { Details } from "electron" import { getLogger } from "./logging" import { getUserShell, loadShellEnv } from "./shell-env" import { getStore } from "./store" import { DEFAULT_SERVER_URL_KEY } from "./store-keys" -export type HealthCheck = { wait: Promise } - -type SidecarMessage = - | { type: "ready" } - | { type: "stopped" } - | { type: "error"; error: { message: string; stack?: string } } - -export type SidecarListener = { stop: () => Promise } - -const SIDECAR_SERVICE_NAME = "opencode server" -const SIDECAR_START_STALL_TIMEOUT = 60_000 -const SIDECAR_STOP_TIMEOUT = 6_000 - -type SpawnLocalServerOptions = { - userDataPath: string - onStdout?: (message: string) => void - onStderr?: (message: string) => void - onExit?: (code: number) => void -} - export function getDefaultServerUrl(): string | null { const value = getStore().get(DEFAULT_SERVER_URL_KEY) return typeof value === "string" ? value : null @@ -54,135 +30,6 @@ export function preferAppEnv(userDataPath: string) { return shellEnv } -export async function spawnLocalServer( - hostname: string, - port: number, - password: string, - options: SpawnLocalServerOptions, -) { - const sidecar = join(dirname(fileURLToPath(import.meta.url)), "sidecar.js") - const child = utilityProcess.fork(sidecar, [], { - cwd: process.cwd(), - env: createSidecarEnv(), - serviceName: SIDECAR_SERVICE_NAME, - stdio: "pipe", - }) - let exited = false - const exit = defer() - - const onProcessGone = (_event: unknown, details: Details) => { - if (details.type !== "Utility" || details.name !== SIDECAR_SERVICE_NAME) return - options.onStderr?.(`utility process gone reason=${details.reason} exitCode=${details.exitCode}`) - } - - app.on("child-process-gone", onProcessGone) - child.once("exit", (code) => { - exited = true - app.off("child-process-gone", onProcessGone) - options.onExit?.(code) - exit.resolve(code) - }) - child.on("error", (error) => options.onStderr?.(`utility process error: ${serializeError(error).message}`)) - - child.stdout?.on("data", (chunk: Buffer) => options.onStdout?.(chunk.toString("utf8").trimEnd())) - child.stderr?.on("data", (chunk: Buffer) => options.onStderr?.(chunk.toString("utf8").trimEnd())) - - await new Promise((resolve, reject) => { - let done = false - let timeout: NodeJS.Timeout - - const fail = (error: Error) => { - if (done) return - done = true - cleanup() - reject(error) - } - - const refreshTimeout = () => { - clearTimeout(timeout) - timeout = setTimeout(() => { - fail(new Error(`Sidecar did not become ready within ${SIDECAR_START_STALL_TIMEOUT}ms: ${sidecar}`)) - }, SIDECAR_START_STALL_TIMEOUT) - } - - const onMessage = (message: SidecarMessage) => { - if (message.type === "ready") { - if (done) return - done = true - cleanup() - resolve() - return - } - if (message.type === "error") { - fail(Object.assign(new Error(message.error.message), { stack: message.error.stack })) - } - } - const onExit = (code: number) => { - fail(new Error(`Sidecar exited before ready with code ${code}`)) - } - const cleanup = () => { - clearTimeout(timeout) - child.off("message", onMessage) - child.off("exit", onExit) - } - - child.on("message", onMessage) - child.on("exit", onExit) - refreshTimeout() - child.postMessage({ - type: "start", - hostname, - port, - password, - userDataPath: options.userDataPath, - }) - }).catch((error) => { - if (!exited) child.kill() - throw error - }) - - const wait = (async () => { - const url = `http://${hostname}:${port}` - let healthy = false - const gone = exit.promise.then((code) => { - if (healthy) return - throw new Error(`Sidecar exited before health check passed with code ${code}`) - }) - - const ready = async () => { - while (true) { - await new Promise((resolve) => setTimeout(resolve, 100)) - if (await checkHealth(url, password)) { - healthy = true - return - } - } - } - - await Promise.race([ready(), gone]) - })() - - let stopping: Promise | undefined - - return { - listener: { - stop: () => { - if (stopping) return stopping - if (exited) return Promise.resolve() - child.postMessage({ type: "stop" }) - stopping = Promise.race([ - exit.promise.then(() => undefined), - delay(SIDECAR_STOP_TIMEOUT).then(() => { - if (!exited) child.kill() - }), - ]) - return stopping - }, - }, - health: { wait }, - } -} - export async function checkHealth(url: string, password?: string | null): Promise { let healthUrls: URL[] try { @@ -209,31 +56,3 @@ export async function checkHealth(url: string, password?: string | null): Promis } return false } - -function createSidecarEnv(): Record { - const env = Object.fromEntries( - Object.entries(process.env).flatMap(([key, value]) => (value === undefined ? [] : [[key, String(value)]])), - ) - delete env.DEBUG - if (process.platform === "linux") delete env.LD_PRELOAD - return env -} - -function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -function serializeError(error: unknown) { - if (error instanceof Error) return { message: error.message, stack: error.stack } - return { message: String(error) } -} - -function defer() { - let resolve!: (value: T) => void - let reject!: (error: Error) => void - const promise = new Promise((res, rej) => { - resolve = res - reject = rej - }) - return { promise, resolve, reject } -} diff --git a/packages/desktop/src/main/sidecar.ts b/packages/desktop/src/main/sidecar.ts deleted file mode 100644 index 246871fb2b4..00000000000 --- a/packages/desktop/src/main/sidecar.ts +++ /dev/null @@ -1,157 +0,0 @@ -import * as http from "node:http" -import * as tls from "node:tls" - -type NodeHttpWithEnvProxy = typeof http & { - setGlobalProxyFromEnv: () => void -} - -type NodeTlsWithSystemCertificates = typeof tls & { - getCACertificates: (type: "default" | "system") => string[] - setDefaultCACertificates: (certificates: string[]) => void -} - -type StartCommand = { - type: "start" - hostname: string - port: number - password: string - userDataPath: string -} - -type StopCommand = { type: "stop" } -type SidecarCommand = StartCommand | StopCommand - -type SidecarMessage = - | { type: "ready" } - | { type: "stopped" } - | { type: "error"; error: { message: string; stack?: string } } - -type ParentPort = { - postMessage(message: SidecarMessage): void - on(event: "message", listener: (event: { data: unknown }) => void): void -} - -type Listener = { - stop(close?: boolean): void | Promise -} - -const parentPort = getParentPort() -let listener: Listener | undefined - -parentPort.on("message", (event) => { - const command = parseCommand(event.data) - if (!command) return - if (command.type === "stop") { - void stop() - return - } - void start(command) -}) - -async function start(command: StartCommand) { - try { - prepareSidecarEnv(command.password, command.userDataPath) - ensureLoopbackNoProxy() - useSystemCertificates() - useEnvProxy() - const { Server } = await import("virtual:opencode-server") - - listener = await Server.listen({ - port: command.port, - hostname: command.hostname, - username: "opencode", - password: command.password, - cors: ["oc://renderer"], - }) - parentPort.postMessage({ type: "ready" }) - } catch (error) { - parentPort.postMessage({ type: "error", error: serializeError(error) }) - setImmediate(() => process.exit(1)) - } -} - -async function stop() { - try { - await listener?.stop() - } finally { - listener = undefined - parentPort.postMessage({ type: "stopped" }) - setImmediate(() => process.exit(0)) - } -} - -function prepareSidecarEnv(password: string, userDataPath: string) { - Object.assign(process.env, { - OPENCODE_SERVER_USERNAME: "opencode", - OPENCODE_SERVER_PASSWORD: password, - XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath, - }) -} - -function ensureLoopbackNoProxy() { - const loopback = ["127.0.0.1", "localhost", "::1"] - const upsert = (key: string) => { - const items = (process.env[key] ?? "") - .split(",") - .map((value: string) => value.trim()) - .filter((value: string) => Boolean(value)) - - for (const host of loopback) { - if (items.some((value: string) => value.toLowerCase() === host)) continue - items.push(host) - } - - process.env[key] = items.join(",") - } - - upsert("NO_PROXY") - upsert("no_proxy") -} - -function useSystemCertificates() { - try { - const nodeTls = tls as NodeTlsWithSystemCertificates - nodeTls.setDefaultCACertificates([ - ...new Set([...nodeTls.getCACertificates("default"), ...nodeTls.getCACertificates("system")]), - ]) - } catch (error) { - console.warn("failed to load system certificates", error) - } -} - -function useEnvProxy() { - try { - ;(http as NodeHttpWithEnvProxy).setGlobalProxyFromEnv() - } catch (error) { - console.warn("failed to load proxy environment", error) - } -} - -function parseCommand(value: unknown): SidecarCommand | undefined { - if (!value || typeof value !== "object") return - const command = value as Partial - if (command.type === "stop") return { type: "stop" } - if (command.type !== "start") return - if (typeof command.hostname !== "string") return - if (typeof command.port !== "number") return - if (typeof command.password !== "string") return - if (typeof command.userDataPath !== "string") return - return { - type: "start", - hostname: command.hostname, - port: command.port, - password: command.password, - userDataPath: command.userDataPath, - } -} - -function serializeError(error: unknown) { - if (error instanceof Error) return { message: error.message, stack: error.stack } - return { message: String(error) } -} - -function getParentPort() { - const port = process.parentPort as ParentPort | undefined - if (!port) throw new Error("Sidecar parent port unavailable") - return port -}