1- import { execFile , spawn , type ChildProcess } from "node:child_process" ;
2- import { closeSync , existsSync , mkdirSync , openSync , realpathSync } from "node:fs" ;
1+ import { execFile , spawn , spawnSync , type ChildProcess } from "node:child_process" ;
2+ import {
3+ closeSync ,
4+ existsSync ,
5+ mkdirSync ,
6+ openSync ,
7+ readSync ,
8+ realpathSync ,
9+ statSync ,
10+ } from "node:fs" ;
311import { dirname , join , resolve } from "node:path" ;
412import { promisify } from "node:util" ;
513import { fileURLToPath } from "node:url" ;
@@ -19,7 +27,7 @@ import {
1927type StartupStep = "checking" ;
2028
2129const HEALTH_POLL_INTERVAL_MS = 500 ;
22- const GATEWAY_START_TIMEOUT_MS = 20_000 ;
30+ const GATEWAY_START_TIMEOUT_MS = 120_000 ;
2331
2432interface GatewayIdentity {
2533 root : string ;
@@ -267,6 +275,8 @@ async function launchGatewayProcess(request: GatewayLaunchRequest): Promise<stri
267275 if ( ! executable ) throw new Error ( t ( "gatewayMissingBinary" ) ) ;
268276 const targetUrl = stripTrailingSlash ( request . targetUrl ) ;
269277 let exited : { code : number | null ; signal : NodeJS . Signals | null } | undefined ;
278+ const stderrPath = gatewayAutostartLogPath ( request . instanceHome , "stderr" ) ;
279+ const stderrOffset = fileSize ( stderrPath ) ;
270280 const stdio = gatewayLogStdio ( request . instanceHome ) ;
271281 const child = spawn ( executable , [ ] , {
272282 detached : true ,
@@ -279,10 +289,16 @@ async function launchGatewayProcess(request: GatewayLaunchRequest): Promise<stri
279289 exited = { code, signal } ;
280290 } ) ;
281291 child . unref ( ) ;
282- const deadline = Date . now ( ) + GATEWAY_START_TIMEOUT_MS ;
292+ const deadline = Date . now ( ) + gatewayStartTimeoutMs ;
283293 while ( Date . now ( ) < deadline ) {
284294 if ( exited ) {
285- throw new Error ( `Gateway exited before becoming healthy (${ exitDescription ( exited ) } ).` ) ;
295+ throw new Error (
296+ gatewayLaunchError (
297+ `Gateway exited before becoming healthy (${ exitDescription ( exited ) } ).` ,
298+ stderrPath ,
299+ stderrOffset ,
300+ ) ,
301+ ) ;
286302 }
287303 const activeUrl = readActiveGatewayUrl ( request . instanceHome ) ;
288304 const candidateUrl = activeUrl ? stripTrailingSlash ( activeUrl ) : targetUrl ;
@@ -296,7 +312,7 @@ async function launchGatewayProcess(request: GatewayLaunchRequest): Promise<stri
296312 await delay ( HEALTH_POLL_INTERVAL_MS ) ;
297313 }
298314 stopUnreadyChild ( child ) ;
299- throw new Error ( t ( "gatewayStartTimeout" ) ) ;
315+ throw new Error ( gatewayLaunchError ( t ( "gatewayStartTimeout" ) , stderrPath , stderrOffset ) ) ;
300316}
301317
302318function gatewayProcessEnv ( request : GatewayLaunchRequest ) : NodeJS . ProcessEnv {
@@ -340,19 +356,64 @@ function exitDescription(exit: { code: number | null; signal: NodeJS.Signals | n
340356
341357function stopUnreadyChild ( child : ChildProcess ) : void {
342358 try {
343- if ( child . exitCode === null && child . signalCode === null ) child . kill ( ) ;
359+ if ( child . exitCode !== null || child . signalCode !== null ) return ;
360+ if ( process . platform === "win32" && child . pid ) {
361+ const result = spawnSync ( "taskkill.exe" , [ "/PID" , String ( child . pid ) , "/T" , "/F" ] , {
362+ stdio : "ignore" ,
363+ windowsHide : true ,
364+ } ) ;
365+ if ( ! result . error && result . status === 0 ) return ;
366+ }
367+ child . kill ( ) ;
344368 } catch {
345369 // Best effort: the gateway may have already detached or exited.
346370 }
347371}
348372
373+ function gatewayAutostartLogPath ( instanceHome : string , stream : "stdout" | "stderr" ) : string {
374+ return join ( instanceHome , ".tura" , "logs" , `gateway-autostart.${ stream } .log` ) ;
375+ }
376+
377+ function fileSize ( path : string ) : number {
378+ try {
379+ return statSync ( path ) . size ;
380+ } catch {
381+ return 0 ;
382+ }
383+ }
384+
385+ function gatewayLaunchError ( message : string , stderrPath : string , offset : number ) : string {
386+ try {
387+ const size = fileSize ( stderrPath ) ;
388+ if ( size <= offset ) return message ;
389+ const start = Math . max ( offset , size - 8_192 ) ;
390+ const buffer = Buffer . alloc ( size - start ) ;
391+ const file = openSync ( stderrPath , "r" ) ;
392+ try {
393+ readSync ( file , buffer , 0 , buffer . length , start ) ;
394+ } finally {
395+ closeSync ( file ) ;
396+ }
397+ const detail = buffer
398+ . toString ( "utf8" )
399+ . split ( / \r ? \n / u)
400+ . map ( ( line ) => line . trim ( ) )
401+ . filter ( Boolean )
402+ . slice ( - 8 )
403+ . join ( " | " ) ;
404+ return detail ? `${ message } Gateway stderr: ${ detail } ` : message ;
405+ } catch {
406+ return message ;
407+ }
408+ }
409+
349410function gatewayLogStdio ( instanceHome : string ) : [ "ignore" , number , number ] {
350411 const logDir = join ( instanceHome , ".tura" , "logs" ) ;
351412 mkdirSync ( logDir , { recursive : true } ) ;
352413 return [
353414 "ignore" ,
354- openSync ( join ( logDir , "gateway-autostart. stdout.log " ) , "a" ) ,
355- openSync ( join ( logDir , "gateway-autostart. stderr.log " ) , "a" ) ,
415+ openSync ( gatewayAutostartLogPath ( instanceHome , "stdout" ) , "a" ) ,
416+ openSync ( gatewayAutostartLogPath ( instanceHome , "stderr" ) , "a" ) ,
356417 ] ;
357418}
358419
0 commit comments