@@ -4,7 +4,7 @@ import { Runner, RunnerEvent, Options } from 'jest-editor-support';
44import { JestExtContext , WatchMode } from '../JestExt/types' ;
55import { extensionId } from '../appGlobals' ;
66import { Logging } from '../logging' ;
7- import { JestProcessInfo , JestProcessRequest , UserDataType } from './types' ;
7+ import { JestProcessInfo , JestProcessRequest , ProcessStatus , UserDataType } from './types' ;
88import { requestString } from './helper' ;
99import { toFilePath , removeSurroundingQuote , escapeRegExp , shellQuote } from '../helpers' ;
1010
@@ -23,20 +23,18 @@ interface RunnerTask {
2323 reject : ( reason : unknown ) => unknown ;
2424 runner : Runner ;
2525}
26- export type StopReason = 'on-demand' | 'process-end' ;
2726
2827let SEQ = 0 ;
2928
3029export class JestProcess implements JestProcessInfo {
31- static readonly stopHangTimeout = 500 ;
32-
3330 private task ?: RunnerTask ;
3431 private extContext : JestExtContext ;
3532 private logging : Logging ;
36- private _stopReason ?: StopReason ;
3733 public readonly id : string ;
3834 private desc : string ;
3935 public readonly request : JestProcessRequest ;
36+ public _status : ProcessStatus ;
37+ private autoStopTimer ?: NodeJS . Timeout ;
4038
4139 constructor (
4240 extContext : JestExtContext ,
@@ -48,10 +46,11 @@ export class JestProcess implements JestProcessInfo {
4846 this . logging = extContext . loggingFactory . create ( `JestProcess ${ request . type } ` ) ;
4947 this . id = `${ request . type } -${ SEQ ++ } ` ;
5048 this . desc = `id: ${ this . id } , request: ${ requestString ( request ) } ` ;
49+ this . _status = ProcessStatus . Pending ;
5150 }
5251
53- public get stopReason ( ) : StopReason | undefined {
54- return this . _stopReason ;
52+ public get status ( ) : ProcessStatus {
53+ return this . _status ;
5554 }
5655
5756 private get watchMode ( ) : WatchMode {
@@ -64,15 +63,39 @@ export class JestProcess implements JestProcessInfo {
6463 return WatchMode . None ;
6564 }
6665
66+ public get isWatchMode ( ) : boolean {
67+ return this . watchMode !== WatchMode . None ;
68+ }
69+
6770 public toString ( ) : string {
68- return `JestProcess: ${ this . desc } ; stopReason: ${ this . stopReason } ` ;
71+ return `JestProcess: ${ this . desc } ; status: " ${ this . status } " ` ;
6972 }
70- public start ( ) : Promise < void > {
71- this . _stopReason = undefined ;
72- return this . startRunner ( ) ;
73+
74+ /**
75+ * To prevent zombie process, this method will automatically stops the Jest process if it is running for too long. The process will be marked as "Cancelled" and stopped.
76+ * Warning: This should only be called when you are certain the process should end soon, for example a non-watch mode process should end after the test results have been processed.
77+ * @param delay The delay in milliseconds after which the process will be considered hung and stopped. Default is 30000 milliseconds (30 seconds ).
78+ */
79+ public autoStop ( delay = 30000 , onStop ?: ( process : JestProcessInfo ) => void ) : void {
80+ if ( this . status === ProcessStatus . Running ) {
81+ if ( this . autoStopTimer ) {
82+ clearTimeout ( this . autoStopTimer ) ;
83+ }
84+ this . autoStopTimer = setTimeout ( ( ) => {
85+ if ( this . status === ProcessStatus . Running ) {
86+ console . warn (
87+ `Jest Process "${ this . id } ": will be force closed due to the autoStop Timer (${ delay } msec) `
88+ ) ;
89+ this . stop ( ) ;
90+ onStop ?.( this ) ;
91+ }
92+ } , delay ) ;
93+ }
7394 }
95+
7496 public stop ( ) : Promise < void > {
75- this . _stopReason = 'on-demand' ;
97+ this . _status = ProcessStatus . Cancelled ;
98+
7699 if ( ! this . task ) {
77100 this . logging ( 'debug' , 'nothing to stop, no pending runner/promise' ) ;
78101 this . taskDone ( ) ;
@@ -99,12 +122,19 @@ export class JestProcess implements JestProcessInfo {
99122 return `"${ removeSurroundingQuote ( aString ) } "` ;
100123 }
101124
102- private startRunner ( ) : Promise < void > {
125+ public start ( ) : Promise < void > {
126+ if ( this . status === ProcessStatus . Cancelled ) {
127+ this . logging ( 'warn' , `the runner task has been cancelled!` ) ;
128+ return Promise . resolve ( ) ;
129+ }
130+
103131 if ( this . task ) {
104132 this . logging ( 'warn' , `the runner task has already started!` ) ;
105133 return this . task . promise ;
106134 }
107135
136+ this . _status = ProcessStatus . Running ;
137+
108138 const options : Options = {
109139 noColor : false ,
110140 reporters : [ 'default' , `"${ this . getReporterPath ( ) } "` ] ,
@@ -196,7 +226,13 @@ export class JestProcess implements JestProcessInfo {
196226 if ( event === 'processClose' || event === 'processExit' ) {
197227 this . task ?. resolve ( ) ;
198228 this . task = undefined ;
199- this . _stopReason = this . _stopReason ?? 'process-end' ;
229+
230+ clearTimeout ( this . autoStopTimer ) ;
231+ this . autoStopTimer = undefined ;
232+
233+ if ( this . _status !== ProcessStatus . Cancelled ) {
234+ this . _status = ProcessStatus . Done ;
235+ }
200236 }
201237 this . request . listener . onEvent ( this , event , ...args ) ;
202238 }
0 commit comments