@@ -4,14 +4,53 @@ import { LRUMap } from 'lru_map';
4
4
5
5
import type { NodeClientOptions } from '../types' ;
6
6
7
+ type Variables = Record < string , unknown > ;
8
+ type OnPauseEvent = InspectorNotification < Debugger . PausedEventDataType > ;
7
9
export interface DebugSession {
8
10
/** Configures and connects to the debug session */
9
- configureAndConnect (
10
- onPause : ( message : InspectorNotification < Debugger . PausedEventDataType > ) => void ,
11
- captureAll : boolean ,
12
- ) : void ;
11
+ configureAndConnect ( onPause : ( message : OnPauseEvent , complete : ( ) => void ) => void , captureAll : boolean ) : void ;
13
12
/** Gets local variables for an objectId */
14
- getLocalVariables ( objectId : string ) : Promise < Record < string , unknown > > ;
13
+ getLocalVariables ( objectId : string , callback : ( vars : Variables ) => void ) : void ;
14
+ }
15
+
16
+ type Next < T > = ( result : T ) => void ;
17
+ type Add < T > = ( fn : Next < T > ) => void ;
18
+ type CallbackWrapper < T > = { add : Add < T > ; next : Next < T > } ;
19
+
20
+ /** Creates a container for callbacks to be called sequentially */
21
+ export function createCallbackList < T > ( complete : Next < T > ) : CallbackWrapper < T > {
22
+ // A collection of callbacks to be executed last to first
23
+ let callbacks : Next < T > [ ] = [ ] ;
24
+
25
+ let completedCalled = false ;
26
+ function checkedComplete ( result : T ) : void {
27
+ callbacks = [ ] ;
28
+ if ( completedCalled ) {
29
+ return ;
30
+ }
31
+ completedCalled = true ;
32
+ complete ( result ) ;
33
+ }
34
+
35
+ // complete should be called last
36
+ callbacks . push ( checkedComplete ) ;
37
+
38
+ function add ( fn : Next < T > ) : void {
39
+ callbacks . push ( fn ) ;
40
+ }
41
+
42
+ function next ( result : T ) : void {
43
+ const popped = callbacks . pop ( ) || checkedComplete ;
44
+
45
+ try {
46
+ popped ( result ) ;
47
+ } catch ( _ ) {
48
+ // If there is an error, we still want to call the complete callback
49
+ checkedComplete ( result ) ;
50
+ }
51
+ }
52
+
53
+ return { add, next } ;
15
54
}
16
55
17
56
/**
@@ -41,86 +80,109 @@ class AsyncSession implements DebugSession {
41
80
were reported any more. We probably missed a place where we need to await the promise, too.
42
81
*/
43
82
44
- // Node can be build without inspector support so this can throw
83
+ // Node can be built without inspector support so this can throw
45
84
// eslint-disable-next-line @typescript-eslint/no-var-requires
46
85
const { Session } = require ( 'inspector' ) ;
47
86
this . _session = new Session ( ) ;
48
87
}
49
88
50
89
/** @inheritdoc */
51
- public configureAndConnect (
52
- onPause : ( message : InspectorNotification < Debugger . PausedEventDataType > ) => void ,
53
- captureAll : boolean ,
54
- ) : void {
90
+ public configureAndConnect ( onPause : ( event : OnPauseEvent , complete : ( ) => void ) => void , captureAll : boolean ) : void {
55
91
this . _session . connect ( ) ;
56
- this . _session . on ( 'Debugger.paused' , onPause ) ;
92
+
93
+ this . _session . on ( 'Debugger.paused' , event => {
94
+ onPause ( event , ( ) => {
95
+ // After the pause work is complete, resume execution or the exception context memory is leaked
96
+ this . _session . post ( 'Debugger.resume' ) ;
97
+ } ) ;
98
+ } ) ;
99
+
57
100
this . _session . post ( 'Debugger.enable' ) ;
58
- // We only want to pause on uncaught exceptions
59
101
this . _session . post ( 'Debugger.setPauseOnExceptions' , { state : captureAll ? 'all' : 'uncaught' } ) ;
60
102
}
61
103
62
104
/** @inheritdoc */
63
- public async getLocalVariables ( objectId : string ) : Promise < Record < string , unknown > > {
64
- const props = await this . _getProperties ( objectId ) ;
65
- const unrolled : Record < string , unknown > = { } ;
66
-
67
- for ( const prop of props ) {
68
- if ( prop ?. value ?. objectId && prop ?. value . className === 'Array' ) {
69
- unrolled [ prop . name ] = await this . _unrollArray ( prop . value . objectId ) ;
70
- } else if ( prop ?. value ?. objectId && prop ?. value ?. className === 'Object' ) {
71
- unrolled [ prop . name ] = await this . _unrollObject ( prop . value . objectId ) ;
72
- } else if ( prop ?. value ?. value || prop ?. value ?. description ) {
73
- unrolled [ prop . name ] = prop . value . value || `<${ prop . value . description } >` ;
105
+ public getLocalVariables ( objectId : string , complete : ( vars : Variables ) => void ) : void {
106
+ this . _getProperties ( objectId , props => {
107
+ const { add, next } = createCallbackList < Variables > ( complete ) ;
108
+
109
+ for ( const prop of props ) {
110
+ if ( prop ?. value ?. objectId && prop ?. value . className === 'Array' ) {
111
+ const id = prop . value . objectId ;
112
+ add ( vars => this . _unrollArray ( id , prop . name , vars , next ) ) ;
113
+ } else if ( prop ?. value ?. objectId && prop ?. value ?. className === 'Object' ) {
114
+ const id = prop . value . objectId ;
115
+ add ( vars => this . _unrollObject ( id , prop . name , vars , next ) ) ;
116
+ } else if ( prop ?. value ?. value || prop ?. value ?. description ) {
117
+ add ( vars => this . _unrollOther ( prop , vars , next ) ) ;
118
+ }
74
119
}
75
- }
76
120
77
- return unrolled ;
121
+ next ( { } ) ;
122
+ } ) ;
78
123
}
79
124
80
125
/**
81
126
* Gets all the PropertyDescriptors of an object
82
127
*/
83
- private _getProperties ( objectId : string ) : Promise < Runtime . PropertyDescriptor [ ] > {
84
- return new Promise ( ( resolve , reject ) => {
85
- this . _session . post (
86
- 'Runtime.getProperties' ,
87
- {
88
- objectId,
89
- ownProperties : true ,
90
- } ,
91
- ( err , params ) => {
92
- if ( err ) {
93
- reject ( err ) ;
94
- } else {
95
- resolve ( params . result ) ;
96
- }
97
- } ,
98
- ) ;
99
- } ) ;
128
+ private _getProperties ( objectId : string , next : ( result : Runtime . PropertyDescriptor [ ] ) => void ) : void {
129
+ this . _session . post (
130
+ 'Runtime.getProperties' ,
131
+ {
132
+ objectId,
133
+ ownProperties : true ,
134
+ } ,
135
+ ( err , params ) => {
136
+ if ( err ) {
137
+ next ( [ ] ) ;
138
+ } else {
139
+ next ( params . result ) ;
140
+ }
141
+ } ,
142
+ ) ;
100
143
}
101
144
102
145
/**
103
146
* Unrolls an array property
104
147
*/
105
- private async _unrollArray ( objectId : string ) : Promise < unknown > {
106
- const props = await this . _getProperties ( objectId ) ;
107
- return props
108
- . filter ( v => v . name !== 'length' && ! isNaN ( parseInt ( v . name , 10 ) ) )
109
- . sort ( ( a , b ) => parseInt ( a . name , 10 ) - parseInt ( b . name , 10 ) )
110
- . map ( v => v ?. value ?. value ) ;
148
+ private _unrollArray ( objectId : string , name : string , vars : Variables , next : ( vars : Variables ) => void ) : void {
149
+ this . _getProperties ( objectId , props => {
150
+ vars [ name ] = props
151
+ . filter ( v => v . name !== 'length' && ! isNaN ( parseInt ( v . name , 10 ) ) )
152
+ . sort ( ( a , b ) => parseInt ( a . name , 10 ) - parseInt ( b . name , 10 ) )
153
+ . map ( v => v ?. value ?. value ) ;
154
+
155
+ next ( vars ) ;
156
+ } ) ;
111
157
}
112
158
113
159
/**
114
160
* Unrolls an object property
115
161
*/
116
- private async _unrollObject ( objectId : string ) : Promise < Record < string , unknown > > {
117
- const props = await this . _getProperties ( objectId ) ;
118
- return props
119
- . map < [ string , unknown ] > ( v => [ v . name , v ?. value ?. value ] )
120
- . reduce ( ( obj , [ key , val ] ) => {
121
- obj [ key ] = val ;
122
- return obj ;
123
- } , { } as Record < string , unknown > ) ;
162
+ private _unrollObject ( objectId : string , name : string , vars : Variables , next : ( obj : Variables ) => void ) : void {
163
+ this . _getProperties ( objectId , props => {
164
+ vars [ name ] = props
165
+ . map < [ string , unknown ] > ( v => [ v . name , v ?. value ?. value ] )
166
+ . reduce ( ( obj , [ key , val ] ) => {
167
+ obj [ key ] = val ;
168
+ return obj ;
169
+ } , { } as Variables ) ;
170
+
171
+ next ( vars ) ;
172
+ } ) ;
173
+ }
174
+
175
+ /**
176
+ * Unrolls other properties
177
+ */
178
+ private _unrollOther ( prop : Runtime . PropertyDescriptor , vars : Variables , next : ( vars : Variables ) => void ) : void {
179
+ if ( prop ?. value ?. value ) {
180
+ vars [ prop . name ] = prop . value . value ;
181
+ } else if ( prop ?. value ?. description && prop ?. value ?. type !== 'function' ) {
182
+ vars [ prop . name ] = `<${ prop . value . description } >` ;
183
+ }
184
+
185
+ next ( vars ) ;
124
186
}
125
187
}
126
188
@@ -178,7 +240,7 @@ function hashFromStack(stackParser: StackParser, stack: string | undefined): str
178
240
179
241
export interface FrameVariables {
180
242
function : string ;
181
- vars ?: Record < string , unknown > ;
243
+ vars ?: Variables ;
182
244
}
183
245
184
246
/** There are no options yet. This allows them to be added later without breaking changes */
@@ -200,7 +262,7 @@ export class LocalVariables implements Integration {
200
262
201
263
public readonly name : string = LocalVariables . id ;
202
264
203
- private readonly _cachedFrames : LRUMap < string , Promise < FrameVariables [ ] > > = new LRUMap ( 20 ) ;
265
+ private readonly _cachedFrames : LRUMap < string , FrameVariables [ ] > = new LRUMap ( 20 ) ;
204
266
205
267
public constructor (
206
268
private readonly _options : Options = { } ,
@@ -221,7 +283,8 @@ export class LocalVariables implements Integration {
221
283
) : void {
222
284
if ( this . _session && clientOptions ?. includeLocalVariables ) {
223
285
this . _session . configureAndConnect (
224
- ev => this . _handlePaused ( clientOptions . stackParser , ev as InspectorNotification < PausedExceptionEvent > ) ,
286
+ ( ev , complete ) =>
287
+ this . _handlePaused ( clientOptions . stackParser , ev as InspectorNotification < PausedExceptionEvent > , complete ) ,
225
288
! ! this . _options . captureAllExceptions ,
226
289
) ;
227
290
@@ -232,47 +295,64 @@ export class LocalVariables implements Integration {
232
295
/**
233
296
* Handle the pause event
234
297
*/
235
- private async _handlePaused (
298
+ private _handlePaused (
236
299
stackParser : StackParser ,
237
300
{ params : { reason, data, callFrames } } : InspectorNotification < PausedExceptionEvent > ,
238
- ) : Promise < void > {
301
+ complete : ( ) => void ,
302
+ ) : void {
239
303
if ( reason !== 'exception' && reason !== 'promiseRejection' ) {
304
+ complete ( ) ;
240
305
return ;
241
306
}
242
307
243
308
// data.description contains the original error.stack
244
309
const exceptionHash = hashFromStack ( stackParser , data ?. description ) ;
245
310
246
311
if ( exceptionHash == undefined ) {
312
+ complete ( ) ;
247
313
return ;
248
314
}
249
315
250
- const framePromises = callFrames . map ( async ( { scopeChain, functionName, this : obj } ) => {
316
+ const { add, next } = createCallbackList < FrameVariables [ ] > ( frames => {
317
+ this . _cachedFrames . set ( exceptionHash , frames ) ;
318
+ complete ( ) ;
319
+ } ) ;
320
+
321
+ // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack
322
+ // For this reason we only attempt to get local variables for the first 5 frames
323
+ for ( let i = 0 ; i < Math . min ( callFrames . length , 5 ) ; i ++ ) {
324
+ const { scopeChain, functionName, this : obj } = callFrames [ i ] ;
325
+
251
326
const localScope = scopeChain . find ( scope => scope . type === 'local' ) ;
252
327
253
328
// obj.className is undefined in ESM modules
254
329
const fn = obj . className === 'global' || ! obj . className ? functionName : `${ obj . className } .${ functionName } ` ;
255
330
256
331
if ( localScope ?. object . objectId === undefined ) {
257
- return { function : fn } ;
332
+ add ( frames => {
333
+ frames [ i ] = { function : fn } ;
334
+ next ( frames ) ;
335
+ } ) ;
336
+ } else {
337
+ const id = localScope . object . objectId ;
338
+ add ( frames =>
339
+ this . _session ?. getLocalVariables ( id , vars => {
340
+ frames [ i ] = { function : fn , vars } ;
341
+ next ( frames ) ;
342
+ } ) ,
343
+ ) ;
258
344
}
345
+ }
259
346
260
- const vars = await this . _session ?. getLocalVariables ( localScope . object . objectId ) ;
261
-
262
- return { function : fn , vars } ;
263
- } ) ;
264
-
265
- // We add the un-awaited promise to the cache rather than await here otherwise the event processor
266
- // can be called before we're finished getting all the vars
267
- this . _cachedFrames . set ( exceptionHash , Promise . all ( framePromises ) ) ;
347
+ next ( [ ] ) ;
268
348
}
269
349
270
350
/**
271
351
* Adds local variables event stack frames.
272
352
*/
273
- private async _addLocalVariables ( event : Event ) : Promise < Event > {
353
+ private _addLocalVariables ( event : Event ) : Event {
274
354
for ( const exception of event ?. exception ?. values || [ ] ) {
275
- await this . _addLocalVariablesToException ( exception ) ;
355
+ this . _addLocalVariablesToException ( exception ) ;
276
356
}
277
357
278
358
return event ;
@@ -281,7 +361,7 @@ export class LocalVariables implements Integration {
281
361
/**
282
362
* Adds local variables to the exception stack frames.
283
363
*/
284
- private async _addLocalVariablesToException ( exception : Exception ) : Promise < void > {
364
+ private _addLocalVariablesToException ( exception : Exception ) : void {
285
365
const hash = hashFrames ( exception ?. stacktrace ?. frames ) ;
286
366
287
367
if ( hash === undefined ) {
@@ -290,7 +370,7 @@ export class LocalVariables implements Integration {
290
370
291
371
// Check if we have local variables for an exception that matches the hash
292
372
// delete is identical to get but also removes the entry from the cache
293
- const cachedFrames = await this . _cachedFrames . delete ( hash ) ;
373
+ const cachedFrames = this . _cachedFrames . delete ( hash ) ;
294
374
295
375
if ( cachedFrames === undefined ) {
296
376
return ;
0 commit comments