Bug 777428 - Make it possible to debug webapps running in desktop webapp runtime. r=past
authorMarco Castelluccio <mar.castelluccio@studenti.unina.it>
Tue, 10 Sep 2013 20:59:04 -0400
changeset 159413 8a0b5eb332ff958338e5a6bf8e57eff2ff5830fe
parent 159365 f73bed2856a8eeae0dbeb395f50718cf7ec23942
child 159414 cdc016c22761a88039747c324f42f08c47df68bb
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs777428
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 777428 - Make it possible to debug webapps running in desktop webapp runtime. r=past
browser/installer/package-manifest.in
toolkit/devtools/server/actors/webbrowser.js
toolkit/devtools/server/main.js
webapprt/CommandLineHandler.js
webapprt/RemoteDebugger.jsm
webapprt/content/dbg-webapp-actors.js
webapprt/jar.mn
webapprt/moz.build
webapprt/prefs.js
webapprt/test/chrome/Makefile.in
webapprt/test/chrome/browser_debugger.js
webapprt/test/chrome/debugger.html
webapprt/test/chrome/debugger.webapp
webapprt/test/chrome/debugger.webapp^headers^
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -760,16 +760,17 @@ bin/libfreebl_32int64_3.so
 @BINPATH@/webapprt/components/CommandLineHandler.js
 @BINPATH@/webapprt/components/ContentPermission.js
 @BINPATH@/webapprt/components/DirectoryProvider.js
 @BINPATH@/webapprt/components/components.manifest
 @BINPATH@/webapprt/defaults/preferences/prefs.js
 @BINPATH@/webapprt/modules/Startup.jsm
 @BINPATH@/webapprt/modules/WebappRT.jsm
 @BINPATH@/webapprt/modules/WebappsHandler.jsm
+@BINPATH@/webapprt/modules/RemoteDebugger.jsm
 #endif
 
 #ifdef MOZ_METRO
 @BINPATH@/components/MetroUIUtils.js
 @BINPATH@/components/MetroUIUtils.manifest
 [metro]
 ; gre resources
 @BINPATH@/CommandExecuteHandler@BIN_SUFFIX@
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -32,17 +32,17 @@ function appShellDOMWindowType(aWindow) 
   /* This is what nsIWindowMediator's enumerator checks. */
   return aWindow.document.documentElement.getAttribute('windowtype');
 }
 
 /**
  * Send Debugger:Shutdown events to all "navigator:browser" windows.
  */
 function sendShutdownEvent() {
-  for (let win of allAppShellDOMWindows("navigator:browser")) {
+  for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
     let evt = win.document.createEvent("Event");
     evt.initEvent("Debugger:Shutdown", true, false);
     win.document.documentElement.dispatchEvent(evt);
   }
 }
 
 /**
  * Construct a root actor appropriate for use in a server running in a
@@ -191,30 +191,30 @@ BrowserTabList.prototype._getSelectedBro
   return aWindow.gBrowser.selectedBrowser;
 };
 
 BrowserTabList.prototype._getChildren = function(aWindow) {
   return aWindow.gBrowser.browsers;
 };
 
 BrowserTabList.prototype.getList = function() {
-  let topXULWindow = windowMediator.getMostRecentWindow("navigator:browser");
+  let topXULWindow = windowMediator.getMostRecentWindow(DebuggerServer.chromeWindowType);
 
   // As a sanity check, make sure all the actors presently in our map get
   // picked up when we iterate over all windows' tabs.
   let initialMapSize = this._actorByBrowser.size;
   let foundCount = 0;
 
   // To avoid mysterious behavior if tabs are closed or opened mid-iteration,
   // we update the map first, and then make a second pass over it to yield
   // the actors. Thus, the sequence yielded is always a snapshot of the
   // actors that were live when we began the iteration.
 
   // Iterate over all navigator:browser XUL windows.
-  for (let win of allAppShellDOMWindows("navigator:browser")) {
+  for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
     let selectedBrowser = this._getSelectedBrowser(win);
 
     // For each tab in this XUL window, ensure that we have an actor for
     // it, reusing existing actors where possible. We actually iterate
     // over 'browser' XUL elements, and BrowserTabActor uses
     // browser.contentWindow.wrappedJSObject as the debuggee global.
     for (let browser of this._getChildren(win)) {
       // Do we have an existing actor for this browser? If not, create one.
@@ -328,17 +328,17 @@ BrowserTabList.prototype._checkListening
  *    The name of a guard property of 'this', indicating whether we're
  *    already listening for those events.
  * @param aEventNames array of strings
  *    An array of event names.
  */
 BrowserTabList.prototype._listenForEventsIf = function(aShouldListen, aGuard, aEventNames) {
   if (!aShouldListen !== !this[aGuard]) {
     let op = aShouldListen ? "addEventListener" : "removeEventListener";
-    for (let win of allAppShellDOMWindows("navigator:browser")) {
+    for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
       for (let name of aEventNames) {
         win[op](name, this, false);
       }
     }
     this[aGuard] = aShouldListen;
   }
 };
 
