/** * Electron Main Process Entry * Manages window creation, system tray, and IPC handlers */ import { app, BrowserWindow, nativeImage, safeStorage, shell } from 'electron'; import type { Server } from 'node:http'; import { createServer } from 'node:http'; import { join } from 'node:path'; import { registerIpcHandlers } from './ipc-handlers'; import { createTray } from './tray'; import { createMenu } from './menu'; import { registerZoomShortcuts } from './zoom-shortcuts'; import { getNativeWindowMaterialOptions } from './window-material'; import { appUpdater, registerUpdateHandlers } from './updater'; import { logger } from '../utils/logger'; import { initTelemetry } from '../utils/telemetry'; import { isQuitting, setQuitting } from './app-state'; import { applyProxySettings } from './proxy'; import { syncLaunchAtStartupSettingFromStore } from './launch-at-startup'; import { clearPendingSecondInstanceFocus, consumeMainWindowReady, createMainWindowFocusState, requestSecondInstanceFocus, } from './main-window-focus'; import { findNianCodeDeepLinkUrl, NIANCODE_APP_PROTOCOL, parseNianCodeDeepLinkUrl, } from './app-deep-link'; import { installMediaPermissionHandler } from './media-permissions'; import { createQuitLifecycleState, markQuitCleanupCompleted, requestQuitLifecycleAction, } from './quit-lifecycle'; import { executableSnapshotChanged, snapshotExecutable, } from './relaunch-on-replaced-app'; import { createSignalQuitHandler } from './signal-quit'; import { acquireProcessInstanceFileLock } from './process-instance-lock'; import { BackgroundLifecycleController } from './background-lifecycle'; import { readGpuFallbackState, recordGpuCrash, shouldUseSoftwareRendering, } from './gpu-fallback'; import { getHostApiToken, startHostApiServer } from '../api/server'; import type { HostApiContext } from '../api/context'; import { HostEventBus } from '../api/event-bus'; import { AgentBrowserModule, ElectronAgentBrowserAdapter } from '../agent-browser'; import { browserOAuthManager } from '../utils/browser-oauth'; import { createProjectProgressSync } from '../services/project-progress-sync'; import { createWorksSubmissionBindingStore } from '../services/works-submission-binding'; import { ReleaseJobManager } from '../services/release-job'; import { createReleaseUtilityPreparer } from '../services/release-utility-process'; import { createStaticArtifactSnapshot } from '../services/static-release-server'; import { consumeWorksSquareStartupRuntimeCleanupRequired, getWorksSquareSessionRestoreStatus, getWorksSquareSessionSnapshot, initializeWorksSquareSession, subscribeWorksSquareSession, type WorksSquareSessionChangeReason, } from '../services/works-square-session'; import { shouldUseSecureWorksSquareSessionPersistence } from '../services/works-square-session-persistence-policy'; import { initializeRememberedPassword } from '../services/remembered-password'; import { clearManagedWorksSquareRuntimeBestEffort } from '../services/works-square-runtime'; import { WorksSquareDesignWorkspace } from '../image-workspace/works-square-workspace'; import type { DesignWorkspaceModule } from '../image-workspace/module'; import { createCodingProjectStore, createElectronCodingProjectStorage, type CodingProjectStore, } from '../coding-projects/project-store'; import { resolveBundledCodingSkillsDir } from '../coding-runtime/pi/resource-loader'; import type { CodingProductComposition } from '../api/coding-product-services'; import { createCodingComposition, resolveCodingPiRuntimePaths, } from '../api/coding-composition'; import { armFinalAsarResilienceCompactDelay, armFinalAsarResiliencePromptDelay, abortFinalAsarResilienceTarget, disposeFinalAsarResilienceTarget, finishFinalAsarProxyCompositionProof, finishFinalAsarPressureProof, finishFinalAsarResilienceProof, getFinalAsarProxyCompositionStatus, getFinalAsarResilienceIdleStatus, getFinalAsarResilienceStatus, injectFinalAsarResilienceFailure, releaseFinalAsarProxyCompositionChild, releaseFinalAsarResilienceParents, restartFinalAsarResilienceOther, runFinalAsarExtensionProof, startFinalAsarProxyCompositionProof, startFinalAsarPressureProof, startFinalAsarResilienceProof, } from '../coding-runtime/pi/release-proof'; // Diagnostic package: force Chromium networking onto HTTP/1.1 for transport A/B testing. app.commandLine.appendSwitch('disable-http2'); const WINDOWS_APP_USER_MODEL_ID = 'app.niancode.desktop'; const isE2EMode = process.env.NIANCODE_E2E === '1'; const requestedUserDataDir = process.env.NIANCODE_USER_DATA_DIR?.trim(); const useSecureWorksSquareSessionPersistence = shouldUseSecureWorksSquareSessionPersistence( app.isPackaged, ); if (isE2EMode && requestedUserDataDir) { app.setPath('userData', requestedUserDataDir); } // Hardware compositing is the fast path for Makelore's opaque light surface. // Keep an explicit escape hatch for driver-specific failures rather than // forcing every installation through Chromium's software compositor. const gpuFallbackPath = join(app.getPath('userData'), 'gpu-fallback.json'); const disableHardwareAcceleration = shouldUseSoftwareRendering({ envDisabled: process.env.MAKELORE_DISABLE_GPU === '1', cliDisabled: process.argv.includes('--disable-gpu'), state: readGpuFallbackState(gpuFallbackPath), }); if (disableHardwareAcceleration) { app.disableHardwareAcceleration(); } app.on('child-process-gone', (_event, details) => { if (details.type !== 'GPU' || !['crashed', 'abnormal-exit', 'oom'].includes(details.reason)) { return; } try { const state = recordGpuCrash(gpuFallbackPath, `${details.reason}:${details.exitCode}`); logger.warn('[gpu] GPU process failure recorded; software rendering will be used after repeated failures', state); } catch (error) { logger.warn('[gpu] Failed to persist GPU process failure state', error); } }); // On Linux, set CHROME_DESKTOP so Chromium can find the correct .desktop file. // On Wayland this maps the running window to niancode.desktop (→ icon + app grouping); // on X11 it supplements the StartupWMClass matching. // Must be called before app.whenReady() / before any window is created. if (process.platform === 'linux') { app.setDesktopName('niancode.desktop'); } // Prevent multiple instances of the app from running simultaneously. // Without this, two instances can each spawn their own local runtime process on // the same port. The losing process must exit immediately. const gotElectronLock = isE2EMode ? true : app.requestSingleInstanceLock(); if (!gotElectronLock) { console.info('[Makelore] Another instance already holds the single-instance lock; exiting duplicate process'); app.exit(0); } let releaseProcessInstanceFileLock: () => void = () => {}; let gotFileLock = true; if (gotElectronLock && !isE2EMode) { try { const fileLock = acquireProcessInstanceFileLock({ userDataDir: app.getPath('userData'), lockName: 'niancode', force: true, // Electron lock already guarantees exclusivity; force-clean orphan/recycled-PID locks }); gotFileLock = fileLock.acquired; releaseProcessInstanceFileLock = fileLock.release; if (!fileLock.acquired) { const ownerDescriptor = fileLock.ownerPid ? `${fileLock.ownerFormat ?? 'legacy'} pid=${fileLock.ownerPid}` : fileLock.ownerFormat === 'unknown' ? 'unknown lock format/content' : 'unknown owner'; console.info( `[Makelore] Another instance already holds process lock (${fileLock.lockPath}, ${ownerDescriptor}); exiting duplicate process`, ); app.exit(0); } } catch (error) { console.warn('[Makelore] Failed to acquire process instance file lock; continuing with Electron single-instance lock only', error); } } const gotTheLock = gotElectronLock && gotFileLock; const executableSnapshotAtLaunch = snapshotExecutable(process.execPath); // Global references let mainWindow: BrowserWindow | null = null; let codingProjectStore!: CodingProjectStore; let hostEventBus!: HostEventBus; let hostApiServer: Server | null = null; let projectProgressSync: ReturnType | null = null; let worksSubmissionBinding: ReturnType | null = null; let agentBrowser: AgentBrowserModule | null = null; let applicationInitialization: Promise | null = null; let imageWorkspaceModule: DesignWorkspaceModule | null = null; let backgroundLifecycle!: BackgroundLifecycleController; let releaseJobs: ReleaseJobManager | null = null; let codingProducts: CodingProductComposition | null = null; const mainWindowFocusState = createMainWindowFocusState(); const quitLifecycleState = createQuitLifecycleState(); const launchDeepLinkUrl = findNianCodeDeepLinkUrl(process.argv); /** * Resolve the icons directory path (works in both dev and packaged mode) */ function getIconsDir(): string { if (app.isPackaged) { // Packaged: icons are in extraResources → process.resourcesPath/resources/icons return join(process.resourcesPath, 'resources', 'icons'); } // Development: relative to dist-electron/main/ return join(__dirname, '../../resources/icons'); } /** * Get the app icon for the current platform */ function getAppIcon(): Electron.NativeImage | undefined { if (process.platform === 'darwin') return undefined; // macOS uses the app bundle icon const iconsDir = getIconsDir(); const iconPath = process.platform === 'win32' ? join(iconsDir, 'icon.ico') : join(iconsDir, 'icon.png'); const icon = nativeImage.createFromPath(iconPath); return icon.isEmpty() ? undefined : icon; } /** * Create the main application window */ function createWindow(): BrowserWindow { const isMac = process.platform === 'darwin'; const isWindows = process.platform === 'win32'; const useCustomTitleBar = isWindows; const shouldSkipSetupForE2E = process.env.NIANCODE_E2E_SKIP_SETUP === '1'; const minimumWorkspaceColumnWidth = 256; const minimumWorkspaceWidth = minimumWorkspaceColumnWidth * 4; const win = new BrowserWindow({ title: 'Makelore', width: 1280, height: 800, minWidth: minimumWorkspaceWidth, minHeight: 700, icon: getAppIcon(), ...getNativeWindowMaterialOptions(process.platform), webPreferences: { preload: join(__dirname, '../preload/index.js'), nodeIntegration: false, contextIsolation: true, sandbox: false, webviewTag: false, }, // `hiddenInset` leaves a native top inset in the renderer viewport. That // makes a full-height Canvas look like it has an extra horizontal bar and // clips the top of the columns. `hidden` keeps the traffic lights while // giving the renderer the full window bounds. titleBarStyle: isMac ? 'hidden' : useCustomTitleBar ? 'hidden' : 'default', // Keep the native traffic lights on the same centerline as the 40px // renderer title bar. The native glyphs sit about 7px below the // configured origin, so y=13 centers them on the renderer controls. trafficLightPosition: isMac ? { x: 16, y: 13 } : undefined, frame: isMac || !useCustomTitleBar, show: false, }); installMediaPermissionHandler(win); registerZoomShortcuts(win); // Handle external links — only allow safe protocols to prevent arbitrary // command execution via shell.openExternal() (e.g. file://, ms-msdt:, etc.) win.webContents.setWindowOpenHandler(({ url }) => { try { const parsed = new URL(url); if (parsed.protocol === 'https:' || parsed.protocol === 'http:') { shell.openExternal(url); } else { logger.warn(`Blocked openExternal for disallowed protocol: ${parsed.protocol}`); } } catch { logger.warn(`Blocked openExternal for malformed URL: ${url}`); } return { action: 'deny' }; }); // Load the app if (process.env.VITE_DEV_SERVER_URL) { const rendererUrl = new URL(process.env.VITE_DEV_SERVER_URL); if (shouldSkipSetupForE2E) { rendererUrl.searchParams.set('e2eSkipSetup', '1'); } win.loadURL(rendererUrl.toString()); if (!isE2EMode) { win.webContents.openDevTools(); } } else { win.loadFile(join(__dirname, '../../dist/index.html'), { query: shouldSkipSetupForE2E ? { e2eSkipSetup: '1' } : undefined, }); } return win; } function focusWindow(win: BrowserWindow): void { if (win.isDestroyed()) { return; } if (win.isMinimized()) { win.restore(); } win.show(); win.focus(); } function focusMainWindow(): void { if (!mainWindow || mainWindow.isDestroyed()) { return; } clearPendingSecondInstanceFocus(mainWindowFocusState); focusWindow(mainWindow); } function requestMainWindowFocus(reason: string): void { const focusRequest = requestSecondInstanceFocus( mainWindowFocusState, Boolean(mainWindow && !mainWindow.isDestroyed()), ); if (focusRequest === 'focus-now') { focusMainWindow(); return; } logger.debug(`Main window is not ready yet; deferring focus for ${reason}`); } function handleAppDeepLinkActivation(rawUrl: string): boolean { const deepLink = parseNianCodeDeepLinkUrl(rawUrl); if (!deepLink) { return false; } logger.info(`Received Makelore app link: type=${deepLink.type}, request_id=${deepLink.requestId}`); requestMainWindowFocus('app deep link'); return true; } function registerMakeloreProtocolClient(): void { const devEntrypoint = app.isPackaged ? undefined : process.argv[1]; const registered = devEntrypoint ? app.setAsDefaultProtocolClient(NIANCODE_APP_PROTOCOL, process.execPath, [devEntrypoint]) : app.setAsDefaultProtocolClient(NIANCODE_APP_PROTOCOL); if (!registered) { logger.warn(`Failed to register ${NIANCODE_APP_PROTOCOL}:// protocol handler`); } } function createMainWindow(): BrowserWindow { const win = createWindow(); const sendAuthSession = ( session: ReturnType, reason: WorksSquareSessionChangeReason = 'changed', previousSession: ReturnType = null, ) => { if (!session && getWorksSquareSessionRestoreStatus() === 'unavailable') return; if (!win.isDestroyed() && !win.webContents.isDestroyed()) { win.webContents.send('auth:session-changed', session); } if (!session && reason === 'terminal') { void clearManagedWorksSquareRuntimeBestEffort({ codingProducts: codingProducts ?? undefined, imageWorkspace: imageWorkspaceModule ?? undefined, }, 'terminal session invalidation', previousSession?.accessToken); } }; const unsubscribeAuthSession = subscribeWorksSquareSession(sendAuthSession); win.webContents.on('did-finish-load', () => { sendAuthSession(getWorksSquareSessionSnapshot()); }); const closeAgentBrowserForHostRenderer = (reason: string): void => { void agentBrowser?.close().catch((error) => { logger.warn(`Failed to close Agent Browser after ${reason}:`, error); }); }; win.webContents.on('render-process-gone', () => { closeAgentBrowserForHostRenderer('the main renderer exited'); }); win.webContents.on( 'did-start-navigation', (_event, _url, isInPlace, isMainFrame) => { if (isMainFrame && !isInPlace) { closeAgentBrowserForHostRenderer('the main renderer started navigation'); } }, ); win.once('ready-to-show', () => { if (mainWindow !== win) { return; } const action = consumeMainWindowReady(mainWindowFocusState); if (action === 'focus') { focusWindow(win); return; } win.show(); }); win.on('close', (event) => { if (!isQuitting() && !isE2EMode) { event.preventDefault(); win.hide(); } }); win.on('hide', () => { backgroundLifecycle?.setActivity({ ...backgroundLifecycle.getActivity(), visible: false, }); // The browser is a disposable renderer and must not survive a hidden // application window. Active cloud tasks are reconciled when reopened. void agentBrowser?.close().catch((error) => { logger.warn('Failed to close Agent Browser after the main window was hidden:', error); }); }); win.on('show', () => { backgroundLifecycle?.setActivity({ ...backgroundLifecycle.getActivity(), visible: true, }); }); win.on('closed', () => { unsubscribeAuthSession(); const browser = agentBrowser; agentBrowser = null; void browser?.dispose().catch((error) => { logger.warn('Failed to dispose Agent Browser after the main window closed:', error); }); if (mainWindow === win) { mainWindow = null; } }); mainWindow = win; return win; } /** * Initialize the application */ async function initialize(): Promise { // Initialize logger first logger.init(); logger.info('=== Makelore Application Starting ==='); logger.debug( `Runtime: platform=${process.platform}/${process.arch}, electron=${process.versions.electron}, node=${process.versions.node}, packaged=${app.isPackaged}, pid=${process.pid}, ppid=${process.ppid}` ); if (!isE2EMode) { registerMakeloreProtocolClient(); } if (launchDeepLinkUrl) { handleAppDeepLinkActivation(launchDeepLinkUrl); } if (isE2EMode) { logger.info('Running in E2E mode: startup side effects minimized'); } // Only local storage and the lightweight workspace object are needed to // construct the first window. Authentication and remote setup are restored // after the renderer has had a chance to paint. const codingProjectStorage = await createElectronCodingProjectStorage(); codingProjectStore = createCodingProjectStore(codingProjectStorage); worksSubmissionBinding = createWorksSubmissionBindingStore(codingProjectStore); const imageWorkspace = new WorksSquareDesignWorkspace(); imageWorkspaceModule = imageWorkspace; logger.info('AI painting workspace is using the Works Square V2 cloud contract'); // Set application menu createMenu(); // Create the main window const window = createMainWindow(); agentBrowser = new AgentBrowserModule(new ElectronAgentBrowserAdapter(window)); // Create system tray if (!isE2EMode) { createTray(window); } // Register IPC handlers backgroundLifecycle = new BackgroundLifecycleController({ ...(isE2EMode ? { idleStopMs: 250 } : {}), onSleep: () => { if (!window.isDestroyed() && !window.webContents.isDestroyed()) { window.webContents.send('lifecycle:sleep'); } }, onStopRuntime: async () => await codingProducts?.sleep('background_sleep'), }); const releaseUtilityPreparer = createReleaseUtilityPreparer({ runtimeContext: { isPackaged: app.isPackaged, resourcesPath: process.resourcesPath, npmCacheDir: join(app.getPath('userData'), 'npm-cache'), }, }); releaseJobs = new ReleaseJobManager({ prepare: releaseUtilityPreparer.prepare, dispose: releaseUtilityPreparer.dispose, acquireLease: (lease) => backgroundLifecycle.acquireLease(lease), }); releaseJobs.on('status', (status) => { hostEventBus.emit('release-job:status', status); if (!window.isDestroyed() && !window.webContents.isDestroyed()) { window.webContents.send('release-job:status', status); } }); const codingAppPath = app.isPackaged ? app.getAppPath() : join(__dirname, '..', '..'); codingProducts = createCodingComposition({ storage: codingProjectStorage, projectStore: codingProjectStore, browser: agentBrowser, requestAgentBrowserPresentation: (snapshot) => { hostEventBus.emit('agent-browser:show', snapshot); if (!window.isDestroyed() && !window.webContents.isDestroyed()) { window.webContents.send('agent-browser:show', snapshot); } }, publishAgentBrowserState: (snapshot) => { hostEventBus.emit('agent-browser:state', snapshot); if (!window.isDestroyed() && !window.webContents.isDestroyed()) { window.webContents.send('agent-browser:state', snapshot); } }, getLocalProxyCredential: () => getHostApiToken() || undefined, acquireBackgroundLease: (lease) => backgroundLifecycle!.acquireLease(lease), paths: { ...resolveCodingPiRuntimePaths({ isPackaged: app.isPackaged, resourcesPath: process.resourcesPath, appPath: codingAppPath, executablePath: process.execPath, }), userDataDir: app.getPath('userData'), bundledSkillsDir: resolveBundledCodingSkillsDir({ isPackaged: app.isPackaged, resourcesPath: process.resourcesPath, appPath: codingAppPath, }), }, }); const hostApiContext: HostApiContext = { codingProjectStore, eventBus: hostEventBus, mainWindow: window, agentBrowser, worksSubmissionBinding: worksSubmissionBinding ?? undefined, imageWorkspace, lifecycle: backgroundLifecycle, releaseJobs, codingProducts, previewDataSession: codingProducts.previewDataSession, }; registerIpcHandlers(window, backgroundLifecycle, hostApiContext); void initializeRememberedPassword({ secureStorage: useSecureWorksSquareSessionPersistence ? safeStorage : null, }); hostApiServer = startHostApiServer(hostApiContext); // Register update handlers registerUpdateHandlers(appUpdater, window); // Note: Auto-check for updates is driven by the renderer (update store init) // so it respects the user's "Auto-check for updates" setting. const initializeBackgroundServices = async (): Promise => { const runDeferred = async (label: string, task: () => Promise): Promise => { try { await task(); } catch (error) { logger.warn(`Deferred ${label} initialization failed:`, error); } }; if (!isE2EMode) { // Apply the proxy before any external request made by background // services, but never before the first local paint. await runDeferred('proxy', applyProxySettings); await runDeferred('telemetry', initTelemetry); await runDeferred('launch-at-startup setting', syncLaunchAtStartupSettingFromStore); } if (!useSecureWorksSquareSessionPersistence) { logger.info('[works-square-session] Unpackaged development keeps credentials in memory; OS secure storage is disabled'); } await runDeferred('Works Square session', async () => { await initializeWorksSquareSession({ secureStorage: useSecureWorksSquareSessionPersistence ? safeStorage : null, }); }); if (consumeWorksSquareStartupRuntimeCleanupRequired()) { await runDeferred('expired runtime cleanup', async () => { await clearManagedWorksSquareRuntimeBestEffort({ codingProducts: codingProducts ?? undefined, imageWorkspace, }, 'expired persisted session during startup'); }); } if (!isE2EMode) { projectProgressSync = createProjectProgressSync(codingProjectStore); const activateProgrammingServices = (): void => { void projectProgressSync?.start() .then(() => projectProgressSync?.activate()) .catch((error) => logger.warn('Failed to activate project progress sync:', error)); void worksSubmissionBinding?.start() .catch((error) => logger.warn('Failed to migrate active submission binding:', error)); }; backgroundLifecycle.on('activity', (activity) => { if (activity.visible && activity.module === 'programming') { activateProgrammingServices(); } else { projectProgressSync?.stop(); } }); const activity = backgroundLifecycle.getActivity(); if (activity.visible && activity.module === 'programming') { activateProgrammingServices(); } // Project watchers and binding migration now start from module // activation; the idle shell does not scan every project. } }; browserOAuthManager.on('oauth:start', (payload) => { hostEventBus.emit('oauth:start', payload); }); browserOAuthManager.on('oauth:code', (payload) => { hostEventBus.emit('oauth:code', payload); }); browserOAuthManager.on('oauth:success', (payload) => { hostEventBus.emit('oauth:success', { ...payload, success: true }); }); browserOAuthManager.on('oauth:error', (error) => { hostEventBus.emit('oauth:error', error); }); // Coding workers are started on demand. Keeping them cold for // Canvas/Robot avoids a resident child process on every launch. void initializeBackgroundServices().catch((error) => { logger.warn('Deferred background initialization failed:', error); }); } if (gotTheLock) { const requestQuitOnSignal = createSignalQuitHandler({ logInfo: (message) => logger.info(message), requestQuit: () => app.quit(), }); process.on('exit', () => { releaseProcessInstanceFileLock(); }); process.once('SIGINT', () => requestQuitOnSignal('SIGINT')); process.once('SIGTERM', () => requestQuitOnSignal('SIGTERM')); app.on('will-quit', () => { releaseProcessInstanceFileLock(); }); if (process.platform === 'win32') { app.setAppUserModelId(WINDOWS_APP_USER_MODEL_ID); } hostEventBus = new HostEventBus(); // When a second instance is launched, focus the existing window instead. app.on('second-instance', (_event, commandLine) => { logger.info('Second Makelore instance detected; redirecting to the existing window'); if ( app.isPackaged && process.platform === 'darwin' && executableSnapshotChanged(executableSnapshotAtLaunch, snapshotExecutable(process.execPath)) ) { logger.info('Detected that the installed app bundle changed while this instance was running; relaunching into the updated app'); setQuitting(); app.relaunch({ execPath: process.execPath }); app.quit(); return; } const deepLinkUrl = findNianCodeDeepLinkUrl(commandLine); if (deepLinkUrl && handleAppDeepLinkActivation(deepLinkUrl)) { return; } requestMainWindowFocus('second instance'); }); app.on('open-url', (event, url) => { event.preventDefault(); if (!handleAppDeepLinkActivation(url)) { logger.warn('Ignored unsupported Makelore app link'); } }); // Application lifecycle app.whenReady().then(() => { applicationInitialization = initialize(); void applicationInitialization.catch((error) => { logger.error('Application initialization failed:', error); }); // Register activate handler AFTER app is ready to prevent // "Cannot create BrowserWindow before app is ready" on macOS. app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createMainWindow(); } else { focusMainWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin' || isE2EMode) { app.quit(); } }); app.on('before-quit', (event) => { setQuitting(); const action = requestQuitLifecycleAction(quitLifecycleState); if (action === 'allow-quit') { return; } event.preventDefault(); if (action === 'cleanup-in-progress') { logger.debug('Quit requested while cleanup already in progress; waiting for shutdown task to finish'); return; } hostEventBus.closeAll(); codingProducts?.previewDataSession?.invalidate('main_shutdown'); hostApiServer?.close(); projectProgressSync?.stop(); backgroundLifecycle?.dispose(); releaseJobs?.dispose(); const stopAgentBrowserPromise = agentBrowser?.dispose().catch((err) => { logger.warn('agentBrowser.dispose() error during quit:', err); }) ?? Promise.resolve(); const closeImageWorkspacePromise = imageWorkspaceModule?.closeEventSessions?.().catch((err) => { logger.warn('imageWorkspace.closeEventSessions() error during quit:', err); }) ?? Promise.resolve(); const stopCodingProductsPromise = codingProducts?.shutdown().catch((err) => { logger.warn('codingProducts.shutdown() error during quit:', err); }) ?? Promise.resolve(); const stopPromise = Promise.allSettled([ stopAgentBrowserPromise, closeImageWorkspacePromise, stopCodingProductsPromise, ]); const timeoutPromise = new Promise<'timeout'>((resolve) => { setTimeout(() => resolve('timeout'), 5000); }); void Promise.race([stopPromise.then(() => 'stopped' as const), timeoutPromise]).then((result) => { if (result === 'timeout') { logger.warn('runtime shutdown timed out during app quit; proceeding with forced quit'); } markQuitCleanupCompleted(quitLifecycleState); app.quit(); }); }); // Best-effort runtime cleanup on unexpected crashes. const emergencyRuntimeCleanup = (reason: string, error: unknown): void => { logger.error(`${reason}:`, error); projectProgressSync?.stop(); codingProducts?.previewDataSession?.invalidate('main_shutdown'); try { void agentBrowser?.dispose().catch(() => { /* ignore */ }); } catch { // ignore — dispose() may not be callable if state is corrupted } try { void codingProducts?.shutdown().catch(() => { /* ignore */ }); } catch { // ignore — shutdown() may not be callable if state is corrupted } // Give runtime stop a brief window, then force-exit. setTimeout(() => { process.exit(1); }, 3000).unref(); }; process.on('uncaughtException', (error) => { emergencyRuntimeCleanup('Uncaught exception in main process', error); }); process.on('unhandledRejection', (reason) => { emergencyRuntimeCleanup('Unhandled promise rejection in main process', reason); }); } // Export for testing export { mainWindow, codingProjectStore }; export async function runLocalPreviewPreflightE2E( scenario: 'success' | 'external' = 'success', ): Promise<{ ok: boolean; externalRequests: number; code?: string }> { if (!isE2EMode) throw new Error('E2E local preview preflight is unavailable'); const initialization = applicationInitialization; if (!initialization) { throw new Error('E2E local preview preflight is not ready'); } await initialization; const browser = agentBrowser; if (!browser) { throw new Error('E2E local preview preflight is not ready'); } let externalRequests = 0; const externalServer = createServer((_request, response) => { externalRequests += 1; response.end('blocked'); }); try { await new Promise((resolveListen, reject) => { externalServer.once('error', reject); externalServer.listen(0, '127.0.0.1', resolveListen); }); const address = externalServer.address(); if (!address || typeof address === 'string') throw new Error('E2E external server unavailable'); const externalScript = scenario === 'external' ? `` : ''; const artifact = createStaticArtifactSnapshot([{ path: 'index.html', bytes: Buffer.from(`
Playable
${externalScript}`), }]); try { await browser.preflightStaticArtifact(artifact); return { ok: true, externalRequests }; } catch (error) { return { ok: false, externalRequests, code: error && typeof error === 'object' && 'code' in error ? String(error.code) : 'PUBLISH_PREFLIGHT_UNAVAILABLE', }; } } finally { await new Promise((resolveClose) => externalServer.close(() => resolveClose())); } } type PiReleaseProofAction = | 'extension' | 'proxy.start' | 'proxy.status' | 'proxy.release-child' | 'proxy.finish' | 'resilience.start' | 'resilience.status' | 'resilience.arm-compact-delay' | 'resilience.arm-prompt-delay' | 'resilience.idle-status' | 'resilience.restart-other' | 'resilience.dispose-target' | 'resilience.inject-exit' | 'resilience.inject-protocol' | 'resilience.abort-after-exit' | 'resilience.release-parents' | 'resilience.finish' | 'pressure.start' | 'pressure.finish' | 'pressure.finish.inject-failure'; export async function runPiReleaseProofE2E(action: PiReleaseProofAction) { if (!isE2EMode) throw new Error('PI release proof is unavailable'); const appPath = app.getAppPath(); const packagedMain = { isPackaged: app.isPackaged, appPath, appPathUsesAsar: app.isPackaged && /[\\/]app\.asar$/i.test(appPath), }; if (action === 'extension') { return { action, packagedMain, extension: await runFinalAsarExtensionProof() }; } if (action === 'proxy.start') { if (!codingProducts) throw new Error('Packaged Main coding composition is unavailable'); const address = hostApiServer?.address(); if (!address || typeof address === 'string') throw new Error('Packaged Main Host API is unavailable'); return { action, packagedMain, proxy: await startFinalAsarProxyCompositionProof({ composition: codingProducts, projectPath: join(app.getPath('userData'), 'pi-proxy-proof-project'), hostProxyBaseUrl: `http://127.0.0.1:${address.port}/api/ai-proxy/v1`, hostToken: getHostApiToken(), }), }; } if (action === 'proxy.status') { return { action, packagedMain, proxy: await getFinalAsarProxyCompositionStatus() }; } if (action === 'proxy.release-child') { releaseFinalAsarProxyCompositionChild(); return { action, packagedMain, proxy: { released: 'child' } }; } if (action === 'proxy.finish') { return { action, packagedMain, proxy: await finishFinalAsarProxyCompositionProof() }; } if (action === 'resilience.start') { if (!codingProducts) throw new Error('Packaged Main coding composition is unavailable'); const address = hostApiServer?.address(); if (!address || typeof address === 'string') throw new Error('Packaged Main Host API is unavailable'); return { action, packagedMain, resilience: await startFinalAsarResilienceProof({ composition: codingProducts, projectPath: join(app.getPath('userData'), 'pi-resilience-proof-project'), userDataDir: app.getPath('userData'), hostProxyBaseUrl: `http://127.0.0.1:${address.port}/api/ai-proxy/v1`, hostToken: getHostApiToken(), }), }; } if (action === 'resilience.status') { return { action, packagedMain, resilience: await getFinalAsarResilienceStatus() }; } if (action === 'resilience.arm-compact-delay') { return { action, packagedMain, resilience: armFinalAsarResilienceCompactDelay() }; } if (action === 'resilience.arm-prompt-delay') { return { action, packagedMain, resilience: armFinalAsarResiliencePromptDelay() }; } if (action === 'resilience.idle-status') { return { action, packagedMain, resilience: await getFinalAsarResilienceIdleStatus() }; } if (action === 'resilience.restart-other') { return { action, packagedMain, resilience: await restartFinalAsarResilienceOther() }; } if (action === 'resilience.dispose-target') { return { action, packagedMain, resilience: await disposeFinalAsarResilienceTarget() }; } if (action === 'resilience.inject-exit' || action === 'resilience.inject-protocol') { return { action, packagedMain, resilience: await injectFinalAsarResilienceFailure( action === 'resilience.inject-exit' ? 'unexpected_exit' : 'protocol_invalidation', ), }; } if (action === 'resilience.abort-after-exit') { return { action, packagedMain, resilience: await abortFinalAsarResilienceTarget() }; } if (action === 'resilience.release-parents') { releaseFinalAsarResilienceParents(); return { action, packagedMain, resilience: { released: 'parents' } }; } if (action === 'resilience.finish') { return { action, packagedMain, resilience: await finishFinalAsarResilienceProof() }; } if (action === 'pressure.start') { return { action, packagedMain, pressure: await startFinalAsarPressureProof() }; } return { action, packagedMain, pressure: await finishFinalAsarPressureProof( action === 'pressure.finish.inject-failure' ? { injectFailureAt: 'parents.settle' } : undefined, ), }; } if (isE2EMode) { (globalThis as typeof globalThis & { __niancodeRunLocalPreviewPreflightE2E?: typeof runLocalPreviewPreflightE2E; __niancodeRunPiReleaseProofE2E?: typeof runPiReleaseProofE2E; }).__niancodeRunLocalPreviewPreflightE2E = runLocalPreviewPreflightE2E; (globalThis as typeof globalThis & { __niancodeRunPiReleaseProofE2E?: typeof runPiReleaseProofE2E; }).__niancodeRunPiReleaseProofE2E = runPiReleaseProofE2E; }