@@ -55,6 +55,7 @@ import { showDesktopConfirmDialog } from "./confirmDialog";
5555import { resolveDesktopServerExposure } from "./serverExposure" ;
5656import { syncShellEnvironment } from "./syncShellEnvironment" ;
5757import { getAutoUpdateDisabledReason , shouldBroadcastDownloadProgress } from "./updateState" ;
58+ import { ServerListeningDetector } from "./serverListeningDetector" ;
5859import {
5960 createInitialDesktopUpdateState ,
6061 reduceDesktopUpdateStateOnCheckFailure ,
@@ -144,6 +145,8 @@ let backendWsUrl = "";
144145let backendEndpointUrl : string | null = null ;
145146let backendAdvertisedHost : string | null = null ;
146147let backendReadinessAbortController : AbortController | null = null ;
148+ let backendInitialWindowOpenInFlight : Promise < void > | null = null ;
149+ let backendListeningDetector : ServerListeningDetector | null = null ;
147150let restartAttempt = 0 ;
148151let restartTimer : ReturnType < typeof setTimeout > | null = null ;
149152let isQuitting = false ;
@@ -362,13 +365,17 @@ function getSafeTheme(rawTheme: unknown): DesktopTheme | null {
362365 return null ;
363366}
364367
365- async function waitForBackendHttpReady ( baseUrl : string ) : Promise < void > {
368+ async function waitForBackendHttpReady (
369+ baseUrl : string ,
370+ options ?: Parameters < typeof waitForHttpReady > [ 1 ] ,
371+ ) : Promise < void > {
366372 cancelBackendReadinessWait ( ) ;
367373 const controller = new AbortController ( ) ;
368374 backendReadinessAbortController = controller ;
369375
370376 try {
371377 await waitForHttpReady ( baseUrl , {
378+ ...options ,
372379 signal : controller . signal ,
373380 } ) ;
374381 } finally {
@@ -383,6 +390,88 @@ function cancelBackendReadinessWait(): void {
383390 backendReadinessAbortController = null ;
384391}
385392
393+ async function waitForBackendWindowReady ( baseUrl : string ) : Promise < "listening" | "http" > {
394+ const httpReadyPromise = waitForBackendHttpReady ( baseUrl , {
395+ timeoutMs : 60_000 ,
396+ } ) ;
397+ const listeningPromise = backendListeningDetector ?. promise ;
398+
399+ if ( ! listeningPromise ) {
400+ await httpReadyPromise ;
401+ return "http" ;
402+ }
403+
404+ return await new Promise < "listening" | "http" > ( ( resolve , reject ) => {
405+ let settled = false ;
406+
407+ const settleResolve = ( source : "listening" | "http" ) => {
408+ if ( settled ) {
409+ return ;
410+ }
411+ settled = true ;
412+ if ( source === "listening" ) {
413+ cancelBackendReadinessWait ( ) ;
414+ }
415+ resolve ( source ) ;
416+ } ;
417+
418+ const settleReject = ( error : unknown ) => {
419+ if ( settled ) {
420+ return ;
421+ }
422+ settled = true ;
423+ reject ( error ) ;
424+ } ;
425+
426+ listeningPromise . then (
427+ ( ) => settleResolve ( "listening" ) ,
428+ ( error ) => settleReject ( error ) ,
429+ ) ;
430+ httpReadyPromise . then (
431+ ( ) => settleResolve ( "http" ) ,
432+ ( error ) => {
433+ if ( settled && isBackendReadinessAborted ( error ) ) {
434+ return ;
435+ }
436+ settleReject ( error ) ;
437+ } ,
438+ ) ;
439+ } ) ;
440+ }
441+
442+ function ensureInitialBackendWindowOpen ( ) : void {
443+ const existingWindow = mainWindow ?? BrowserWindow . getAllWindows ( ) [ 0 ] ?? null ;
444+ if ( isDevelopment || existingWindow !== null || backendInitialWindowOpenInFlight !== null ) {
445+ return ;
446+ }
447+
448+ const nextOpen = waitForBackendWindowReady ( backendHttpUrl )
449+ . then ( ( source ) => {
450+ writeDesktopLogHeader ( `bootstrap backend ready source=${ source } ` ) ;
451+ if ( mainWindow ?? BrowserWindow . getAllWindows ( ) [ 0 ] ) {
452+ return ;
453+ }
454+ mainWindow = createWindow ( ) ;
455+ writeDesktopLogHeader ( "bootstrap main window created" ) ;
456+ } )
457+ . catch ( ( error ) => {
458+ if ( isBackendReadinessAborted ( error ) ) {
459+ return ;
460+ }
461+ writeDesktopLogHeader (
462+ `bootstrap backend readiness warning message=${ formatErrorMessage ( error ) } ` ,
463+ ) ;
464+ console . warn ( "[desktop] backend readiness check timed out during packaged bootstrap" , error ) ;
465+ } )
466+ . finally ( ( ) => {
467+ if ( backendInitialWindowOpenInFlight === nextOpen ) {
468+ backendInitialWindowOpenInFlight = null ;
469+ }
470+ } ) ;
471+
472+ backendInitialWindowOpenInFlight = nextOpen ;
473+ }
474+
386475function writeDesktopStreamChunk (
387476 streamName : "stdout" | "stderr" ,
388477 chunk : unknown ,
@@ -460,14 +549,16 @@ function initializePackagedLogging(): void {
460549}
461550
462551function captureBackendOutput ( child : ChildProcess . ChildProcess ) : void {
463- if ( ! app . isPackaged || backendLogSink === null ) return ;
464- const writeChunk = ( chunk : unknown ) : void => {
465- if ( ! backendLogSink ) return ;
466- const buffer = Buffer . isBuffer ( chunk ) ? chunk : Buffer . from ( String ( chunk ) , "utf8" ) ;
467- backendLogSink . write ( buffer ) ;
552+ const attachStream = ( stream : NodeJS . ReadableStream | null | undefined ) : void => {
553+ stream ?. on ( "data" , ( chunk : unknown ) => {
554+ const buffer = Buffer . isBuffer ( chunk ) ? chunk : Buffer . from ( String ( chunk ) , "utf8" ) ;
555+ backendLogSink ?. write ( buffer ) ;
556+ backendListeningDetector ?. push ( buffer ) ;
557+ } ) ;
468558 } ;
469- child . stdout ?. on ( "data" , writeChunk ) ;
470- child . stderr ?. on ( "data" , writeChunk ) ;
559+
560+ attachStream ( child . stdout ) ;
561+ attachStream ( child . stderr ) ;
471562}
472563
473564initializePackagedLogging ( ) ;
@@ -1222,7 +1313,7 @@ function startBackend(): void {
12221313 return ;
12231314 }
12241315
1225- const captureBackendLogs = app . isPackaged && backendLogSink !== null ;
1316+ const captureBackendLogs = ! isDevelopment ;
12261317 const child = ChildProcess . spawn ( process . execPath , [ backendEntry , "--bootstrap-fd" , "3" ] , {
12271318 cwd : resolveBackendCwd ( ) ,
12281319 // In Electron main, process.execPath points to the Electron binary.
@@ -1259,6 +1350,8 @@ function startBackend(): void {
12591350 scheduleBackendRestart ( "missing desktop bootstrap pipe" ) ;
12601351 return ;
12611352 }
1353+ const listeningDetector = new ServerListeningDetector ( ) ;
1354+ backendListeningDetector = listeningDetector ;
12621355 backendProcess = child ;
12631356 let backendSessionClosed = false ;
12641357 const closeBackendSession = ( details : string ) => {
@@ -1277,6 +1370,10 @@ function startBackend(): void {
12771370 } ) ;
12781371
12791372 child . on ( "error" , ( error ) => {
1373+ if ( backendListeningDetector === listeningDetector ) {
1374+ listeningDetector . fail ( error ) ;
1375+ backendListeningDetector = null ;
1376+ }
12801377 const wasExpected = expectedBackendExitChildren . has ( child ) ;
12811378 if ( backendProcess === child ) {
12821379 backendProcess = null ;
@@ -1289,6 +1386,14 @@ function startBackend(): void {
12891386 } ) ;
12901387
12911388 child . on ( "exit" , ( code , signal ) => {
1389+ if ( backendListeningDetector === listeningDetector ) {
1390+ listeningDetector . fail (
1391+ new Error (
1392+ `backend exited before logging readiness (code=${ code ?? "null" } signal=${ signal ?? "null" } )` ,
1393+ ) ,
1394+ ) ;
1395+ backendListeningDetector = null ;
1396+ }
12921397 const wasExpected = expectedBackendExitChildren . has ( child ) ;
12931398 if ( backendProcess === child ) {
12941399 backendProcess = null ;
@@ -1300,10 +1405,13 @@ function startBackend(): void {
13001405 const reason = `code=${ code ?? "null" } signal=${ signal ?? "null" } ` ;
13011406 scheduleBackendRestart ( reason ) ;
13021407 } ) ;
1408+
1409+ ensureInitialBackendWindowOpen ( ) ;
13031410}
13041411
13051412function stopBackend ( ) : void {
13061413 cancelBackendReadinessWait ( ) ;
1414+ backendListeningDetector = null ;
13071415 if ( restartTimer ) {
13081416 clearTimeout ( restartTimer ) ;
13091417 restartTimer = null ;
@@ -1705,7 +1813,7 @@ function createWindow(): BrowserWindow {
17051813 height : 780 ,
17061814 minWidth : 840 ,
17071815 minHeight : 620 ,
1708- show : isDevelopment ,
1816+ show : false ,
17091817 autoHideMenuBar : true ,
17101818 backgroundColor : getInitialWindowBackgroundColor ( ) ,
17111819 ...getIconOption ( ) ,
@@ -1779,20 +1887,23 @@ function createWindow(): BrowserWindow {
17791887 window . setTitle ( APP_DISPLAY_NAME ) ;
17801888 emitUpdateState ( ) ;
17811889 } ) ;
1782- if ( ! isDevelopment ) {
1783- window . once ( "ready-to-show" , ( ) => {
1784- revealWindow ( window ) ;
1785- } ) ;
1786- }
1890+
1891+ let initialRevealScheduled = false ;
1892+ const revealInitialWindow = ( ) => {
1893+ if ( initialRevealScheduled ) {
1894+ return ;
1895+ }
1896+ initialRevealScheduled = true ;
1897+ revealWindow ( window ) ;
1898+ } ;
1899+
1900+ window . once ( "ready-to-show" , revealInitialWindow ) ;
17871901
17881902 if ( isDevelopment ) {
17891903 void window . loadURL ( resolveDesktopDevServerUrl ( ) ) ;
17901904 window . webContents . openDevTools ( { mode : "detach" } ) ;
1791- setImmediate ( ( ) => {
1792- revealWindow ( window ) ;
1793- } ) ;
17941905 } else {
1795- void window . loadURL ( resolveDesktopWindowUrl ( ) ) ;
1906+ void window . loadURL ( backendHttpUrl ) ;
17961907 }
17971908
17981909 window . on ( "closed" , ( ) => {
@@ -1804,14 +1915,6 @@ function createWindow(): BrowserWindow {
18041915 return window ;
18051916}
18061917
1807- function resolveDesktopWindowUrl ( ) : string {
1808- if ( backendHttpUrl ) {
1809- return backendHttpUrl ;
1810- }
1811-
1812- return `${ DESKTOP_SCHEME } ://app` ;
1813- }
1814-
18151918// Override Electron's userData path before the `ready` event so that
18161919// Chromium session data uses a filesystem-friendly directory name.
18171920// Must be called synchronously at the top level — before `app.whenReady()`.
@@ -1885,10 +1988,7 @@ async function bootstrap(): Promise<void> {
18851988 return ;
18861989 }
18871990
1888- await waitForBackendHttpReady ( backendHttpUrl ) ;
1889- writeDesktopLogHeader ( "bootstrap backend ready" ) ;
1890- mainWindow = createWindow ( ) ;
1891- writeDesktopLogHeader ( "bootstrap main window created" ) ;
1991+ ensureInitialBackendWindowOpen ( ) ;
18921992}
18931993
18941994app . on ( "before-quit" , ( ) => {
@@ -1922,7 +2022,11 @@ app
19222022 revealWindow ( existingWindow ) ;
19232023 return ;
19242024 }
1925- mainWindow = createWindow ( ) ;
2025+ if ( isDevelopment ) {
2026+ mainWindow = createWindow ( ) ;
2027+ return ;
2028+ }
2029+ ensureInitialBackendWindowOpen ( ) ;
19262030 } ) ;
19272031 } )
19282032 . catch ( ( error ) => {
0 commit comments