diff --git a/src/main/startup/configure-process.test.ts b/src/main/startup/configure-process.test.ts index ef7ec6a9b69..9d97dda6dae 100644 --- a/src/main/startup/configure-process.test.ts +++ b/src/main/startup/configure-process.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { homedir, tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -444,6 +444,43 @@ describe('configureElectronNetworkCompatibility', () => { ).toBe(false) }) + it('answers from the marker without reading the settings file', async () => { + const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process') + const { writeHttp1CompatibilityMarker } = await import('./http1-compatibility-marker') + const userDataPath = createUserDataDir({ electronHttp1CompatibilityMode: false }) + writeHttp1CompatibilityMarker(userDataPath, true) + rmSync(join(userDataPath, 'orca-data.json'), { force: true }) + + expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(true) + }) + + it('falls back to the settings file when no marker has been written yet', async () => { + const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process') + const userDataPath = createUserDataDir({ electronHttp1CompatibilityMode: true }) + + expect(existsSync(join(userDataPath, 'http1-compatibility.json'))).toBe(false) + expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(true) + }) + + it('falls back to the settings file when the marker is corrupt', async () => { + const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process') + const userDataPath = createUserDataDir({ electronHttp1CompatibilityMode: true }) + writeFileSync(join(userDataPath, 'http1-compatibility.json'), '{ not json', 'utf-8') + + expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(true) + }) + + it('lets the environment override the marker', async () => { + const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process') + const { writeHttp1CompatibilityMarker } = await import('./http1-compatibility-marker') + const userDataPath = createUserDataDir({}) + writeHttp1CompatibilityMarker(userDataPath, true) + + expect( + shouldDisableHttp2ForElectronNetworking({ env: { ORCA_DISABLE_HTTP2: '0' }, userDataPath }) + ).toBe(false) + }) + it('appends Electron disable-http2 before sessions are created', async () => { const { app } = await import('electron') const { configureElectronNetworkCompatibility } = await import('./configure-process') diff --git a/src/main/startup/configure-process.ts b/src/main/startup/configure-process.ts index 6b167d7fe14..13dbd70c2c3 100644 --- a/src/main/startup/configure-process.ts +++ b/src/main/startup/configure-process.ts @@ -5,6 +5,7 @@ import { join, resolve } from 'node:path' import { getVersionManagerBinPaths } from '../codex-cli/command' import { getMainE2EConfig } from '../e2e-config' import { DISABLED_CHROMIUM_FEATURES } from './disabled-chromium-features' +import { readHttp1CompatibilityMarker } from './http1-compatibility-marker' const DEV_PARENT_SHUTDOWN_GRACE_MS = 3000 const HTTP1_COMPATIBILITY_ENV_VAR = 'ORCA_DISABLE_HTTP2' @@ -54,7 +55,13 @@ export function shouldDisableHttp2ForElectronNetworking( if (envValue !== null) { return envValue } - return readPersistedHttp1CompatibilityMode(options.userDataPath ?? app.getPath('userData')) + const userDataPath = options.userDataPath ?? app.getPath('userData') + // Why the marker first: this runs before app.whenReady(), and the settings file is the multi-MB + // orca-data.json the Store parses again moments later. The marker is refreshed whenever settings + // change, so the full read only happens on a profile that has never written one. + return ( + readHttp1CompatibilityMarker(userDataPath) ?? readPersistedHttp1CompatibilityMode(userDataPath) + ) } export function configureElectronNetworkCompatibility( diff --git a/src/main/startup/http1-compatibility-marker.ts b/src/main/startup/http1-compatibility-marker.ts new file mode 100644 index 00000000000..6278b7ec576 --- /dev/null +++ b/src/main/startup/http1-compatibility-marker.ts @@ -0,0 +1,59 @@ +import { readFileSync, rmSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' + +/** + * Cached copy of `settings.electronHttp1CompatibilityMode` for pre-`ready` startup. + * + * Why a standalone file (not the Store): app.commandLine.appendSwitch('disable-http2') must run + * before the first Electron session exists, which is before the settings Store is constructed. + * Reading it from the settings file meant a synchronous read + JSON.parse of the whole multi-MB + * orca-data.json on the critical path of every cold start, duplicating the parse the Store does a + * moment later. This marker is a few bytes, mirroring gpu-fallback-marker.ts. + */ + +export const HTTP1_COMPATIBILITY_MARKER_FILE = 'http1-compatibility.json' +const MARKER_SCHEME_VERSION = 1 + +type Http1CompatibilityMarker = { + schemeVersion: number + enabled: boolean +} + +function markerPath(userDataPath: string): string { + return join(userDataPath, HTTP1_COMPATIBILITY_MARKER_FILE) +} + +/** Returns null when the marker is missing or unreadable, so callers fall back to the settings file. */ +export function readHttp1CompatibilityMarker(userDataPath: string): boolean | null { + try { + const parsed = JSON.parse( + readFileSync(markerPath(userDataPath), 'utf-8') + ) as Partial + if (parsed.schemeVersion !== MARKER_SCHEME_VERSION || typeof parsed.enabled !== 'boolean') { + return null + } + return parsed.enabled + } catch { + return null + } +} + +export function writeHttp1CompatibilityMarker(userDataPath: string, enabled: boolean): void { + if (readHttp1CompatibilityMarker(userDataPath) === enabled) { + return + } + const marker: Http1CompatibilityMarker = { schemeVersion: MARKER_SCHEME_VERSION, enabled } + try { + writeFileSync(markerPath(userDataPath), JSON.stringify(marker)) + } catch { + // Best effort: a missing marker just costs the next launch the settings-file fallback. + } +} + +export function clearHttp1CompatibilityMarker(userDataPath: string): void { + try { + rmSync(markerPath(userDataPath), { force: true }) + } catch { + // Best effort; a stale marker is revalidated on the next settings change. + } +} diff --git a/src/main/startup/main-process-ready-foundation.ts b/src/main/startup/main-process-ready-foundation.ts index 171aaf50421..3c0e01fe09e 100644 --- a/src/main/startup/main-process-ready-foundation.ts +++ b/src/main/startup/main-process-ready-foundation.ts @@ -41,6 +41,7 @@ import { registerDocPreviewGrantHandlers } from '../ipc/doc-preview-grant-ipc' import { initializeBrowserSessionsForApp } from '../browser/browser-session-startup' import { browserSessionRegistry } from '../browser/browser-session-registry' import { logStartupMilestone } from './startup-diagnostics' +import { writeHttp1CompatibilityMarker } from './http1-compatibility-marker' import { mainProcessState as state } from './main-process-state' import { recordDurableCrashBreadcrumb } from '../crash-reporting/durable-crash-breadcrumb' import { syncMacMenuBarIcon } from './main-window-actions' @@ -193,9 +194,20 @@ export async function initializeReadyFoundation(): Promise { } wslHookRelayManager.setManagedHookSettingsResolver(() => state.store?.getSettings() ?? null) logStartupMilestone('store-loaded') + // Why: pre-`ready` startup reads this flag from a marker so it never has to parse orca-data.json. + writeHttp1CompatibilityMarker( + canonicalUserDataPath, + store.getSettings().electronHttp1CompatibilityMode === true + ) // Why: apply initial fallback WSL distro from store settings for global git/CLI calls. setDefaultWslDistroOverride(store.getSettings().terminalWindowsWslDistro ?? null) store.onSettingsChanged((updates, settings) => { + if ('electronHttp1CompatibilityMode' in updates) { + writeHttp1CompatibilityMarker( + canonicalUserDataPath, + settings.electronHttp1CompatibilityMode === true + ) + } if ('terminalWindowsWslDistro' in updates) { // Why: synchronize fallback WSL distro updates to runner. setDefaultWslDistroOverride(settings.terminalWindowsWslDistro ?? null)