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 146506 8a0b5eb332ff958338e5a6bf8e57eff2ff5830fe
parent 146458 f73bed2856a8eeae0dbeb395f50718cf7ec23942
child 146507 cdc016c22761a88039747c324f42f08c47df68bb
push idunknown
push userunknown
push dateunknown
reviewerspast
bugs777428
milestone26.0a1
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