@@ -48,6 +48,9 @@ export default class Maestro {
4848
4949 private appId : number | undefined = undefined ;
5050 private detectedPlatform : 'Android' | 'iOS' | undefined = undefined ;
51+ private activeRunIds : number [ ] = [ ] ;
52+ private isShuttingDown = false ;
53+ private signalHandler : ( ( ) => void ) | null = null ;
5154
5255 public constructor ( credentials : Credentials , options : MaestroOptions ) {
5356 this . credentials = credentials ;
@@ -175,12 +178,22 @@ export default class Maestro {
175178 return { success : true , runs : [ ] } ;
176179 }
177180
181+ // Set up signal handlers before waiting for completion
182+ this . setupSignalHandlers ( ) ;
183+
178184 if ( ! this . options . quiet ) {
179185 logger . info ( 'Waiting for test results...' ) ;
180186 }
181187 const result = await this . waitForCompletion ( ) ;
188+
189+ // Clean up signal handlers
190+ this . removeSignalHandlers ( ) ;
191+
182192 return result ;
183193 } catch ( error ) {
194+ // Clean up signal handlers on error
195+ this . removeSignalHandlers ( ) ;
196+
184197 logger . error ( error instanceof Error ? error . message : error ) ;
185198 // Display the cause if available
186199 if ( error instanceof Error && error . cause ) {
@@ -522,8 +535,18 @@ export default class Maestro {
522535 const previousStatus : Map < number , MaestroRunInfo [ 'status' ] > = new Map ( ) ;
523536
524537 while ( attempts < this . MAX_POLL_ATTEMPTS ) {
538+ // Check if we're shutting down
539+ if ( this . isShuttingDown ) {
540+ throw new TestingBotError ( 'Test run cancelled by user' ) ;
541+ }
542+
525543 const status = await this . getStatus ( ) ;
526544
545+ // Track active run IDs for graceful shutdown
546+ this . activeRunIds = status . runs
547+ . filter ( ( run ) => run . status !== 'DONE' && run . status !== 'FAILED' )
548+ . map ( ( run ) => run . id ) ;
549+
527550 // Log current status of runs (unless quiet mode)
528551 if ( ! this . options . quiet ) {
529552 this . displayRunStatus ( status . runs , startTime , previousStatus ) ;
@@ -760,4 +783,92 @@ export default class Maestro {
760783
761784 return null ;
762785 }
786+
787+ private setupSignalHandlers ( ) : void {
788+ this . signalHandler = ( ) => {
789+ this . handleShutdown ( ) ;
790+ } ;
791+
792+ process . on ( 'SIGINT' , this . signalHandler ) ;
793+ process . on ( 'SIGTERM' , this . signalHandler ) ;
794+ }
795+
796+ private removeSignalHandlers ( ) : void {
797+ if ( this . signalHandler ) {
798+ process . removeListener ( 'SIGINT' , this . signalHandler ) ;
799+ process . removeListener ( 'SIGTERM' , this . signalHandler ) ;
800+ this . signalHandler = null ;
801+ }
802+ }
803+
804+ private handleShutdown ( ) : void {
805+ if ( this . isShuttingDown ) {
806+ // Already shutting down, force exit on second signal
807+ logger . warn ( 'Force exiting...' ) ;
808+ process . exit ( 1 ) ;
809+ }
810+
811+ this . isShuttingDown = true ;
812+ this . clearLine ( ) ;
813+ logger . warn ( 'Received interrupt signal, stopping test runs...' ) ;
814+
815+ // Stop all active runs
816+ this . stopActiveRuns ( )
817+ . then ( ( ) => {
818+ logger . info ( 'All test runs have been stopped.' ) ;
819+ process . exit ( 1 ) ;
820+ } )
821+ . catch ( ( error ) => {
822+ logger . error (
823+ `Failed to stop some test runs: ${ error instanceof Error ? error . message : error } ` ,
824+ ) ;
825+ process . exit ( 1 ) ;
826+ } ) ;
827+ }
828+
829+ private async stopActiveRuns ( ) : Promise < void > {
830+ if ( ! this . appId || this . activeRunIds . length === 0 ) {
831+ return ;
832+ }
833+
834+ const stopPromises = this . activeRunIds . map ( ( runId ) =>
835+ this . stopRun ( runId ) . catch ( ( error ) => {
836+ logger . error (
837+ `Failed to stop run ${ runId } : ${ error instanceof Error ? error . message : error } ` ,
838+ ) ;
839+ } ) ,
840+ ) ;
841+
842+ await Promise . all ( stopPromises ) ;
843+ }
844+
845+ private async stopRun ( runId : number ) : Promise < void > {
846+ if ( ! this . appId ) {
847+ return ;
848+ }
849+
850+ try {
851+ await axios . post (
852+ `${ this . URL } /${ this . appId } /${ runId } /stop` ,
853+ { } ,
854+ {
855+ headers : {
856+ 'User-Agent' : utils . getUserAgent ( ) ,
857+ } ,
858+ auth : {
859+ username : this . credentials . userName ,
860+ password : this . credentials . accessKey ,
861+ } ,
862+ } ,
863+ ) ;
864+
865+ if ( ! this . options . quiet ) {
866+ logger . info ( ` Stopped run ${ runId } ` ) ;
867+ }
868+ } catch ( error ) {
869+ throw new TestingBotError ( `Failed to stop run ${ runId } ` , {
870+ cause : error ,
871+ } ) ;
872+ }
873+ }
763874}
0 commit comments