@@ -148,6 +148,7 @@ export const COMMAND_STDERR_TAIL_CHARS = 256 * 1024;
148148export const COMMAND_FAILURE_STDOUT_TAIL_CHARS = 64 * 1024 ;
149149export const COMMAND_TIMEOUT_MS = 30 * 60 * 1000 ;
150150export const COMMAND_TIMEOUT_KILL_GRACE_MS = 5_000 ;
151+ const COMMAND_PROCESS_TREE_EXIT_POLL_MS = 25 ;
151152export const REMOTE_SETUP_COMMAND_TIMEOUT_MS = 90 * 60 * 1000 ;
152153const REMOTE_ROOT = "/tmp/openclaw-telegram-user-crabbox" ;
153154const CREDENTIAL_SCRIPT = fileURLToPath ( new URL ( "./telegram-user-credential.ts" , import . meta. url ) ) ;
@@ -608,6 +609,44 @@ function commandProcessTreeAlive(child: ChildProcess) {
608609 }
609610}
610611
612+ async function waitForCommandProcessTreeExit ( child : ChildProcess , timeoutMs : number ) {
613+ const deadlineAt = Date . now ( ) + timeoutMs ;
614+ while ( Date . now ( ) < deadlineAt ) {
615+ if ( ! commandProcessTreeAlive ( child ) ) {
616+ return true ;
617+ }
618+ await new Promise ( ( resolvePoll ) => {
619+ setTimeout ( resolvePoll , COMMAND_PROCESS_TREE_EXIT_POLL_MS ) ;
620+ } ) ;
621+ }
622+ return ! commandProcessTreeAlive ( child ) ;
623+ }
624+
625+ async function finishTimedOutCommandProcessTree (
626+ child : ChildProcess ,
627+ options : {
628+ forceKillAt : number | undefined ;
629+ timeoutKillGraceMs : number ;
630+ } ,
631+ ) {
632+ if ( ! commandProcessTreeAlive ( child ) ) {
633+ activeCommandChildren . delete ( child ) ;
634+ return ;
635+ }
636+ const graceRemainingMs =
637+ options . forceKillAt === undefined
638+ ? options . timeoutKillGraceMs
639+ : Math . max ( 0 , options . forceKillAt - Date . now ( ) ) ;
640+ if ( graceRemainingMs > 0 ) {
641+ await waitForCommandProcessTreeExit ( child , graceRemainingMs ) ;
642+ }
643+ if ( commandProcessTreeAlive ( child ) ) {
644+ signalCommandTree ( child , "SIGKILL" ) ;
645+ await waitForCommandProcessTreeExit ( child , options . timeoutKillGraceMs ) ;
646+ }
647+ activeCommandChildren . delete ( child ) ;
648+ }
649+
611650function untrackCommandChild ( child : ChildProcess ) {
612651 if ( ! commandProcessTreeAlive ( child ) ) {
613652 activeCommandChildren . delete ( child ) ;
@@ -664,6 +703,7 @@ export function runCommand(params: {
664703 let settled = false ;
665704 let stdoutLimitError : string | null = null ;
666705 let timeoutError : Error | null = null ;
706+ let forceKillAt : number | undefined ;
667707 let killTimer : NodeJS . Timeout | undefined ;
668708 const timeoutMs = params . timeoutMs ?? COMMAND_TIMEOUT_MS ;
669709 const timeoutKillGraceMs = params . timeoutKillGraceMs ?? COMMAND_TIMEOUT_KILL_GRACE_MS ;
@@ -684,6 +724,7 @@ export function runCommand(params: {
684724 ) } `,
685725 ) ;
686726 signalCommandTree ( child , "SIGTERM" ) ;
727+ forceKillAt = Date . now ( ) + timeoutKillGraceMs ;
687728 killTimer = setTimeout ( ( ) => {
688729 signalCommandTree ( child , "SIGKILL" ) ;
689730 } , timeoutKillGraceMs ) ;
@@ -736,9 +777,16 @@ export function runCommand(params: {
736777 settled = true ;
737778 untrackCommandChild ( child ) ;
738779 if ( timeoutError ) {
739- signalCommandTree ( child , "SIGKILL" ) ;
780+ const error = timeoutError ;
740781 clearTimers ( ) ;
741- reject ( timeoutError ) ;
782+ void finishTimedOutCommandProcessTree ( child , {
783+ forceKillAt,
784+ timeoutKillGraceMs,
785+ } ) . then (
786+ ( ) => reject ( error ) ,
787+ ( cleanupError : unknown ) =>
788+ reject ( cleanupError instanceof Error ? cleanupError : new Error ( String ( cleanupError ) ) ) ,
789+ ) ;
742790 return ;
743791 }
744792 clearTimers ( ) ;
0 commit comments