/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/desktop/src/main/index.ts
399 строк
12 KB
Luke Parker
fix(desktop): guard destroyed recovery windows (#37406)
17 июл 2026, 04:22
Не верифицирован
17 июл 2026, 04:22
4436678
Код
Авторство
О чём код?
import { randomUUID } from "node:crypto" import { mkdirSync, rmSync } from "node:fs" import * as http from "node:http" import { createServer } from "node:net" import { homedir, tmpdir } from "node:os" import { join } from "node:path" import { getCACertificates, setDefaultCACertificates } from "node:tls" import type { Event } from "electron" import { app } from "electron" import { Deferred, Effect, Fiber } from "effect" import contextMenu from "electron-context-menu" import type { ServerReadyData } from "../preload/types" import { checkAppExists, resolveAppPath } from "./apps" import { CHANNEL } from "./constants" import { registerIpcHandlers, sendDeepLinks, sendMenuCommand } from "./ipc" import { forwardInitializationFailure } from "./initialization" import { exportDebugLogs, initCrashReporter, initLogging, startNetLog, write as writeLog } from "./logging" import { parseMarkdown } from "./markdown" import { createMenu } from "./menu" import { finishFirstLaunchOnboarding, initializeOldLayoutEligibility, isFirstLaunchOnboardingPending, isOldLayoutEligible, } from "./onboarding" import { getDefaultServerUrl, preferAppEnv, setDefaultServerUrl, spawnLocalServer, type SidecarListener, } from "./server" import { setupAutoUpdater, showUpdaterDialog } from "./updater" import { safeWebContentsURL } from "./window-state" import { getLastFocusedWindow, registerRendererProtocol, setRelaunchHandler, setAppQuitting, setBackgroundColor, setDockIcon, restoreMainWindows, } from "./windows" import { createWslServersController } from "./wsl/servers" import { registerWslIpcHandlers } from "./wsl/ipc" import { spawnWslSidecar } from "./wsl/sidecar" import { migrate } from "./migrate" import { cleanupStoreFiles } from "./store-cleanup" const APP_NAMES: Record<string, string> = { dev: "OpenCode Dev", beta: "OpenCode Beta", prod: "OpenCode", } const APP_IDS: Record<string, string> = { dev: "ai.opencode.desktop.dev", beta: "ai.opencode.desktop.beta", prod: "ai.opencode.desktop", } const TEST_ONBOARDING = process.env.OPENCODE_TEST_ONBOARDING === "1" const jsCallStackFeature = "DocumentPolicyIncludeJSCallStacksInCrashReports" let logger: ReturnType<typeof initLogging> let server: SidecarListener | null = null const pendingDeepLinks: string[] = [] function useEnvProxy() { try { // Electron 41.2 runs Node 24.14.1; latest @types/node@24 is 24.12.2. ;(http as any).setGlobalProxyFromEnv() } catch (error) { logger.warn("failed to load proxy environment", error) } } function emitDeepLinks(urls: string[]) { if (urls.length === 0) return pendingDeepLinks.push(...urls) const win = getLastFocusedWindow() if (win) sendDeepLinks(win, urls) } async function killSidecar() { if (!server) return const current = server server = null await current.stop() } 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") } const main = Effect.gen(function* () { contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false }) // on macOS apps run in `/` which can cause issues with ripgrep try { process.chdir(homedir()) } catch {} process.env.OPENCODE_DISABLE_EMBEDDED_WEB_UI = "true" const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev" const onboardingTestRoot = ((): string | undefined => { if (!TEST_ONBOARDING) return const root = join(tmpdir(), `opencode-onboarding-${randomUUID()}`) rmSync(root, { recursive: true, force: true }) ;["data", "config", "cache", "state", "desktop", "session"].forEach((dir) => mkdirSync(join(root, dir), { recursive: true }), ) process.env.OPENCODE_DB = ":memory:" process.env.XDG_DATA_HOME = join(root, "data") process.env.XDG_CONFIG_HOME = join(root, "config") process.env.XDG_CACHE_HOME = join(root, "cache") process.env.XDG_STATE_HOME = join(root, "state") return root })() app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev") app.setAppUserModelId(appId) app.setPath( "userData", onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId), ) if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session")) initializeOldLayoutEligibility(app.getPath("userData")) logger = initLogging() initCrashReporter() const wslServers = createWslServersController( app.getVersion(), async (distro) => { logger.log("spawning wsl sidecar", { distro }) return spawnWslSidecar(distro, { onLine: (line) => logger.log("wsl sidecar", { distro, stream: line.stream, text: line.text }), }) }, { logger: { log: (message, meta) => logger.log(message, meta), error: (message, meta) => logger.error(message, meta), }, }, ) const stopSidecars = async () => { await killSidecar() wslServers.stopAll() } const relaunch = () => { setAppQuitting() void stopSidecars().finally(() => { app.relaunch() app.exit(0) }) } try { setDefaultCACertificates([...new Set([...getCACertificates("default"), ...getCACertificates("system")])]) } catch (error) { logger.warn("failed to load system certificates", error) } logger.log("app starting", { version: app.getVersion(), packaged: app.isPackaged, onboardingTest: Boolean(onboardingTestRoot), }) ensureLoopbackNoProxy() useEnvProxy() app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>") const features = app.commandLine.getSwitchValue("enable-features") app.commandLine.appendSwitch("enable-features", features ? `${jsCallStackFeature},${features}` : jsCallStackFeature) if (!app.isPackaged) app.commandLine.appendSwitch("remote-debugging-port", "9222") if (!app.requestSingleInstanceLock()) { app.quit() return } preferAppEnv(app.getPath("userData")) app.on("second-instance", (_event: Event, argv: string[]) => { const urls = argv.filter((arg: string) => arg.startsWith("opencode://")) if (urls.length) { logger.log("deep link received via second-instance", { urls }) emitDeepLinks(urls) } const win = getLastFocusedWindow() if (win) { win.show() win.focus() } }) app.on("open-url", (event: Event, url: string) => { event.preventDefault() logger.log("deep link received via open-url", { url }) emitDeepLinks([url]) }) app.on("before-quit", () => { setAppQuitting() void stopSidecars() }) app.on("will-quit", () => { setAppQuitting() void stopSidecars() }) app.on("child-process-gone", (_event, details) => { writeLog("utility", "child process gone", { details }, "error") }) app.on("render-process-gone", (_event, webContents, details) => { writeLog("window", "app render process gone", { url: safeWebContentsURL(webContents), details }, "error") }) setRelaunchHandler(() => { relaunch() }) for (const signal of ["SIGINT", "SIGTERM"] as const) { process.on(signal, () => { setAppQuitting() void stopSidecars().finally(() => app.exit(0)) }) } const serverReady = Deferred.makeUnsafe<ServerReadyData, unknown>() yield* Effect.promise(() => app.whenReady()) if (!TEST_ONBOARDING) migrate() yield* Effect.promise(() => cleanupStoreFiles(app.getPath("userData"))).pipe( Effect.tap((result) => Effect.sync(() => { if (result.deleted.length === 0) return logger.log("cleaned scoped store files", { count: result.deleted.length, scanned: result.scanned }) }), ), Effect.catch((error) => Effect.sync(() => { logger.warn("failed to clean scoped store files", error) }), ), ) app.setAsDefaultProtocolClient("opencode") registerRendererProtocol() setDockIcon() const updater = setupAutoUpdater(stopSidecars) registerIpcHandlers({ killSidecar: () => killSidecar(), relaunch, awaitInitialization: Effect.fnUntraced( function* () { logger.log("awaiting server ready") const res = yield* Deferred.await(serverReady) logger.log("server ready", { url: res.url }) return res }, (e) => Effect.runPromise(e), ), consumeInitialDeepLinks: () => pendingDeepLinks.splice(0), getDefaultServerUrl: () => getDefaultServerUrl(), setDefaultServerUrl: (url) => setDefaultServerUrl(url), isFirstLaunchOnboardingPending, finishFirstLaunchOnboarding, isOldLayoutEligible, getDisplayBackend: async () => null, setDisplayBackend: async () => undefined, parseMarkdown: async (markdown) => parseMarkdown(markdown), checkAppExists: (appName) => checkAppExists(appName), resolveAppPath: async (appName) => resolveAppPath(appName), updater, showUpdater: () => showUpdaterDialog(updater, true), setBackgroundColor: (color) => setBackgroundColor(color), exportDebugLogs: () => exportDebugLogs(), recordFatalRendererError: (error) => writeLog("renderer", "fatal renderer error", { ...error }, "error"), }) registerWslIpcHandlers(wslServers) void updater.start() const updateTimer = setInterval(() => void updater.check(), 10 * 60 * 1000) updateTimer.unref() app.once("will-quit", () => clearInterval(updateTimer)) yield* Effect.promise(() => startNetLog()).pipe( Effect.catch((error) => Effect.sync(() => { logger.warn("failed to start net log", error) }), ), ) const port = yield* Effect.gen(function* () { const fromEnv = process.env.OPENCODE_PORT if (fromEnv) { const parsed = Number.parseInt(fromEnv, 10) if (!Number.isNaN(parsed)) return parsed } const res = yield* Deferred.make<number, unknown>() const server = createServer() server.on("error", (e) => Deferred.failSync(res, () => e)) server.listen(0, "127.0.0.1", () => { const address = server.address() if (typeof address !== "object" || !address) { server.close() Deferred.failSync(res, () => new Error("Failed to get port")) return } const port = address.port server.close(() => Effect.runSync(Deferred.succeed(res, port))) }) return yield* Deferred.await(res) }) const hostname = "127.0.0.1" const url = `http://${hostname}:${port}` const password = randomUUID() const loadingTask = yield* Effect.gen(function* () { logger.log("sidecar connection started", { url }) ensureLoopbackNoProxy() useEnvProxy() logger.log("spawning sidecar", { url }) const { listener, health } = yield* Effect.promise(() => spawnLocalServer(hostname, port, password, { userDataPath: app.getPath("userData"), onStdout: (message) => writeLog("server", "stdout", { message }), onStderr: (message) => writeLog("server", "stderr", { message }, "warn"), onExit: (code) => writeLog("utility", "sidecar exited", { code }, "warn"), }), ) server = listener yield* Deferred.succeed(serverReady, { url, username: "opencode", password, }) if (process.platform === "win32") { void wslServers.initialize().catch((error) => logger.error("wsl server initialization failed", error)) } yield* Effect.promise(() => health.wait).pipe( Effect.timeout("30 seconds"), Effect.catch((e) => Effect.sync(() => { logger.error("sidecar health check failed", e.toString()) }), ), ) logger.log("loading task finished") }).pipe(forwardInitializationFailure(serverReady), Effect.forkChild) yield* Fiber.await(loadingTask) const windows = restoreMainWindows() if (windows.length) { createMenu({ trigger: (id) => { const win = getLastFocusedWindow() if (win) sendMenuCommand(win, id) }, checkForUpdates: () => { void showUpdaterDialog(updater, true) }, relaunch: () => { relaunch() }, }) } }) Effect.runFork(main)