@@ -31,6 +31,7 @@ const DEFAULT_MAX_COMMAND_RSS_MIB = 8192;
3131const DEFAULT_OUTPUT_CAPTURE_CHARS = 1024 * 1024 ;
3232const GATEWAY_TEARDOWN_GRACE_MS = 10000 ;
3333const GATEWAY_TEARDOWN_KILL_GRACE_MS = 2000 ;
34+ const COMMAND_PROCESS_TREE_EXIT_POLL_MS = 50 ;
3435const LOG_SCAN_CHUNK_BYTES = 64 * 1024 ;
3536const LOG_SCAN_MAX_LINE_CHARS = 16 * 1024 ;
3637const LOG_TAIL_BYTES = 256 * 1024 ;
@@ -380,49 +381,94 @@ export function runCommand(command, args, options = {}) {
380381 } ) ;
381382 child . on ( "close" , ( status , signal ) => {
382383 clearTimeout ( timer ) ;
383- clearTimeout ( forceKillTimer ) ;
384- void stopResourceSampling ( ) . then ( ( resourceSampleFailure ) => {
385- if ( ! timedOut && status === 0 ) {
386- if ( resourceSampleFailure ) {
387- reject ( resourceSampleFailure ) ;
384+ const finish = ( ) => {
385+ clearTimeout ( forceKillTimer ) ;
386+ void stopResourceSampling ( ) . then ( ( resourceSampleFailure ) => {
387+ if ( ! timedOut && status === 0 ) {
388+ if ( resourceSampleFailure ) {
389+ reject ( resourceSampleFailure ) ;
390+ return ;
391+ }
392+ resolve ( {
393+ stdout : stdout . text ,
394+ stderr : stderr . text ,
395+ stdoutTruncatedChars : stdout . truncatedChars ,
396+ stderrTruncatedChars : stderr . truncatedChars ,
397+ } ) ;
388398 return ;
389399 }
390- resolve ( {
391- stdout : stdout . text ,
392- stderr : stderr . text ,
393- stdoutTruncatedChars : stdout . truncatedChars ,
394- stderrTruncatedChars : stderr . truncatedChars ,
395- } ) ;
396- return ;
397- }
398- const detail = [
399- formatCapturedOutput ( "stdout" , stdout ) ,
400- formatCapturedOutput ( "stderr" , stderr ) ,
401- ]
402- . filter ( Boolean )
403- . join ( "\n" )
404- . trim ( ) ;
405- const failure = timedOut
406- ? `timed out after ${ timeoutMs } ms`
407- : `failed with ${ signal || status } ` ;
408- reject (
409- Object . assign (
410- new Error (
411- `${ command } ${ args . join ( " " ) } ${ failure } ${ detail ? `\n${ tailText ( detail ) } ` : "" } ` ,
400+ const detail = [
401+ formatCapturedOutput ( "stdout" , stdout ) ,
402+ formatCapturedOutput ( "stderr" , stderr ) ,
403+ ]
404+ . filter ( Boolean )
405+ . join ( "\n" )
406+ . trim ( ) ;
407+ const failure = timedOut
408+ ? `timed out after ${ timeoutMs } ms`
409+ : `failed with ${ signal || status } ` ;
410+ reject (
411+ Object . assign (
412+ new Error (
413+ `${ command } ${ args . join ( " " ) } ${ failure } ${ detail ? `\n${ tailText ( detail ) } ` : "" } ` ,
414+ ) ,
415+ {
416+ signal,
417+ status,
418+ stderr : stderr . text ,
419+ stdout : stdout . text ,
420+ } ,
412421 ) ,
413- {
414- signal,
415- status,
416- stderr : stderr . text ,
417- stdout : stdout . text ,
418- } ,
419- ) ,
420- ) ;
421- } ) ;
422+ ) ;
423+ } ) ;
424+ } ;
425+
426+ if ( timedOut ) {
427+ void finishTimedOutCommandProcessTree ( child , timeoutKillGraceMs ) . then ( finish , finish ) ;
428+ return ;
429+ }
430+
431+ finish ( ) ;
422432 } ) ;
423433 } ) ;
424434}
425435
436+ async function finishTimedOutCommandProcessTree ( child , timeoutKillGraceMs ) {
437+ if ( ! commandProcessTreeIsAlive ( child ) ) {
438+ return ;
439+ }
440+ signalProcessGroup ( child , "SIGKILL" ) ;
441+ await waitForCommandProcessTreeExit ( child , timeoutKillGraceMs ) ;
442+ }
443+
444+ async function waitForCommandProcessTreeExit ( child , timeoutMs ) {
445+ const deadlineAt = Date . now ( ) + timeoutMs ;
446+ while ( Date . now ( ) < deadlineAt ) {
447+ if ( ! commandProcessTreeIsAlive ( child ) ) {
448+ return true ;
449+ }
450+ await new Promise ( ( resolvePoll ) => {
451+ setTimeout ( resolvePoll , COMMAND_PROCESS_TREE_EXIT_POLL_MS ) ;
452+ } ) ;
453+ }
454+ return ! commandProcessTreeIsAlive ( child ) ;
455+ }
456+
457+ function commandProcessTreeIsAlive ( child ) {
458+ if ( process . platform === "win32" || typeof child . pid !== "number" ) {
459+ return ! hasChildExited ( child ) ;
460+ }
461+ try {
462+ process . kill ( - child . pid , 0 ) ;
463+ return true ;
464+ } catch ( error ) {
465+ if ( error ?. code === "EPERM" ) {
466+ return true ;
467+ }
468+ return false ;
469+ }
470+ }
471+
426472function signalProcessGroup ( child , signal ) {
427473 if ( process . platform !== "win32" && typeof child . pid === "number" ) {
428474 try {
0 commit comments