Bug 741324 - Make it possible to start a debugger in a new firefox instance; r=past,rcampbell,zpao
authorVictor Porof <vporof@mozilla.com>
Thu, 12 Apr 2012 18:47:56 +0300
changeset 92059 906f3bb4c5aa1d9f200ffaef7cc8eb55a623fa42
parent 92058 b9914d4ebc923ab54a2921a5347d07d6ecc4903d
child 92060 3bdab8c3149290ef3945d3c037afdb0d28a7ab61
push id700
push userrcampbell@mozilla.com
push dateMon, 23 Apr 2012 15:52:22 +0000
treeherderfx-team@d636e92961fa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast, rcampbell, zpao
bugs741324
milestone14.0a1
Bug 741324 - Make it possible to start a debugger in a new firefox instance; r=past,rcampbell,zpao
browser/app/profile/firefox.js
browser/base/content/browser-appmenu.inc
browser/base/content/browser-menubar.inc
browser/base/content/browser-sets.inc
browser/base/content/browser.js
browser/devtools/debugger/DebuggerUI.jsm
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/test/Makefile.in
browser/devtools/debugger/test/browser_dbg_createRemote.js
browser/devtools/debugger/test/head.js
browser/locales/en-US/chrome/browser/devtools/debugger.dtd
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1053,19 +1053,24 @@ pref("devtools.inspector.sidebarOpen", f
 pref("devtools.inspector.activeSidebar", "ruleview");
 
 // Enable the Layout View
 pref("devtools.layoutview.enabled", false);
 pref("devtools.layoutview.open", false);
 
 // Enable the Debugger
 pref("devtools.debugger.enabled", false);
+pref("devtools.debugger.remote-enabled", false);
+pref("devtools.debugger.remote-host", "localhost");
+pref("devtools.debugger.remote-port", 6000);
 
 // The default Debugger UI height
 pref("devtools.debugger.ui.height", 250);
+pref("devtools.debugger.ui.remote-win.width", 900);
+pref("devtools.debugger.ui.remote-win.height", 400);
 
 // Enable the style inspector
 pref("devtools.styleinspector.enabled", true);
 
 // Enable the Tilt inspector
 pref("devtools.tilt.enabled", true);
 pref("devtools.tilt.intro_transition", true);
 pref("devtools.tilt.outro_transition", true);
--- a/browser/base/content/browser-appmenu.inc
+++ b/browser/base/content/browser-appmenu.inc
@@ -189,16 +189,20 @@
                     type="checkbox"
                     command="Tools:Inspect"
                     key="key_inspect"/>
           <menuitem id="appmenu_debugger"
                     hidden="true"
                     label="&debuggerMenu.label;"
                     key="key_debugger"
                     command="Tools:Debugger"/>
+          <menuitem id="appmenu_remoteDebugger"
+                    hidden="true"
+                    label="&remoteDebuggerMenu.label;"
+                    command="Tools:RemoteDebugger"/>
           <menuitem id="appmenu_scratchpad"
                     hidden="true"
                     label="&scratchpad.label;"
                     key="key_scratchpad"
                     command="Tools:Scratchpad"/>
           <menuitem id="appmenu_styleeditor"
                     hidden="true"
                     label="&styleeditor.label;"
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -546,16 +546,20 @@
                             accesskey="&inspectMenu.accesskey;"
                             key="key_inspect"
                             command="Tools:Inspect"/>
                   <menuitem id="menu_debugger"
                             hidden="true"
                             label="&debuggerMenu.label;"
                             key="key_debugger"
                             command="Tools:Debugger"/>
+                  <menuitem id="menu_remoteDebugger"
+                            hidden="true"
+                            label="&remoteDebuggerMenu.label;"
+                            command="Tools:RemoteDebugger"/>
                   <menuitem id="menu_scratchpad"
                             hidden="true"
                             label="&scratchpad.label;"
                             accesskey="&scratchpad.accesskey;"
                             key="key_scratchpad"
                             command="Tools:Scratchpad"/>
                   <menuitem id="menu_styleeditor"
                             hidden="true"
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -124,16 +124,17 @@
     <command id="cmd_fullZoomToggle"  oncommand="ZoomManager.toggleZoom();"/>
     <command id="Browser:OpenLocation" oncommand="openLocation();"/>
 
     <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/>
     <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/>
     <command id="Tools:WebConsole" oncommand="HUDConsoleUI.toggleHUD();"/>
     <command id="Tools:Inspect" oncommand="InspectorUI.toggleInspectorUI();" disabled="true"/>
     <command id="Tools:Debugger" oncommand="DebuggerUI.toggleDebugger();" disabled="true"/>
+    <command id="Tools:RemoteDebugger" oncommand="DebuggerUI.toggleRemoteDebugger();" disabled="true"/>
     <command id="Tools:Scratchpad" oncommand="Scratchpad.openScratchpad();" disabled="true"/>
     <command id="Tools:StyleEditor" oncommand="StyleEditor.openChrome();" disabled="true"/>
     <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/>
     <command id="Tools:Sanitize"
      oncommand="Cc['@mozilla.org/browser/browserglue;1'].getService(Ci.nsIBrowserGlue).sanitize(window);"/>
     <command id="Tools:PrivateBrowsing" oncommand="gPrivateBrowsingUI.toggleMode();"/>
     <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/>
     <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1711,16 +1711,26 @@ function delayedStartup(isLoadingBlank, 
   if (enabled) {
     document.getElementById("menu_debugger").hidden = false;
     document.getElementById("Tools:Debugger").removeAttribute("disabled");
 #ifdef MENUBAR_CAN_AUTOHIDE
     document.getElementById("appmenu_debugger").hidden = false;
 #endif
   }
 
+  // Enable Remote Debugger?
+  let enabled = gPrefService.getBoolPref("devtools.debugger.remote-enabled");
+  if (enabled) {
+    document.getElementById("menu_remoteDebugger").hidden = false;
+    document.getElementById("Tools:RemoteDebugger").removeAttribute("disabled");
+#ifdef MENUBAR_CAN_AUTOHIDE
+    document.getElementById("appmenu_remoteDebugger").hidden = false;
+#endif
+  }
+
   // Enable Error Console?
   // XXX Temporarily always-enabled, see bug 601201
   let consoleEnabled = true || gPrefService.getBoolPref("devtools.errorconsole.enabled");
   if (consoleEnabled) {
     document.getElementById("javascriptConsole").hidden = false;
     document.getElementById("key_errorConsole").removeAttribute("disabled");
 #ifdef MENUBAR_CAN_AUTOHIDE
     document.getElementById("appmenu_errorConsole").hidden = false;
--- a/browser/devtools/debugger/DebuggerUI.jsm
+++ b/browser/devtools/debugger/DebuggerUI.jsm
@@ -40,17 +40,23 @@
  *
  * ***** END LICENSE BLOCK ***** */
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+const DBG_XUL = "chrome://browser/content/debugger.xul";
+const REMOTE_PROFILE_NAME = "_remote-debug";
+
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 let EXPORTED_SYMBOLS = ["DebuggerUI"];
 
 /**
  * Provides a simple mechanism of managing debugger instances per tab.
  *
  * @param nsIDOMWindow aWindow
  *        The chrome window for which the DebuggerUI instance is created.
@@ -71,59 +77,85 @@ DebuggerUI.prototype = {
     if (tab._scriptDebugger) {
       tab._scriptDebugger.close();
       return null;
     }
     return new DebuggerPane(tab);
   },
 
   /**
+   * Starts a remote debugger in a new process, or stops it if already started.
+   * @see DebuggerProcess.constructor
+   * @return DebuggerProcess if the debugger is started, null if it's stopped.
+   */
+  toggleRemoteDebugger: function DUI_toggleRemoteDebugger(aOnClose, aOnRun) {
+    let win = this.chromeWindow;
+
+    if (win._remoteDebugger) {
+      win._remoteDebugger.close();
+      return null;
+    }
+    return new DebuggerProcess(win, aOnClose, aOnRun);
+  },
+
+  /**
    * Get the debugger for a specified tab.
    * @return DebuggerPane if a debugger exists for the tab, null otherwise
    */
   getDebugger: function DUI_getDebugger(aTab) {
     return aTab._scriptDebugger;
   },
 
   /**
    * Get the preferences associated with the debugger frontend.
    * @return object
    */
   get preferences() {
-    return DebuggerUIPreferences;
+    return DebuggerPreferences;
   }
 };
 
 /**
  * Creates a pane that will host the debugger.
  *
  * @param XULElement aTab
  *        The tab in which to create the debugger.
  */
 function DebuggerPane(aTab) {
   this._tab = aTab;
+  this._initServer();
   this._create();
 }
 
 DebuggerPane.prototype = {
 
   /**
+   * Initializes the debugger server.
+   */
+  _initServer: function DP__initServer() {
+    if (!DebuggerServer.initialized) {
+      DebuggerServer.init();
+      DebuggerServer.addBrowserActors();
+    }
+  },
+
+  /**
    * Creates and initializes the widgets containing the debugger UI.
    */
   _create: function DP__create() {
     this._tab._scriptDebugger = this;
 
     let gBrowser = this._tab.linkedBrowser.getTabBrowser();
     let ownerDocument = gBrowser.parentNode.ownerDocument;
 
     this._splitter = ownerDocument.createElement("splitter");
     this._splitter.setAttribute("class", "hud-splitter");
 
     this._frame = ownerDocument.createElement("iframe");
-    this._frame.height = DebuggerUIPreferences.height;
+    this._frame.height = DebuggerPreferences.height;
 
     this._nbox = gBrowser.getNotificationBox(this._tab.linkedBrowser);
     this._nbox.appendChild(this._splitter);
     this._nbox.appendChild(this._frame);
 
     this.close = this.close.bind(this);
     let self = this;
 
@@ -134,30 +166,30 @@ DebuggerPane.prototype = {
 
       // Bind shortcuts for accessing the breakpoint methods in the debugger.
       let bkp = self.debuggerWindow.DebuggerController.Breakpoints;
       self.addBreakpoint = bkp.addBreakpoint;
       self.removeBreakpoint = bkp.removeBreakpoint;
       self.getBreakpoint = bkp.getBreakpoint;
     }, true);
 
-    this._frame.setAttribute("src", "chrome://browser/content/debugger.xul");
+    this._frame.setAttribute("src", DBG_XUL);
   },
 
   /**
    * Closes the debugger, removing child nodes and event listeners.
    */
   close: function DP_close() {
     if (!this._tab) {
       return;
     }
-    this._tab._scriptDebugger = null;
+    delete this._tab._scriptDebugger;
     this._tab = null;
 
-    DebuggerUIPreferences.height = this._frame.height;
+    DebuggerPreferences.height = this._frame.height;
     this._frame.removeEventListener("Debugger:Close", this.close, true);
     this._frame.removeEventListener("unload", this.close, true);
 
     this._nbox.removeChild(this._splitter);
     this._nbox.removeChild(this._frame);
 
     this._splitter = null;
     this._frame = null;
@@ -181,19 +213,123 @@ DebuggerPane.prototype = {
     if (debuggerWindow) {
       return debuggerWindow.DebuggerController.Breakpoints.store;
     }
     return null;
   }
 };
 
 /**
- * Various debugger UI preferences (currently just the pane height).
+ * Creates a process that will hold the remote debugger.
+ *
+ * @param function aOnClose
+ *        Optional, a function called when the process exits.
+ * @param function aOnRun
+ *        Optional, a function called when the process starts running.
+ * @param nsIDOMWindow aWindow
+ *        The chrome window for which the remote debugger instance is created.
  */
-let DebuggerUIPreferences = {
+function DebuggerProcess(aWindow, aOnClose, aOnRun) {
+  this._win = aWindow;
+  this._closeCallback = aOnClose;
+  this._runCallback = aOnRun;
+  this._initServer();
+  this._initProfile();
+  this._create();
+}
+
+DebuggerProcess.prototype = {
+
+  /**
+   * Initializes the debugger server.
+   */
+  _initServer: function RDP__initServer() {
+    if (!DebuggerServer.initialized) {
+      DebuggerServer.init();
+      DebuggerServer.addBrowserActors();
+    }
+    DebuggerServer.closeListener();
+    DebuggerServer.openListener(DebuggerPreferences.remotePort, false);
+  },
+
+  /**
+   * Initializes a profile for the remote debugger process.
+   */
+  _initProfile: function RDP__initProfile() {
+    let profileService = Cc["@mozilla.org/toolkit/profile-service;1"]
+      .createInstance(Ci.nsIToolkitProfileService);
+
+    let dbgProfileName;
+    try {
+      dbgProfileName = profileService.selectedProfile.name + REMOTE_PROFILE_NAME;
+    } catch(e) {
+      dbgProfileName = REMOTE_PROFILE_NAME;
+      Cu.reportError(e);
+    }
+
+    this._dbgProfile = profileService.createProfile(null, null, dbgProfileName);
+    profileService.flush();
+  },
+
+  /**
+   * Creates and initializes the profile & process for the remote debugger.
+   */
+  _create: function RDP__create() {
+    this._win._remoteDebugger = this;
+
+    let file = FileUtils.getFile("CurProcD",
+      [Services.appinfo.OS == "WINNT" ? "firefox.exe"
+                                      : "firefox-bin"]);
+
+    let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+    process.init(file);
+
+    let args = [
+      "-no-remote", "-P", this._dbgProfile.name,
+      "-chrome", DBG_XUL,
+      "-width", DebuggerPreferences.remoteWinWidth,
+      "-height", DebuggerPreferences.remoteWinHeight];
+
+    process.runwAsync(args, args.length, { observe: this.close.bind(this) });
+    this._dbgProcess = process;
+
+    if (typeof this._runCallback === "function") {
+      this._runCallback.call({}, this);
+    }
+  },
+
+  /**
+   * Closes the remote debugger, removing the profile and killing the process.
+   */
+  close: function RDP_close() {
+    if (!this._win) {
+      return;
+    }
+    delete this._win._remoteDebugger;
+    this._win = null;
+
+    if (this._dbgProcess.isRunning) {
+      this._dbgProcess.kill();
+    }
+    if (this._dbgProfile) {
+      this._dbgProfile.remove(false);
+    }
+    if (typeof this._closeCallback === "function") {
+      this._closeCallback.call({}, this);
+    }
+
+    this._dbgProcess = null;
+    this._dbgProfile = null;
+  }
+};
+
+/**
+ * Various debugger preferences.
+ */
+let DebuggerPreferences = {
 
   /**
    * Gets the preferred height of the debugger pane.
    * @return number
    */
   get height() {
     if (this._height === undefined) {
       this._height = Services.prefs.getIntPref("devtools.debugger.ui.height");
@@ -205,8 +341,40 @@ let DebuggerUIPreferences = {
    * Sets the preferred height of the debugger pane.
    * @param number value
    */
   set height(value) {
     Services.prefs.setIntPref("devtools.debugger.ui.height", value);
     this._height = value;
   }
 };
+
+/**
+ * Gets the preferred width of the remote debugger window.
+ * @return number
+ */
+XPCOMUtils.defineLazyGetter(DebuggerPreferences, "remoteWinWidth", function() {
+  return Services.prefs.getIntPref("devtools.debugger.ui.remote-win.width");
+});
+
+/**
+ * Gets the preferred height of the remote debugger window.
+ * @return number
+ */
+XPCOMUtils.defineLazyGetter(DebuggerPreferences, "remoteWinHeight", function() {
+  return Services.prefs.getIntPref("devtools.debugger.ui.remote-win.height");
+});
+
+/**
+ * Gets the preferred default remote debugging host.
+ * @return string
+ */
+XPCOMUtils.defineLazyGetter(DebuggerPreferences, "remoteHost", function() {
+  return Services.prefs.getCharPref("devtools.debugger.remote-host");
+});
+
+/**
+ * Gets the preferred default remote debugging port.
+ * @return number
+ */
+XPCOMUtils.defineLazyGetter(DebuggerPreferences, "remotePort", function() {
+  return Services.prefs.getIntPref("devtools.debugger.remote-port");
+});
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -109,29 +109,28 @@ let DebuggerController = {
     DebuggerView.Properties.destroy();
 
     DebuggerController.SourceScripts.disconnect();
     DebuggerController.StackFrames.disconnect();
     DebuggerController.ThreadState.disconnect();
 
     this.dispatchEvent("Debugger:Unloaded");
     this._disconnect();
+    this._isRemote && this._quitApp();
   },
 
   /**
    * Initializes a debugger client and connects it to the debugger server,
    * wiring event handlers as necessary.
    */
   _connect: function DC__connect() {
-    if (!DebuggerServer.initialized) {
-      DebuggerServer.init();
-      DebuggerServer.addBrowserActors();
-    }
+    let transport =
+      this._isRemote ? debuggerSocketConnect(Prefs.remoteHost, Prefs.remotePort)
+                     : DebuggerServer.connectPipe();
 
-    let transport = DebuggerServer.connectPipe();
     let client = this.client = new DebuggerClient(transport);
 
     client.addListener("tabNavigated", this._onTabNavigated);
     client.addListener("tabDetached", this._onTabDetached);
 
     client.connect(function(aType, aTraits) {
       client.listTabs(function(aResponse) {
         let tab = aResponse.tabs[aResponse.selected];
@@ -216,16 +215,41 @@ let DebuggerController = {
           });
         });
 
       }.bind(this));
     }.bind(this));
   },
 
   /**
+   * Returns true if this is a remote debugger instance.
+   * @return boolean
+   */
+  get _isRemote() {
+    return !window.parent.content;
+  },
+
+  /**
+   * Attempts to quit the current process if allowed.
+   */
+  _quitApp: function DC__quitApp() {
+    let canceled = Cc["@mozilla.org/supports-PRBool;1"]
+      .createInstance(Ci.nsISupportsPRBool);
+
+    Services.obs.notifyObservers(canceled, "quit-application-requested", null);
+
+    // Somebody canceled our quit request.
+    if (canceled.data) {
+      return;
+    }
+
+    Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit);
+  },
+
+  /**
    * Convenience method, dispatching a custom event.
    *
    * @param string aType
    *        The name of the event.
    * @param string aDetail
    *        The data passed when initializing the event.
    */
   dispatchEvent: function DC_dispatchEvent(aType, aDetail) {
@@ -743,51 +767,70 @@ SourceScripts.prototype = {
     let q = aUrl.indexOf('?');
     if (q > -1) {
       return aUrl.slice(0, q);
     }
     return aUrl;
   },
 
   /**
+   * Gets the prePath for a script URL.
+   *
+   * @param string aUrl
+   *        The script url.
+   * @return string
+   *         The script prePath if the url is valid, null otherwise.
+   */
+  _getScriptPrePath: function SS__getScriptDomain(aUrl) {
+    try {
+      return Services.io.newURI(aUrl, null, null).prePath + "/";
+    } catch (e) {
+    }
+    return null;
+  },
+
+  /**
    * Gets a unique, simplified label from a script url.
    * ex: a). ici://some.address.com/random/subrandom/
    *     b). ni://another.address.org/random/subrandom/page.html
    *     c). san://interesting.address.gro/random/script.js
    *     d). si://interesting.address.moc/random/another/script.js
    * =>
    *     a). subrandom/
    *     b). page.html
    *     c). script.js
    *     d). another/script.js
    *
    * @param string aUrl
    *        The script url.
    * @param string aHref
    *        The content location href to be used. If unspecified, it will
-   *        defalult to debugged panrent window location.
+   *        default to the script url prepath.
    * @return string
    *         The simplified label.
    */
   _getScriptLabel: function SS__getScriptLabel(aUrl, aHref) {
     let url = this._trimUrlQuery(aUrl);
 
     if (this._labelsCache[url]) {
       return this._labelsCache[url];
     }
 
-    let href = aHref || window.parent.content.location.href;
+    let content = window.parent.content;
+    let domain = content ? content.location.href : this._getScriptPrePath(aUrl);
+
+    let href = aHref || domain;
     let pathElements = url.split("/");
     let label = pathElements.pop() || (pathElements.pop() + "/");
 
-    // if the label as a leaf name is alreay present in the scripts list
+    // If the label as a leaf name is already present in the scripts list.
     if (DebuggerView.Scripts.containsLabel(label)) {
       label = url.replace(href.substring(0, href.lastIndexOf("/") + 1), "");
 
-      // if the path/to/script is exactly the same, we're in different domains
+      // If the path/to/script is exactly the same, we're in different domains.
       if (DebuggerView.Scripts.containsLabel(label)) {
         label = url;
       }
     }
 
     return this._labelsCache[url] = label;
   },
 
@@ -871,17 +914,17 @@ SourceScripts.prototype = {
       url: aScript.url
     });
   },
 
   /**
    * Handles notifications to load a source script from the cache or from a
    * local file.
    *
-   * XXX: Tt may be better to use nsITraceableChannel to get to the sources
+   * XXX: It may be better to use nsITraceableChannel to get to the sources
    * without relying on caching when we can (not for eval, etc.):
    * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
    */
   _onLoadSource: function SS__onLoadSource(aEvent) {
     let url = aEvent.detail.url;
     let options = aEvent.detail.options;
     let self = this;
 
@@ -962,17 +1005,17 @@ SourceScripts.prototype = {
    * Log an error message in the error console when a script fails to load.
    *
    * @param string aUrl
    *        The URL of the source script.
    * @param string aStatus
    *        The failure status code.
    */
   _logError: function SS__logError(aUrl, aStatus) {
-    Components.utils.reportError(L10N.getFormatStr("loadingError", [aUrl, aStatus]));
+    Cu.reportError(L10N.getFormatStr("loadingError", [aUrl, aStatus]));
   },
 };
 
 /**
  * Handles all the breakpoints in the current debugger.
  */
 function Breakpoints() {
   this._onEditorBreakpointChange = this._onEditorBreakpointChange.bind(this);
@@ -1254,16 +1297,37 @@ let L10N = {
   }
 };
 
 XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function() {
   return Services.strings.createBundle(DBG_STRINGS_URI);
 });
 
 /**
+ * Shortcuts for accessing various debugger preferences.
+ */
+let Prefs = {};
+
+/**
+ * Gets the preferred default remote debugging host.
+ * @return string
+ */
+XPCOMUtils.defineLazyGetter(Prefs, "remoteHost", function() {
+  return Services.prefs.getCharPref("devtools.debugger.remote-host");
+});
+
+/**
+ * Gets the preferred default remote debugging port.
+ * @return number
+ */
+XPCOMUtils.defineLazyGetter(Prefs, "remotePort", function() {
+  return Services.prefs.getIntPref("devtools.debugger.remote-port");
+});
+
+/**
  * Preliminary setup for the DebuggerController object.
  */
 DebuggerController.init();
 DebuggerController.ThreadState = new ThreadState();
 DebuggerController.StackFrames = new StackFrames();
 DebuggerController.SourceScripts = new SourceScripts();
 DebuggerController.Breakpoints = new Breakpoints();
 
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -41,16 +41,17 @@ topsrcdir       = @top_srcdir@
 srcdir          = @srcdir@
 VPATH           = @srcdir@
 relativesrcdir  = browser/devtools/debugger/test
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
+	browser_dbg_createRemote.js \
 	browser_dbg_debuggerstatement.js \
 	browser_dbg_listtabs.js \
 	browser_dbg_tabactor-01.js \
 	browser_dbg_tabactor-02.js \
 	browser_dbg_contextactor-01.js \
 	browser_dbg_contextactor-02.js \
 	testactors.js \
 	browser_dbg_nav-01.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_createRemote.js
@@ -0,0 +1,86 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+var gProcess = null;
+var gTab = null;
+var gDebuggee = null;
+
+function test() {
+  remote_debug_tab_pane(STACK_URL, aOnClosing, function(aTab, aDebuggee, aProcess) {
+    gTab = aTab;
+    gDebuggee = aDebuggee;
+    gProcess = aProcess;
+
+    testSimpleCall();
+  });
+}
+
+function testSimpleCall() {
+  Services.tm.currentThread.dispatch({ run: function() {
+
+    ok(gProcess._dbgProcess,
+      "The remote debugger process wasn't created properly!");
+    ok(gProcess._dbgProcess.isRunning,
+      "The remote debugger process isn't running!");
+    is(typeof gProcess._dbgProcess.pid, "number",
+      "The remote debugger process doesn't have a pid (?!)");
+
+    info("process location: " + gProcess._dbgProcess.location);
+    info("process pid: " + gProcess._dbgProcess.pid);
+    info("process name: " + gProcess._dbgProcess.processName);
+    info("process sig: " + gProcess._dbgProcess.processSignature);
+
+    ok(gProcess._dbgProfile,
+      "The remote debugger profile wasn't created properly!");
+    ok(gProcess._dbgProfile.localDir,
+      "The remote debugger profile doesn't have a localDir...");
+    ok(gProcess._dbgProfile.rootDir,
+      "The remote debugger profile doesn't have a rootDir...");
+    ok(gProcess._dbgProfile.name,
+      "The remote debugger profile doesn't have a name...");
+
+    info("profile localDir: " + gProcess._dbgProfile.localDir);
+    info("profile rootDir: " + gProcess._dbgProfile.rootDir);
+    info("profile name: " + gProcess._dbgProfile.name);
+
+    let profileService = Cc["@mozilla.org/toolkit/profile-service;1"]
+      .createInstance(Ci.nsIToolkitProfileService);
+
+    let profile = profileService.getProfileByName(gProcess._dbgProfile.name);
+
+    ok(profile,
+      "The remote debugger profile wasn't *actually* created properly!");
+    is(profile.localDir.path, gProcess._dbgProfile.localDir.path,
+      "The remote debugger profile doesn't have the correct localDir!");
+    is(profile.rootDir.path, gProcess._dbgProfile.rootDir.path,
+      "The remote debugger profile doesn't have the correct rootDir!");
+
+    DebuggerUI.toggleRemoteDebugger();
+  }}, 0);
+}
+
+function aOnClosing() {
+  ok(!gProcess._dbgProcess.isRunning,
+    "The remote debugger process isn't closed as it should be!");
+  is(gProcess._dbgProcess.exitValue, (Services.appinfo.OS == "WINNT" ? 0 : 256),
+    "The remote debugger process didn't die cleanly.");
+
+  info("process exit value: " + gProcess._dbgProcess.exitValue);
+
+  info("profile localDir: " + gProcess._dbgProfile.localDir.path);
+  info("profile rootDir: " + gProcess._dbgProfile.rootDir.path);
+  info("profile name: " + gProcess._dbgProfile.name);
+
+  executeSoon(function() {
+    finish();
+  });
+}
+
+registerCleanupFunction(function() {
+  removeTab(gTab);
+  gProcess = null;
+  gTab = null;
+  gDebuggee = null;
+});
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -87,22 +87,35 @@ function attach_thread_actor_for_url(aCl
     });
   });
 }
 
 function debug_tab_pane(aURL, aOnDebugging)
 {
   let tab = addTab(aURL, function() {
     gBrowser.selectedTab = gTab;
-
     let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject;
 
     let pane = DebuggerUI.toggleDebugger();
     pane._frame.addEventListener("Debugger:Connecting", function dbgConnected() {
       pane._frame.removeEventListener("Debugger:Connecting", dbgConnected, true);
 
       // Wait for the initial resume...
       pane.debuggerWindow.gClient.addOneTimeListener("resumed", function() {
         aOnDebugging(tab, debuggee, pane);
       });
     }, true);
   });
 }
