1
1
import type { Event , StackFrame } from '@sentry/types' ;
2
2
import { logger } from '@sentry/utils' ;
3
- import { fork } from 'child_process' ;
3
+ import { spawn } from 'child_process' ;
4
4
import * as inspector from 'inspector' ;
5
5
6
6
import { addGlobalEventProcessor , captureEvent , flush } from '..' ;
@@ -98,28 +98,44 @@ function sendEvent(blockedMs: number, frames?: StackFrame[]): void {
98
98
} ) ;
99
99
}
100
100
101
+ /**
102
+ * Starts the node debugger and returns the inspector url.
103
+ *
104
+ * When inspector.url() returns undefined, it means the port is already in use so we try the next port.
105
+ */
106
+ function startInspector ( startPort : number = 9229 ) : string | undefined {
107
+ let inspectorUrl : string | undefined = undefined ;
108
+ let port = startPort ;
109
+
110
+ while ( inspectorUrl === undefined && port < startPort + 100 ) {
111
+ inspector . open ( port ) ;
112
+ inspectorUrl = inspector . url ( ) ;
113
+ port ++ ;
114
+ }
115
+
116
+ return inspectorUrl ;
117
+ }
118
+
101
119
function startChildProcess ( options : Options ) : void {
102
- function log ( message : string , err ? : unknown ) : void {
120
+ function log ( message : string , ... args : unknown [ ] ) : void {
103
121
if ( options . debug ) {
104
- if ( err ) {
105
- logger . log ( `[ANR] ${ message } ` , err ) ;
106
- } else {
107
- logger . log ( `[ANR] ${ message } ` ) ;
108
- }
122
+ logger . log ( `[ANR] ${ message } ` , ...args ) ;
109
123
}
110
124
}
111
125
112
126
try {
113
127
const env = { ...process . env } ;
128
+ env . SENTRY_ANR_CHILD_PROCESS = 'true' ;
114
129
115
130
if ( options . captureStackTrace ) {
116
- inspector . open ( ) ;
117
- env . SENTRY_INSPECT_URL = inspector . url ( ) ;
131
+ env . SENTRY_INSPECT_URL = startInspector ( ) ;
118
132
}
119
133
120
- const child = fork ( options . entryScript , {
134
+ log ( `Spawning child process with execPath:'${ process . execPath } ' and entryScript'${ options . entryScript } '` ) ;
135
+
136
+ const child = spawn ( process . execPath , [ options . entryScript ] , {
121
137
env,
122
- stdio : options . debug ? 'inherit' : 'ignore' ,
138
+ stdio : options . debug ? [ 'inherit' , 'inherit' , 'inherit' , 'ipc' ] : [ 'ignore' , 'ignore' , 'ignore' , 'ipc' ] ,
123
139
} ) ;
124
140
// The child process should not keep the main process alive
125
141
child . unref ( ) ;
@@ -133,14 +149,16 @@ function startChildProcess(options: Options): void {
133
149
}
134
150
} , options . pollInterval ) ;
135
151
136
- const end = ( err : unknown ) : void => {
137
- clearInterval ( timer ) ;
138
- log ( 'Child process ended' , err ) ;
152
+ const end = ( type : string ) : ( ( ...args : unknown [ ] ) => void ) => {
153
+ return ( ...args ) : void => {
154
+ clearInterval ( timer ) ;
155
+ log ( `Child process ${ type } ` , ...args ) ;
156
+ } ;
139
157
} ;
140
158
141
- child . on ( 'error' , end ) ;
142
- child . on ( 'disconnect' , end ) ;
143
- child . on ( 'exit' , end ) ;
159
+ child . on ( 'error' , end ( 'error' ) ) ;
160
+ child . on ( 'disconnect' , end ( 'disconnect' ) ) ;
161
+ child . on ( 'exit' , end ( 'exit' ) ) ;
144
162
} catch ( e ) {
145
163
log ( 'Failed to start child process' , e ) ;
146
164
}
@@ -153,6 +171,8 @@ function handleChildProcess(options: Options): void {
153
171
}
154
172
}
155
173
174
+ process . title = 'sentry-anr' ;
175
+
156
176
log ( 'Started' ) ;
157
177
158
178
addGlobalEventProcessor ( event => {
@@ -197,6 +217,13 @@ function handleChildProcess(options: Options): void {
197
217
} ) ;
198
218
}
199
219
220
+ /**
221
+ * Returns true if the current process is an ANR child process.
222
+ */
223
+ export function isAnrChildProcess ( ) : boolean {
224
+ return ! ! process . send && ! ! process . env . SENTRY_ANR_CHILD_PROCESS ;
225
+ }
226
+
200
227
/**
201
228
* **Note** This feature is still in beta so there may be breaking changes in future releases.
202
229
*
@@ -221,17 +248,19 @@ function handleChildProcess(options: Options): void {
221
248
* ```
222
249
*/
223
250
export function enableAnrDetection ( options : Partial < Options > ) : Promise < void > {
224
- const isChildProcess = ! ! process . send ;
251
+ // When pm2 runs the script in cluster mode, process.argv[1] is the pm2 script and process.env.pm_exec_path is the
252
+ // path to the entry script
253
+ const entryScript = options . entryScript || process . env . pm_exec_path || process . argv [ 1 ] ;
225
254
226
255
const anrOptions : Options = {
227
- entryScript : options . entryScript || process . argv [ 1 ] ,
256
+ entryScript,
228
257
pollInterval : options . pollInterval || DEFAULT_INTERVAL ,
229
258
anrThreshold : options . anrThreshold || DEFAULT_HANG_THRESHOLD ,
230
259
captureStackTrace : ! ! options . captureStackTrace ,
231
260
debug : ! ! options . debug ,
232
261
} ;
233
262
234
- if ( isChildProcess ) {
263
+ if ( isAnrChildProcess ( ) ) {
235
264
handleChildProcess ( anrOptions ) ;
236
265
// In the child process, the promise never resolves which stops the app code from running
237
266
return new Promise < void > ( ( ) => {
0 commit comments