@@ -386,17 +386,17 @@ BrowserTabList.prototype._listenToMediat
  */
 BrowserTabList.prototype.onWindowTitleChange = () => { };
 
 BrowserTabList.prototype.onOpenWindow = makeInfallible(function(aWindow) {
   let handleLoad = makeInfallible(() => {
     /* We don't want any further load events from this window. */
     aWindow.removeEventListener("load", handleLoad, false);
 
-    if (appShellDOMWindowType(aWindow) !== "navigator:browser")
+    if (appShellDOMWindowType(aWindow) !== DebuggerServer.chromeWindowType)
       return;
 
     // Listen for future tab activity.
     if (this._listeningForTabOpen) {
       aWindow.addEventListener("TabOpen", this, false);
       aWindow.addEventListener("TabSelect", this, false);
     }
     if (this._listeningForTabClose) {
@@ -420,17 +420,17 @@ BrowserTabList.prototype.onOpenWindow = 
 
   aWindow.addEventListener("load", handleLoad, false);
 }, "BrowserTabList.prototype.onOpenWindow");
 
 BrowserTabList.prototype.onCloseWindow = makeInfallible(function(aWindow) {
   aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindow);
 
-  if (appShellDOMWindowType(aWindow) !== "navigator:browser")
+  if (appShellDOMWindowType(aWindow) !== DebuggerServer.chromeWindowType)
     return;
 
   /*
    * nsIWindowMediator deadlocks if you call its GetEnumerator method from
    * a nsIWindowMediatorListener's onCloseWindow hook (bug 873589), so
    * handle the close in a different tick.
    */
   Services.tm.currentThread.dispatch(makeInfallible(() => {
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -345,18 +345,18 @@ var DebuggerServer = {
     mod.module.unregister(mod.api);
     mod.api.destroy();
     delete gRegisteredModules[id];
   },
 
   /**
    * Install Firefox-specific actors.
    */
-  addBrowserActors: function DS_addBrowserActors() {
-    this.chromeWindowType = "navigator:browser";
+  addBrowserActors: function(aWindowType) {
+    this.chromeWindowType = aWindowType ? aWindowType : "navigator:browser";
     this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
     this.addActors("resource://gre/modules/devtools/server/actors/script.js");
     this.addGlobalActor(this.ChromeDebuggerActor, "chromeDebugger");
     this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
     this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
     if ("nsIProfiler" in Ci)
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
 
--- a/webapprt/CommandLineHandler.js
+++ b/webapprt/CommandLineHandler.js
@@ -16,16 +16,22 @@ CommandLineHandler.prototype = {
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
 
   handle: function handle(cmdLine) {
     let args = Cc["@mozilla.org/hash-property-bag;1"].
                createInstance(Ci.nsIWritablePropertyBag);
     let inTestMode = this._handleTestMode(cmdLine, args);
 
+    let debugPort = this._handleDebugMode(cmdLine);
+    if (!isNaN(debugPort)) {
+      Cu.import("resource://webapprt/modules/RemoteDebugger.jsm");
+      RemoteDebugger.init(debugPort);
+    }
+
     if (inTestMode) {
       // Open the mochitest shim window, which configures the runtime for tests.
       Services.ww.openWindow(null,
                              "chrome://webapprt/content/mochitest.xul",
                              "_blank",
                              "chrome,dialog=no",
                              args);
     } else {
@@ -36,16 +42,44 @@ CommandLineHandler.prototype = {
                                           "chrome,dialog=no,resizable,scrollbars,centerscreen",
                                           null);
       // Load the module to start up the app
       Cu.import("resource://webapprt/modules/Startup.jsm");
       startup(window);
     }
   },
 
+  /**
+   * Handle debug command line option.
+   *
+   * @param cmdLine A nsICommandLine object.
+   *
+   * @returns the port number if it's specified, the default port number if
+   *          the debug option is specified, NaN if the debug option isn't
+   *          specified or the port number isn't valid.
+   */
+  _handleDebugMode: function(cmdLine) {
+    // -debug [port]
+    let idx = cmdLine.findFlag("debug", true);
+    if (idx < 0) {
+      return NaN;
+    }
+
+    let port;
+    let portIdx = idx + 1;
+    if (portIdx < cmdLine.length) {
+      port = parseInt(cmdLine.getArgument(portIdx));
+      if (port != NaN) {
+        return port;
+      }
+    }
+
+    return Services.prefs.getIntPref('devtools.debugger.remote-port');
+  },
+
   _handleTestMode: function _handleTestMode(cmdLine, args) {
     // -test-mode [url]
     let idx = cmdLine.findFlag("test-mode", true);
     if (idx < 0)
       return false;
     let url;
     let urlIdx = idx + 1;
     if (urlIdx < cmdLine.length) {
new file mode 100644
--- /dev/null
+++ b/webapprt/RemoteDebugger.jsm
@@ -0,0 +1,26 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["RemoteDebugger"];
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import('resource://gre/modules/devtools/dbg-server.jsm');
+
+this.RemoteDebugger = {
+  init: function(port) {
+    if (!DebuggerServer.initialized) {
+      DebuggerServer.init();
+      DebuggerServer.addBrowserActors("webapprt:webapp");
+      DebuggerServer.addActors("chrome://webapprt/content/dbg-webapp-actors.js");
+    }
+    DebuggerServer.openListener(port);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/webapprt/content/dbg-webapp-actors.js
@@ -0,0 +1,126 @@
+/* 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';
+
+let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise;
+
+/**
+ * WebappRT-specific actors.
+ */
+
+/**
+ * Construct a root actor appropriate for use in a server running in the webapp
+ * runtime. The returned root actor:
+ * - respects the factories registered with DebuggerServer.addGlobalActor,
+ * - uses a WebappTabList to supply tab actors,
+ * - sends all webapprt:webapp window documents a Debugger:Shutdown event
+ *   when it exits.
+ *
+ * * @param connection DebuggerServerConnection
+ *        The conection to the client.
+ */
+function createRootActor(connection)
+{
+  let parameters = {
+    tabList: new WebappTabList(connection),
+    globalActorFactories: DebuggerServer.globalActorFactories,
+    onShutdown: sendShutdownEvent
+  };
+  return new RootActor(connection, parameters);
+}
+
+/**
+ * A live list of BrowserTabActors representing the current webapp windows,
+ * to be provided to the root actor to answer 'listTabs' requests. In the
+ * webapp runtime, only a single tab per window is ever present.
+ *
+ * @param connection DebuggerServerConnection
+ *     The connection in which this list's tab actors may participate.
+ *
+ * @see BrowserTabList for more a extensive description of how tab list objects
+ *      work.
+ */
+function WebappTabList(connection)
+{
+  BrowserTabList.call(this, connection);
+}
+
+WebappTabList.prototype = Object.create(BrowserTabList.prototype);
+
+WebappTabList.prototype.constructor = WebappTabList;
+
+WebappTabList.prototype.getList = function() {
+  let topXULWindow = windowMediator.getMostRecentWindow(this._windowType);
+
+  // As a sanity check, make sure all the actors presently in our map get
+  // picked up when we iterate over all windows.
+  let initialMapSize = this._actorByBrowser.size;
+  let foundCount = 0;
+
+  // To avoid mysterious behavior if windows are closed or opened mid-iteration,
+  // we update the map first, and then make a second pass over it to yield
+  // the actors. Thus, the sequence yielded is always a snapshot of the
+  // actors that were live when we began the iteration.
+
+  // Iterate over all webapprt:webapp XUL windows.
+  for (let win of allAppShellDOMWindows(this._windowType)) {
+    let browser = win.document.getElementById("content");
+    if (!browser) {
+      continue;
+    }
+
+    // Do we have an existing actor for this browser? If not, create one.
+    let actor = this._actorByBrowser.get(browser);
+    if (actor) {
+      foundCount++;
+    } else {
+      actor = new WebappTabActor(this._connection, browser);
+      this._actorByBrowser.set(browser, actor);
+    }
+
+    actor.selected = (win == topXULWindow);
+  }
+
+  if (this._testing && initialMapSize !== foundCount) {
+    throw Error("_actorByBrowser map contained actors for dead tabs");
+  }
+
+  this._mustNotify = true;
+  this._checkListening();
+
+  return promise.resolve([actor for ([_, actor] of this._actorByBrowser)]);
+};
+
+/**
+ * Creates a tab actor for handling requests to the single tab, like
+ * attaching and detaching. WebappTabActor respects the actor factories
+ * registered with DebuggerServer.addTabActor.
+ *
+ * We override the title of the XUL window in content/webapp.js so here
+ * we need to override the title property to avoid confusion to the user.
+ * We won't return the title of the contained browser, but the title of
+ * the webapp window.
+ *
+ * @param connection DebuggerServerConnection
+ *        The conection to the client.
+ * @param browser browser
+ *        The browser instance that contains this tab.
+ */
+function WebappTabActor(connection, browser)
+{
+  BrowserTabActor.call(this, connection, browser);
+}
+
+WebappTabActor.prototype.constructor = WebappTabActor;
+
+WebappTabActor.prototype = Object.create(BrowserTabActor.prototype);
+
+Object.defineProperty(WebappTabActor.prototype, "title", {
+  get: function() {
+    return this.browser.ownerDocument.defaultView.document.title;
+  },
+  enumerable: true,
+  configurable: false
+});
--- a/webapprt/jar.mn
+++ b/webapprt/jar.mn
@@ -4,8 +4,9 @@
 
 webapprt.jar:
 % content webapprt %content/
 * content/webapp.js                     (content/webapp.js)
 * content/webapp.xul                    (content/webapp.xul)
   content/mochitest-shared.js           (content/mochitest-shared.js)
   content/mochitest.js                  (content/mochitest.js)
   content/mochitest.xul                 (content/mochitest.xul)
+  content/dbg-webapp-actors.js          (content/dbg-webapp-actors.js)
--- a/webapprt/moz.build
+++ b/webapprt/moz.build
@@ -17,12 +17,13 @@ TEST_DIRS += ['test']
 EXTRA_COMPONENTS += [
     'CommandLineHandler.js',
     'ContentPermission.js',
     'DirectoryProvider.js',
     'components.manifest',
 ]
 
 EXTRA_JS_MODULES += [
+    'RemoteDebugger.jsm',
     'Startup.jsm',
     'WebappRT.jsm',
     'WebappsHandler.jsm',
 ]
--- a/webapprt/prefs.js
+++ b/webapprt/prefs.js
@@ -46,16 +46,19 @@ pref("dom.mozTCPSocket.enabled", true);
 // Enable smooth scrolling
 pref("general.smoothScroll", true);
 
 // Enable window resize and move
 pref("dom.always_allow_move_resize_window", true);
 
 pref("plugin.allowed_types", "application/x-shockwave-flash,application/futuresplash");
 
+pref("devtools.debugger.remote-enabled", true);
+pref("devtools.debugger.force-local", true);
+
 // The default for this pref reflects whether the build is capable of IPC.
 // (Turning it on in a no-IPC build will have no effect.)
 #ifdef XP_MACOSX
 // i386 ipc preferences
 pref("dom.ipc.plugins.enabled.i386", false);
 pref("dom.ipc.plugins.enabled.i386.flash player.plugin", true);
 // x86_64 ipc preferences
 pref("dom.ipc.plugins.enabled.x86_64", true);
--- a/webapprt/test/chrome/Makefile.in
+++ b/webapprt/test/chrome/Makefile.in
@@ -23,9 +23,13 @@ MOCHITEST_WEBAPPRT_CHROME_FILES = \
   browser_geolocation-prompt-perm.js \
   browser_geolocation-prompt-noperm.js \
     geolocation-prompt-perm.webapp \
     geolocation-prompt-perm.webapp^headers^ \
     geolocation-prompt-noperm.webapp \
     geolocation-prompt-noperm.webapp^headers^ \
     geolocation-prompt-perm.html \
     geolocation-prompt-noperm.html \
+  browser_debugger.js \
+    debugger.webapp \
+    debugger.webapp^headers^ \
+    debugger.html \
   $(NULL)
new file mode 100644
--- /dev/null
+++ b/webapprt/test/chrome/browser_debugger.js
@@ -0,0 +1,39 @@
+Cu.import("resource://gre/modules/Services.jsm");
+let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
+let { RemoteDebugger } = Cu.import("resource://webapprt/modules/RemoteDebugger.jsm", {});
+
+function test() {
+  waitForExplicitFinish();
+
+  loadWebapp("debugger.webapp", undefined, () => {
+    RemoteDebugger.init(Services.prefs.getIntPref('devtools.debugger.remote-port'));
+
+    let client = new DebuggerClient(DebuggerServer.connectPipe());
+    client.connect(() => {
+      client.listTabs((aResponse) => {
+        is(aResponse.tabs[0].title, "Debugger Test Webapp", "Title correct");
+        is(aResponse.tabs[0].url, "http://test/webapprtChrome/webapprt/test/chrome/debugger.html", "URL correct");
+        ok(aResponse.tabs[0].consoleActor, "consoleActor set");
+        ok(aResponse.tabs[0].gcliActor, "gcliActor set");
+        ok(aResponse.tabs[0].styleEditorActor, "styleEditorActor set");
+        ok(aResponse.tabs[0].inspectorActor, "inspectorActor set");
+        ok(aResponse.tabs[0].traceActor, "traceActor set");
+        ok(aResponse.chromeDebugger, "chromeDebugger set");
+        ok(aResponse.consoleActor, "consoleActor set");
+        ok(aResponse.gcliActor, "gcliActor set");
+        ok(aResponse.profilerActor, "profilerActor set");
+        ok(aResponse.gcliActor, "gcliActor set");
+        ok(aResponse.deviceActor, "deviceActor set");
+
+        client.close(() => {
+          finish();
+        });
+      });
+    });
+  });
+
+  registerCleanupFunction(function() {
+    DebuggerServer.destroy();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/webapprt/test/chrome/debugger.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    <p>This is the test webapp.</p>
+  </body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/webapprt/test/chrome/debugger.webapp
@@ -0,0 +1,1 @@
+{"name": "Debugger Test Webapp", "description": "A debugger test app.", "launch_path": "/webapprtChrome/webapprt/test/chrome/debugger.html" }
new file mode 100644
--- /dev/null
+++ b/webapprt/test/chrome/debugger.webapp^headers^
@@ -0,0 +1,1 @@
+Content-Type: application/x-web-app-manifest+json