@@ -28,6 +28,38 @@ type TestChildParams = {
28
28
onOutput ?: ( ) => void ;
29
29
} ;
30
30
31
+ import childProcess from 'child_process' ;
32
+
33
+ type ProcessData = {
34
+ pid : number , // process ID
35
+ pgid : number , // process groupd ID
36
+ children : Set < ProcessData > , // direct children of the process
37
+ } ;
38
+
39
+ function buildProcessTreePosix ( pid : number ) : ProcessData {
40
+ const processTree = childProcess . spawnSync ( 'ps' , [ '-eo' , 'pid,pgid,ppid' ] ) ;
41
+ const lines = processTree . stdout . toString ( ) . trim ( ) . split ( '\n' ) ;
42
+
43
+ const pidToProcess = new Map < number , ProcessData > ( ) ;
44
+ const edges : { pid : number , ppid : number } [ ] = [ ] ;
45
+ for ( const line of lines ) {
46
+ const [ pid , pgid , ppid ] = line . trim ( ) . split ( / \s + / ) . map ( token => + token ) ;
47
+ // On linux, the very first line of `ps` is the header with "PID PGID PPID".
48
+ if ( isNaN ( pid ) || isNaN ( pgid ) || isNaN ( ppid ) )
49
+ continue ;
50
+ pidToProcess . set ( pid , { pid, pgid, children : new Set ( ) } ) ;
51
+ edges . push ( { pid, ppid } ) ;
52
+ }
53
+ for ( const { pid, ppid } of edges ) {
54
+ const parent = pidToProcess . get ( ppid ) ;
55
+ const child = pidToProcess . get ( pid ) ;
56
+ // On POSIX, certain processes might not have parent (e.g. PID=1 and occasionally PID=2).
57
+ if ( parent && child )
58
+ parent . children . add ( child ) ;
59
+ }
60
+ return pidToProcess . get ( pid ) ;
61
+ }
62
+
31
63
export class TestChildProcess {
32
64
params : TestChildParams ;
33
65
process : ChildProcess ;
@@ -72,7 +104,7 @@ export class TestChildProcess {
72
104
this . process . stderr . on ( 'data' , appendChunk ) ;
73
105
this . process . stdout . on ( 'data' , appendChunk ) ;
74
106
75
- const killProcessGroup = this . _killProcessGroup . bind ( this ) ;
107
+ const killProcessGroup = this . _killProcessTree . bind ( this , 'SIGKILL' ) ;
76
108
process . on ( 'exit' , killProcessGroup ) ;
77
109
this . exited = new Promise ( f => {
78
110
this . process . on ( 'exit' , ( exitCode , signal ) => f ( { exitCode, signal } ) ) ;
@@ -86,28 +118,49 @@ export class TestChildProcess {
86
118
return strippedOutput . split ( '\n' ) . filter ( line => line . startsWith ( '%%' ) ) . map ( line => line . substring ( 2 ) . trim ( ) ) ;
87
119
}
88
120
89
- async close ( ) {
90
- if ( this . process . kill ( 0 ) )
91
- this . _killProcessGroup ( 'SIGINT' ) ;
121
+ async kill ( signal : 'SIGINT' | 'SIGKILL' = 'SIGKILL' ) {
122
+ this . _killProcessTree ( signal ) ;
92
123
return this . exited ;
93
124
}
94
125
95
- async kill ( ) {
96
- if ( this . process . kill ( 0 ) )
97
- this . _killProcessGroup ( 'SIGKILL' ) ;
98
- return this . exited ;
99
- }
100
-
101
- private _killProcessGroup ( signal : 'SIGINT' | 'SIGKILL' ) {
126
+ private _killProcessTree ( signal : 'SIGINT' | 'SIGKILL' ) {
102
127
if ( ! this . process . pid || ! this . process . kill ( 0 ) )
103
128
return ;
104
- try {
105
- if ( process . platform === 'win32' )
129
+
130
+ // On Windows, we always call `taskkill` no matter signal.
131
+ if ( process . platform === 'win32' ) {
132
+ try {
106
133
execSync ( `taskkill /pid ${ this . process . pid } /T /F /FI "MEMUSAGE gt 0"` , { stdio : 'ignore' } ) ;
107
- else
108
- process . kill ( - this . process . pid , signal ) ;
109
- } catch ( e ) {
110
- // the process might have already stopped
134
+ } catch ( e ) {
135
+ // the process might have already stopped
136
+ }
137
+ return ;
138
+ }
139
+
140
+ // In case of POSIX and `SIGINT` signal, send it to the main process group only.
141
+ if ( signal === 'SIGINT' ) {
142
+ try {
143
+ process . kill ( - this . process . pid , 'SIGINT' ) ;
144
+ } catch ( e ) {
145
+ // the process might have already stopped
146
+ }
147
+ return ;
148
+ }
149
+
150
+ // In case of POSIX and `SIGKILL` signal, we should send it to all descendant process groups.
151
+ const rootProcess = buildProcessTreePosix ( this . process . pid ) ;
152
+ const descendantProcessGroups = ( function flatten ( processData : ProcessData , result : Set < number > = new Set ( ) ) {
153
+ // Process can nullify its own process group with `setpgid`. Use its PID instead.
154
+ result . add ( processData . pgid || processData . pid ) ;
155
+ processData . children . forEach ( child => flatten ( child , result ) ) ;
156
+ return result ;
157
+ } ) ( rootProcess ) ;
158
+ for ( const pgid of descendantProcessGroups ) {
159
+ try {
160
+ process . kill ( - pgid , 'SIGKILL' ) ;
161
+ } catch ( e ) {
162
+ // the process might have already stopped
163
+ }
111
164
}
112
165
}
113
166
@@ -150,16 +203,7 @@ export const commonFixtures: Fixtures<CommonFixtures, CommonWorkerFixtures> = {
150
203
processes . push ( process ) ;
151
204
return process ;
152
205
} ) ;
153
- await Promise . all ( processes . map ( async child => {
154
- await Promise . race ( [
155
- child . exited ,
156
- new Promise ( f => setTimeout ( f , 3_000 ) ) ,
157
- ] ) ;
158
- if ( child . process . kill ( 0 ) ) {
159
- await child . kill ( ) ;
160
- throw new Error ( `Process ${ child . params . command . join ( ' ' ) } is still running. Leaking process?\nOutput:${ child . output } ` ) ;
161
- }
162
- } ) ) ;
206
+ await Promise . all ( processes . map ( async child => child . kill ( ) ) ) ;
163
207
if ( testInfo . status !== 'passed' && testInfo . status !== 'skipped' && ! process . env . PWTEST_DEBUG ) {
164
208
for ( const process of processes ) {
165
209
console . log ( '====== ' + process . params . command . join ( ' ' ) ) ;
@@ -176,7 +220,7 @@ export const commonFixtures: Fixtures<CommonFixtures, CommonWorkerFixtures> = {
176
220
processes . push ( process ) ;
177
221
return process ;
178
222
} ) ;
179
- await Promise . all ( processes . map ( child => child . close ( ) ) ) ;
223
+ await Promise . all ( processes . map ( child => child . kill ( 'SIGINT' ) ) ) ;
180
224
} , { scope : 'worker' } ] ,
181
225
182
226
waitForPort : async ( { } , use ) => {
0 commit comments