@@ -7,6 +7,8 @@ import * as proc from 'child_process' // eslint-disable-line no-restricted-impor
7
7
import * as crossSpawn from 'cross-spawn'
8
8
import * as logger from '../logger'
9
9
import { Timeout , CancellationError , waitUntil } from './timeoutUtils'
10
+ import { PollingSet } from './pollingSet'
11
+ import { getLogger } from '../logger/logger'
10
12
11
13
export interface RunParameterContext {
12
14
/** Reports an error parsed from the stdin/stdout streams. */
@@ -61,14 +63,144 @@ export interface ChildProcessResult {
61
63
62
64
export const eof = Symbol ( 'EOF' )
63
65
66
+ export interface ProcessStats {
67
+ memory : number
68
+ cpu : number
69
+ }
70
+ export class ChildProcessTracker {
71
+ static readonly pollingInterval : number = 10000 // Check usage every 10 seconds
72
+ static readonly thresholds : ProcessStats = {
73
+ memory : 100 * 1024 * 1024 , // 100 MB
74
+ cpu : 50 ,
75
+ }
76
+ static readonly logger = getLogger ( 'childProcess' )
77
+ #processByPid: Map < number , ChildProcess > = new Map < number , ChildProcess > ( )
78
+ #pids: PollingSet < number >
79
+
80
+ public constructor ( ) {
81
+ this . #pids = new PollingSet ( ChildProcessTracker . pollingInterval , ( ) => this . monitor ( ) )
82
+ }
83
+
84
+ private cleanUp ( ) {
85
+ const terminatedProcesses = Array . from ( this . #pids. values ( ) ) . filter (
86
+ ( pid : number ) => this . #processByPid. get ( pid ) ?. stopped
87
+ )
88
+ for ( const pid of terminatedProcesses ) {
89
+ this . delete ( pid )
90
+ }
91
+ }
92
+
93
+ private async monitor ( ) {
94
+ this . cleanUp ( )
95
+ ChildProcessTracker . logger . debug ( `Active running processes size: ${ this . #pids. size } ` )
96
+
97
+ for ( const pid of this . #pids. values ( ) ) {
98
+ await this . checkProcessUsage ( pid )
99
+ }
100
+ }
101
+
102
+ private async checkProcessUsage ( pid : number ) : Promise < void > {
103
+ if ( ! this . #pids. has ( pid ) ) {
104
+ ChildProcessTracker . logger . warn ( `Missing process with id ${ pid } ` )
105
+ return
106
+ }
107
+ const stats = this . getUsage ( pid )
108
+ if ( stats ) {
109
+ ChildProcessTracker . logger . debug ( `Process ${ pid } usage: %O` , stats )
110
+ if ( stats . memory > ChildProcessTracker . thresholds . memory ) {
111
+ ChildProcessTracker . logger . warn ( `Process ${ pid } exceeded memory threshold: ${ stats . memory } ` )
112
+ }
113
+ if ( stats . cpu > ChildProcessTracker . thresholds . cpu ) {
114
+ ChildProcessTracker . logger . warn ( `Process ${ pid } exceeded cpu threshold: ${ stats . cpu } ` )
115
+ }
116
+ }
117
+ }
118
+
119
+ public add ( childProcess : ChildProcess ) {
120
+ const pid = childProcess . pid ( )
121
+ this . #processByPid. set ( pid , childProcess )
122
+ this . #pids. start ( pid )
123
+ }
124
+
125
+ public delete ( childProcessId : number ) {
126
+ this . #processByPid. delete ( childProcessId )
127
+ this . #pids. delete ( childProcessId )
128
+ }
129
+
130
+ public get size ( ) {
131
+ return this . #pids. size
132
+ }
133
+
134
+ public has ( childProcess : ChildProcess ) {
135
+ return this . #pids. has ( childProcess . pid ( ) )
136
+ }
137
+
138
+ public clear ( ) {
139
+ for ( const childProcess of this . #processByPid. values ( ) ) {
140
+ childProcess . stop ( true )
141
+ }
142
+ this . #pids. clear ( )
143
+ this . #processByPid. clear ( )
144
+ }
145
+
146
+ public getUsage ( pid : number ) : ProcessStats {
147
+ try {
148
+ // isWin() leads to circular dependency.
149
+ return process . platform === 'win32' ? getWindowsUsage ( ) : getUnixUsage ( )
150
+ } catch ( e ) {
151
+ ChildProcessTracker . logger . warn ( `Failed to get process stats for ${ pid } : ${ e } ` )
152
+ return { cpu : 0 , memory : 0 }
153
+ }
154
+
155
+ function getWindowsUsage ( ) {
156
+ const cpuOutput = proc
157
+ . execFileSync ( 'wmic' , [
158
+ 'path' ,
159
+ 'Win32_PerfFormattedData_PerfProc_Process' ,
160
+ 'where' ,
161
+ `IDProcess=${ pid } ` ,
162
+ 'get' ,
163
+ 'PercentProcessorTime' ,
164
+ ] )
165
+ . toString ( )
166
+ const memOutput = proc
167
+ . execFileSync ( 'wmic' , [ 'process' , 'where' , `ProcessId=${ pid } ` , 'get' , 'WorkingSetSize' ] )
168
+ . toString ( )
169
+
170
+ const cpuPercentage = parseFloat ( cpuOutput . split ( '\n' ) [ 1 ] )
171
+ const memoryBytes = parseInt ( memOutput . split ( '\n' ) [ 1 ] ) * 1024
172
+
173
+ return {
174
+ cpu : isNaN ( cpuPercentage ) ? 0 : cpuPercentage ,
175
+ memory : memoryBytes ,
176
+ }
177
+ }
178
+
179
+ function getUnixUsage ( ) {
180
+ const cpuMemOutput = proc . execFileSync ( 'ps' , [ '-p' , pid . toString ( ) , '-o' , '%cpu,%mem' ] ) . toString ( )
181
+ const rssOutput = proc . execFileSync ( 'ps' , [ '-p' , pid . toString ( ) , '-o' , 'rss' ] ) . toString ( )
182
+
183
+ const cpuMemLines = cpuMemOutput . split ( '\n' ) [ 1 ] . trim ( ) . split ( / \s + / )
184
+ const cpuPercentage = parseFloat ( cpuMemLines [ 0 ] )
185
+ const memoryBytes = parseInt ( rssOutput . split ( '\n' ) [ 1 ] ) * 1024
186
+
187
+ return {
188
+ cpu : isNaN ( cpuPercentage ) ? 0 : cpuPercentage ,
189
+ memory : memoryBytes ,
190
+ }
191
+ }
192
+ }
193
+ }
194
+
64
195
/**
65
196
* Convenience class to manage a child process
66
197
* To use:
67
198
* - instantiate
68
199
* - call and await run to get the results (pass or fail)
69
200
*/
70
201
export class ChildProcess {
71
- static #runningProcesses: Map < number , ChildProcess > = new Map ( )
202
+ static #runningProcesses = new ChildProcessTracker ( )
203
+ static stopTimeout = 3000
72
204
#childProcess: proc . ChildProcess | undefined
73
205
#processErrors: Error [ ] = [ ]
74
206
#processResult: ChildProcessResult | undefined
@@ -285,7 +417,7 @@ export class ChildProcess {
285
417
child . kill ( signal )
286
418
287
419
if ( force === true ) {
288
- waitUntil ( async ( ) => this . stopped , { timeout : 3000 , interval : 200 , truthy : true } )
420
+ waitUntil ( async ( ) => this . stopped , { timeout : ChildProcess . stopTimeout , interval : 200 , truthy : true } )
289
421
. then ( ( stopped ) => {
290
422
if ( ! stopped ) {
291
423
child . kill ( 'SIGKILL' )
@@ -309,7 +441,7 @@ export class ChildProcess {
309
441
if ( pid === undefined ) {
310
442
return
311
443
}
312
- ChildProcess . #runningProcesses. set ( pid , this )
444
+ ChildProcess . #runningProcesses. add ( this )
313
445
314
446
const timeoutListener = options ?. timeout ?. token . onCancellationRequested ( ( { agent } ) => {
315
447
const message = agent === 'user' ? 'Cancelled: ' : 'Timed out: '
@@ -319,7 +451,7 @@ export class ChildProcess {
319
451
320
452
const dispose = ( ) => {
321
453
timeoutListener ?. dispose ( )
322
- ChildProcess . #runningProcesses. delete ( pid )
454
+ ChildProcess . #runningProcesses. delete ( this . pid ( ) )
323
455
}
324
456
325
457
process . on ( 'exit' , dispose )
0 commit comments