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
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
|
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cc, Ci, Cu} = require("chrome");
var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
var { extend } = require("sdk/core/heritage");
var {TargetFactory} = require("devtools/client/framework/target");
var {Tools} = require("devtools/client/definitions");
const { Task } = require("devtools/shared/task");
var promise = require("promise");
var Services = require("Services");
loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
loader.lazyRequireGetter(this, "WebConsoleFrame", "devtools/client/webconsole/webconsole", true);
loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
loader.lazyRequireGetter(this, "showDoorhanger", "devtools/client/shared/doorhanger", true);
loader.lazyRequireGetter(this, "viewSource", "devtools/client/shared/view-source");
const STRINGS_URI = "devtools/client/locales/webconsole.properties";
var l10n = new WebConsoleUtils.L10n(STRINGS_URI);
const BROWSER_CONSOLE_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
// The preference prefix for all of the Browser Console filters.
const BROWSER_CONSOLE_FILTER_PREFS_PREFIX = "devtools.browserconsole.filter.";
var gHudId = 0;
// The HUD service
function HUD_SERVICE()
{
this.consoles = new Map();
this.lastFinishedRequest = { callback: null };
}
HUD_SERVICE.prototype =
{
_browserConsoleID: null,
_browserConsoleDefer: null,
/**
* Keeps a reference for each Web Console / Browser Console that is created.
* @type Map
*/
consoles: null,
_browerConsoleSessionState: false,
storeBrowserConsoleSessionState() {
this._browerConsoleSessionState = !!this.getBrowserConsole();
},
getBrowserConsoleSessionState() {
return this._browerConsoleSessionState;
},
/**
* Restore the Browser Console as provided by SessionStore.
*/
restoreBrowserConsoleSession: function HS_restoreBrowserConsoleSession() {
if (!HUDService.getBrowserConsole()) {
HUDService.toggleBrowserConsole();
}
},
/**
* Assign a function to this property to listen for every request that
* completes. Used by unit tests. The callback takes one argument: the HTTP
* activity object as received from the remote Web Console.
*
* @type object
* Includes a property named |callback|. Assign the function to the
* |callback| property of this object.
*/
lastFinishedRequest: null,
/**
* Get the current context, which is the main application window.
*
* @returns nsIDOMWindow
*/
currentContext: function HS_currentContext() {
return Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
},
/**
* Open a Web Console for the given target.
*
* @see devtools/framework/target.js for details about targets.
*
* @param object aTarget
* The target that the web console will connect to.
* @param nsIDOMWindow aIframeWindow
* The window where the web console UI is already loaded.
* @param nsIDOMWindow aChromeWindow
* The window of the web console owner.
* @return object
* A promise object for the opening of the new WebConsole instance.
*/
openWebConsole:
function HS_openWebConsole(aTarget, aIframeWindow, aChromeWindow)
{
let hud = new WebConsole(aTarget, aIframeWindow, aChromeWindow);
this.consoles.set(hud.hudId, hud);
return hud.init();
},
/**
* Open a Browser Console for the given target.
*
* @see devtools/framework/target.js for details about targets.
*
* @param object aTarget
* The target that the browser console will connect to.
* @param nsIDOMWindow aIframeWindow
* The window where the browser console UI is already loaded.
* @param nsIDOMWindow aChromeWindow
* The window of the browser console owner.
* @return object
* A promise object for the opening of the new BrowserConsole instance.
*/
openBrowserConsole:
function HS_openBrowserConsole(aTarget, aIframeWindow, aChromeWindow)
{
let hud = new BrowserConsole(aTarget, aIframeWindow, aChromeWindow);
this._browserConsoleID = hud.hudId;
this.consoles.set(hud.hudId, hud);
return hud.init();
},
/**
* Returns the Web Console object associated to a content window.
*
* @param nsIDOMWindow aContentWindow
* @returns object
*/
getHudByWindow: function HS_getHudByWindow(aContentWindow)
{
for (let [hudId, hud] of this.consoles) {
let target = hud.target;
if (target && target.tab && target.window === aContentWindow) {
return hud;
}
}
return null;
},
/**
* Returns the console instance for a given id.
*
* @param string aId
* @returns Object
*/
getHudReferenceById: function HS_getHudReferenceById(aId)
{
return this.consoles.get(aId);
},
/**
* Find if there is a Web Console open for the current tab and return the
* instance.
* @return object|null
* The WebConsole object or null if the active tab has no open Web
* Console.
*/
getOpenWebConsole: function HS_getOpenWebConsole()
{
let tab = this.currentContext().gBrowser.selectedTab;
if (!tab || !TargetFactory.isKnownTab(tab)) {
return null;
}
let target = TargetFactory.forTab(tab);
let toolbox = gDevTools.getToolbox(target);
let panel = toolbox ? toolbox.getPanel("webconsole") : null;
return panel ? panel.hud : null;
},
/**
* Toggle the Browser Console.
*/
toggleBrowserConsole: function HS_toggleBrowserConsole()
{
if (this._browserConsoleID) {
let hud = this.getHudReferenceById(this._browserConsoleID);
return hud.destroy();
}
if (this._browserConsoleDefer) {
return this._browserConsoleDefer.promise;
}
this._browserConsoleDefer = promise.defer();
function connect()
{
let deferred = promise.defer();
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
DebuggerServer.allowChromeProcess = true;
let client = new DebuggerClient(DebuggerServer.connectPipe());
return client.connect()
.then(() => client.getProcess())
.then(aResponse => {
// Set chrome:false in order to attach to the target
// (i.e. send an `attach` request to the chrome actor)
return { form: aResponse.form, client: client, chrome: false };
});
}
let target;
function getTarget(aConnection)
{
return TargetFactory.forRemoteTab(aConnection);
}
function openWindow(aTarget)
{
target = aTarget;
let deferred = promise.defer();
let win = Services.ww.openWindow(null, Tools.webConsole.url, "_blank",
BROWSER_CONSOLE_WINDOW_FEATURES, null);
win.addEventListener("DOMContentLoaded", function onLoad() {
win.removeEventListener("DOMContentLoaded", onLoad);
// Set the correct Browser Console title.
let root = win.document.documentElement;
root.setAttribute("title", root.getAttribute("browserConsoleTitle"));
deferred.resolve(win);
});
return deferred.promise;
}
connect().then(getTarget).then(openWindow).then((aWindow) => {
return this.openBrowserConsole(target, aWindow, aWindow)
.then((aBrowserConsole) => {
this._browserConsoleDefer.resolve(aBrowserConsole);
this._browserConsoleDefer = null;
});
}, console.error.bind(console));
return this._browserConsoleDefer.promise;
},
/**
* Opens or focuses the Browser Console.
*/
openBrowserConsoleOrFocus: function HS_openBrowserConsoleOrFocus()
{
let hud = this.getBrowserConsole();
if (hud) {
hud.iframeWindow.focus();
return promise.resolve(hud);
}
else {
return this.toggleBrowserConsole();
}
},
/**
* Get the Browser Console instance, if open.
*
* @return object|null
* A BrowserConsole instance or null if the Browser Console is not
* open.
*/
getBrowserConsole: function HS_getBrowserConsole()
{
return this.getHudReferenceById(this._browserConsoleID);
},
};
/**
* A WebConsole instance is an interactive console initialized *per target*
* that displays console log data as well as provides an interactive terminal to
* manipulate the target's document content.
*
* This object only wraps the iframe that holds the Web Console UI. This is
* meant to be an integration point between the Firefox UI and the Web Console
* UI and features.
*
* @constructor
* @param object aTarget
* The target that the web console will connect to.
* @param nsIDOMWindow aIframeWindow
* The window where the web console UI is already loaded.
* @param nsIDOMWindow aChromeWindow
* The window of the web console owner.
*/
function WebConsole(aTarget, aIframeWindow, aChromeWindow)
{
this.iframeWindow = aIframeWindow;
this.chromeWindow = aChromeWindow;
this.hudId = "hud_" + ++gHudId;
this.target = aTarget;
this.browserWindow = this.chromeWindow.top;
let element = this.browserWindow.document.documentElement;
if (element.getAttribute("windowtype") != gDevTools.chromeWindowType) {
this.browserWindow = HUDService.currentContext();
}
this.ui = new WebConsoleFrame(this);
}
WebConsole.prototype = {
iframeWindow: null,
chromeWindow: null,
browserWindow: null,
hudId: null,
target: null,
ui: null,
_browserConsole: false,
_destroyer: null,
/**
* Getter for a function to to listen for every request that completes. Used
* by unit tests. The callback takes one argument: the HTTP activity object as
* received from the remote Web Console.
*
* @type function
*/
get lastFinishedRequestCallback()
{
return HUDService.lastFinishedRequest.callback;
},
/**
* Getter for the window that can provide various utilities that the web
* console makes use of, like opening links, managing popups, etc. In
* most cases, this will be |this.browserWindow|, but in some uses (such as
* the Browser Toolbox), there is no browser window, so an alternative window
* hosts the utilities there.
* @type nsIDOMWindow
*/
get chromeUtilsWindow()
{
if (this.browserWindow) {
return this.browserWindow;
}
return this.chromeWindow.top;
},
/**
* Getter for the xul:popupset that holds any popups we open.
* @type nsIDOMElement
*/
get mainPopupSet()
{
return this.chromeUtilsWindow.document.getElementById("mainPopupSet");
},
/**
* Getter for the output element that holds messages we display.
* @type nsIDOMElement
*/
get outputNode()
{
return this.ui ? this.ui.outputNode : null;
},
get gViewSourceUtils()
{
return this.chromeUtilsWindow.gViewSourceUtils;
},
/**
* Initialize the Web Console instance.
*
* @return object
* A promise for the initialization.
*/
init: function WC_init()
{
return this.ui.init().then(() => this);
},
/**
* Retrieve the Web Console panel title.
*
* @return string
* The Web Console panel title.
*/
getPanelTitle: function WC_getPanelTitle()
{
let url = this.ui ? this.ui.contentLocation : "";
return l10n.getFormatStr("webConsoleWindowTitleAndURL", [url]);
},
/**
* The JSTerm object that manages the console's input.
* @see webconsole.js::JSTerm
* @type object
*/
get jsterm()
{
return this.ui ? this.ui.jsterm : null;
},
/**
* The clear output button handler.
* @private
*/
_onClearButton: function WC__onClearButton()
{
if (this.target.isLocalTab) {
this.browserWindow.DeveloperToolbar.resetErrorsCount(this.target.tab);
}
},
/**
* Alias for the WebConsoleFrame.setFilterState() method.
* @see webconsole.js::WebConsoleFrame.setFilterState()
*/
setFilterState: function WC_setFilterState()
{
this.ui && this.ui.setFilterState.apply(this.ui, arguments);
},
/**
* Open a link in a new tab.
*
* @param string aLink
* The URL you want to open in a new tab.
*/
openLink: function WC_openLink(aLink)
{
this.chromeUtilsWindow.openUILinkIn(aLink, "tab");
},
/**
* Open a link in Firefox's view source.
*
* @param string aSourceURL
* The URL of the file.
* @param integer aSourceLine
* The line number which should be highlighted.
*/
viewSource: function WC_viewSource(aSourceURL, aSourceLine) {
// Attempt to access view source via a browser first, which may display it in
// a tab, if enabled.
let browserWin = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
if (browserWin && browserWin.BrowserViewSourceOfDocument) {
return browserWin.BrowserViewSourceOfDocument({
URL: aSourceURL,
lineNumber: aSourceLine
});
}
this.gViewSourceUtils.viewSource(aSourceURL, null, this.iframeWindow.document, aSourceLine || 0);
},
/**
* Tries to open a Stylesheet file related to the web page for the web console
* instance in the Style Editor. If the file is not found, it is opened in
* source view instead.
*
* Manually handle the case where toolbox does not exist (Browser Console).
*
* @param string aSourceURL
* The URL of the file.
* @param integer aSourceLine
* The line number which you want to place the caret.
*/
viewSourceInStyleEditor: function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine) {
let toolbox = gDevTools.getToolbox(this.target);
if (!toolbox) {
this.viewSource(aSourceURL, aSourceLine);
return;
}
toolbox.viewSourceInStyleEditor(aSourceURL, aSourceLine);
},
/**
* Tries to open a JavaScript file related to the web page for the web console
* instance in the Script Debugger. If the file is not found, it is opened in
* source view instead.
*
* Manually handle the case where toolbox does not exist (Browser Console).
*
* @param string aSourceURL
* The URL of the file.
* @param integer aSourceLine
* The line number which you want to place the caret.
*/
viewSourceInDebugger: function WC_viewSourceInDebugger(aSourceURL, aSourceLine) {
let toolbox = gDevTools.getToolbox(this.target);
if (!toolbox) {
this.viewSource(aSourceURL, aSourceLine);
return;
}
toolbox.viewSourceInDebugger(aSourceURL, aSourceLine).then(() => {
this.ui.emit("source-in-debugger-opened");
});
},
/**
* Tries to open a JavaScript file related to the web page for the web console
* instance in the corresponding Scratchpad.
*
* @param string aSourceURL
* The URL of the file which corresponds to a Scratchpad id.
*/
viewSourceInScratchpad: function WC_viewSourceInScratchpad(aSourceURL, aSourceLine) {
viewSource.viewSourceInScratchpad(aSourceURL, aSourceLine);
},
/**
* Retrieve information about the JavaScript debugger's stackframes list. This
* is used to allow the Web Console to evaluate code in the selected
* stackframe.
*
* @return object|null
* An object which holds:
* - frames: the active ThreadClient.cachedFrames array.
* - selected: depth/index of the selected stackframe in the debugger
* UI.
* If the debugger is not open or if it's not paused, then |null| is
* returned.
*/
getDebuggerFrames: function WC_getDebuggerFrames()
{
let toolbox = gDevTools.getToolbox(this.target);
if (!toolbox) {
return null;
}
let panel = toolbox.getPanel("jsdebugger");
if (!panel) {
return null;
}
return panel.getFrames();
},
/**
* Retrieves the current selection from the Inspector, if such a selection
* exists. This is used to pass the ID of the selected actor to the Web
* Console server for the $0 helper.
*
* @return object|null
* A Selection referring to the currently selected node in the
* Inspector.
* If the inspector was never opened, or no node was ever selected,
* then |null| is returned.
*/
getInspectorSelection: function WC_getInspectorSelection()
{
let toolbox = gDevTools.getToolbox(this.target);
if (!toolbox) {
return null;
}
let panel = toolbox.getPanel("inspector");
if (!panel || !panel.selection) {
return null;
}
return panel.selection;
},
/**
* Destroy the object. Call this method to avoid memory leaks when the Web
* Console is closed.
*
* @return object
* A promise object that is resolved once the Web Console is closed.
*/
destroy: function WC_destroy()
{
if (this._destroyer) {
return this._destroyer.promise;
}
HUDService.consoles.delete(this.hudId);
this._destroyer = promise.defer();
// The document may already be removed
if (this.chromeUtilsWindow && this.mainPopupSet) {
let popupset = this.mainPopupSet;
let panels = popupset.querySelectorAll("panel[hudId=" + this.hudId + "]");
for (let panel of panels) {
panel.hidePopup();
}
}
let onDestroy = Task.async(function* () {
if (!this._browserConsole) {
try {
yield this.target.activeTab.focus();
}
catch (ex) {
// Tab focus can fail if the tab or target is closed.
}
}
let id = WebConsoleUtils.supportsString(this.hudId);
Services.obs.notifyObservers(id, "web-console-destroyed", null);
this._destroyer.resolve(null);
}.bind(this));
if (this.ui) {
this.ui.destroy().then(onDestroy);
}
else {
onDestroy();
}
return this._destroyer.promise;
},
};
/**
* A BrowserConsole instance is an interactive console initialized *per target*
* that displays console log data as well as provides an interactive terminal to
* manipulate the target's document content.
*
* This object only wraps the iframe that holds the Browser Console UI. This is
* meant to be an integration point between the Firefox UI and the Browser Console
* UI and features.
*
* @constructor
* @param object aTarget
* The target that the browser console will connect to.
* @param nsIDOMWindow aIframeWindow
* The window where the browser console UI is already loaded.
* @param nsIDOMWindow aChromeWindow
* The window of the browser console owner.
*/
function BrowserConsole()
{
WebConsole.apply(this, arguments);
this._telemetry = new Telemetry();
}
BrowserConsole.prototype = extend(WebConsole.prototype, {
_browserConsole: true,
_bc_init: null,
_bc_destroyer: null,
$init: WebConsole.prototype.init,
/**
* Initialize the Browser Console instance.
*
* @return object
* A promise for the initialization.
*/
init: function BC_init()
{
if (this._bc_init) {
return this._bc_init;
}
// Only add the shutdown observer if we've opened a Browser Console window.
ShutdownObserver.init();
this.ui._filterPrefsPrefix = BROWSER_CONSOLE_FILTER_PREFS_PREFIX;
let window = this.iframeWindow;
// Make sure that the closing of the Browser Console window destroys this
// instance.
let onClose = () => {
window.removeEventListener("unload", onClose);
window.removeEventListener("focus", onFocus);
this.destroy();
};
window.addEventListener("unload", onClose);
this._telemetry.toolOpened("browserconsole");
// Create an onFocus handler just to display the dev edition promo.
// This is to prevent race conditions in some environments.
// Hook to display promotional Developer Edition doorhanger. Only displayed once.
let onFocus = () => showDoorhanger({ window, type: "deveditionpromo" });
window.addEventListener("focus", onFocus);
this._bc_init = this.$init();
return this._bc_init;
},
$destroy: WebConsole.prototype.destroy,
/**
* Destroy the object.
*
* @return object
* A promise object that is resolved once the Browser Console is closed.
*/
destroy: function BC_destroy()
{
if (this._bc_destroyer) {
return this._bc_destroyer.promise;
}
this._telemetry.toolClosed("browserconsole");
this._bc_destroyer = promise.defer();
let chromeWindow = this.chromeWindow;
this.$destroy().then(() =>
this.target.client.close().then(() => {
HUDService._browserConsoleID = null;
chromeWindow.close();
this._bc_destroyer.resolve(null);
}));
return this._bc_destroyer.promise;
},
});
const HUDService = new HUD_SERVICE();
exports.HUDService = HUDService;
/**
* The ShutdownObserver listens for app shutdown and saves the current state
* of the Browser Console for session restore.
*/
var ShutdownObserver = {
_initialized: false,
init() {
if (this._initialized) {
return;
}
Services.obs.addObserver(this, "quit-application-granted", false);
this._initialized = true;
},
observe(message, topic) {
if (topic == "quit-application-granted") {
HUDService.storeBrowserConsoleSessionState();
this.uninit();
}
},
uninit() {
Services.obs.removeObserver(this, "quit-application-granted");
}
};
|