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 ;
@@ -271,6 +279,8 @@ async function launchGatewayProcess(request: GatewayLaunchRequest): Promise<stri
271279 if ( ! executable ) throw new Error ( t ( "gatewayMissingBinary" ) ) ;
272280 const targetUrl = stripTrailingSlash ( request . targetUrl ) ;
273281 let exited : { code : number | null ; signal : NodeJS . Signals | null } | undefined ;
282+ const stderrPath = gatewayAutostartLogPath ( request . instanceHome , "stderr" ) ;
283+ const stderrOffset = fileSize ( stderrPath ) ;
274284 const stdio = gatewayLogStdio ( request . instanceHome ) ;
275285 const child = spawn ( executable , [ ] , {
276286 detached : true ,
@@ -283,10 +293,16 @@ async function launchGatewayProcess(request: GatewayLaunchRequest): Promise<stri
283293 exited = { code, signal } ;
284294 } ) ;
285295 child . unref ( ) ;
286- const deadline = Date . now ( ) + GATEWAY_START_TIMEOUT_MS ;
296+ const deadline = Date . now ( ) + gatewayStartTimeoutMs ;
287297 while ( Date . now ( ) < deadline ) {
288298 if ( exited ) {
289- throw new Error ( `Gateway exited before becoming healthy (${ exitDescription ( exited ) } ).` ) ;
299+ throw new Error (
300+ gatewayLaunchError (
301+ `Gateway exited before becoming healthy (${ exitDescription ( exited ) } ).` ,
302+ stderrPath ,
303+ stderrOffset ,
304+ ) ,
305+ ) ;
290306 }
291307 const activeUrl = readActiveGatewayUrl ( request . instanceHome ) ;
292308 const candidateUrl = activeUrl ? stripTrailingSlash ( activeUrl ) : targetUrl ;
@@ -300,7 +316,7 @@ async function launchGatewayProcess(request: GatewayLaunchRequest): Promise<stri
300316 await delay ( HEALTH_POLL_INTERVAL_MS ) ;
301317 }
302318 stopUnreadyChild ( child ) ;
303- throw new Error ( t ( "gatewayStartTimeout" ) ) ;
319+ throw new Error ( gatewayLaunchError ( t ( "gatewayStartTimeout" ) , stderrPath , stderrOffset ) ) ;
304320}
305321
306322function gatewayProcessEnv ( request : GatewayLaunchRequest ) : NodeJS . ProcessEnv {
@@ -344,19 +360,64 @@ function exitDescription(exit: { code: number | null; signal: NodeJS.Signals | n
344360
345361function stopUnreadyChild ( child : ChildProcess ) : void {
346362 try {
347- if ( child . exitCode === null && child . signalCode === null ) child . kill ( ) ;
363+ if ( child . exitCode !== null || child . signalCode !== null ) return ;
364+ if ( process . platform === "win32" && child . pid ) {
365+ const result = spawnSync ( "taskkill.exe" , [ "/PID" , String ( child . pid ) , "/T" , "/F" ] , {
366+ stdio : "ignore" ,
367+ windowsHide : true ,
368+ } ) ;
369+ if ( ! result . error && result . status === 0 ) return ;
370+ }
371+ child . kill ( ) ;
348372 } catch {
349373 // Best effort: the gateway may have already detached or exited.
350374 }
351375}
352376
377+ function gatewayAutostartLogPath ( instanceHome : string , stream : "stdout" | "stderr" ) : string {
378+ return join ( instanceHome , ".tura" , "logs" , `gateway-autostart.${ stream } .log` ) ;
379+ }
380+
381+ function fileSize ( path : string ) : number {
382+ try {
383+ return statSync ( path ) . size ;
384+ } catch {
385+ return 0 ;
386+ }
387+ }
388+
389+ function gatewayLaunchError ( message : string , stderrPath : string , offset : number ) : string {
390+ try {
391+ const size = fileSize ( stderrPath ) ;
392+ if ( size <= offset ) return message ;
393+ const start = Math . max ( offset , size - 8_192 ) ;
394+ const buffer = Buffer . alloc ( size - start ) ;
395+ const file = openSync ( stderrPath , "r" ) ;
396+ try {
397+ readSync ( file , buffer , 0 , buffer . length , start ) ;
398+ } finally {
399+ closeSync ( file ) ;
400+ }
401+ const detail = buffer
402+ . toString ( "utf8" )
403+ . split ( / \r ? \n / u)
404+ . map ( ( line ) => line . trim ( ) )
405+ . filter ( Boolean )
406+ . slice ( - 8 )
407+ . join ( " | " ) ;
408+ return detail ? `${ message } Gateway stderr: ${ detail } ` : message ;
409+ } catch {
410+ return message ;
411+ }
412+ }
413+
353414function gatewayLogStdio ( instanceHome : string ) : [ "ignore" , number , number ] {
354415 const logDir = join ( instanceHome , ".tura" , "logs" ) ;
355416 mkdirSync ( logDir , { recursive : true } ) ;
356417 return [
357418 "ignore" ,
358- openSync ( join ( logDir , "gateway-autostart. stdout.log " ) , "a" ) ,
359- openSync ( join ( logDir , "gateway-autostart. stderr.log " ) , "a" ) ,
419+ openSync ( gatewayAutostartLogPath ( instanceHome , "stdout" ) , "a" ) ,
420+ openSync ( gatewayAutostartLogPath ( instanceHome , "stderr" ) , "a" ) ,
360421 ] ;
361422}
362423
0 commit comments