import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { shell } from "electron"; import { BrowserWindow, app, ipcMain, nativeTheme } from "electron/main"; import type { BrowserWindow as ElectronBrowserWindow, WebContents as ElectronWebContents, } from "electron/main"; import type { DesktopAccountInfo, DesktopRuntimeSessionSyncRequest } from "@geo/shared-types"; import { initAccountHealth } from "./account-health"; import { applyDesktopHealthIndicatorFromSnapshot, createDesktopHealthIcon, startDesktopHealthIndicator, syncDesktopHealthIndicator, } from "./app-issue-indicator"; import { getDesktopAppSettings, initDesktopAppSettings, setDesktopAppSetting, type DesktopAppSettingKey, } from "./app-settings"; import { bindPublishAccount, openPublishAccountConsole } from "./account-binder"; import { installObservedGlobalFetch, registerObservedRequestRendererTarget } from "./network-observer"; import { initProcessMetricsSampler } from "./process-metrics"; import { getPlaywrightCDPPort, startHiddenPlaywrightReaper } from "./playwright-cdp"; import { registerRendererDevtoolsProxyTarget } from "./renderer-devtools-proxy"; import { onRuntimeInvalidated } from "./runtime-events"; import { noteRuntimeAccountBound, requestRuntimeAccountProbe, refreshRuntimeAccounts, releaseRuntimeSession, syncRuntimeSession, unbindRuntimeAccount, } from "./runtime-controller"; import { createRuntimeAccountSnapshot, createRuntimeSnapshot } from "./runtime-snapshot"; import { resolveDesktopClientID, resolveDesktopDeviceInfo } from "./device-id"; import { initScheduler } from "./scheduler"; import { initSessionRegistry } from "./session-registry"; import { initSingleInstance } from "./single-instance"; import { initTransport, listDesktopPublishTasks, retryDesktopPublishTask } from "./transport/api-client"; import { initTray } from "./tray"; import { STANDARD_USER_AGENT } from "./user-agent"; import { startHotViewReaper } from "./view-pool"; app.commandLine.appendSwitch( "disable-features", "PostQuantumKyber,EncryptedClientHello,UseDnsHttpsSvcb,UseDnsHttpsSvcbAlpn", ); app.commandLine.appendSwitch("disable-quic"); app.commandLine.appendSwitch("disable-background-networking"); app.commandLine.appendSwitch("remote-debugging-port", String(getPlaywrightCDPPort())); app.userAgentFallback = STANDARD_USER_AGENT; installObservedGlobalFetch(); console.info("[desktop-main] boot", { electron: process.versions.electron, chrome: process.versions.chrome, node: process.versions.node, ua: STANDARD_USER_AGENT, }); process.on("unhandledRejection", (reason) => { console.error("[desktop-main] unhandled rejection", reason); }); process.on("uncaughtException", (error) => { console.error("[desktop-main] uncaught exception", error); }); let mainWindow: ElectronBrowserWindow | null = null; let loginWindow: ElectronBrowserWindow | null = null; let settingsWindow: ElectronBrowserWindow | null = null; let mainRendererContents: ElectronWebContents | null = null; let quitReleaseInFlight = false; const forceCloseWindows = new WeakSet(); type WindowMode = "login" | "main"; type RendererWindowMode = WindowMode | "settings"; type WindowModeSource = "auth-state" | "login" | "logout"; interface WindowSizePreset { width: number; height: number; minWidth: number; minHeight: number; resizable: boolean; } interface WindowModeRequest { source?: WindowModeSource; animate?: boolean; } const WINDOW_SIZES: Record = { login: { width: 340, height: 540, minWidth: 340, minHeight: 540, resizable: false }, main: { width: 1320, height: 860, minWidth: 900, minHeight: 600, resizable: true }, }; const SETTINGS_WINDOW_SIZE: WindowSizePreset = { width: 920, height: 640, minWidth: 760, minHeight: 520, resizable: true, }; function windowModePath(): string { return join(app.getPath("userData"), "window-mode.json"); } function readPersistedWindowMode(): WindowMode { try { const raw = readFileSync(windowModePath(), "utf8"); const data = JSON.parse(raw) as { mode?: unknown }; if (data?.mode === "main" || data?.mode === "login") { return data.mode; } } catch { // First run, missing file, or corrupt — fall through to default. } return "login"; } function persistWindowMode(mode: WindowMode): void { try { writeFileSync(windowModePath(), JSON.stringify({ mode }), "utf8"); } catch (err) { console.warn("[desktop-main] persist window mode failed", err); } } let currentWindowMode: WindowMode = "login"; function normalizeWindowModeRequest(input: unknown): WindowModeRequest { if (!input || typeof input !== "object") { return {}; } const candidate = input as { source?: unknown; animate?: unknown }; return { source: candidate.source === "login" || candidate.source === "logout" || candidate.source === "auth-state" ? candidate.source : undefined, animate: candidate.animate === true, }; } function currentMainWindow(): ElectronBrowserWindow | null { if (!mainWindow) { return null; } if (mainWindow.isDestroyed()) { mainWindow = null; return null; } return mainWindow; } function currentLoginWindow(): ElectronBrowserWindow | null { if (!loginWindow) { return null; } if (loginWindow.isDestroyed()) { loginWindow = null; return null; } return loginWindow; } function currentSettingsWindow(): ElectronBrowserWindow | null { if (!settingsWindow) { return null; } if (settingsWindow.isDestroyed()) { settingsWindow = null; return null; } return settingsWindow; } function currentActiveWindow(): ElectronBrowserWindow | null { return currentMainWindow() ?? currentLoginWindow(); } function closeWindowAfterIpcReturn(window: ElectronBrowserWindow): void { forceCloseWindows.add(window); setTimeout(() => { if (!window.isDestroyed()) { window.close(); } }, 50); } function captureRuntimeSnapshot() { const snapshot = createRuntimeSnapshot(); applyDesktopHealthIndicatorFromSnapshot(snapshot, currentMainWindow()); return snapshot; } function captureRuntimeAccountSnapshot(accountId: string) { return createRuntimeAccountSnapshot(accountId); } async function ensureMainWindow(): Promise { const existing = currentMainWindow(); if (existing) { return existing; } const window = await createAppWindow("main"); mainWindow = window; return window; } async function ensureLoginWindow(): Promise { const existing = currentLoginWindow(); if (existing) { return existing; } const window = await createAppWindow("login"); loginWindow = window; return window; } async function ensureSettingsWindow(): Promise { const existing = currentSettingsWindow(); if (existing) { return existing; } const window = await createSettingsWindow(); settingsWindow = window; return window; } async function ensureWindow(mode: WindowMode): Promise { return mode === "main" ? ensureMainWindow() : ensureLoginWindow(); } async function revealWindow(window: ElectronBrowserWindow): Promise { if (window.isMinimized()) { window.restore(); } if (!window.isVisible()) { window.show(); } window.focus(); app.focus({ steal: true }); } async function revealActiveWindow(): Promise { const window = currentActiveWindow() ?? await ensureWindow(currentWindowMode); await revealWindow(window); } function revealActiveWindowSafely(source: string): void { void revealActiveWindow().catch((error) => { console.error("[desktop-main] reveal active window failed", { source, error }); }); } async function switchWindowMode(mode: WindowMode, _request: WindowModeRequest = {}): Promise { currentWindowMode = mode; persistWindowMode(mode); const target = await ensureWindow(mode); const previous = mode === "main" ? currentLoginWindow() : currentMainWindow(); if (!target.isVisible()) { target.center(); } await revealWindow(target); if (previous && previous !== target && !previous.isDestroyed()) { closeWindowAfterIpcReturn(previous); } const settings = currentSettingsWindow(); if (mode === "login" && settings) { closeWindowAfterIpcReturn(settings); } } async function openSettingsWindow(): Promise { const window = await ensureSettingsWindow(); await revealWindow(window); } function rendererURL(): string | null { return process.env.ELECTRON_RENDERER_URL ?? null; } function preloadPath(): string { return join(__dirname, "../preload/bridge.cjs"); } async function loadRenderer(window: ElectronBrowserWindow, mode: RendererWindowMode): Promise { const devServerURL = rendererURL(); if (devServerURL) { const url = new URL(devServerURL); url.searchParams.set("window", mode); await window.webContents.loadURL(url.toString()); if (mode === "main") { window.webContents.openDevTools({ mode: "detach" }); } return; } await window.webContents.loadFile(join(__dirname, "../renderer/index.html"), { query: { window: mode }, }); } async function mountRendererWebContents(window: ElectronBrowserWindow, mode: RendererWindowMode): Promise { const contents = window.webContents; contents.setWindowOpenHandler(() => ({ action: "deny" })); if (mode === "main") { registerRendererDevtoolsProxyTarget(contents); registerObservedRequestRendererTarget(contents); mainRendererContents = contents; window.on("closed", () => { if (mainRendererContents === contents) { mainRendererContents = null; } }); } await loadRenderer(window, mode); } async function createAppWindow(mode: WindowMode): Promise { const initial = WINDOW_SIZES[mode]; const window = new BrowserWindow({ width: initial.width, height: initial.height, minWidth: initial.minWidth, minHeight: initial.minHeight, show: false, title: "GEO Rankly Desktop", icon: createDesktopHealthIcon("normal"), titleBarStyle: "hiddenInset", trafficLightPosition: { x: 12, y: 14 }, resizable: initial.resizable, maximizable: initial.resizable, fullscreenable: initial.resizable, backgroundColor: mode === "login" ? "#eaf2ff" : nativeTheme.shouldUseDarkColors ? "#15191a" : "#f0f2f5", webPreferences: { preload: preloadPath(), sandbox: true, contextIsolation: true, nodeIntegration: false, }, }); if (!initial.resizable) { window.setMinimumSize(initial.minWidth, initial.minHeight); window.setMaximumSize(initial.width, initial.height); } window.once("closed", () => { if (mode === "main" && mainWindow === window) { mainWindow = null; } if (mode === "login" && loginWindow === window) { loginWindow = null; } }); window.on("close", (event) => { if ( mode === "main" && !quitReleaseInFlight && !forceCloseWindows.has(window) && getDesktopAppSettings().keepRunningInBackground ) { event.preventDefault(); window.hide(); } }); window.on("minimize", () => { console.warn("[desktop-main] main window minimize event", { at: new Date().toISOString(), stack: new Error("trace").stack, }); }); window.on("hide", () => { console.warn("[desktop-main] main window hide event", { at: new Date().toISOString(), stack: new Error("trace").stack, }); }); window.on("blur", () => { console.warn("[desktop-main] main window blur event", { at: new Date().toISOString(), visible: window.isVisible(), minimized: window.isMinimized(), }); }); await mountRendererWebContents(window, mode); return window; } async function createSettingsWindow(): Promise { const initial = SETTINGS_WINDOW_SIZE; const window = new BrowserWindow({ width: initial.width, height: initial.height, minWidth: initial.minWidth, minHeight: initial.minHeight, show: false, title: "GEO Rankly Desktop 设置", icon: createDesktopHealthIcon("normal"), titleBarStyle: "hiddenInset", trafficLightPosition: { x: 14, y: 15 }, resizable: initial.resizable, maximizable: initial.resizable, fullscreenable: false, autoHideMenuBar: true, backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f2f4f7", webPreferences: { preload: preloadPath(), sandbox: true, contextIsolation: true, nodeIntegration: false, }, }); window.once("closed", () => { if (settingsWindow === window) { settingsWindow = null; } }); await mountRendererWebContents(window, "settings"); return window; } function flattenError(error: unknown): Error { if (error instanceof Error) { const copy = new Error(error.message || "ipc_handler_failed"); copy.name = error.name; return copy; } return new Error(typeof error === "string" ? error : "ipc_handler_failed"); } function safeHandle( channel: string, handler: (...args: Args) => Promise | Result, ): void { ipcMain.handle(channel, async (...args) => { try { return await handler(...(args as unknown as Args)); } catch (error) { throw flattenError(error); } }); } function isSafeExternalUrl(url: string): boolean { try { const parsed = new URL(url); return parsed.protocol === "http:" || parsed.protocol === "https:"; } catch { return false; } } function registerBridgeHandlers(): void { ipcMain.handle("desktop:ping", () => "pong"); ipcMain.handle("desktop:device-info", () => resolveDesktopDeviceInfo()); safeHandle( "desktop:client-id", (_event, scope: { tenant_id?: unknown; workspace_id?: unknown; user_id?: unknown }) => resolveDesktopClientID({ tenant_id: typeof scope?.tenant_id === "number" ? scope.tenant_id : 0, workspace_id: typeof scope?.workspace_id === "number" ? scope.workspace_id : 0, user_id: typeof scope?.user_id === "number" ? scope.user_id : 0, }), ); ipcMain.handle("desktop:runtime-snapshot", () => captureRuntimeSnapshot()); ipcMain.handle("desktop:runtime-account-snapshot", (_event, accountId: string) => captureRuntimeAccountSnapshot(accountId), ); safeHandle("desktop:bind-publish-account", async (_event, platformId: string) => { const account = (await bindPublishAccount(platformId)) as DesktopAccountInfo; noteRuntimeAccountBound(account); void refreshRuntimeAccounts().catch((error) => { console.warn("[desktop-bind] background account refresh failed", { message: error instanceof Error ? error.message : String(error), }); }); return account; }); safeHandle("desktop:refresh-runtime-accounts", async () => { await refreshRuntimeAccounts(); return null; }); safeHandle("desktop:probe-runtime-account", async (_event, accountId: string) => { await requestRuntimeAccountProbe(accountId); return captureRuntimeAccountSnapshot(accountId); }); safeHandle( "desktop:open-publish-account-console", async ( _event, account: { id: string; platform: string; platformUid: string; displayName: string }, ) => { await openPublishAccountConsole(account); void requestRuntimeAccountProbe(account.id, { account, force: true, }).catch((error) => { console.warn("[desktop-console] background account probe failed", { accountId: account.id, platform: account.platform, message: error instanceof Error ? error.message : String(error), }); }); return null; }, ); safeHandle( "desktop:unbind-publish-account", async (_event, accountId: string, syncVersion: number) => { await unbindRuntimeAccount(accountId, syncVersion); return null; }, ); safeHandle("desktop:list-publish-tasks", async (_event, params?: { page?: number; page_size?: number; title?: string }) => { return listDesktopPublishTasks(params); }); safeHandle("desktop:retry-publish-task", async (_event, taskId: string) => { return retryDesktopPublishTask(taskId); }); safeHandle("desktop:open-external-url", async (_event, url: string) => { if (!isSafeExternalUrl(url)) { throw new Error("unsupported_external_url"); } await shell.openExternal(url); return null; }); safeHandle("desktop:open-settings-window", async () => { await openSettingsWindow(); return null; }); ipcMain.handle("desktop:get-app-settings", () => getDesktopAppSettings()); safeHandle("desktop:set-app-setting", async (_event, key: DesktopAppSettingKey, value: boolean) => { if (key !== "openAtLogin" && key !== "keepRunningInBackground") { throw new Error("unsupported_app_setting"); } return setDesktopAppSetting(key, value); }); ipcMain.handle( "desktop:runtime-session-sync", (_event, session: DesktopRuntimeSessionSyncRequest | null) => { syncRuntimeSession(session); return null; }, ); safeHandle("desktop:runtime-session-release", async (_event, revoke?: boolean) => { await releaseRuntimeSession({ revoke: Boolean(revoke) }); return null; }); ipcMain.handle("desktop:set-window-mode", async (_event, mode: WindowMode, request?: unknown) => { if (mode === "login" || mode === "main") { await switchWindowMode(mode, normalizeWindowModeRequest(request)); } return null; }); } const hasSingleInstanceLock = initSingleInstance(() => { revealActiveWindowSafely("single-instance"); }); if (!hasSingleInstanceLock) { console.info("[desktop-main] another instance is already running; exiting"); app.exit(0); } else { app.whenReady().then(async () => { currentWindowMode = readPersistedWindowMode(); initDesktopAppSettings(); registerBridgeHandlers(); await initSessionRegistry(); initAccountHealth(); initTransport(); initScheduler(); initProcessMetricsSampler(); startHotViewReaper(); startHiddenPlaywrightReaper(); onRuntimeInvalidated((event) => { syncDesktopHealthIndicator(currentMainWindow()); if (!mainRendererContents || mainRendererContents.isDestroyed()) { return; } mainRendererContents.send("desktop:runtime-invalidated", event); }); await switchWindowMode(currentWindowMode); initTray(() => { revealActiveWindowSafely("tray"); }); startDesktopHealthIndicator(() => currentMainWindow()); }).catch((error) => { console.error("[desktop-main] app.whenReady failed", error); }); app.on("activate", () => { revealActiveWindowSafely("activate"); }); app.on("window-all-closed", () => { if (getDesktopAppSettings().keepRunningInBackground) { return; } if (process.platform !== "darwin") { app.quit(); } }); app.on("before-quit", (event) => { if (quitReleaseInFlight) { return; } quitReleaseInFlight = true; event.preventDefault(); void Promise.race([ releaseRuntimeSession(), new Promise((resolve) => setTimeout(resolve, 1500)), ]).finally(() => { app.quit(); }); }); }