From aa99330a696d901bbacfc04dce671029c0b6dde3 Mon Sep 17 00:00:00 2001 From: Mihai Sucan Date: Fri, 11 Jan 2013 19:31:09 +0200 Subject: [PATCH] Bug 827083 - Cannot attach remote web console to Firefox Android; r=past --- browser/devtools/framework/Toolbox.jsm | 4 + browser/devtools/webconsole/HUDService.jsm | 540 +++++------------- .../devtools/webconsole/WebConsolePanel.jsm | 76 +-- ...ole_bug_580001_closing_after_completion.js | 11 +- ...le_bug_595350_multiple_windows_and_tabs.js | 3 +- ...le_bug_782653_CSS_links_in_Style_Editor.js | 1 + browser/devtools/webconsole/test/head.js | 6 +- browser/devtools/webconsole/webconsole.js | 386 +++++++------ .../webconsole/dbg-webconsole-actors.js | 2 +- 9 files changed, 394 insertions(+), 635 deletions(-) diff --git a/browser/devtools/framework/Toolbox.jsm b/browser/devtools/framework/Toolbox.jsm index d4c9e1ad7a69..69c82bd49c18 100644 --- a/browser/devtools/framework/Toolbox.jsm +++ b/browser/devtools/framework/Toolbox.jsm @@ -468,6 +468,10 @@ Toolbox.prototype = { /** * Create a host object based on the given host type. * + * Warning: some hosts require that the toolbox target provides a reference to + * the attached tab. Not all Targets have a tab property - make sure you correctly + * mix and match hosts and targets. + * * @param {string} hostType * The host type of the new host object * diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index 4547c286d348..bd8e4c4d7bb2 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -13,56 +13,43 @@ const Cu = Components.utils; const CONSOLEAPI_CLASS_ID = "{b49c18f8-3379-4fc0-8c90-d7772c1a9ff3}"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource:///modules/devtools/gDevTools.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", + "resource:///modules/devtools/gDevTools.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TargetFactory", - "resource:///modules/devtools/Target.jsm"); + "resource:///modules/devtools/Target.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); + "resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", - "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/commonjs/promise/core.js"); const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let l10n = new WebConsoleUtils.l10n(STRINGS_URI); this.EXPORTED_SYMBOLS = ["HUDService"]; -function LogFactory(aMessagePrefix) -{ - function log(aMessage) { - var _msg = aMessagePrefix + " " + aMessage + "\n"; - dump(_msg); - } - return log; -} - -let log = LogFactory("*** HUDService:"); - -// The HTML namespace. -const HTML_NS = "http://www.w3.org/1999/xhtml"; - -// The XUL namespace. -const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - /////////////////////////////////////////////////////////////////////////// //// The HUD service function HUD_SERVICE() { - // These methods access the "this" object, but they're registered as - // event listeners. So we hammer in the "this" binding. - this.onWindowUnload = this.onWindowUnload.bind(this); - - /** - * Keeps a reference for each HeadsUpDisplay that is created - */ this.hudReferences = {}; -}; +} HUD_SERVICE.prototype = { + /** + * Keeps a reference for each HeadsUpDisplay that is created + * @type object + */ + hudReferences: null, + /** * getter for UI commands to be used by the frontend * @@ -72,12 +59,6 @@ HUD_SERVICE.prototype = return HeadsUpDisplayUICommands; }, - /** - * The sequencer is a generator (after initialization) that returns unique - * integers - */ - sequencer: null, - /** * Firefox-specific current tab getter * @@ -88,134 +69,22 @@ HUD_SERVICE.prototype = }, /** - * Activate a HeadsUpDisplay for the given tab context. + * Open a Web Console for the given target. * - * @param nsIDOMElement aTab - * The xul:tab element. + * @see devtools/framework/Target.jsm for details about targets. + * + * @param object aTarget + * The target that the web console will connect to. * @param nsIDOMElement aIframe * The iframe element into which to place the web console. - * @param RemoteTarget aTarget - * The target that the web console will connect to. * @return object - * The new HeadsUpDisplay instance. + * A Promise object for the opening of the new WebConsole instance. */ - activateHUDForContext: function HS_activateHUDForContext(aTab, aIframe, - aTarget) + openWebConsole: function HS_openWebConsole(aTarget, aIframe) { - let hudId = "hud_" + aTab.linkedPanel; - if (hudId in this.hudReferences) { - return this.hudReferences[hudId]; - } - - this.wakeup(); - - let window = aTab.ownerDocument.defaultView; - let gBrowser = window.gBrowser; - - window.addEventListener("unload", this.onWindowUnload, false); - - let hud = new WebConsole(aTab, aIframe, aTarget); - this.hudReferences[hudId] = hud; - - return hud; - }, - - /** - * Deactivate a HeadsUpDisplay for the given tab context. - * - * @param nsIDOMElement aTab - * The xul:tab element you want to enable the Web Console for. - * @return void - */ - deactivateHUDForContext: function HS_deactivateHUDForContext(aTab) - { - let hudId = "hud_" + aTab.linkedPanel; - if (!(hudId in this.hudReferences)) { - return; - } - - let hud = this.getHudReferenceById(hudId); - let document = hud.chromeDocument; - - hud.destroy(function() { - let id = WebConsoleUtils.supportsString(hudId); - Services.obs.notifyObservers(id, "web-console-destroyed", null); - }); - - delete this.hudReferences[hudId]; - - if (Object.keys(this.hudReferences).length == 0) { - let autocompletePopup = document. - getElementById("webConsole_autocompletePopup"); - if (autocompletePopup) { - autocompletePopup.parentNode.removeChild(autocompletePopup); - } - - let window = document.defaultView; - - window.removeEventListener("unload", this.onWindowUnload, false); - - let gBrowser = window.gBrowser; - let tabContainer = gBrowser.tabContainer; - - this.suspend(); - } - - let contentWindow = aTab.linkedBrowser.contentWindow; - contentWindow.focus(); - }, - - /** - * get a unique ID from the sequence generator - * - * @returns integer - */ - sequenceId: function HS_sequencerId() - { - if (!this.sequencer) { - this.sequencer = this.createSequencer(-1); - } - return this.sequencer.next(); - }, - - /** - * "Wake up" the Web Console activity. This is called when the first Web - * Console is open. This method initializes the various observers we have. - * - * @returns void - */ - wakeup: function HS_wakeup() - { - if (Object.keys(this.hudReferences).length > 0) { - return; - } - - WebConsoleObserver.init(); - }, - - /** - * Suspend Web Console activity. This is called when all Web Consoles are - * closed. - * - * @returns void - */ - suspend: function HS_suspend() - { - delete this.lastFinishedRequestCallback; - - WebConsoleObserver.uninit(); - }, - - /** - * Shutdown all HeadsUpDisplays on quit-application-granted. - * - * @returns void - */ - shutdown: function HS_shutdown() - { - for (let hud of this.hudReferences) { - this.deactivateHUDForContext(hud.tab); - } + let hud = new WebConsole(aTarget, aIframe); + this.hudReferences[hud.hudId] = hud; + return hud.init(); }, /** @@ -226,8 +95,13 @@ HUD_SERVICE.prototype = */ getHudByWindow: function HS_getHudByWindow(aContentWindow) { - let hudId = this.getHudIdByWindow(aContentWindow); - return hudId ? this.hudReferences[hudId] : null; + for each (let hud in this.hudReferences) { + let target = hud.target; + if (target && target.tab && target.window === aContentWindow) { + return hud; + } + } + return null; }, /** @@ -239,16 +113,8 @@ HUD_SERVICE.prototype = */ getHudIdByWindow: function HS_getHudIdByWindow(aContentWindow) { - let window = this.currentContext(); - let index = - window.gBrowser.getBrowserIndexForDocument(aContentWindow.document); - if (index == -1) { - return null; - } - - let tab = window.gBrowser.tabs[index]; - let hudId = "hud_" + tab.linkedPanel; - return hudId in this.hudReferences ? hudId : null; + let hud = this.getHudByWindow(aContentWindow); + return hud ? hud.hudId : null; }, /** @@ -270,101 +136,45 @@ HUD_SERVICE.prototype = * @type function */ lastFinishedRequestCallback: null, - - /** - * Creates a generator that always returns a unique number for use in the - * indexes - * - * @returns Generator - */ - createSequencer: function HS_createSequencer(aInt) - { - function sequencer(aInt) - { - while(1) { - aInt++; - yield aInt; - } - } - return sequencer(aInt); - }, - - /** - * Called whenever a browser window closes. Cleans up any consoles still - * around. - * - * @param nsIDOMEvent aEvent - * The dispatched event. - * @returns void - */ - onWindowUnload: function HS_onWindowUnload(aEvent) - { - let window = aEvent.target.defaultView; - - window.removeEventListener("unload", this.onWindowUnload, false); - - let gBrowser = window.gBrowser; - let tabContainer = gBrowser.tabContainer; - - let tab = tabContainer.firstChild; - while (tab != null) { - this.deactivateHUDForContext(tab); - tab = tab.nextSibling; - } - }, }; /** - * A WebConsole instance is an interactive console initialized *per tab* + * 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 current tab's document content. + * manipulate the target's document content. * * This object only wraps the iframe that holds the Web Console UI. * - * @param nsIDOMElement aTab - * The xul:tab for which you want the WebConsole object. + * @constructor + * @param object aTarget + * The target that the web console will connect to. * @param nsIDOMElement aIframe * iframe into which we should create the WebConsole UI. - * @param RemoteTarget aTarget - * The target that the web console will connect to. */ -function WebConsole(aTab, aIframe, aTarget) +function WebConsole(aTarget, aIframe) { - this.tab = aTab; - if (this.tab == null) { - throw new Error('Missing tab'); - } - this.iframe = aIframe; - if (this.iframe == null) { - console.trace(); - throw new Error('Missing iframe'); - } - - this.chromeDocument = this.tab.ownerDocument; - this.chromeWindow = this.chromeDocument.defaultView; - this.hudId = "hud_" + this.tab.linkedPanel; - - this.target = aTarget; - - this._onIframeLoad = this._onIframeLoad.bind(this); - this.iframe.className = "web-console-frame"; - this.iframe.addEventListener("load", this._onIframeLoad, true); - - this.positionConsole(); + this.chromeDocument = this.iframe.ownerDocument; + this.chromeWindow = this.chromeDocument.defaultView; + this.hudId = "hud_" + Date.now(); + this.target = aTarget; } WebConsole.prototype = { - /** - * The xul:tab for which the current Web Console instance was created. - * @type nsIDOMElement - */ - tab: null, - chromeWindow: null, chromeDocument: null, + hudId: null, + target: null, + iframe: null, + _destroyer: null, + + get browserWindow() + { + return this.target.isLocalTab ? + this.chromeWindow.top : HUDService.currentContext(); + }, /** * Getter for HUDService.lastFinishedRequestCallback. @@ -380,7 +190,7 @@ WebConsole.prototype = { */ get mainPopupSet() { - return this.chromeDocument.getElementById("mainPopupSet"); + return this.browserWindow.document.getElementById("mainPopupSet"); }, /** @@ -392,18 +202,48 @@ WebConsole.prototype = { return this.ui ? this.ui.outputNode : null; }, - get gViewSourceUtils() this.chromeWindow.gViewSourceUtils, + get gViewSourceUtils() this.browserWindow.gViewSourceUtils, /** - * The "load" event handler for the Web Console iframe. - * @private + * Initialize the Web Console instance. + * + * @return object + * A Promise for the initialization. */ - _onIframeLoad: function WC__onIframeLoad() + init: function WC_init() { - this.iframe.removeEventListener("load", this._onIframeLoad, true); + let deferred = Promise.defer(); - this.iframeWindow = this.iframe.contentWindow.wrappedJSObject; - this.ui = new this.iframeWindow.WebConsoleFrame(this); + let onIframeLoad = function() { + this.iframe.removeEventListener("load", onIframeLoad, true); + initUI(); + }.bind(this); + + let initUI = function() { + this.iframeWindow = this.iframe.contentWindow.wrappedJSObject; + this.ui = new this.iframeWindow.WebConsoleFrame(this); + this.ui.init().then(onSuccess, onFailure); + }.bind(this); + + let onSuccess = function() { + deferred.resolve(this); + }.bind(this); + + let onFailure = function(aReason) { + deferred.reject(aReason); + }; + + let win, doc; + if ((win = this.iframe.contentWindow) && + (doc = win.document) && + doc.readyState == "complete") { + this.iframe.addEventListener("load", onIframeLoad, true); + } + else { + initUI(); + } + + return deferred.promise; }, /** @@ -418,50 +258,6 @@ WebConsole.prototype = { return l10n.getFormatStr("webConsoleWindowTitleAndURL", [url]); }, - consoleWindowUnregisterOnHide: true, - - /** - * Position the Web Console UI. - */ - positionConsole: function WC_positionConsole() - { - let lastIndex = -1; - - if (this.outputNode && this.outputNode.getIndexOfFirstVisibleRow) { - lastIndex = this.outputNode.getIndexOfFirstVisibleRow() + - this.outputNode.getNumberOfVisibleRows() - 1; - } - - this._beforePositionConsole(lastIndex); - }, - - /** - * Common code that needs to execute before the Web Console is repositioned. - * @private - * @param number aLastIndex - * The last visible message in the console output before repositioning - * occurred. - */ - _beforePositionConsole: - function WC__beforePositionConsole(aLastIndex) - { - if (!this.ui) { - return; - } - - let onLoad = function() { - this.iframe.removeEventListener("load", onLoad, true); - this.iframeWindow = this.iframe.contentWindow.wrappedJSObject; - this.ui.positionConsole(this.iframeWindow); - - if (aLastIndex > -1 && aLastIndex < this.outputNode.getRowCount()) { - this.outputNode.ensureIndexIsVisible(aLastIndex); - } - }.bind(this); - - this.iframe.addEventListener("load", onLoad, true); - }, - /** * The JSTerm object that manages the console's input. * @see webconsole.js::JSTerm @@ -478,7 +274,9 @@ WebConsole.prototype = { */ _onClearButton: function WC__onClearButton() { - this.chromeWindow.DeveloperToolbar.resetErrorsCount(this.tab); + if (this.target.isLocalTab) { + this.browserWindow.DeveloperToolbar.resetErrorsCount(this.target.tab); + } }, /** @@ -498,7 +296,7 @@ WebConsole.prototype = { */ openLink: function WC_openLink(aLink) { - this.chromeWindow.openUILinkIn(aLink, "tab"); + this.browserWindow.openUILinkIn(aLink, "tab"); }, /** @@ -530,12 +328,13 @@ WebConsole.prototype = { viewSourceInStyleEditor: function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine) { - let styleSheets = this.tab.linkedBrowser.contentWindow.document.styleSheets; + let styleSheets = {}; + if (this.target.isLocalTab) { + styleSheets = this.target.window.document.styleSheets; + } for each (let style in styleSheets) { if (style.href == aSourceURL) { - let target = TargetFactory.forTab(this.tab); - let gDevTools = this.chromeWindow.gDevTools; - gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) { + gDevTools.showToolbox(this.target, "styleeditor").then(function(toolbox) { toolbox.getCurrentPanel().selectStyleSheet(style, aSourceLine); }); return; @@ -549,15 +348,20 @@ WebConsole.prototype = { * Destroy the object. Call this method to avoid memory leaks when the Web * Console is closed. * - * @param function [aOnDestroy] - * Optional function to invoke when the Web Console instance is - * destroyed. + * @return object + * A Promise object that is resolved once the Web Console is closed. */ - destroy: function WC_destroy(aOnDestroy) + destroy: function WC_destroy() { - // Make sure that the console panel does not try to call - // deactivateHUDForContext() again. - this.consoleWindowUnregisterOnHide = false; + if (this._destroyer) { + return this._destroyer.promise; + } + + delete HUDService.hudReferences[this.hudId]; + + let tabWindow = this.target.isLocalTab ? this.target.window : null; + + this._destroyer = Promise.defer(); let popupset = this.mainPopupSet; let panels = popupset.querySelectorAll("panel[hudId=" + this.hudId + "]"); @@ -566,27 +370,27 @@ WebConsole.prototype = { } let onDestroy = function WC_onDestroyUI() { - // Remove the iframe and the consolePanel if the Web Console is inside a - // floating panel. - if (this.consolePanel && this.consolePanel.parentNode) { - this.consolePanel.hidePopup(); - this.consolePanel.parentNode.removeChild(this.consolePanel); - this.consolePanel = null; + try { + tabWindow && tabWindow.focus(); + } + catch (ex) { + // Tab focus can fail if the tab is closed. } - if (this.iframe.parentNode) { - this.iframe.parentNode.removeChild(this.iframe); - } + let id = WebConsoleUtils.supportsString(this.hudId); + Services.obs.notifyObservers(id, "web-console-destroyed", null); - aOnDestroy && aOnDestroy(); + this._destroyer.resolve(null); }.bind(this); if (this.ui) { - this.ui.destroy(onDestroy); + this.ui.destroy().then(onDestroy); } else { onDestroy(); } + + return this._destroyer.promise; }, }; @@ -595,9 +399,16 @@ WebConsole.prototype = { ////////////////////////////////////////////////////////////////////////// var HeadsUpDisplayUICommands = { - toggleHUD: function UIC_toggleHUD(aOptions) + /** + * Toggle the Web Console for the current tab. + * + * @return object + * A Promise for either the opening of the toolbox that holds the Web + * Console, or a Promise for the closing of the toolbox. + */ + toggleHUD: function UIC_toggleHUD() { - var window = HUDService.currentContext(); + let window = HUDService.currentContext(); let target = TargetFactory.forTab(window.gBrowser.selectedTab); let toolbox = gDevTools.getToolbox(target); @@ -606,84 +417,21 @@ var HeadsUpDisplayUICommands = { gDevTools.showToolbox(target, "webconsole"); }, - toggleRemoteHUD: function UIC_toggleRemoteHUD() - { - if (this.getOpenHUD()) { - this.toggleHUD(); - return; - } - - let host = Services.prefs.getCharPref("devtools.debugger.remote-host"); - let port = Services.prefs.getIntPref("devtools.debugger.remote-port"); - - let check = { value: false }; - let input = { value: host + ":" + port }; - - let result = Services.prompt.prompt(null, - l10n.getStr("remoteWebConsolePromptTitle"), - l10n.getStr("remoteWebConsolePromptMessage"), - input, null, check); - - if (!result) { - return; - } - - let parts = input.value.split(":"); - if (parts.length != 2) { - return; - } - - [host, port] = parts; - if (!host.length || !port.length) { - return; - } - - Services.prefs.setCharPref("devtools.debugger.remote-host", host); - Services.prefs.setIntPref("devtools.debugger.remote-port", port); - - this.toggleHUD({ - host: host, - port: port, - }); - }, - /** - * Find the hudId for the active chrome window. - * @return string|null - * The hudId or null if the active chrome window has no open Web + * 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. */ - getOpenHUD: function UIC_getOpenHUD() { - let chromeWindow = HUDService.currentContext(); - let hudId = "hud_" + chromeWindow.gBrowser.selectedTab.linkedPanel; - return hudId in HUDService.hudReferences ? hudId : null; - }, -}; - -////////////////////////////////////////////////////////////////////////// -// WebConsoleObserver -////////////////////////////////////////////////////////////////////////// - -var WebConsoleObserver = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), - - init: function WCO_init() + getOpenHUD: function UIC_getOpenHUD() { - Services.obs.addObserver(this, "quit-application-granted", false); - }, - - observe: function WCO_observe(aSubject, aTopic) - { - if (aTopic == "quit-application-granted") { - HUDService.shutdown(); - } - }, - - uninit: function WCO_uninit() - { - Services.obs.removeObserver(this, "quit-application-granted"); + let window = HUDService.currentContext(); + let target = TargetFactory.forTab(window.gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + let panel = toolbox ? toolbox.getPanel("webconsole") : null; + return panel ? panel.hud : null; }, }; const HUDService = new HUD_SERVICE(); - diff --git a/browser/devtools/webconsole/WebConsolePanel.jsm b/browser/devtools/webconsole/WebConsolePanel.jsm index 3b017db8a259..5200718c6f76 100644 --- a/browser/devtools/webconsole/WebConsolePanel.jsm +++ b/browser/devtools/webconsole/WebConsolePanel.jsm @@ -9,12 +9,12 @@ this.EXPORTED_SYMBOLS = [ "WebConsolePanel" ]; const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/commonjs/promise/core.js"); -Cu.import("resource:///modules/devtools/EventEmitter.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "HUDService", - "resource:///modules/HUDService.jsm"); + "resource:///modules/HUDService.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", + "resource:///modules/devtools/EventEmitter.jsm"); /** * A DevToolPanel that controls the Web Console. @@ -26,32 +26,29 @@ function WebConsolePanel(iframeWindow, toolbox) { } WebConsolePanel.prototype = { + hud: null, + /** - * open is effectively an asynchronous constructor + * Open is effectively an asynchronous constructor. + * + * @return object + * A Promise that is resolved when the Web Console completes opening. */ - open: function StyleEditor_open() { - let parentDoc = this._frameWindow.document.defaultView.parent.document; + open: function WCP_open() + { + let parentDoc = this._toolbox.doc; let iframe = parentDoc.getElementById("toolbox-panel-iframe-webconsole"); - this.hud = HUDService.activateHUDForContext(this.target.tab, iframe, - this._toolbox.target); + let promise = HUDService.openWebConsole(this.target, iframe); - let deferred = Promise.defer(); - - let hudId = this.hud.hudId; - let onOpen = function _onWebConsoleOpen(aSubject) { - aSubject.QueryInterface(Ci.nsISupportsString); - if (hudId == aSubject.data) { - Services.obs.removeObserver(onOpen, "web-console-created"); - - this._isReady = true; - this.emit("ready"); - deferred.resolve(this); - } - }.bind(this); - - Services.obs.addObserver(onOpen, "web-console-created", false); - - return deferred.promise; + return promise.then(function onSuccess(aWebConsole) { + this.hud = aWebConsole; + this._isReady = true; + this.emit("ready"); + return this; + }.bind(this), function onError(aReason) { + Cu.reportError("WebConsolePanel open failed. " + + aReason.error + ": " + aReason.message); + }); }, get target() this._toolbox.target, @@ -61,28 +58,15 @@ WebConsolePanel.prototype = { destroy: function WCP_destroy() { - if (this.destroyer) { - return this.destroyer.promise; + if (this._destroyer) { + return this._destroyer; } - this.destroyer = Promise.defer(); + this._destroyer = this.hud.destroy(); + this._destroyer.then(function() { + this.emit("destroyed"); + }.bind(this)); - let hudId = this.hud.hudId; - - let onClose = function _onWebConsoleClose(aSubject) - { - aSubject.QueryInterface(Ci.nsISupportsString); - if (hudId == aSubject.data) { - Services.obs.removeObserver(onClose, "web-console-destroyed"); - - this.emit("destroyed"); - this.destroyer.resolve(null); - } - }.bind(this); - - Services.obs.addObserver(onClose, "web-console-destroyed", false); - HUDService.deactivateHUDForContext(this.hud.tab, false); - - return this.destroyer.promise; + return this._destroyer; }, }; diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js index 4b8c7f315854..86a9d8ff8afc 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js @@ -28,14 +28,17 @@ function testClosingAfterCompletion(hud) { // Focus the inputNode and perform the keycombo to close the WebConsole. inputNode.focus(); - EventUtils.synthesizeKey("k", { accelKey: true, shiftKey: true }); - // We can't test for errors right away, because the error occurs after a - // setTimeout(..., 0) in the WebConsole code. - executeSoon(function() { + gDevTools.once("toolbox-destroyed", function() { browser.removeEventListener("error", errorListener, false); is(errorWhileClosing, false, "no error while closing the WebConsole"); finishTest(); }); + + if (Services.appinfo.OS == "Darwin") { + EventUtils.synthesizeKey("k", { accelKey: true, altKey: true }); + } else { + EventUtils.synthesizeKey("k", { accelKey: true, shiftKey: true }); + } } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js b/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js index ab6e0f3ac855..fa35bd21d4a9 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js @@ -57,13 +57,14 @@ function openConsoles() { let tab = openTabs[i]; openConsole(tab, function(index, hud) { ok(hud, "HUD is open for tab " + index); - let window = hud.tab.linkedBrowser.contentWindow; + let window = hud.target.tab.linkedBrowser.contentWindow; window.console.log("message for tab " + index); consolesOpen++; }.bind(null, i)); } waitForSuccess({ + timeout: 10000, name: "4 web consoles opened", validatorFn: function() { diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js b/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js index ac3a67ab518d..478f6f0a1df4 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js @@ -67,6 +67,7 @@ function onStyleEditorReady(aEvent, aPanel) return sheet; } } + return null; } waitForFocus(function() { diff --git a/browser/devtools/webconsole/test/head.js b/browser/devtools/webconsole/test/head.js index 0284fb39f2c5..413339a1f35a 100644 --- a/browser/devtools/webconsole/test/head.js +++ b/browser/devtools/webconsole/test/head.js @@ -238,9 +238,11 @@ function finishTest() finish(); return; } - hud.jsterm.clearOutput(true); + if (hud.jsterm) { + hud.jsterm.clearOutput(true); + } - closeConsole(hud.tab, finish); + closeConsole(hud.target.tab, finish); hud = null; } diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index 2d9ebc753944..a32d97db5cc3 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -43,6 +43,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup", XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", "resource://gre/modules/devtools/WebConsoleUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/commonjs/promise/core.js"); + const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let l10n = new WebConsoleUtils.l10n(STRINGS_URI); @@ -168,9 +171,9 @@ const MIN_FONT_SIZE = 10; const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout"; /** - * A WebConsoleFrame instance is an interactive console initialized *per tab* + * A WebConsoleFrame instance is an interactive console initialized *per target* * that displays console log data as well as provides an interactive terminal to - * manipulate the current tab's document content. + * manipulate the target's document content. * * The WebConsoleFrame is responsible for the actual Web Console UI * implementation. @@ -190,18 +193,9 @@ function WebConsoleFrame(aWebConsoleOwner) this._toggleFilter = this._toggleFilter.bind(this); this._flushMessageQueue = this._flushMessageQueue.bind(this); - this._connectionTimeout = this._connectionTimeout.bind(this); this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this._outputTimerInitialized = false; - - this._initDefaultFilterPrefs(); - this._commandController = new CommandController(this); - this.positionConsole(window); - - this.jsterm = new JSTerm(this); - this.jsterm.inputNode.focus(); - this._initConnection(); } WebConsoleFrame.prototype = { @@ -222,13 +216,6 @@ WebConsoleFrame.prototype = { */ proxy: null, - /** - * Timer used for the connection. - * @private - * @type object - */ - _connectTimer: null, - /** * Getter for the xul:popupset that holds any popups we open. * @type nsIDOMElement @@ -306,7 +293,7 @@ WebConsoleFrame.prototype = { groupDepth: 0, /** - * The current tab location. + * The current target location. * @type string */ contentLocation: "", @@ -336,6 +323,8 @@ WebConsoleFrame.prototype = { */ get webConsoleClient() this.proxy ? this.proxy.webConsoleClient : null, + _destroyer: null, + _saveRequestAndResponseBodies: false, /** @@ -364,60 +353,51 @@ WebConsoleFrame.prototype = { }.bind(this)); }, + /** + * Initialize the WebConsoleFrame instance. + * @return object + * A Promise object for the initialization. + */ + init: function WCF_init() + { + this._initUI(); + return this._initConnection(); + }, + /** * Connect to the server using the remote debugging protocol. + * * @private + * @return object + * A Promise object that is resolved/reject based on the connection + * result. */ _initConnection: function WCF__initConnection() { + let deferred = Promise.defer(); + this.proxy = new WebConsoleConnectionProxy(this, this.owner.target); - let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT); - this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this._connectTimer.initWithCallback(this._connectionTimeout, - timeout, Ci.nsITimer.TYPE_ONE_SHOT); + let onSuccess = function() { + this.saveRequestAndResponseBodies = this._saveRequestAndResponseBodies; + deferred.resolve(this); + }.bind(this); - this.proxy.connect(function() { - // Don't complete connection if the connection timed-out. - if (this._connectTimer) { - this._connectTimer.cancel(); - this._connectTimer = null; - this.saveRequestAndResponseBodies = this._saveRequestAndResponseBodies; - this._onInitComplete(); - } - }.bind(this)); - }, + let onFailure = function(aReason) { + let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR, + aReason.error + ": " + aReason.message); + this.outputMessage(CATEGORY_JS, node); + deferred.reject(aReason); + }.bind(this); - /** - * Connection timeout handler. This method simply prints a message informing - * the user that the connection timed-out. - * @private - */ - _connectionTimeout: function WCF__connectionTimeout() - { - this._connectTimer = null; + let sendNotification = function() { + let id = WebConsoleUtils.supportsString(this.hudId); + Services.obs.notifyObservers(id, "web-console-created", null); + }.bind(this); - let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR, - l10n.getStr("connectionTimeout")); - this.outputMessage(CATEGORY_JS, node); + this.proxy.connect().then(onSuccess, onFailure).then(sendNotification); - // Allow initialization to complete. - this._onInitComplete(); - }, - - /** - * Reset the connection timeout timer. - * @private - */ - _resetConnectionTimeout: function WCF__resetConnectionTimeout() - { - let timer = this._connectTimer; - if (timer) { - let timeout = timer.delay; - timer.cancel(); - timer.initWithCallback(this._connectionTimeout, timeout, - Ci.nsITimer.TYPE_ONE_SHOT); - } + return deferred.promise; }, /** @@ -426,6 +406,18 @@ WebConsoleFrame.prototype = { */ _initUI: function WCF__initUI() { + // Remember that this script is loaded in the webconsole.xul context: + // |window| is the iframe global. + this.window = window; + this.document = this.window.document; + this.rootElement = this.document.documentElement; + + this._initDefaultFilterPrefs(); + + // Register the controller to handle "select all" properly. + this._commandController = new CommandController(this); + this.window.controllers.insertControllerAt(0, this._commandController); + let doc = this.document; this.filterBox = doc.querySelector(".hud-filter-box"); @@ -482,6 +474,10 @@ WebConsoleFrame.prototype = { this.owner._onClearButton(); this.jsterm.clearOutput(true); }.bind(this)); + + this.jsterm = new JSTerm(this); + this.jsterm.init(); + this.jsterm.inputNode.focus(); }, /** @@ -567,54 +563,6 @@ WebConsoleFrame.prototype = { }, this); }, - /** - * Callback method for when the Web Console initialization is complete. For - * now this method sends the web-console-created notification using the - * nsIObserverService. - * - * @private - */ - _onInitComplete: function WC__onInitComplete() - { - let id = WebConsoleUtils.supportsString(this.hudId); - Services.obs.notifyObservers(id, "web-console-created", null); - }, - - /** - * Position the console in a different location. - * - * Note: you do not usually call this method. This is called by the WebConsole - * instance that owns this iframe. You need to call this if you write - * a different owner or you manually reposition the iframe. - * - * @param object aNewWindow - * Repositioning causes the iframe to reload - bug 254144. You need to - * provide the new window object so we can reinitialize the UI as - * needed. - */ - positionConsole: function WCF_positionConsole(aNewWindow) - { - this.window = aNewWindow; - this.document = this.window.document; - this.rootElement = this.document.documentElement; - - // register the controller to handle "select all" properly - this.window.controllers.insertControllerAt(0, this._commandController); - - let oldOutputNode = this.outputNode; - - this._initUI(); - this.jsterm && this.jsterm._initUI(); - - if (oldOutputNode && oldOutputNode.childNodes.length) { - let parentNode = this.outputNode.parentNode; - parentNode.replaceChild(oldOutputNode, this.outputNode); - this.outputNode = oldOutputNode; - } - - this.jsterm && this.jsterm.inputNode.focus(); - }, - /** * Increase, decrease or reset the font size. * @@ -2706,15 +2654,21 @@ WebConsoleFrame.prototype = { }, /** - * Destroy the HUD object. Call this method to avoid memory leaks when the Web - * Console is closed. + * Destroy the WebConsoleFrame object. Call this method to avoid memory leaks + * when the Web Console is closed. * - * @param function [aOnDestroy] - * Optional function to invoke when the Web Console instance is - * destroyed. + * @return object + * A Promise that is resolved when the WebConsoleFrame instance is + * destroyed. */ - destroy: function WCF_destroy(aOnDestroy) + destroy: function WCF_destroy() { + if (this._destroyer) { + return this._destroyer.promise; + } + + this._destroyer = Promise.defer(); + this._cssNodes = {}; this._outputQueue = []; this._pruneCategoriesQueue = {}; @@ -2726,22 +2680,26 @@ WebConsoleFrame.prototype = { } this._outputTimer = null; - if (this._connectTimer) { - this._connectTimer.cancel(); - } - this._connectTimer = null; - - if (this.proxy) { - this.proxy.disconnect(aOnDestroy); - this.proxy = null; - } - if (this.jsterm) { this.jsterm.destroy(); this.jsterm = null; } this._commandController = null; + + let onDestroy = function() { + this._destroyer.resolve(null); + }.bind(this); + + if (this.proxy) { + this.proxy.disconnect().then(onDestroy); + this.proxy = null; + } + else { + onDestroy(); + } + + return this._destroyer.promise; }, }; @@ -2763,12 +2721,8 @@ function JSTerm(aWebConsoleFrame) this.history = []; this.historyIndex = 0; this.historyPlaceHolder = 0; // this.history.length; - this.autocompletePopup = new AutocompletePopup(this.hud.owner.chromeDocument); - this.autocompletePopup.onSelect = this.onAutocompleteSelect.bind(this); - this.autocompletePopup.onClick = this.acceptProposedCompletion.bind(this); this._keyPress = this.keyPress.bind(this); this._inputEventHandler = this.inputEventHandler.bind(this); - this._initUI(); } JSTerm.prototype = { @@ -2790,6 +2744,10 @@ JSTerm.prototype = { */ history: null, + autocompletePopup: null, + inputNode: null, + completeNode: null, + /** * Getter for the element that holds the messages we display. * @type nsIDOMElement @@ -2808,10 +2766,14 @@ JSTerm.prototype = { /** * Initialize the JSTerminal UI. - * @private */ - _initUI: function JST__initUI() + init: function JST_init() { + let chromeDocument = this.hud.owner.chromeDocument; + this.autocompletePopup = new AutocompletePopup(chromeDocument); + this.autocompletePopup.onSelect = this.onAutocompleteSelect.bind(this); + this.autocompletePopup.onClick = this.acceptProposedCompletion.bind(this); + let doc = this.hud.document; this.completeNode = doc.querySelector(".jsterm-complete-node"); this.inputNode = doc.querySelector(".jsterm-input-node"); @@ -3819,6 +3781,12 @@ JSTerm.prototype = { this.autocompletePopup.destroy(); this.autocompletePopup = null; + let popup = this.hud.owner.chromeDocument + .getElementById("webConsole_autocompletePopup"); + if (popup) { + popup.parentNode.removeChild(popup); + } + this.inputNode.removeEventListener("keypress", this._keyPress, false); this.inputNode.removeEventListener("input", this._inputEventHandler, false); this.inputNode.removeEventListener("keyup", this._inputEventHandler, false); @@ -4047,6 +4015,11 @@ function WebConsoleConnectionProxy(aWebConsole, aTarget) this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this); this._onFileActivity = this._onFileActivity.bind(this); this._onTabNavigated = this._onTabNavigated.bind(this); + this._onListTabs = this._onListTabs.bind(this); + this._onAttachTab = this._onAttachTab.bind(this); + this._onAttachConsole = this._onAttachConsole.bind(this); + this._onCachedMessages = this._onCachedMessages.bind(this); + this._connectionTimeout = this._connectionTimeout.bind(this); } WebConsoleConnectionProxy.prototype = { @@ -4092,6 +4065,16 @@ WebConsoleConnectionProxy.prototype = { */ connected: false, + /** + * Timer used for the connection. + * @private + * @type object + */ + _connectTimer: null, + + _connectDefer: null, + _disconnecter: null, + /** * The WebConsoleActor ID. * @@ -4130,11 +4113,31 @@ WebConsoleConnectionProxy.prototype = { /** * Initialize a debugger client and connect it to the debugger server. * - * @param function [aCallback] - * Optional function to invoke when connection is established. + * @return object + * A Promise object that is resolved/rejected based on the success of + * the connection initialization. */ - connect: function WCCP_connect(aCallback) + connect: function WCCP_connect() { + if (this._connectDefer) { + return this._connectDefer.promise; + } + + this._connectDefer = Promise.defer(); + + let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT); + this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._connectTimer.initWithCallback(this._connectionTimeout, + timeout, Ci.nsITimer.TYPE_ONE_SHOT); + + let promise = this._connectDefer.promise; + promise.then(function _onSucess() { + this._connectTimer.cancel(); + this._connectTimer = null; + }.bind(this), function _onFailure() { + this._connectTimer = null; + }.bind(this)); + // TODO: convert the non-remote path to use the target API as well. let transport, client; if (this.target.isRemote) { @@ -4143,7 +4146,6 @@ WebConsoleConnectionProxy.prototype = { else { this.initServer(); transport = DebuggerServer.connectPipe(); - client = this.client = new DebuggerClient(transport); } @@ -4157,39 +4159,54 @@ WebConsoleConnectionProxy.prototype = { if (this.target.isRemote) { if (!this.target.chrome) { // target.form is a TabActor grip - this._attachTab(this.target.form, aCallback); + this._attachTab(this.target.form); } else { // target.form is a RootActor grip this._consoleActor = this.target.form.consoleActor; - this._attachConsole(aCallback); + this._attachConsole(); } } else { client.connect(function(aType, aTraits) { - client.listTabs(this._onListTabs.bind(this, aCallback)); + client.listTabs(this._onListTabs); }.bind(this)); } + + return promise; + }, + + /** + * Connection timeout handler. + * @private + */ + _connectionTimeout: function WCCP__connectionTimeout() + { + let error = { + error: "timeout", + message: l10n.getStr("connectionTimeout"), + }; + + this._connectDefer.reject(error); }, /** * The "listTabs" response handler. * * @private - * @param function [aCallback] - * Optional function to invoke once the connection is established. * @param object aResponse * The JSON response object received from the server. */ - _onListTabs: function WCCP__onListTabs(aCallback, aResponse) + _onListTabs: function WCCP__onListTabs(aResponse) { if (aResponse.error) { Cu.reportError("listTabs failed: " + aResponse.error + " " + aResponse.message); + this._connectDefer.reject(aResponse); return; } - this._attachTab(aResponse.tabs[aResponse.selected], aCallback); + this._attachTab(aResponse.tabs[aResponse.selected]); }, /** @@ -4198,74 +4215,65 @@ WebConsoleConnectionProxy.prototype = { * @private * @param object aTab * Grip for the tab to attach to. - * @param function aCallback - * Function to invoke when the connection is established. */ - _attachTab: function WCCP__attachTab(aTab, aCallback) + _attachTab: function WCCP__attachTab(aTab) { this._consoleActor = aTab.consoleActor; this._tabActor = aTab.actor; this.owner.onLocationChange(aTab.url, aTab.title); - this.client.attachTab(this._tabActor, - this._onAttachTab.bind(this, aCallback)); + this.client.attachTab(this._tabActor, this._onAttachTab); }, /** * The "attachTab" response handler. * * @private - * @param function [aCallback] - * Optional function to invoke once the connection is established. * @param object aResponse * The JSON response object received from the server. * @param object aTabClient * The TabClient instance for the attached tab. */ - _onAttachTab: function WCCP__onAttachTab(aCallback, aResponse, aTabClient) + _onAttachTab: function WCCP__onAttachTab(aResponse, aTabClient) { if (aResponse.error) { Cu.reportError("attachTab failed: " + aResponse.error + " " + aResponse.message); + this._connectDefer.reject(aResponse); return; } this.tabClient = aTabClient; - this._attachConsole(aCallback); + this._attachConsole(); }, /** * Attach to the Web Console actor. - * * @private - * @param function aCallback - * Function to invoke when the connection is established. */ - _attachConsole: function WCCP__attachConsole(aCallback) + _attachConsole: function WCCP__attachConsole() { let listeners = ["PageError", "ConsoleAPI", "NetworkActivity", "FileActivity"]; this.client.attachConsole(this._consoleActor, listeners, - this._onAttachConsole.bind(this, aCallback)); + this._onAttachConsole); }, /** * The "attachConsole" response handler. * * @private - * @param function [aCallback] - * Optional function to invoke once the connection is established. * @param object aResponse * The JSON response object received from the server. * @param object aWebConsoleClient * The WebConsoleClient instance for the attached console, for the * specific tab we work with. */ - _onAttachConsole: - function WCCP__onAttachConsole(aCallback, aResponse, aWebConsoleClient) + _onAttachConsole: function WCCP__onAttachConsole(aResponse, aWebConsoleClient) { if (aResponse.error) { Cu.reportError("attachConsole failed: " + aResponse.error + " " + aResponse.message); + this._connectDefer.reject(aResponse); return; } @@ -4274,27 +4282,31 @@ WebConsoleConnectionProxy.prototype = { this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI; let msgs = ["PageError", "ConsoleAPI"]; - this.webConsoleClient.getCachedMessages(msgs, - this._onCachedMessages.bind(this, aCallback)); + this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages); }, /** * The "cachedMessages" response handler. * * @private - * @param function [aCallback] - * Optional function to invoke once the connection is established. * @param object aResponse * The JSON response object received from the server. */ - _onCachedMessages: function WCCP__onCachedMessages(aCallback, aResponse) + _onCachedMessages: function WCCP__onCachedMessages(aResponse) { if (aResponse.error) { Cu.reportError("Web Console getCachedMessages error: " + aResponse.error + " " + aResponse.message); + this._connectDefer.reject(aResponse); return; } + if (!this._connectTimer) { + // This happens if the Promise is rejected (eg. a timeout), but the + // connection attempt is successful, nonetheless. + Cu.reportError("Web Console getCachedMessages error: invalid state."); + } + this.owner.displayCachedMessages(aResponse.messages); if (!this._hasNativeConsoleAPI) { @@ -4302,7 +4314,7 @@ WebConsoleConnectionProxy.prototype = { } this.connected = true; - aCallback && aCallback(); + this._connectDefer.resolve(this); }, /** @@ -4432,30 +4444,33 @@ WebConsoleConnectionProxy.prototype = { /** * Disconnect the Web Console from the remote server. * - * @param function [aOnDisconnect] - * Optional function to invoke when the connection is dropped. + * @return object + * A Promise object that is resolved when disconnect completes. */ - disconnect: function WCCP_disconnect(aOnDisconnect) + disconnect: function WCCP_disconnect() { + if (this._disconnecter) { + return this._disconnecter.promise; + } + + this._disconnecter = Promise.defer(); + if (!this.client) { - aOnDisconnect && aOnDisconnect(); - return; + this._disconnecter.resolve(null); + return this._disconnecter.promise; } let onDisconnect = function() { if (timer) { timer.cancel(); timer = null; + this._disconnecter.resolve(null); } - if (aOnDisconnect) { - aOnDisconnect(); - aOnDisconnect = null; - } - }; + }.bind(this); let timer = null; let remoteTarget = this.target.isRemote; - if (aOnDisconnect && !remoteTarget) { + if (!remoteTarget) { timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); timer.initWithCallback(onDisconnect, 1500, Ci.nsITimer.TYPE_ONE_SHOT); } @@ -4476,20 +4491,21 @@ WebConsoleConnectionProxy.prototype = { this.connected = false; this.owner = null; - try { - if (!remoteTarget) { + if (!remoteTarget) { + try { client.close(onDisconnect); } + catch (ex) { + Cu.reportError("Web Console disconnect exception: " + ex); + Cu.reportError(ex.stack); + onDisconnect(); + } } - catch (ex) { - Cu.reportError("Web Console disconnect exception: " + ex); - Cu.reportError(ex.stack); + else { onDisconnect(); } - if (remoteTarget) { - onDisconnect(); - } + return this._disconnecter.promise; }, }; diff --git a/toolkit/devtools/webconsole/dbg-webconsole-actors.js b/toolkit/devtools/webconsole/dbg-webconsole-actors.js index 91665cc17c51..f58abd3fe465 100644 --- a/toolkit/devtools/webconsole/dbg-webconsole-actors.js +++ b/toolkit/devtools/webconsole/dbg-webconsole-actors.js @@ -183,7 +183,7 @@ WebConsoleActor.prototype = this.consoleProgressListener.destroy(); this.consoleProgressListener = null; } - this.conn.removeActorPool(this.actorPool); + this.conn.removeActorPool(this._actorPool); this._actorPool = null; this.sandbox = null; this._sandboxWindowId = 0;