+
+function remote_debug_tab_pane(aURL, aOnClosing, aOnDebugging)
+{
+  let tab = addTab(aURL, function() {
+    gBrowser.selectedTab = gTab;
+    let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject;
+
+    DebuggerUI.toggleRemoteDebugger(aOnClosing, function dbgRan(process) {
+
+      // Wait for the remote debugging process to start...
+      aOnDebugging(tab, debuggee, process);
+    });
+  });
+}
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
@@ -6,16 +6,20 @@
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
 
 <!-- LOCALIZATION NOTE (debuggerMenu.label): This is the label for the
   -  application menu item that opens the debugger UI. -->
 <!ENTITY debuggerMenu.label          "Script Debugger">
 
+<!-- LOCALIZATION NOTE (remoteDebuggerMenu.label): This is the label for the
+  -  application menu item that opens the remote debugger UI. -->
+<!ENTITY remoteDebuggerMenu.label    "Remote Debugger">
+
 <!-- LOCALIZATION NOTE (debuggerMenu.commandkey): This is the command key that
   -  launches the debugger UI. Do not translate this one! -->
 <!ENTITY debuggerMenu.commandkey     "S">
 
 <!-- LOCALIZATION NOTE (debuggerUI.closeButton): This is the label for the
   -  button that closes the debugger UI. -->
 <!ENTITY debuggerUI.closeButton      "Close">