import * as ChildProcess from "node:child_process";import * as Crypto from "node:crypto";import * as FS from "node:fs";import * as OS from "node:os";import * as Path from "node:path"; import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, nativeImage, nativeTheme, protocol, safeStorage, shell,} from "electron";import type { MenuItemConstructorOptions } from "electron";import type { ClientSettings, DesktopTheme, DesktopServerExposureMode, DesktopServerExposureState, PersistedSavedEnvironmentRecord, DesktopUpdateActionResult, DesktopUpdateCheckResult, DesktopUpdateState,} from "@t3tools/contracts";import { autoUpdater } from "electron-updater"; import type { ContextMenuItem } from "@t3tools/contracts";import { RotatingFileSink } from "@t3tools/shared/logging";import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";import { DEFAULT_DESKTOP_BACKEND_PORT, resolveDesktopBackendPort } from "./backendPort";import { DEFAULT_DESKTOP_SETTINGS, readDesktopSettings, setDesktopServerExposurePreference, writeDesktopSettings,} from "./desktopSettings";import { readClientSettings, readSavedEnvironmentRegistry, readSavedEnvironmentSecret, removeSavedEnvironmentSecret, writeClientSettings, writeSavedEnvironmentRegistry, writeSavedEnvironmentSecret,} from "./clientPersistence";import { isBackendReadinessAborted, waitForHttpReady } from "./backendReadiness";import { showDesktopConfirmDialog } from "./confirmDialog";import { resolveDesktopServerExposure } from "./serverExposure";import { syncShellEnvironment } from "./syncShellEnvironment";import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";import { createInitialDesktopUpdateState, reduceDesktopUpdateStateOnCheckFailure, reduceDesktopUpdateStateOnCheckStart, reduceDesktopUpdateStateOnDownloadComplete, reduceDesktopUpdateStateOnDownloadFailure, reduceDesktopUpdateStateOnDownloadProgress, reduceDesktopUpdateStateOnDownloadStart, reduceDesktopUpdateStateOnInstallFailure, reduceDesktopUpdateStateOnNoUpdate, reduceDesktopUpdateStateOnUpdateAvailable,} from "./updateMachine";import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch"; syncShellEnvironment(); const PICK_FOLDER_CHANNEL = "desktop:pick-folder";const CONFIRM_CHANNEL = "desktop:confirm";const SET_THEME_CHANNEL = "desktop:set-theme";const CONTEXT_MENU_CHANNEL = "desktop:context-menu";const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";const MENU_ACTION_CHANNEL = "desktop:menu-action";const UPDATE_STATE_CHANNEL = "desktop:update-state";const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";const UPDATE_INSTALL_CHANNEL = "desktop:update-install";const UPDATE_CHECK_CHANNEL = "desktop:update-check";const GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL = "desktop:get-local-environment-bootstrap";const GET_CLIENT_SETTINGS_CHANNEL = "desktop:get-client-settings";const SET_CLIENT_SETTINGS_CHANNEL = "desktop:set-client-settings";const GET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL = "desktop:get-saved-environment-registry";const SET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL = "desktop:set-saved-environment-registry";const GET_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:get-saved-environment-secret";const SET_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:set-saved-environment-secret";const REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:remove-saved-environment-secret";const GET_SERVER_EXPOSURE_STATE_CHANNEL = "desktop:get-server-exposure-state";const SET_SERVER_EXPOSURE_MODE_CHANNEL = "desktop:set-server-exposure-mode";const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");const STATE_DIR = Path.join(BASE_DIR, "userdata");const DESKTOP_SETTINGS_PATH = Path.join(STATE_DIR, "desktop-settings.json");const CLIENT_SETTINGS_PATH = Path.join(STATE_DIR, "client-settings.json");const SAVED_ENVIRONMENT_REGISTRY_PATH = Path.join(STATE_DIR, "saved-environments.json");const DESKTOP_SCHEME = "t3";const ROOT_DIR = Path.resolve(__dirname, "../../..");const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";const APP_USER_MODEL_ID = "com.t3tools.t3code";const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "t3code-dev.desktop" : "t3code.desktop";const LINUX_WM_CLASS = isDevelopment ? "t3code-dev" : "t3code";const USER_DATA_DIR_NAME = isDevelopment ? "t3code-dev" : "t3code";const LEGACY_USER_DATA_DIR_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i;const COMMIT_HASH_DISPLAY_LENGTH = 12;const LOG_DIR = Path.join(STATE_DIR, "logs");const LOG_FILE_MAX_BYTES = 10 * 1024 * 1024;const LOG_FILE_MAX_FILES = 10;const APP_RUN_ID = Crypto.randomBytes(6).toString("hex");const SERVER_SETTINGS_PATH = Path.join(STATE_DIR, "settings.json");const AUTO_UPDATE_STARTUP_DELAY_MS = 15_000;const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000;const DESKTOP_UPDATE_CHANNEL = "latest";const DESKTOP_UPDATE_ALLOW_PRERELEASE = false;const DESKTOP_LOOPBACK_HOST = "127.0.0.1"; type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"];type LinuxDesktopNamedApp = Electron.App & { setDesktopName?: (desktopName: string) => void;}; let mainWindow: BrowserWindow | null = null;let backendProcess: ChildProcess.ChildProcess | null = null;let backendPort = 0;let backendBindHost = DESKTOP_LOOPBACK_HOST;let backendBootstrapToken = "";let backendHttpUrl = "";let backendWsUrl = "";let backendEndpointUrl: string | null = null;let backendAdvertisedHost: string | null = null;let backendReadinessAbortController: AbortController | null = null;let restartAttempt = 0;let restartTimer: ReturnType<typeof setTimeout> | null = null;let isQuitting = false;let desktopProtocolRegistered = false;let aboutCommitHashCache: string | null | undefined;let desktopLogSink: RotatingFileSink | null = null;let backendLogSink: RotatingFileSink | null = null;let restoreStdIoCapture: (() => void) | null = null;let backendObservabilitySettings = readPersistedBackendObservabilitySettings();let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH);let desktopServerExposureMode: DesktopServerExposureMode = desktopSettings.serverExposureMode; let destructiveMenuIconCache: Electron.NativeImage | null | undefined;const expectedBackendExitChildren = new WeakSet<ChildProcess.ChildProcess>();const desktopRuntimeInfo = resolveDesktopRuntimeInfo({ platform: process.platform, processArch: process.arch, runningUnderArm64Translation: app.runningUnderARM64Translation === true,});const initialUpdateState = (): DesktopUpdateState => createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo); function logTimestamp(): string { return new Date().toISOString();} function logScope(scope: string): string { return `${scope} run=${APP_RUN_ID}`;} function sanitizeLogValue(value: string): string { return value.replace(/\s+/g, " ").trim();} function readPersistedBackendObservabilitySettings(): { readonly otlpTracesUrl: string | undefined; readonly otlpMetricsUrl: string | undefined;} { try { if (!FS.existsSync(SERVER_SETTINGS_PATH)) { return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; } return parsePersistedServerObservabilitySettings(FS.readFileSync(SERVER_SETTINGS_PATH, "utf8")); } catch (error) { console.warn("[desktop] failed to read persisted backend observability settings", error); return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; }} function resolveConfiguredDesktopBackendPort(rawPort: string | undefined): number | undefined { if (!rawPort) { return undefined; } const parsedPort = Number.parseInt(rawPort, 10); if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65_535) { return undefined; } return parsedPort;} function resolveDesktopDevServerUrl(): string { const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim(); if (!devServerUrl) { throw new Error("VITE_DEV_SERVER_URL is required in desktop development."); } return devServerUrl;} function backendChildEnv(): NodeJS.ProcessEnv { const env = { ...process.env }; delete env.T3CODE_PORT; delete env.T3CODE_MODE; delete env.T3CODE_NO_BROWSER; delete env.T3CODE_HOST; delete env.T3CODE_DESKTOP_WS_URL; delete env.T3CODE_DESKTOP_LAN_ACCESS; delete env.T3CODE_DESKTOP_LAN_HOST; return env;} function getDesktopServerExposureState(): DesktopServerExposureState { return { mode: desktopServerExposureMode, endpointUrl: backendEndpointUrl, advertisedHost: backendAdvertisedHost, };} function getDesktopSecretStorage() { return { isEncryptionAvailable: () => safeStorage.isEncryptionAvailable(), encryptString: (value: string) => safeStorage.encryptString(value), decryptString: (value: Buffer) => safeStorage.decryptString(value), } as const;} function resolveAdvertisedHostOverride(): string | undefined { const override = process.env.T3CODE_DESKTOP_LAN_HOST?.trim(); return override && override.length > 0 ? override : undefined;} async function applyDesktopServerExposureMode( mode: DesktopServerExposureMode, options?: { readonly persist?: boolean; readonly rejectIfUnavailable?: boolean },): Promise<DesktopServerExposureState> { const advertisedHostOverride = resolveAdvertisedHostOverride(); const requestedMode = mode; let exposure = resolveDesktopServerExposure({ mode, port: backendPort, networkInterfaces: OS.networkInterfaces(), ...(advertisedHostOverride ? { advertisedHostOverride } : {}), }); if (requestedMode === "network-accessible" && exposure.endpointUrl === null) { if (options?.rejectIfUnavailable) { throw new Error("No reachable network address is available for this desktop right now."); } exposure = resolveDesktopServerExposure({ mode: "local-only", port: backendPort, networkInterfaces: OS.networkInterfaces(), ...(advertisedHostOverride ? { advertisedHostOverride } : {}), }); } desktopServerExposureMode = exposure.mode; desktopSettings = setDesktopServerExposurePreference(desktopSettings, requestedMode); backendBindHost = exposure.bindHost; backendHttpUrl = exposure.localHttpUrl; backendWsUrl = exposure.localWsUrl; backendEndpointUrl = exposure.endpointUrl; backendAdvertisedHost = exposure.advertisedHost; if (options?.persist) { writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings); } return getDesktopServerExposureState();} function relaunchDesktopApp(reason: string): void { writeDesktopLogHeader(`desktop relaunch requested reason=${reason}`); setImmediate(() => { isQuitting = true; clearUpdatePollTimer(); cancelBackendReadinessWait(); void stopBackendAndWaitForExit() .catch((error) => { writeDesktopLogHeader( `desktop relaunch backend shutdown warning message=${formatErrorMessage(error)}`, ); }) .finally(() => { restoreStdIoCapture?.(); if (isDevelopment) { app.exit(75); return; } app.relaunch({ execPath: process.execPath, args: process.argv.slice(1), }); app.exit(0); }); });} function writeDesktopLogHeader(message: string): void { if (!desktopLogSink) return; desktopLogSink.write(`[${logTimestamp()}] [${logScope("desktop")}] ${message}\n`);} function writeBackendSessionBoundary(phase: "START" | "END", details: string): void { if (!backendLogSink) return; const normalizedDetails = sanitizeLogValue(details); backendLogSink.write( `[${logTimestamp()}] ---- APP SESSION ${phase} run=${APP_RUN_ID} ${normalizedDetails} ----\n`, );} function formatErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error);} function getSafeExternalUrl(rawUrl: unknown): string | null { if (typeof rawUrl !== "string" || rawUrl.length === 0) { return null; } let parsedUrl: URL; try { parsedUrl = new URL(rawUrl); } catch { return null; } if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") { return null; } return parsedUrl.toString();} function getSafeTheme(rawTheme: unknown): DesktopTheme | null { if (rawTheme === "light" || rawTheme === "dark" || rawTheme === "system") { return rawTheme; } return null;} async function waitForBackendHttpReady(baseUrl: string): Promise<void> { cancelBackendReadinessWait(); const controller = new AbortController(); backendReadinessAbortController = controller; try { await waitForHttpReady(baseUrl, { signal: controller.signal, }); } finally { if (backendReadinessAbortController === controller) { backendReadinessAbortController = null; } }} function cancelBackendReadinessWait(): void { backendReadinessAbortController?.abort(); backendReadinessAbortController = null;} function writeDesktopStreamChunk( streamName: "stdout" | "stderr", chunk: unknown, encoding: BufferEncoding | undefined,): void { if (!desktopLogSink) return; const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), typeof chunk === "string" ? encoding : undefined); desktopLogSink.write(`[${logTimestamp()}] [${logScope(streamName)}] `); desktopLogSink.write(buffer); if (buffer.length === 0 || buffer[buffer.length - 1] !== 0x0a) { desktopLogSink.write("\n"); }} function installStdIoCapture(): void { if (!app.isPackaged || desktopLogSink === null || restoreStdIoCapture !== null) { return; } const originalStdoutWrite = process.stdout.write.bind(process.stdout); const originalStderrWrite = process.stderr.write.bind(process.stderr); const patchWrite = (streamName: "stdout" | "stderr", originalWrite: typeof process.stdout.write) => ( chunk: string | Uint8Array, encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), callback?: (error?: Error | null) => void, ): boolean => { const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : undefined; writeDesktopStreamChunk(streamName, chunk, encoding); if (typeof encodingOrCallback === "function") { return originalWrite(chunk, encodingOrCallback); } if (callback !== undefined) { return originalWrite(chunk, encoding, callback); } if (encoding !== undefined) { return originalWrite(chunk, encoding); } return originalWrite(chunk); }; process.stdout.write = patchWrite("stdout", originalStdoutWrite); process.stderr.write = patchWrite("stderr", originalStderrWrite); restoreStdIoCapture = () => { process.stdout.write = originalStdoutWrite; process.stderr.write = originalStderrWrite; restoreStdIoCapture = null; };} function initializePackagedLogging(): void { if (!app.isPackaged) return; try { desktopLogSink = new RotatingFileSink({ filePath: Path.join(LOG_DIR, "desktop-main.log"), maxBytes: LOG_FILE_MAX_BYTES, maxFiles: LOG_FILE_MAX_FILES, }); backendLogSink = new RotatingFileSink({ filePath: Path.join(LOG_DIR, "server-child.log"), maxBytes: LOG_FILE_MAX_BYTES, maxFiles: LOG_FILE_MAX_FILES, }); installStdIoCapture(); writeDesktopLogHeader(`runtime log capture enabled logDir=${LOG_DIR}`); } catch (error) { // Logging setup should never block app startup. console.error("[desktop] failed to initialize packaged logging", error); }} function captureBackendOutput(child: ChildProcess.ChildProcess): void { if (!app.isPackaged || backendLogSink === null) return; const writeChunk = (chunk: unknown): void => { if (!backendLogSink) return; const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8"); backendLogSink.write(buffer); }; child.stdout?.on("data", writeChunk); child.stderr?.on("data", writeChunk);} initializePackagedLogging(); if (process.platform === "linux") { app.commandLine.appendSwitch("class", LINUX_WM_CLASS);} function getDestructiveMenuIcon(): Electron.NativeImage | undefined { if (process.platform !== "darwin") return undefined; if (destructiveMenuIconCache !== undefined) { return destructiveMenuIconCache ?? undefined; } try { const icon = nativeImage.createFromNamedImage("trash").resize({ width: 14, height: 14, }); if (icon.isEmpty()) { destructiveMenuIconCache = null; return undefined; } icon.setTemplateImage(true); destructiveMenuIconCache = icon; return icon; } catch { destructiveMenuIconCache = null; return undefined; }}let updatePollTimer: ReturnType<typeof setInterval> | null = null;let updateStartupTimer: ReturnType<typeof setTimeout> | null = null;let updateCheckInFlight = false;let updateDownloadInFlight = false;let updateInstallInFlight = false;let updaterConfigured = false;let updateState: DesktopUpdateState = initialUpdateState(); function resolveUpdaterErrorContext(): DesktopUpdateErrorContext { if (updateInstallInFlight) return "install"; if (updateDownloadInFlight) return "download";Lines 1–500 of 1900. Load more
import * as ChildProcess from "node:child_process"; import * as Crypto from "node:crypto"; import * as FS from "node:fs"; import * as OS from "node:os"; import * as Path from "node:path"; import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, nativeImage, nativeTheme, protocol, safeStorage, shell, } from "electron"; import type { MenuItemConstructorOptions } from "electron"; import type { ClientSettings, DesktopTheme, DesktopServerExposureMode, DesktopServerExposureState, PersistedSavedEnvironmentRecord, DesktopUpdateActionResult, DesktopUpdateCheckResult, DesktopUpdateState, } from "@t3tools/contracts"; import { autoUpdater } from "electron-updater"; import type { ContextMenuItem } from "@t3tools/contracts"; import { RotatingFileSink } from "@t3tools/shared/logging"; import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; import { DEFAULT_DESKTOP_BACKEND_PORT, resolveDesktopBackendPort } from "./backendPort"; import { DEFAULT_DESKTOP_SETTINGS, readDesktopSettings, setDesktopServerExposurePreference, writeDesktopSettings, } from "./desktopSettings"; import { readClientSettings, readSavedEnvironmentRegistry, readSavedEnvironmentSecret, removeSavedEnvironmentSecret, writeClientSettings, writeSavedEnvironmentRegistry, writeSavedEnvironmentSecret, } from "./clientPersistence"; import { isBackendReadinessAborted, waitForHttpReady } from "./backendReadiness"; import { showDesktopConfirmDialog } from "./confirmDialog"; import { resolveDesktopServerExposure } from "./serverExposure"; import { syncShellEnvironment } from "./syncShellEnvironment"; import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; import { createInitialDesktopUpdateState, reduceDesktopUpdateStateOnCheckFailure, reduceDesktopUpdateStateOnCheckStart, reduceDesktopUpdateStateOnDownloadComplete, reduceDesktopUpdateStateOnDownloadFailure, reduceDesktopUpdateStateOnDownloadProgress, reduceDesktopUpdateStateOnDownloadStart, reduceDesktopUpdateStateOnInstallFailure, reduceDesktopUpdateStateOnNoUpdate, reduceDesktopUpdateStateOnUpdateAvailable, } from "./updateMachine"; import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch"; syncShellEnvironment(); const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; const CONFIRM_CHANNEL = "desktop:confirm"; const SET_THEME_CHANNEL = "desktop:set-theme"; const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; const MENU_ACTION_CHANNEL = "desktop:menu-action"; const UPDATE_STATE_CHANNEL = "desktop:update-state"; const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state"; const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download"; const UPDATE_INSTALL_CHANNEL = "desktop:update-install"; const UPDATE_CHECK_CHANNEL = "desktop:update-check"; const GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL = "desktop:get-local-environment-bootstrap"; const GET_CLIENT_SETTINGS_CHANNEL = "desktop:get-client-settings"; const SET_CLIENT_SETTINGS_CHANNEL = "desktop:set-client-settings"; const GET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL = "desktop:get-saved-environment-registry"; const SET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL = "desktop:set-saved-environment-registry"; const GET_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:get-saved-environment-secret"; const SET_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:set-saved-environment-secret"; const REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:remove-saved-environment-secret"; const GET_SERVER_EXPOSURE_STATE_CHANNEL = "desktop:get-server-exposure-state"; const SET_SERVER_EXPOSURE_MODE_CHANNEL = "desktop:set-server-exposure-mode"; const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3"); const STATE_DIR = Path.join(BASE_DIR, "userdata"); const DESKTOP_SETTINGS_PATH = Path.join(STATE_DIR, "desktop-settings.json"); const CLIENT_SETTINGS_PATH = Path.join(STATE_DIR, "client-settings.json"); const SAVED_ENVIRONMENT_REGISTRY_PATH = Path.join(STATE_DIR, "saved-environments.json"); const DESKTOP_SCHEME = "t3"; const ROOT_DIR = Path.resolve(__dirname, "../../.."); const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; const APP_USER_MODEL_ID = "com.t3tools.t3code"; const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "t3code-dev.desktop" : "t3code.desktop"; const LINUX_WM_CLASS = isDevelopment ? "t3code-dev" : "t3code"; const USER_DATA_DIR_NAME = isDevelopment ? "t3code-dev" : "t3code"; const LEGACY_USER_DATA_DIR_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i; const COMMIT_HASH_DISPLAY_LENGTH = 12; const LOG_DIR = Path.join(STATE_DIR, "logs"); const LOG_FILE_MAX_BYTES = 10 * 1024 * 1024; const LOG_FILE_MAX_FILES = 10; const APP_RUN_ID = Crypto.randomBytes(6).toString("hex"); const SERVER_SETTINGS_PATH = Path.join(STATE_DIR, "settings.json"); const AUTO_UPDATE_STARTUP_DELAY_MS = 15_000; const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000; const DESKTOP_UPDATE_CHANNEL = "latest"; const DESKTOP_UPDATE_ALLOW_PRERELEASE = false; const DESKTOP_LOOPBACK_HOST = "127.0.0.1"; type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"]; type LinuxDesktopNamedApp = Electron.App & { setDesktopName?: (desktopName: string) => void; }; let mainWindow: BrowserWindow | null = null; let backendProcess: ChildProcess.ChildProcess | null = null; let backendPort = 0; let backendBindHost = DESKTOP_LOOPBACK_HOST; let backendBootstrapToken = ""; let backendHttpUrl = ""; let backendWsUrl = ""; let backendEndpointUrl: string | null = null; let backendAdvertisedHost: string | null = null; let backendReadinessAbortController: AbortController | null = null; let restartAttempt = 0; let restartTimer: ReturnType<typeof setTimeout> | null = null; let isQuitting = false; let desktopProtocolRegistered = false; let aboutCommitHashCache: string | null | undefined; let desktopLogSink: RotatingFileSink | null = null; let backendLogSink: RotatingFileSink | null = null; let restoreStdIoCapture: (() => void) | null = null; let backendObservabilitySettings = readPersistedBackendObservabilitySettings(); let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH); let desktopServerExposureMode: DesktopServerExposureMode = desktopSettings.serverExposureMode; let destructiveMenuIconCache: Electron.NativeImage | null | undefined; const expectedBackendExitChildren = new WeakSet<ChildProcess.ChildProcess>(); const desktopRuntimeInfo = resolveDesktopRuntimeInfo({ platform: process.platform, processArch: process.arch, runningUnderArm64Translation: app.runningUnderARM64Translation === true, }); const initialUpdateState = (): DesktopUpdateState => createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo); function logTimestamp(): string { return new Date().toISOString(); } function logScope(scope: string): string { return `${scope} run=${APP_RUN_ID}`; } function sanitizeLogValue(value: string): string { return value.replace(/\s+/g, " ").trim(); } function readPersistedBackendObservabilitySettings(): { readonly otlpTracesUrl: string | undefined; readonly otlpMetricsUrl: string | undefined; } { try { if (!FS.existsSync(SERVER_SETTINGS_PATH)) { return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; } return parsePersistedServerObservabilitySettings(FS.readFileSync(SERVER_SETTINGS_PATH, "utf8")); } catch (error) { console.warn("[desktop] failed to read persisted backend observability settings", error); return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; } } function resolveConfiguredDesktopBackendPort(rawPort: string | undefined): number | undefined { if (!rawPort) { return undefined; } const parsedPort = Number.parseInt(rawPort, 10); if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65_535) { return undefined; } return parsedPort; } function resolveDesktopDevServerUrl(): string { const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim(); if (!devServerUrl) { throw new Error("VITE_DEV_SERVER_URL is required in desktop development."); } return devServerUrl; } function backendChildEnv(): NodeJS.ProcessEnv { const env = { ...process.env }; delete env.T3CODE_PORT; delete env.T3CODE_MODE; delete env.T3CODE_NO_BROWSER; delete env.T3CODE_HOST; delete env.T3CODE_DESKTOP_WS_URL; delete env.T3CODE_DESKTOP_LAN_ACCESS; delete env.T3CODE_DESKTOP_LAN_HOST; return env; } function getDesktopServerExposureState(): DesktopServerExposureState { return { mode: desktopServerExposureMode, endpointUrl: backendEndpointUrl, advertisedHost: backendAdvertisedHost, }; } function getDesktopSecretStorage() { return { isEncryptionAvailable: () => safeStorage.isEncryptionAvailable(), encryptString: (value: string) => safeStorage.encryptString(value), decryptString: (value: Buffer) => safeStorage.decryptString(value), } as const; } function resolveAdvertisedHostOverride(): string | undefined { const override = process.env.T3CODE_DESKTOP_LAN_HOST?.trim(); return override && override.length > 0 ? override : undefined; } async function applyDesktopServerExposureMode( mode: DesktopServerExposureMode, options?: { readonly persist?: boolean; readonly rejectIfUnavailable?: boolean }, ): Promise<DesktopServerExposureState> { const advertisedHostOverride = resolveAdvertisedHostOverride(); const requestedMode = mode; let exposure = resolveDesktopServerExposure({ mode, port: backendPort, networkInterfaces: OS.networkInterfaces(), ...(advertisedHostOverride ? { advertisedHostOverride } : {}), }); if (requestedMode === "network-accessible" && exposure.endpointUrl === null) { if (options?.rejectIfUnavailable) { throw new Error("No reachable network address is available for this desktop right now."); } exposure = resolveDesktopServerExposure({ mode: "local-only", port: backendPort, networkInterfaces: OS.networkInterfaces(), ...(advertisedHostOverride ? { advertisedHostOverride } : {}), }); } desktopServerExposureMode = exposure.mode; desktopSettings = setDesktopServerExposurePreference(desktopSettings, requestedMode); backendBindHost = exposure.bindHost; backendHttpUrl = exposure.localHttpUrl; backendWsUrl = exposure.localWsUrl; backendEndpointUrl = exposure.endpointUrl; backendAdvertisedHost = exposure.advertisedHost; if (options?.persist) { writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings); } return getDesktopServerExposureState(); } function relaunchDesktopApp(reason: string): void { writeDesktopLogHeader(`desktop relaunch requested reason=${reason}`); setImmediate(() => { isQuitting = true; clearUpdatePollTimer(); cancelBackendReadinessWait(); void stopBackendAndWaitForExit() .catch((error) => { writeDesktopLogHeader( `desktop relaunch backend shutdown warning message=${formatErrorMessage(error)}`, ); }) .finally(() => { restoreStdIoCapture?.(); if (isDevelopment) { app.exit(75); return; } app.relaunch({ execPath: process.execPath, args: process.argv.slice(1), }); app.exit(0); }); }); } function writeDesktopLogHeader(message: string): void { if (!desktopLogSink) return; desktopLogSink.write(`[${logTimestamp()}] [${logScope("desktop")}] ${message}\n`); } function writeBackendSessionBoundary(phase: "START" | "END", details: string): void { if (!backendLogSink) return; const normalizedDetails = sanitizeLogValue(details); backendLogSink.write( `[${logTimestamp()}] ---- APP SESSION ${phase} run=${APP_RUN_ID} ${normalizedDetails} ----\n`, ); } function formatErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error); } function getSafeExternalUrl(rawUrl: unknown): string | null { if (typeof rawUrl !== "string" || rawUrl.length === 0) { return null; } let parsedUrl: URL; try { parsedUrl = new URL(rawUrl); } catch { return null; } if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") { return null; } return parsedUrl.toString(); } function getSafeTheme(rawTheme: unknown): DesktopTheme | null { if (rawTheme === "light" || rawTheme === "dark" || rawTheme === "system") { return rawTheme; } return null; } async function waitForBackendHttpReady(baseUrl: string): Promise<void> { cancelBackendReadinessWait(); const controller = new AbortController(); backendReadinessAbortController = controller; try { await waitForHttpReady(baseUrl, { signal: controller.signal, }); } finally { if (backendReadinessAbortController === controller) { backendReadinessAbortController = null; } } } function cancelBackendReadinessWait(): void { backendReadinessAbortController?.abort(); backendReadinessAbortController = null; } function writeDesktopStreamChunk( streamName: "stdout" | "stderr", chunk: unknown, encoding: BufferEncoding | undefined, ): void { if (!desktopLogSink) return; const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), typeof chunk === "string" ? encoding : undefined); desktopLogSink.write(`[${logTimestamp()}] [${logScope(streamName)}] `); desktopLogSink.write(buffer); if (buffer.length === 0 || buffer[buffer.length - 1] !== 0x0a) { desktopLogSink.write("\n"); } } function installStdIoCapture(): void { if (!app.isPackaged || desktopLogSink === null || restoreStdIoCapture !== null) { return; } const originalStdoutWrite = process.stdout.write.bind(process.stdout); const originalStderrWrite = process.stderr.write.bind(process.stderr); const patchWrite = (streamName: "stdout" | "stderr", originalWrite: typeof process.stdout.write) => ( chunk: string | Uint8Array, encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), callback?: (error?: Error | null) => void, ): boolean => { const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : undefined; writeDesktopStreamChunk(streamName, chunk, encoding); if (typeof encodingOrCallback === "function") { return originalWrite(chunk, encodingOrCallback); } if (callback !== undefined) { return originalWrite(chunk, encoding, callback); } if (encoding !== undefined) { return originalWrite(chunk, encoding); } return originalWrite(chunk); }; process.stdout.write = patchWrite("stdout", originalStdoutWrite); process.stderr.write = patchWrite("stderr", originalStderrWrite); restoreStdIoCapture = () => { process.stdout.write = originalStdoutWrite; process.stderr.write = originalStderrWrite; restoreStdIoCapture = null; }; } function initializePackagedLogging(): void { if (!app.isPackaged) return; try { desktopLogSink = new RotatingFileSink({ filePath: Path.join(LOG_DIR, "desktop-main.log"), maxBytes: LOG_FILE_MAX_BYTES, maxFiles: LOG_FILE_MAX_FILES, }); backendLogSink = new RotatingFileSink({ filePath: Path.join(LOG_DIR, "server-child.log"), maxBytes: LOG_FILE_MAX_BYTES, maxFiles: LOG_FILE_MAX_FILES, }); installStdIoCapture(); writeDesktopLogHeader(`runtime log capture enabled logDir=${LOG_DIR}`); } catch (error) { // Logging setup should never block app startup. console.error("[desktop] failed to initialize packaged logging", error); } } function captureBackendOutput(child: ChildProcess.ChildProcess): void { if (!app.isPackaged || backendLogSink === null) return; const writeChunk = (chunk: unknown): void => { if (!backendLogSink) return; const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8"); backendLogSink.write(buffer); }; child.stdout?.on("data", writeChunk); child.stderr?.on("data", writeChunk); } initializePackagedLogging(); if (process.platform === "linux") { app.commandLine.appendSwitch("class", LINUX_WM_CLASS); } function getDestructiveMenuIcon(): Electron.NativeImage | undefined { if (process.platform !== "darwin") return undefined; if (destructiveMenuIconCache !== undefined) { return destructiveMenuIconCache ?? undefined; } try { const icon = nativeImage.createFromNamedImage("trash").resize({ width: 14, height: 14, }); if (icon.isEmpty()) { destructiveMenuIconCache = null; return undefined; } icon.setTemplateImage(true); destructiveMenuIconCache = icon; return icon; } catch { destructiveMenuIconCache = null; return undefined; } } let updatePollTimer: ReturnType<typeof setInterval> | null = null; let updateStartupTimer: ReturnType<typeof setTimeout> | null = null; let updateCheckInFlight = false; let updateDownloadInFlight = false; let updateInstallInFlight = false; let updaterConfigured = false; let updateState: DesktopUpdateState = initialUpdateState(); function resolveUpdaterErrorContext(): DesktopUpdateErrorContext { if (updateInstallInFlight) return "install"; if (updateDownloadInFlight) return "download";