@@ -4,6 +4,7 @@ import log from 'electron-log'
44import fs from 'fs'
55import path from 'path'
66import * as net from "net" ;
7+ import * as http from "http" ;
78import { ipcMain , BrowserWindow , app } from 'electron'
89import { promisify } from 'util'
910import { detectInstallationLogs , PromiseReturnType } from "./install-deps" ;
@@ -195,45 +196,102 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
195196
196197
197198 let started = false ;
199+ let healthCheckInterval : NodeJS . Timeout | null = null ;
198200 const startTimeout = setTimeout ( ( ) => {
199201 if ( ! started ) {
202+ if ( healthCheckInterval ) clearInterval ( healthCheckInterval ) ;
200203 node_process . kill ( ) ;
201204 reject ( new Error ( 'Backend failed to start within timeout' ) ) ;
202205 }
203206 } , 30000 ) ; // 30 second timeout
204207
208+ // Helper function to poll health endpoint
209+ const pollHealthEndpoint = ( ) : void => {
210+ let attempts = 0 ;
211+ const maxAttempts = 20 ; // 5 seconds total (20 * 250ms)
212+ const intervalMs = 250 ;
213+
214+ healthCheckInterval = setInterval ( ( ) => {
215+ attempts ++ ;
216+ const healthUrl = `http://127.0.0.1:${ port } /health` ;
217+
218+ const req = http . get ( healthUrl , { timeout : 1000 } , ( res ) => {
219+ if ( res . statusCode === 200 ) {
220+ log . info ( `Backend health check passed after ${ attempts } attempts` ) ;
221+ started = true ;
222+ clearTimeout ( startTimeout ) ;
223+ if ( healthCheckInterval ) clearInterval ( healthCheckInterval ) ;
224+ resolve ( node_process ) ;
225+ } else {
226+ // Non-200 status (e.g., 404), continue polling unless max attempts reached
227+ if ( attempts >= maxAttempts ) {
228+ log . error ( `Backend health check failed after ${ attempts } attempts with status ${ res . statusCode } ` ) ;
229+ started = true ;
230+ clearTimeout ( startTimeout ) ;
231+ if ( healthCheckInterval ) clearInterval ( healthCheckInterval ) ;
232+ node_process . kill ( ) ;
233+ reject ( new Error ( `Backend health check failed: HTTP ${ res . statusCode } ` ) ) ;
234+ }
235+ }
236+ } ) ;
237+
238+ req . on ( 'error' , ( ) => {
239+ // Connection error - backend might not be ready yet, continue polling
240+ if ( attempts >= maxAttempts ) {
241+ log . error ( `Backend health check failed after ${ attempts } attempts: unable to connect` ) ;
242+ started = true ;
243+ clearTimeout ( startTimeout ) ;
244+ if ( healthCheckInterval ) clearInterval ( healthCheckInterval ) ;
245+ node_process . kill ( ) ;
246+ reject ( new Error ( 'Backend health check failed: unable to connect' ) ) ;
247+ }
248+ } ) ;
249+
250+ req . on ( 'timeout' , ( ) => {
251+ req . destroy ( ) ;
252+ if ( attempts >= maxAttempts ) {
253+ log . error ( `Backend health check timed out after ${ attempts } attempts` ) ;
254+ started = true ;
255+ clearTimeout ( startTimeout ) ;
256+ if ( healthCheckInterval ) clearInterval ( healthCheckInterval ) ;
257+ node_process . kill ( ) ;
258+ reject ( new Error ( 'Backend health check timed out' ) ) ;
259+ }
260+ } ) ;
261+ } , intervalMs ) ;
262+ } ;
205263
206264 node_process . stdout . on ( 'data' , ( data ) => {
207265 displayFilteredLogs ( data ) ;
208266 // check output content, judge if start success
209267 if ( ! started && data . toString ( ) . includes ( "Uvicorn running on" ) ) {
210- started = true ;
211- clearTimeout ( startTimeout ) ;
212- resolve ( node_process ) ;
268+ log . info ( 'Uvicorn startup detected, starting health check polling...' ) ;
269+ pollHealthEndpoint ( ) ;
213270 }
214271 } ) ;
215272
216273 node_process . stderr . on ( 'data' , ( data ) => {
217274 displayFilteredLogs ( data ) ;
218275
219276 if ( ! started && data . toString ( ) . includes ( "Uvicorn running on" ) ) {
220- started = true ;
221- clearTimeout ( startTimeout ) ;
222- resolve ( node_process ) ;
277+ log . info ( 'Uvicorn startup detected (stderr), starting health check polling...' ) ;
278+ pollHealthEndpoint ( ) ;
223279 }
224280
225281 // Check for port binding errors
226282 if ( data . toString ( ) . includes ( "Address already in use" ) ||
227283 data . toString ( ) . includes ( "bind() failed" ) ) {
228284 started = true ; // Prevent multiple rejections
229285 clearTimeout ( startTimeout ) ;
286+ if ( healthCheckInterval ) clearInterval ( healthCheckInterval ) ;
230287 node_process . kill ( ) ;
231288 reject ( new Error ( `Port ${ port } is already in use` ) ) ;
232289 }
233290 } ) ;
234291
235292 node_process . on ( 'close' , ( code ) => {
236293 clearTimeout ( startTimeout ) ;
294+ if ( healthCheckInterval ) clearInterval ( healthCheckInterval ) ;
237295 if ( ! started ) {
238296 reject ( new Error ( `fastapi exited with code ${ code } ` ) ) ;
239297 }
0 commit comments