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 92601 ef8002a0a2803c584afdf3647551fbda8772afbf
parent 92600 75fd353168e0519047d19f35178a2b707a308592
child 92602 c6db0b79bd98e5dd0d72e4ae6120ef5aea3e796c
push id22545
push usertim.taubert@gmx.de
push dateFri, 27 Apr 2012 12:45:03 +0000
treeherdermozilla-central@0f8ea3826bf7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast, rcampbell, zpao
bugs741324
milestone14.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 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
@@ -1056,19 +1056,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
@@ -196,16 +196,20 @@
                     command="Tools:Inspect"
                     key="key_inspect"/>
           <menuitem id="appmenu_debugger"
                     hidden="true"
                     type="checkbox"
                     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
@@ -554,16 +554,20 @@
                             key="key_inspect"
                             command="Tools:Inspect"/>
                   <menuitem id="menu_debugger"
                             hidden="true"
                             type="checkbox"
                             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
@@ -125,16 +125,17 @@
     <command id="Browser:OpenLocation" oncommand="openLocation();"/>
 
     <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/>
     <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/>
     <command id="Tools:DevToolbar" oncommand="DeveloperToolbar.toggle();" disabled="true"/>
     <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
@@ -1729,16 +1729,26 @@ function delayedStartup(isLoadingBlank, 
     document.getElementById("menu_debugger").hidden = false;
     document.getElementById("Tools:Debugger").removeAttribute("disabled");
 #ifdef MENUBAR_CAN_AUTOHIDE
     document.getElementById("appmenu_debugger").hidden = false;
 #endif
     document.getElementById("developer-toolbar-debugger").hidden = false;
   }
 
+  // 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.
@@ -85,60 +91,86 @@ DebuggerUI.prototype = {
     if (tab._scriptDebugger) {
       tab._scriptDebugger.close();
       return null;
     }
     return new DebuggerPane(this, 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 '_scriptDebugger' in aTab ? aTab._scriptDebugger : null;
   },
 
   /**
    * 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(aDebuggerUI, aTab) {
   this._globalUI = aDebuggerUI;
   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;
 
@@ -149,32 +181,32 @@ 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);
 
     this._globalUI.refreshCommand();
   },
 
   /**
    * 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;
@@ -200,19 +232,122 @@ 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._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");
@@ -224,8 +359,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) {
@@ -763,51 +787,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;
   },
 
@@ -892,17 +935,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;
 
@@ -983,17 +1026,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);
@@ -1275,16 +1318,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">