/
breakpointTool.js
662 lines (557 loc) · 24.1 KB
/
breakpointTool.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
/* See license.txt for terms of usage */
define([
"firebug/firebug",
"firebug/lib/trace",
"firebug/lib/object",
"firebug/chrome/tool",
"firebug/debugger/debuggerLib",
"firebug/debugger/breakpoints/breakpointStore",
"firebug/remoting/debuggerClient",
],
function (Firebug, FBTrace, Obj, Tool, DebuggerLib, BreakpointStore, DebuggerClient) {
// ********************************************************************************************* //
// Constants
var TraceError = FBTrace.toError();
var Trace = FBTrace.to("DBG_BREAKPOINTTOOL");
// ********************************************************************************************* //
// Breakpoint Tool
function BreakpointTool(context)
{
this.context = context;
}
/**
* @object BreakpointTool object is automatically instantiated by the framework for each
* context. The object represents a proxy to the backend and all communication related
* to breakpoints should be done through it.
*
* {@link BreakpointTool} (one instance per context) is also handling events coming from
* {@link BreakpointStore} (one instance per Firebug), performs async operation with the
* server side (using RDP) and forwards results to all registered listeners, which are
* usually panel objects.
*/
BreakpointTool.prototype = Obj.extend(new Tool(),
/** @lends BreakpointTool */
{
dispatchName: "breakpointTool",
// xxxHonza: do we really need this? The underlying framework already has a queue
// for 'setBreakpoint' packets.
queue: new Array(),
setBreakpointInProgress: false,
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// Initialization
onAttach: function(reload)
{
Trace.sysout("breakpointTool.attach; context ID: " + this.context.getId());
// Listen for 'newScript' events.
this.context.getTool("source").addListener(this);
// Listen for {@link BreakpointStore} events to create/remove breakpoints
// in the related backend (thread actor).
BreakpointStore.addListener(this);
},
onDetach: function()
{
Trace.sysout("breakpointTool.detach; context ID: " + this.context.getId());
this.context.getTool("source").removeListener(this);
// Thread has been detached, so clean up also all breakpoint clients. They
// need to be re-created as soon as the thread actor is attached again.
this.context.breakpointClients = [];
BreakpointStore.removeListener(this);
},
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// BreakpointStore Event Listener
onAddBreakpoint: function(bp)
{
Trace.sysout("breakpointTool.onAddBreakpoint; (" + bp.lineNo + ")", bp);
var self = this;
this.setBreakpoint(bp.href, bp.lineNo, function(response, bpClient)
{
Trace.sysout("breakpointTool.onAddBreakpoint; callback executed", response);
// Do not log error if it's 'noScript'. It's quite common that breakpoints
// are set before scripts exists (or no longer exists since garbage collected).
if (response.error && response.error != "noScript")
{
TraceError.sysout("breakpointTool.onAddBreakpoint; ERROR: " +
response.message, response);
return;
}
// Auto-correct shared breakpoint object if necessary and store the original
// line so, listeners (like e.g. the Script panel) can update the UI.
var currentLine = bpClient.location.line - 1;
if (bp.lineNo != currentLine)
{
// The breakpoint line is going to be corrected, let's check if there isn't
// an existing breakpoint at the new line (see issue: 6253). This must be
// done before the correction.
var dupBp = BreakpointStore.findBreakpoint(bp.href, bp.lineNo);
// bpClient deals with 1-based line numbers. Firebug uses 0-based
// line numbers (indexes). Let's fix the line.
bp.params.originLineNo = bp.lineNo;
bp.lineNo = currentLine;
// If an existing breakpoint has been found we need to remove the newly
// created one to avoid duplicities (two breakpoints at the same line).
// Do not fire an event when removing, it's just client side thing.
if (dupBp)
{
BreakpointStore.removeBreakpointInternal(dupBp.href, dupBp.lineNo);
Trace.sysout("breakpointTool.onAddBreakpoint; remove new BP it's a dup");
}
}
// Breakpoint is ready on the server side, let's notify all listeners so,
// the UI is properly (and asynchronously) updated everywhere.
self.dispatch("onBreakpointAdded", [self.context, bp]);
// The info about the original line should not be needed any more.
delete bp.params.originLineNo;
});
},
onRemoveBreakpoint: function(bp)
{
this.removeBreakpoint(bp.href, bp.lineNo, (response) =>
{
this.dispatch("onBreakpointRemoved", [this.context, bp]);
Firebug.dispatchEvent(this.context.browser, "onBreakpointRemoved", [bp]);
});
},
onEnableBreakpoint: function(bp)
{
this.enableBreakpoint(bp.href, bp.lineNo, (response, bpClient) =>
{
this.dispatch("onBreakpointEnabled", [this.context, bp]);
});
},
onDisableBreakpoint: function(bp)
{
this.disableBreakpoint(bp.href, bp.lineNo, (response, bpClient) =>
{
this.dispatch("onBreakpointDisabled", [this.context, bp]);
});
},
onModifyBreakpoint: function(bp)
{
this.dispatch("onBreakpointModified", [this.context, bp]);
},
onRemoveAllBreakpoints: function(bps)
{
Trace.sysout("breakpointTool.onRemoveAllBreakpoints; (" + bps.length + ")", bps);
var deferred = this.context.defer();
this.removeBreakpoints(bps, () =>
{
deferred.resolve();
});
return deferred.promise;
},
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// SourceTool
newSource: function(sourceFile)
{
// Get all breakpoints (including dynamic breakpoints) that belong to the
// newly created source.
var url = sourceFile.getURL();
var bps = BreakpointStore.getBreakpoints(url/*, true*/);
// Filter out those breakpoints that have been already set on the backend
// (i.e. there is a corresponding client object already).
var filtered = bps.filter((bp) =>
{
// xxxHonza: Do not try to create server side breakpoint actors for
// dynamic breakpoints. This is an optimization, it would fail anyway.
// We should avoid leaking dynamic-script related code from
// firebug/debugger/script/sourceTool module, let's fix this later.
if (bp.params.dynamicHandler)
return;
return !this.getBreakpointClient(bp.href, bp.lineNo);
});
// Bail out if there is nothing to set.
if (!filtered.length)
{
Trace.sysout("breakpointTool.newSource; No breakpoints to set for: " + url, bps);
return;
}
// Filter out disabled breakpoints. These won't be set on the server side
// (unless the user enables them later).
// xxxHonza: we shouldn't create server-side breakpoints for normal disabled
// breakpoints, but not in case there are other breakpoints at the same line.
/*filtered = filtered.filter(function(bp, index, array)
{
return bp.isEnabled();
});*/
Trace.sysout("breakpointTool.newSource; Initialize server side breakpoints: (" +
filtered.length + ") " + url, filtered);
// Set breakpoints on the server side.
this.setBreakpoints(filtered, function()
{
// Some breakpoints could have been auto-corrected so, save all now.
// xxxHonza: what about breakpoints in other contexts using the same URL?
// Should they be corrected too?
// xxxHonza: fix me
// If the thread is paused the callback is called too soon (before all
// breakpoints are set on the server and response packets received).
//BreakpointStore.save(url);
Trace.sysout("breakpointTool.newSource; breakpoints initialized " + url, arguments);
});
},
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// Breakpoint Backend API
/**
* Setting a breakpoint is an asynchronous process that requires communication with
* the backend. There can be three round-trips with the server:
* 1) SEND 'interrupt' -> RECEIVED 'paused'
* 2) SEND 'setBreakpoint' -> RECEIVED 'actor'
* 3) SEND 'resume' -> RECEIVED 'resumed'
*
* If the method is called again in the middle of an existing set-breakpoint-sequence,
* arguments are pushed in a |queue| and handled after the first process finishes.
*
* xxxHonza: the thread doesn't have to be interrupted/resumed if there are
* other breakpoints waiting in the queue.
*/
setBreakpoint: function(url, lineNumber, callback)
{
// The context needs to be attached to the thread in order to set a breakpoint.
var thread = this.context.activeThread;
if (!thread)
{
TraceError.sysout("BreakpointTool.setBreakpoint; ERROR Can't set BP, no thread.");
return;
}
if (Trace.active)
{
Trace.sysout("breakpointTool.setBreakpoint; " + url + " (" + lineNumber + ") " +
"thread client state: " + thread.state);
// xxxHonza: I have experienced a problem where the client side state
// was set to "paused", but the server side still failed to set a breakpoint
// due to debugger not being in pause state.
var threadActor = DebuggerLib.getThreadActor(this.context.browser);
Trace.sysout("breakpointTool.setBreakpoint; thread actor state: " +
threadActor.state);
}
// Do not create two server side breakpoints at the same line.
var bpClient = this.getBreakpointClient(url, lineNumber);
if (bpClient)
{
Trace.sysout("breakpointTool.onAddBreakpoint; BP client already exists", bpClient);
//xxxHonza: the callback expects a packet, it should not.
if (callback)
callback({}, bpClient);
return;
}
var self = this;
function doSetBreakpoint(callback)
{
var location = {
url: url,
line: lineNumber + 1
};
Trace.sysout("breakpointTool.doSetBreakpoint; (" + lineNumber + ")", location);
if (!self.context.activeThread)
{
TraceError.sysout("breakpointTool.doSetBreakpoint; ERROR no thread " +
url + "(" + lineNumber + ")");
return;
}
// Send RDP packet to set a breakpoint on the server side. The callback will be
// executed as soon as we receive a response.
self.context.activeThread.setBreakpoint(location,
self.onSetBreakpoint.bind(self, callback));
}
// If the debuggee is paused, just set the breakpoint.
if (thread.paused)
{
doSetBreakpoint(callback);
return;
}
// If the previous async-process hasn't finished yet, put arguments in a queue.
if (this.setBreakpointInProgress)
{
Trace.sysout("breakpointTool.setBreakpoint; Setting BP in progress, wait " +
url + " (" + lineNumber + ")");
this.queue.push(arguments);
return;
}
this.setBreakpointInProgress = true;
// Otherwise, force a pause in order to set the breakpoint.
// xxxHonza: this sometimes generates 'alreadyPaused' packet, fix me.
// Or maybe the interrupt call in setBreakpoints. You need a page with two
// loaded URLs with breakpoints
thread.interrupt(function(response)
{
if (response.error)
{
// Can't set the breakpoint if pausing failed.
callback(response);
return;
}
// Set the breakpoint
doSetBreakpoint(function(response, bpClient)
{
// Wait for resume
thread.resume(function(response)
{
self.setBreakpointInProgress = false;
callback(response, bpClient);
// Set breakpoints waiting in the queue.
if (self.queue.length > 0)
self.setBreakpoint.apply(self, self.queue.shift());
});
});
});
},
/**
* Executed when a breakpoint is set on the backend and confirmation packet
* has been received.
*/
onSetBreakpoint: function(callback, response, bpClient)
{
var actualLocation = response.actualLocation;
Trace.sysout("breakpointTool.onSetBreakpoint; " + bpClient.location.url + " (" +
bpClient.location.line + ")", bpClient);
// Note that both actualLocation and bpClient.location deal with 1-based
// line numbers.
if (actualLocation && actualLocation.line != bpClient.location.line)
{
// To be found when it needs removing.
bpClient.location.line = actualLocation.line;
}
// Store breakpoint clients so, we can use the actors to remove breakpoints.
// xxxFarshid: Shouldn't we save bpClient object only if there is no error?
// xxxHonza: yes, we probably should.
// xxxHonza: we also need an error logging
if (!this.context.breakpointClients)
this.context.breakpointClients = [];
// Check if the breakpoint-client object already exist. The line could
// have been corrected on the server side and there can already be a breakpoint
// on the new line.
if (bpClient.actor && !this.breakpointActorExists(bpClient))
this.context.breakpointClients.push(bpClient);
if (callback)
callback(response, bpClient);
this.setBreakpointInProgress = false;
},
/**
* Creates breakpoint actors on the server side and {@link BreakpointClient} objects
* on the client side. The client objects are stored within {@link TabContext}.
*
* @param arr {Array} List of breakpoints to be created on the server side
* @param cb {Function} Optional callback that is executed as soon as all breakpoints
* are created on the server side and the current thread resumed again.
*
* xxxHonza: Use a better name for the |cb| argument, ideally |callback| (and refactor
* method implementation, so there isn't the other callback variable).
*/
setBreakpoints: function(arr, cb)
{
var self = this;
// Bail out if there is nothing to set.
if (!arr.length)
return;
var thread = this.context.activeThread;
if (!thread)
{
TraceError.sysout("BreakpointTool.setBreakpoints; Can't set breakpoints " +
"if there is no active thread");
return;
}
Trace.sysout("breakpointTool.setBreakpoints; " + arr.length +
", thread state: " + thread.state, arr);
var doSetBreakpoints = function _doSetBreakpoints(callback)
{
Trace.sysout("breakpointTool.doSetBreakpoints; ", arr);
// Iterate all breakpoints in the given array and set them step by step.
// The thread is paused at this point. The following loop generates a set of
// 'setBreakpoint' packets that are put in an internal queue (in the underlying
// RDP framework) and handled step by step, i.e. the next 'setBreakpoint' packet
// is sent as soon as a response for the previous one is received.
for (var i = 0; i < arr.length; i++)
self.onAddBreakpoint(arr[i]);
if (callback)
callback();
};
// If the thread is currently paused, go to set all the breakpoints.
if (thread.paused)
{
// xxxHonza: the callback should be called when the last breakpoint
// is set on the backend, fix me.
doSetBreakpoints(cb);
return;
}
// ... otherwise we need to interrupt the thread first.
// It can happens that the debugger will pause before the "interrupt" packet
// is processed by the server side. In such case the packet passed into the
// following callback wouldn't be "interrupted", but different type (e.g. "paused")
// So, do not resume if this happens (see the condition within the callback).
// You can also observe this by seeing:
// debuggerTool.paused; ERROR no frame, type: alreadyPaused
// xxxHonza: this should solve most of the cases, but still, what if the other
// component also calls interrupt?
thread.interrupt(function(packet)
{
FBTrace.sysout("packet ", packet)
if (packet.error)
{
TraceError.sysout("BreakpointTool.setBreakpoints; Can't set breakpoints: " +
packet.error);
return;
}
// When the thread is interrupted, we can set all the breakpoints.
doSetBreakpoints(function()
{
Trace.sysout("breakpointTool.doSetBreakpoints; done", arguments);
// If interrupt happened at the moment when the thread has already been
// paused, after we checked |thread.paused| (e.g. breakpoints in onload scripts),
// do not resume. See also issue 7118
if (packet.why.type == "alreadyPaused")
{
if (cb)
cb();
}
else
{
// Do not resume if the debugger wasn't interrupted by this method.
if (packet.type != "interrupted")
return;
// At this point, all 'setBreakpoint' packets have been generated (the first
// on already sent) and they are waiting in a queue. The resume packet will
// be received as soon as the last response for 'setBreakpoint' is received.
self.context.getTool("debugger").resume(cb);
}
});
});
},
removeBreakpoint: function(url, lineNumber, callback)
{
Trace.sysout("breakpointTool.removeBreakpoint; " + url + " (" + lineNumber + ")");
if (!this.context.activeThread)
{
TraceError.sysout("breakpointTool.removeBreakpoint; Can't remove breakpoints.");
return;
}
// Do note remove server-side breakpoint if there are still some client side
// breakpoint at the line.
if (BreakpointStore.hasAnyBreakpoint(url, lineNumber))
{
Trace.sysout("breakpointTool.removeBreakpoint; Can't remove BP it's still " +
"in the store! " + url + " (" + lineNumber + ")");
// xxxHonza: the callback expects a packet as an argument, it should not.
if (callback)
callback({});
return;
}
// We need to get the breakpoint client object for this context. The client
// knows how to remove the breakpoint on the server side.
var client = this.removeBreakpointClient(url, lineNumber);
Trace.sysout("breakpointTool.removeBreakpoint; client: " + client, client);
if (client)
{
client.remove(callback);
}
else
{
// xxxHonza: Don't display the error message. It can happen
// that dynamic breakpoint (a breakpoint in dynamically created script)
// is being removed. Such breakpoint doesn't have corresponding
// {@link BreakpointClient} for now.
//
//TraceError.sysout("breakpointTool.removeBreakpoint; ERROR removing " +
// "non existing breakpoint. " + url + ", " + lineNumber);
// Execute the callback in any case, so the UI can be updated.
// xxxHonza: the callback expects a packet as an argument, it should not.
if (callback)
callback({});
}
},
/**
* Removes specified breakpoints. The removal is done asynchronously breakpoint
* by breakpoint. The next breakpoint is removed as soon as there is a confirmation
* from the backend that the previous one has been removed.
*
* @param {Array} bps Array of breakpoints to be removed. Every item in the array
* should specify breakpoint location [{href: "", lineNo: 0}]
* @param {Function} callback A function executed as soon as all breakpoints are removed.
* The removal happens asynchronously since it requires communication with the backend
* over RDP.
*/
removeBreakpoints: function(bps, callback)
{
if (bps.length == 0)
{
if (callback)
callback();
return;
}
var bp = bps[0];
this.removeBreakpoint(bp.href, bp.lineNo, (response) =>
{
if (response.error)
{
TraceError.sysout("breakpointTool.removeBreakpoints; ERROR " +
response.message, response);
}
this.removeBreakpoints(bps.slice(1), callback);
});
},
getBreakpointClient: function(url, lineNumber)
{
var clients = this.context.breakpointClients;
if (!clients)
return;
for (var i=0; i<clients.length; i++)
{
var client = clients[i];
var loc = client.location;
if (loc.url == url && (loc.line - 1) == lineNumber)
return client;
}
},
removeBreakpointClient: function(url, lineNumber)
{
var clients = this.context.breakpointClients;
if (!clients)
return;
for (var i=0; i<clients.length; i++)
{
var client = clients[i];
var loc = client.location;
if (loc.url == url && (loc.line - 1) == lineNumber)
{
clients.splice(i, 1);
return client;
}
}
},
breakpointActorExists: function(bpClient)
{
var clients = this.context.breakpointClients;
if (!clients)
return false;
var client;
for (var i=0, len = clients.length; i < len; i++)
{
client = clients[i];
if (client.actor === bpClient.actor)
return true;
}
return false;
},
enableBreakpoint: function(url, lineNumber, callback)
{
// Enable breakpoint means adding it to the server side.
this.setBreakpoint(url, lineNumber, callback);
},
disableBreakpoint: function(url, lineNumber, callback)
{
// Disable breakpoint means removing it from the server side.
this.removeBreakpoint(url, lineNumber, callback);
},
isBreakpointDisabled: function(url, lineNumber)
{
//return JSDebugger.fbs.isBreakpointDisabled(url, lineNumber);
},
getBreakpointCondition: function(url, lineNumber)
{
//return JSDebugger.fbs.getBreakpointCondition(url, lineNumber);
},
});
// ********************************************************************************************* //
// Registration
Firebug.registerTool("breakpoint", BreakpointTool);
return BreakpointTool;
// ********************************************************************************************* //
});