@@ -801,6 +801,7 @@ export class RocketRideCLI {
801801 private connected : boolean = false ;
802802 private attempt : number = 0 ;
803803 private cancelled : boolean = false ;
804+ private signalShutdownPromise ?: Promise < never > ;
804805
805806 constructor ( ) {
806807 this . setupSignalHandlers ( ) ;
@@ -814,13 +815,53 @@ export class RocketRideCLI {
814815 return this . cancelled ;
815816 }
816817
818+ isShuttingDown ( ) : boolean {
819+ return this . signalShutdownPromise !== undefined ;
820+ }
821+
822+ // Resolves only when the signal handler calls process.exit, so any
823+ // command/run/main flow that awaits this will stand down and let the
824+ // signal handler own the exit code.
825+ awaitShutdown ( ) : Promise < never > {
826+ return this . signalShutdownPromise ?? new Promise < never > ( ( ) => { } ) ;
827+ }
828+
817829 private setupSignalHandlers ( ) : void {
818- // TODO: Enable proper signal handling
819- // const signalHandler = () => {
820- // this.cancel();
821- // };
822- // process.on('SIGINT', signalHandler);
823- // process.on('SIGTERM', signalHandler);
830+ const FORCE_EXIT_TIMEOUT_MS = 5000 ;
831+
832+ const signalHandler = async ( signal : string ) => {
833+ const exitCode = 128 + ( signal === 'SIGINT' ? 2 : 15 ) ;
834+
835+ if ( this . signalShutdownPromise ) {
836+ // Second signal: force exit immediately
837+ process . exit ( exitCode ) ;
838+ }
839+
840+ // Park a promise that never resolves; other flows await it to
841+ // stand down while the signal handler drives the exit.
842+ this . signalShutdownPromise = new Promise < never > ( ( ) => { } ) ;
843+
844+ this . cancel ( ) ;
845+
846+ // Force exit if cleanup hangs
847+ const forceExitTimer = setTimeout ( ( ) => {
848+ console . error ( `\nCleanup timed out after ${ FORCE_EXIT_TIMEOUT_MS } ms, forcing exit` ) ;
849+ process . exit ( exitCode ) ;
850+ } , FORCE_EXIT_TIMEOUT_MS ) ;
851+
852+ try {
853+ await this . cleanupClient ( ) ;
854+ } catch {
855+ // Ignore cleanup errors during signal handling
856+ } finally {
857+ clearTimeout ( forceExitTimer ) ;
858+ }
859+
860+ process . exit ( exitCode ) ;
861+ } ;
862+
863+ process . on ( 'SIGINT' , ( ) => signalHandler ( 'SIGINT' ) ) ;
864+ process . on ( 'SIGTERM' , ( ) => signalHandler ( 'SIGTERM' ) ) ;
824865 }
825866
826867 private createProgram ( ) : Command {
@@ -858,10 +899,14 @@ export class RocketRideCLI {
858899
859900 try {
860901 const exitCode = await this . cmdStart ( ) ;
861- process . exit ( exitCode ) ;
902+ if ( ! this . isCancelled ( ) ) {
903+ process . exit ( exitCode ) ;
904+ }
862905 } finally {
863- this . cancel ( ) ;
864- await this . cleanupClient ( ) ;
906+ if ( ! this . isCancelled ( ) ) {
907+ this . cancel ( ) ;
908+ await this . cleanupClient ( ) ;
909+ }
865910 }
866911 } ) ;
867912
@@ -896,10 +941,14 @@ export class RocketRideCLI {
896941
897942 try {
898943 const exitCode = await this . cmdUpload ( ) ;
899- process . exit ( exitCode ) ;
944+ if ( ! this . isCancelled ( ) ) {
945+ process . exit ( exitCode ) ;
946+ }
900947 } finally {
901- this . cancel ( ) ;
902- await this . cleanupClient ( ) ;
948+ if ( ! this . isCancelled ( ) ) {
949+ this . cancel ( ) ;
950+ await this . cleanupClient ( ) ;
951+ }
903952 }
904953 } ) ;
905954
@@ -925,10 +974,14 @@ export class RocketRideCLI {
925974
926975 try {
927976 const exitCode = await this . cmdStatus ( ) ;
928- process . exit ( exitCode ) ;
977+ if ( ! this . isCancelled ( ) ) {
978+ process . exit ( exitCode ) ;
979+ }
929980 } finally {
930- this . cancel ( ) ;
931- await this . cleanupClient ( ) ;
981+ if ( ! this . isCancelled ( ) ) {
982+ this . cancel ( ) ;
983+ await this . cleanupClient ( ) ;
984+ }
932985 }
933986 } ) ;
934987
@@ -954,10 +1007,14 @@ export class RocketRideCLI {
9541007
9551008 try {
9561009 const exitCode = await this . cmdStop ( ) ;
957- process . exit ( exitCode ) ;
1010+ if ( ! this . isCancelled ( ) ) {
1011+ process . exit ( exitCode ) ;
1012+ }
9581013 } finally {
959- this . cancel ( ) ;
960- await this . cleanupClient ( ) ;
1014+ if ( ! this . isCancelled ( ) ) {
1015+ this . cancel ( ) ;
1016+ await this . cleanupClient ( ) ;
1017+ }
9611018 }
9621019 } ) ;
9631020
@@ -1188,16 +1245,20 @@ export class RocketRideCLI {
11881245 }
11891246 }
11901247
1248+ private _cleanupPromise ?: Promise < void > ;
1249+
11911250 private async cleanupClient ( ) : Promise < void > {
1192- if ( this . client ) {
1193- try {
1194- await this . client . disconnect ( ) ;
1195- } catch {
1196- // Ignore cleanup errors
1197- } finally {
1198- this . client = undefined ;
1199- }
1251+ if ( this . _cleanupPromise ) {
1252+ return this . _cleanupPromise ;
12001253 }
1254+ const client = this . client ;
1255+ if ( ! client ) {
1256+ return;
1257+ }
1258+ this . client = undefined ;
1259+ this . _cleanupPromise = client . disconnect ( ) . catch ( ( ) => { } ) ;
1260+ await this . _cleanupPromise ;
1261+ this . _cleanupPromise = undefined ;
12011262 }
12021263
12031264 private loadPipelineConfig ( pipelineFile : string ) : PipelineConfig {
@@ -1693,8 +1754,15 @@ export class RocketRideCLI {
16931754 // Parse command line arguments - commander will handle command routing
16941755 try {
16951756 await program.parseAsync(process.argv);
1757+ if (this.isShuttingDown()) {
1758+ // Signal handler owns the exit; park until it calls process.exit.
1759+ await this.awaitShutdown();
1760+ }
16961761 return 0; // If we get here, a command was executed successfully
16971762 } catch (error) {
1763+ if (this.isShuttingDown()) {
1764+ await this.awaitShutdown();
1765+ }
16981766 if (error instanceof Error && error.message.includes('interrupted')) {
16991767 console.log('\nOperation interrupted by user');
17001768 return 1;
@@ -1721,11 +1789,18 @@ function formatError(e: Error): string {
17211789}
17221790
17231791export async function main(): Promise<void> {
1792+ const cli = new RocketRideCLI();
17241793 try {
1725- const cli = new RocketRideCLI();
17261794 const exitCode = await cli.run();
1795+ if (cli.isShuttingDown()) {
1796+ // Signal handler owns the exit code; never race it to process.exit.
1797+ await cli.awaitShutdown();
1798+ }
17271799 process.exit(exitCode);
17281800 } catch (error) {
1801+ if (cli.isShuttingDown()) {
1802+ await cli.awaitShutdown();
1803+ }
17291804 if (error instanceof Error && error.message.includes('interrupted')) {
17301805 console.log('\n\nOperation interrupted by user');
17311806 } else {
0 commit comments