11import type { Event , StackFrame } from '@sentry/types' ;
22import { logger } from '@sentry/utils' ;
3- import { fork } from 'child_process' ;
3+ import { spawn } from 'child_process' ;
44import * as inspector from 'inspector' ;
55
66import { addGlobalEventProcessor , captureEvent , flush } from '..' ;
@@ -98,28 +98,44 @@ function sendEvent(blockedMs: number, frames?: StackFrame[]): void {
9898 } ) ;
9999}
100100
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+
101119function startChildProcess ( options : Options ) : void {
102- function log ( message : string , err ? : unknown ) : void {
120+ function log ( message : string , ... args : unknown [ ] ) : void {
103121 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 ) ;
109123 }
110124 }
111125
112126 try {
113127 const env = { ...process . env } ;
128+ env . SENTRY_ANR_CHILD_PROCESS = 'true' ;
114129
115130 if ( options . captureStackTrace ) {
116- inspector . open ( ) ;
117- env . SENTRY_INSPECT_URL = inspector . url ( ) ;
131+ env . SENTRY_INSPECT_URL = startInspector ( ) ;
118132 }
119133
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 ] , {
121137 env,
122- stdio : options . debug ? 'inherit' : 'ignore' ,
138+ stdio : options . debug ? [ 'inherit' , 'inherit' , 'inherit' , 'ipc' ] : [ 'ignore' , 'ignore' , 'ignore' , 'ipc' ] ,
123139 } ) ;
124140 // The child process should not keep the main process alive
125141 child . unref ( ) ;
@@ -133,14 +149,16 @@ function startChildProcess(options: Options): void {
133149 }
134150 } , options . pollInterval ) ;
135151
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+ } ;
139157 } ;
140158
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' ) ) ;
144162 } catch ( e ) {
145163 log ( 'Failed to start child process' , e ) ;
146164 }
@@ -153,6 +171,8 @@ function handleChildProcess(options: Options): void {
153171 }
154172 }
155173
174+ process . title = 'sentry-anr' ;
175+
156176 log ( 'Started' ) ;
157177
158178 addGlobalEventProcessor ( event => {
@@ -197,6 +217,13 @@ function handleChildProcess(options: Options): void {
197217 } ) ;
198218}
199219
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+
200227/**
201228 * **Note** This feature is still in beta so there may be breaking changes in future releases.
202229 *
@@ -221,17 +248,19 @@ function handleChildProcess(options: Options): void {
221248 * ```
222249 */
223250export 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 ] ;
225254
226255 const anrOptions : Options = {
227- entryScript : options . entryScript || process . argv [ 1 ] ,
256+ entryScript,
228257 pollInterval : options . pollInterval || DEFAULT_INTERVAL ,
229258 anrThreshold : options . anrThreshold || DEFAULT_HANG_THRESHOLD ,
230259 captureStackTrace : ! ! options . captureStackTrace ,
231260 debug : ! ! options . debug ,
232261 } ;
233262
234- if ( isChildProcess ) {
263+ if ( isAnrChildProcess ( ) ) {
235264 handleChildProcess ( anrOptions ) ;
236265 // In the child process, the promise never resolves which stops the app code from running
237266 return new Promise < void > ( ( ) => {
0 commit comments