Bug 911098 - Implement Addon Debugger UI, r=fitzgen,harthur,mossop
authorJordan Santell <jsantell@gmail.com>
Tue, 25 Mar 2014 10:59:14 -0700
changeset 175609 0c4f13342090abf95fb2801334d4ec78239f9a40
parent 175608 37b77d7a64c84224e4a9f497444cdf92afe42bca
child 175610 5e08c26402293b6d1d148c92be1574b65ca20252
push id26495
push usercbook@mozilla.com
push dateThu, 27 Mar 2014 13:14:49 +0000
treeherdermozilla-central@bb4dd9872236 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfitzgen, harthur, mossop
bugs911098, 100644
milestone31.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 911098 - Implement Addon Debugger UI, r=fitzgen,harthur,mossop From 8af4148dc10f18bf67e39442ee93169cb66382d5 Mon Sep 17 00:00:00 2001 --- browser/devtools/debugger/debugger-controller.js | 36 ++++++- browser/devtools/debugger/debugger-panes.js | 17 +++- browser/devtools/debugger/test/browser.ini | 1 + .../debugger/test/browser_dbg_addon-sources.js | 108 ++++++++++++++++++++ browser/devtools/debugger/test/head.js | 29 ++++++ browser/devtools/framework/ToolboxProcess.jsm | 31 ++++-- .../devtools/framework/toolbox-process-window.js | 18 +++- modules/libpref/src/init/all.js | 3 + .../en-US/chrome/mozapps/extensions/extensions.dtd | 1 + toolkit/mozapps/extensions/content/extensions.js | 55 +++++++--- toolkit/mozapps/extensions/content/extensions.xml | 31 +++++- toolkit/mozapps/extensions/content/extensions.xul | 6 ++ .../mozapps/extensions/internal/XPIProvider.jsm | 4 + .../extensions/internal/XPIProviderUtils.js | 2 +- .../test/addons/test_jetpack/bootstrap.js | 17 ++++ .../test/addons/test_jetpack/harness-options.json | 1 + .../test/addons/test_jetpack/install.rdf | 28 ++++++ .../extensions/test/browser/browser-common.ini | 1 + .../test/browser/browser_debug_button.js | 112 +++++++++++++++++++++ toolkit/mozapps/extensions/test/browser/head.js | 3 + .../extensions/test/xpcshell/test_isDebuggable.js | 36 +++++++ .../extensions/test/xpcshell/xpcshell-shared.ini | 1 + 22 files changed, 508 insertions(+), 33 deletions(-) create mode 100644 browser/devtools/debugger/test/browser_dbg_addon-sources.js create mode 100644 toolkit/mozapps/extensions/test/addons/test_jetpack/bootstrap.js create mode 100644 toolkit/mozapps/extensions/test/addons/test_jetpack/harness-options.json create mode 100644 toolkit/mozapps/extensions/test/addons/test_jetpack/install.rdf create mode 100644 toolkit/mozapps/extensions/test/browser/browser_debug_button.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js * * * Leak fix
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/debugger-panes.js
browser/devtools/debugger/test/browser.ini
browser/devtools/debugger/test/browser_dbg_addon-sources.js
browser/devtools/debugger/test/head.js
browser/devtools/framework/ToolboxProcess.jsm
browser/devtools/framework/toolbox-process-window.js
modules/libpref/src/init/all.js
toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/content/extensions.xml
toolkit/mozapps/extensions/content/extensions.xul
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/test/addons/test_jetpack/bootstrap.js
toolkit/mozapps/extensions/test/addons/test_jetpack/harness-options.json
toolkit/mozapps/extensions/test/addons/test_jetpack/install.rdf
toolkit/mozapps/extensions/test/browser/browser-common.ini
toolkit/mozapps/extensions/test/browser/browser_debug_button.js
toolkit/mozapps/extensions/test/browser/head.js
toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -186,23 +186,25 @@ let DebuggerController = {
     if (this._connection) {
       return this._connection;
     }
 
     let startedDebugging = promise.defer();
     this._connection = startedDebugging.promise;
 
     let target = this._target;
-    let { client, form: { chromeDebugger, traceActor } } = target;
+    let { client, form: { chromeDebugger, traceActor, addonActor } } = target;
     target.on("close", this._onTabDetached);
     target.on("navigate", this._onTabNavigated);
     target.on("will-navigate", this._onTabNavigated);
     this.client = client;
 
-    if (target.chrome) {
+    if (addonActor) {
+      this._startAddonDebugging(addonActor, startedDebugging.resolve);
+    } else if (target.chrome) {
       this._startChromeDebugging(chromeDebugger, startedDebugging.resolve);
     } else {
       this._startDebuggingTab(startedDebugging.resolve);
       const startedTracing = promise.defer();
       if (Prefs.tracerEnabled && traceActor) {
         this._startTracingTab(traceActor, startedTracing.resolve);
       } else {
         startedTracing.resolve();
@@ -305,16 +307,30 @@ let DebuggerController = {
 
       if (aCallback) {
         aCallback();
       }
     });
   },
 
   /**
+   * Sets up an addon debugging session.
+   *
+   * @param object aAddonActor
+   *        The actor for the addon that is being debugged.
+   * @param function aCallback
+   *        A function to invoke once the client attaches to the active thread.
+   */
+  _startAddonDebugging: function(aAddonActor, aCallback) {
+    this.client.attachAddon(aAddonActor, (aResponse) => {
+      return this._startChromeDebugging(aResponse.threadActor, aCallback);
+    });
+  },
+
+  /**
    * Sets up a chrome debugging session.
    *
    * @param object aChromeDebugger
    *        The remote protocol grip of the chrome debugger.
    * @param function aCallback
    *        A function to invoke once the client attaches to the active thread.
    */
   _startChromeDebugging: function(aChromeDebugger, aCallback) {
@@ -2155,16 +2171,32 @@ Object.defineProperties(window, {
     get: function() DebuggerController.activeThread
   },
   "gCallStackPageSize": {
     get: function() CALL_STACK_PAGE_SIZE
   }
 });
 
 /**
+ * Helper method for parsing a resource URI, like
+ * `resource://gre/modules/commonjs/sdk/tabs.js`, and pulling out `sdk/tabs.js`
+ * if it's in the SDK, or `null` otherwise.
+ *
+ * @param string url
+ * @return string|null
+ */
+function getSDKModuleName(url) {
+  let match = (url || "").match(/^resource:\/\/gre\/modules\/commonjs\/(.*)/);
+  if (match) {
+    return match[1];
+  }
+  return null;
+}
+
+/**
  * Helper method for debugging.
  * @param string
  */
 function dumpn(str) {
   if (wantLogging) {
     dump("DBG-FRONTEND: " + str + "\n");
   }
 }
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -121,30 +121,37 @@ SourcesView.prototype = Heritage.extend(
    *
    * @param object aSource
    *        The source object coming from the active thread.
    * @param object aOptions [optional]
    *        Additional options for adding the source. Supported options:
    *        - staged: true to stage the item to be appended later
    */
   addSource: function(aSource, aOptions = {}) {
-    let url = aSource.url;
-    let label = SourceUtils.getSourceLabel(url.split(" -> ").pop());
-    let group = SourceUtils.getSourceGroup(url.split(" -> ").pop());
-    let unicodeUrl = NetworkHelper.convertToUnicode(unescape(url));
+    let fullUrl = aSource.url;
+    let url = fullUrl.split(" -> ").pop();
+    let label = SourceUtils.getSourceLabel(url);
+    let group = SourceUtils.getSourceGroup(url);
+    let unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl));
+
+    let sdkModuleName = getSDKModuleName(url);
+    if (sdkModuleName) {
+      label = sdkModuleName;
+      group = "Add-on SDK";
+    }
 
     let contents = document.createElement("label");
     contents.className = "plain dbg-source-item";
     contents.setAttribute("value", label);
     contents.setAttribute("crop", "start");
     contents.setAttribute("flex", "1");
     contents.setAttribute("tooltiptext", unicodeUrl);
 
     // Append a source item to this container.
-    this.push([contents, url], {
+    this.push([contents, fullUrl], {
       staged: aOptions.staged, /* stage the item to be appended later? */
       attachment: {
         label: label,
         group: group,
         checkboxState: !aSource.isBlackBoxed,
         checkboxTooltip: this._blackBoxCheckboxTooltip,
         source: aSource
       }
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -77,16 +77,17 @@ support-files =
   doc_watch-expression-button.html
   doc_with-frame.html
   head.js
   sjs_random-javascript.sjs
   testactors.js
 
 [browser_dbg_aaa_run_first_leaktest.js]
 [browser_dbg_addonactor.js]
+[browser_dbg_addon-sources.js]
 [browser_dbg_auto-pretty-print-01.js]
 [browser_dbg_auto-pretty-print-02.js]
 [browser_dbg_bfcache.js]
 [browser_dbg_blackboxing-01.js]
 [browser_dbg_blackboxing-02.js]
 [browser_dbg_blackboxing-03.js]
 [browser_dbg_blackboxing-04.js]
 [browser_dbg_blackboxing-05.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_addon-sources.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure that the sources listed when debugging an addon are either from the 
+// addon itself, or the SDK, with proper groups and labels.
+
+const ADDON3_URL = EXAMPLE_URL + "addon3.xpi";
+
+let gAddon, gClient, gThreadClient, gDebugger, gSources;
+
+function test() {
+  Task.spawn(function () {
+    if (!DebuggerServer.initialized) {
+      DebuggerServer.init(() => true);
+      DebuggerServer.addBrowserActors();
+    }
+
+    gBrowser.selectedTab = gBrowser.addTab();
+    let iframe = document.createElement("iframe");
+    document.documentElement.appendChild(iframe);
+
+    let transport = DebuggerServer.connectPipe();
+    gClient = new DebuggerClient(transport);
+
+    let connected = promise.defer();
+    gClient.connect(connected.resolve);
+    yield connected.promise;
+
+    yield installAddon();
+    let debuggerPanel = yield initAddonDebugger(gClient, ADDON3_URL, iframe);
+    gDebugger = debuggerPanel.panelWin;
+    gThreadClient = gDebugger.gThreadClient;
+    gSources = gDebugger.DebuggerView.Sources;
+
+    yield testSources();
+    yield uninstallAddon();
+    yield closeConnection();
+    yield debuggerPanel._toolbox.destroy();
+    iframe.remove();
+    finish();
+  });
+}
+
+function installAddon () {
+  return addAddon(ADDON3_URL).then(aAddon => {
+    gAddon = aAddon;
+  });
+}
+
+function testSources() {
+  let deferred = promise.defer();
+  let foundAddonModule = false;
+  let foundSDKModule = 0;
+
+  gThreadClient.getSources(({sources}) => {
+    ok(sources.length, "retrieved sources");
+
+    sources.forEach(source => {
+      let url = source.url.split(" -> ").pop();
+      info(source.url + "\n\n\n" + url);
+      let { label, group } = gSources.getItemByValue(source.url).attachment;
+
+      if (url.indexOf("resource://gre/modules/commonjs/sdk") === 0) {
+        is(label.indexOf("sdk/"), 0, "correct truncated label");
+        is(group, "Add-on SDK", "correct SDK group");
+        foundSDKModule++;
+      } else if (url.indexOf("resource://gre/modules/commonjs/method") === 0) {
+        is(label.indexOf("method/"), 0, "correct truncated label");
+        is(group, "Add-on SDK", "correct SDK group");
+        foundSDKModule++;
+      } else if (url.indexOf("resource://jid1-ami3akps3baaeg-at-jetpack") === 0) {
+        is(label, "main.js", "correct label for addon code");
+        is(group, "resource://jid1-ami3akps3baaeg-at-jetpack", "addon code is in its own group");
+        foundAddonModule = true;
+      } else {
+        throw new Error("Found source outside of the SDK or addon");
+      }
+    });
+
+    ok(foundAddonModule, "found code for the addon in the list");
+    // Be flexible in this number, as SDK changes could change the exact number of
+    // built-in browser SDK modules
+    ok(foundSDKModule > 10, "SDK modules are listed");
+
+    deferred.resolve();
+  });
+
+  return deferred.promise;
+}
+
+function uninstallAddon() {
+  return removeAddon(gAddon);
+}
+
+function closeConnection () {
+  let deferred = promise.defer();
+  gClient.close(deferred.resolve);
+  return deferred.promise;
+}
+
+registerCleanupFunction(function() {
+  gClient = null;
+  gAddon = null;
+  gThreadClient = null;
+  gDebugger = null;
+  gSources = null;
+  while (gBrowser.tabs.length > 1)
+    gBrowser.removeCurrentTab();
+});
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -159,16 +159,17 @@ function getTabActorForUrl(aClient, aUrl
 }
 
 function getAddonActorForUrl(aClient, aUrl) {
   info("Get addon actor for URL: " + aUrl);
   let deferred = promise.defer();
 
   aClient.listAddons(aResponse => {
     let addonActor = aResponse.addons.filter(aGrip => aGrip.url == aUrl).pop();
+    info("got addon actor for URL: " + addonActor.actor);
     deferred.resolve(addonActor);
   });
 
   return deferred.promise;
 }
 
 function attachTabActorForUrl(aClient, aUrl) {
   let deferred = promise.defer();
@@ -495,16 +496,44 @@ function initDebugger(aTarget, aWindow) 
         deferred.resolve([aTab, debuggee, debuggerPanel, aWindow]);
       });
     });
 
     return deferred.promise;
   });
 }
 
+function initAddonDebugger(aClient, aUrl, aFrame) {
+  info("Initializing an addon debugger panel.");
+
+  return getAddonActorForUrl(aClient, aUrl).then(({actor}) => {
+    let targetOptions = {
+      form: { addonActor: actor },
+      client: aClient,
+      chrome: true
+    };
+
+    let toolboxOptions = {
+      customIframe: aFrame
+    };
+
+    let target = devtools.TargetFactory.forTab(targetOptions);
+    return gDevTools.showToolbox(target, "jsdebugger", devtools.Toolbox.HostType.CUSTOM, toolboxOptions);
+  }).then(aToolbox => {
+    info("Addon debugger panel shown successfully.");
+
+    let debuggerPanel = aToolbox.getCurrentPanel();
+
+    // Wait for the initial resume...
+    return waitForClientEvents(debuggerPanel, "resumed")
+      .then(() => prepareDebugger(debuggerPanel))
+      .then(() => debuggerPanel);
+  });
+}
+
 function initChromeDebugger(aOnClose) {
   info("Initializing a chrome debugger process.");
 
   let deferred = promise.defer();
 
   // Wait for the toolbox process to start...
   BrowserToolboxProcess.init(aOnClose, aProcess => {
     info("Browser toolbox process started successfully.");
--- a/browser/devtools/framework/ToolboxProcess.jsm
+++ b/browser/devtools/framework/ToolboxProcess.jsm
@@ -21,35 +21,47 @@ this.EXPORTED_SYMBOLS = ["BrowserToolbox
 
 /**
  * Constructor for creating a process that will hold a chrome toolbox.
  *
  * @param function aOnClose [optional]
  *        A function called when the process stops running.
  * @param function aOnRun [optional]
  *        A function called when the process starts running.
+ * @param object aOptions [optional]
+ *        An object with properties for configuring BrowserToolboxProcess.
  */
-this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun) {
-  this._closeCallback = aOnClose;
-  this._runCallback = aOnRun;
+this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun, aOptions) {
+  // If first argument is an object, use those properties instead of
+  // all three arguments
+  if (typeof aOnClose === "object") {
+    this._closeCallback = aOnClose.onClose;
+    this._runCallback = aOnClose.onRun;
+    this._options = aOnClose;
+  } else {
+    this._closeCallback = aOnClose;
+    this._runCallback = aOnRun;
+    this._options = aOptions || {};
+  }
+
   this._telemetry = new Telemetry();
 
   this.close = this.close.bind(this);
   Services.obs.addObserver(this.close, "quit-application", false);
   this._initServer();
   this._initProfile();
   this._create();
 };
 
 /**
  * Initializes and starts a chrome toolbox process.
  * @return object
  */
-BrowserToolboxProcess.init = function(aOnClose, aOnRun) {
-  return new BrowserToolboxProcess(aOnClose, aOnRun);
+BrowserToolboxProcess.init = function(aOnClose, aOnRun, aOptions) {
+  return new BrowserToolboxProcess(aOnClose, aOnRun, aOptions);
 };
 
 BrowserToolboxProcess.prototype = {
   /**
    * Initializes the debugger server.
    */
   _initServer: function() {
     dumpn("Initializing the chrome toolbox server.");
@@ -138,18 +150,25 @@ BrowserToolboxProcess.prototype = {
   /**
    * Creates and initializes the profile & process for the remote debugger.
    */
   _create: function() {
     dumpn("Initializing chrome debugging process.");
     let process = this._dbgProcess = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
     process.init(Services.dirsvc.get("XREExeF", Ci.nsIFile));
 
+    let xulURI = DBG_XUL;
+
+    if (this._options.addonID) {
+      xulURI += "?addonID=" + this._options.addonID;
+    }
+
     dumpn("Running chrome debugging process.");
-    let args = ["-no-remote", "-foreground", "-P", this._dbgProfile.name, "-chrome", DBG_XUL];
+    let args = ["-no-remote", "-foreground", "-P", this._dbgProfile.name, "-chrome", xulURI];
+
     process.runwAsync(args, args.length, { observe: () => this.close() });
 
     this._telemetry.toolOpened("jsbrowserdebugger");
 
     dumpn("Chrome toolbox is now running...");
     if (typeof this._runCallback == "function") {
       this._runCallback.call({}, this);
     }
--- a/browser/devtools/framework/toolbox-process-window.js
+++ b/browser/devtools/framework/toolbox-process-window.js
@@ -27,17 +27,26 @@ function connect() {
   window.removeEventListener("load", connect);
   // Initiate the connection
   let transport = debuggerSocketConnect(
     Prefs.chromeDebuggingHost,
     Prefs.chromeDebuggingPort
   );
   gClient = new DebuggerClient(transport);
   gClient.connect(() => {
-    gClient.listTabs(openToolbox);
+    let addonID = getParameterByName("addonID");
+
+    if (addonID) {
+      gClient.listAddons(({addons}) => {
+        let addonActor = addons.filter(addon => addon.id === addonID).pop();
+        openToolbox({ addonActor: addonActor.actor });
+      });
+    } else {
+      gClient.listTabs(openToolbox);
+    }
   });
 }
 
 window.addEventListener("load", connect);
 
 function openToolbox(form) {
   let options = {
     form: form,
@@ -101,8 +110,15 @@ function quitApp() {
              .createInstance(Ci.nsISupportsPRBool);
   Services.obs.notifyObservers(quit, "quit-application-requested", null);
 
   let shouldProceed = !quit.data;
   if (shouldProceed) {
     Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
   }
 }
+
+function getParameterByName (name) {
+  let name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
+  let regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
+  let results = regex.exec(window.location.search);
+  return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
+}
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -560,16 +560,19 @@ pref("devtools.debugger.remote-enabled",
 pref("devtools.debugger.remote-port", 6000);
 // Force debugger server binding on the loopback interface
 pref("devtools.debugger.force-local", true);
 // Display a prompt when a new connection starts to accept/reject it
 pref("devtools.debugger.prompt-connection", true);
 // Block tools from seeing / interacting with certified apps
 pref("devtools.debugger.forbid-certified-apps", true);
 
+// Disable add-on debugging
+pref("devtools.debugger.addon-enabled", false);
+
 // DevTools default color unit
 pref("devtools.defaultColorUnit", "hex");
 
 // Used for devtools debugging
 pref("devtools.dump.emit", false);
 
 // view source
 pref("view_source.syntax_highlight", true);
--- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
+++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
@@ -96,16 +96,17 @@
 <!ENTITY cmd.alwaysActivate.tooltip           "Always use this add-on">
 <!ENTITY cmd.neverActivate.label              "Never Activate">
 <!ENTITY cmd.neverActivate.tooltip            "Never use this add-on">
 <!ENTITY cmd.stateMenu.tooltip                "Change when this add-on runs">
 <!ENTITY cmd.installAddon.label               "Install">
 <!ENTITY cmd.installAddon.accesskey           "I">
 <!ENTITY cmd.uninstallAddon.label             "Remove">
 <!ENTITY cmd.uninstallAddon.accesskey         "R">
+<!ENTITY cmd.debugAddon.label                 "Debug">
 <!ENTITY cmd.showPreferencesWin.label         "Options">
 <!ENTITY cmd.showPreferencesWin.tooltip       "Change this add-on's options">
 <!ENTITY cmd.showPreferencesUnix.label        "Preferences">
 <!ENTITY cmd.showPreferencesUnix.tooltip      "Change this add-on's preferences">
 <!ENTITY cmd.contribute.label                 "Contribute">
 <!ENTITY cmd.contribute.accesskey             "C">
 <!ENTITY cmd.contribute.tooltip               "Contribute to the development of this add-on">
 
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -11,26 +11,31 @@ const Cr = Components.results;
 
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PluralForm.jsm");
 Cu.import("resource://gre/modules/DownloadUtils.jsm");
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/addons/AddonRepository.jsm");
-
+XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function () {
+  return Cu.import("resource:///modules/devtools/ToolboxProcess.jsm", {}).
+         BrowserToolboxProcess;
+});
 
 const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
 const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
 const PREF_XPI_ENABLED = "xpinstall.enabled";
 const PREF_MAXRESULTS = "extensions.getAddons.maxResults";
 const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
 const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled";
 const PREF_UI_TYPE_HIDDEN = "extensions.ui.%TYPE%.hidden";
 const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
+const PREF_ADDON_DEBUGGING_ENABLED = "devtools.debugger.addon-enabled";
+const PREF_REMOTE_DEBUGGING_ENABLED = "devtools.debugger.remote-enabled";
 
 const LOADING_MSG_DELAY = 100;
 
 const SEARCH_SCORE_MULTIPLIER_NAME = 2;
 const SEARCH_SCORE_MULTIPLIER_DESCRIPTION = 2;
 
 // Use integers so search scores are sortable by nsIXULSortService
 const SEARCH_SCORE_MATCH_WHOLEWORD = 10;
@@ -138,16 +143,19 @@ function initialize(event) {
 
   // Allow passing in a view through the window arguments
   if ("arguments" in window && window.arguments.length > 0 &&
       window.arguments[0] !== null && "view" in window.arguments[0]) {
     view = window.arguments[0].view;
   }
 
   gViewController.loadInitialView(view);
+
+  Services.prefs.addObserver(PREF_ADDON_DEBUGGING_ENABLED, debuggingPrefChanged, false);
+  Services.prefs.addObserver(PREF_REMOTE_DEBUGGING_ENABLED, debuggingPrefChanged, false);
 }
 
 function notifyInitialized() {
   if (!gIsInitializing)
     return;
 
   gPendingInitializations--;
   if (!gIsInitializing) {
@@ -158,16 +166,18 @@ function notifyInitialized() {
 }
 
 function shutdown() {
   gCategories.shutdown();
   gSearchView.shutdown();
   gEventManager.shutdown();
   gViewController.shutdown();
   Services.obs.removeObserver(sendEMPong, "EM-ping");
+  Services.prefs.removeObserver(PREF_ADDON_DEBUGGING_ENABLED, debuggingPrefChanged);
+  Services.prefs.removeObserver(PREF_REMOTE_DEBUGGING_ENABLED, debuggingPrefChanged);
 }
 
 function sendEMPong(aSubject, aTopic, aData) {
   Services.obs.notifyObservers(window, "EM-pong", "");
 }
 
 // Used by external callers to load a specific view into the manager
 function loadView(aViewId) {
@@ -367,29 +377,29 @@ var gEventManager = {
 
     this.refreshGlobalWarning();
     this.refreshAutoUpdateDefault();
 
     var contextMenu = document.getElementById("addonitem-popup");
     contextMenu.addEventListener("popupshowing", function contextMenu_onPopupshowing() {
       var addon = gViewController.currentViewObj.getSelectedAddon();
       contextMenu.setAttribute("addontype", addon.type);
-      
+
       var menuSep = document.getElementById("addonitem-menuseparator");
       var countEnabledMenuCmds = 0;
       for (let child of contextMenu.children) {
         if (child.nodeName == "menuitem" &&
           gViewController.isCommandEnabled(child.command)) {
             countEnabledMenuCmds++;
         }
       }
-      
+
       // with only one menu item, we hide the menu separator
       menuSep.hidden = (countEnabledMenuCmds <= 1);
-      
+
     }, false);
   },
 
   shutdown: function gEM_shutdown() {
     AddonManager.removeManagerListener(this);
     AddonManager.removeInstallListener(this);
     AddonManager.removeAddonListener(this);
   },
@@ -455,24 +465,24 @@ var gEventManager = {
       try {
         listener[aEvent].apply(listener, aParams);
       } catch(e) {
         // this shouldn't be fatal
         Cu.reportError(e);
       }
     }
   },
-  
+
   refreshGlobalWarning: function gEM_refreshGlobalWarning() {
     var page = document.getElementById("addons-page");
 
     if (Services.appinfo.inSafeMode) {
       page.setAttribute("warning", "safemode");
       return;
-    } 
+    }
 
     if (AddonManager.checkUpdateSecurityDefault &&
         !AddonManager.checkUpdateSecurity) {
       page.setAttribute("warning", "updatesecurity");
       return;
     }
 
     if (!AddonManager.checkCompatibility) {
@@ -940,16 +950,30 @@ var gViewController = {
                                              [aAddon]);
           }
         };
         gEventManager.delegateAddonEvent("onCheckingUpdate", [aAddon]);
         aAddon.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
       }
     },
 
+    cmd_debugItem: {
+      doCommand: function cmd_debugItem_doCommand(aAddon) {
+        BrowserToolboxProcess.init({ addonID: aAddon.id });
+      },
+
+      isEnabled: function cmd_debugItem_isEnabled(aAddon) {
+        let debuggerEnabled = Services.prefs.
+                              getBoolPref(PREF_ADDON_DEBUGGING_ENABLED);
+        let remoteEnabled = Services.prefs.
+                            getBoolPref(PREF_REMOTE_DEBUGGING_ENABLED);
+        return aAddon && aAddon.isDebuggable && debuggerEnabled && remoteEnabled;
+      }
+    },
+
     cmd_showItemPreferences: {
       isEnabled: function cmd_showItemPreferences_isEnabled(aAddon) {
         if (!aAddon || !aAddon.isActive || !aAddon.optionsURL)
           return false;
         if (gViewController.currentViewObj == gDetailView &&
             aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE) {
           return false;
         }
@@ -1277,17 +1301,17 @@ function hasInlineOptions(aAddon) {
           aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_INFO);
 }
 
 function openOptionsInTab(optionsURL) {
   var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIWebNavigation)
                          .QueryInterface(Ci.nsIDocShellTreeItem)
                          .rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindow); 
+                         .getInterface(Ci.nsIDOMWindow);
   if ("switchToTabHavingURI" in mainWindow) {
     mainWindow.switchToTabHavingURI(optionsURL, true);
     return true;
   }
   return false;
 }
 
 function formatDate(aDate) {
@@ -1974,17 +1998,17 @@ var gDiscoverView = {
     if (!this.homepageURL) {
       this._loadListeners.push(gViewController.notifyViewChanged.bind(gViewController));
       return;
     }
 
     this._loadURL(this.homepageURL.spec, aIsRefresh,
                   gViewController.notifyViewChanged.bind(gViewController));
   },
-  
+
   canRefresh: function gDiscoverView_canRefresh() {
     if (this._browser.currentURI &&
         this._browser.currentURI.spec == this._browser.homePage)
       return false;
     return true;
   },
 
   refresh: function gDiscoverView_refresh(aParam, aRequest, aState) {
@@ -2274,17 +2298,17 @@ var gSearchView = {
         else
           self._lastRemoteTotal = 0;
 
         var createdCount = createSearchResults(aAddonsList, false, true);
         finishSearch(createdCount);
       }
     });
   },
-  
+
   showLoading: function gSearchView_showLoading(aLoading) {
     this._loading.hidden = !aLoading;
     this._listBox.hidden = aLoading;
   },
 
   updateView: function gSearchView_updateView() {
     var showLocal = this._filter.value == "local";
 
@@ -2615,17 +2639,17 @@ var gDetailView = {
 
     this._autoUpdate = document.getElementById("detail-autoUpdate");
 
     var self = this;
     this._autoUpdate.addEventListener("command", function autoUpdate_onCommand() {
       self._addon.applyBackgroundUpdates = self._autoUpdate.value;
     }, true);
   },
-  
+
   shutdown: function gDetailView_shutdown() {
     AddonManager.removeManagerListener(this);
   },
 
   onUpdateModeChanged: function gDetailView_onUpdateModeChanged() {
     this.onPropertyChanged(["applyBackgroundUpdates"]);
   },
 
@@ -2786,17 +2810,17 @@ var gDetailView = {
       document.getElementById("detail-findUpdates-btn").hidden = hideFindUpdates;
     } else {
       this._autoUpdate.hidden = true;
       document.getElementById("detail-findUpdates-btn").hidden = false;
     }
 
     document.getElementById("detail-prefs-btn").hidden = !aIsRemote &&
       !gViewController.commands.cmd_showItemPreferences.isEnabled(aAddon);
-    
+
     var gridRows = document.querySelectorAll("#detail-grid rows row");
     let first = true;
     for (let gridRow of gridRows) {
       if (first && window.getComputedStyle(gridRow, null).getPropertyValue("display") != "none") {
         gridRow.setAttribute("first-row", true);
         first = false;
       } else {
         gridRow.removeAttribute("first-row");
@@ -3103,17 +3127,17 @@ var gDetailView = {
 
   scrollToPreferencesRows: function gDetailView_scrollToPreferencesRows() {
     // We find this row, rather than remembering it from above,
     // in case it has been changed by the observers.
     let firstRow = gDetailView.node.querySelector('setting[first-row="true"]');
     if (firstRow) {
       let top = firstRow.boxObject.y;
       top -= parseInt(window.getComputedStyle(firstRow, null).getPropertyValue("margin-top"));
-      
+
       let detailViewBoxObject = gDetailView.node.boxObject;
       top -= detailViewBoxObject.y;
 
       detailViewBoxObject.QueryInterface(Ci.nsIScrollBoxObject);
       detailViewBoxObject.scrollTo(0, top);
     }
   },
 
@@ -3343,17 +3367,17 @@ var gUpdatesView = {
       }).length;
       self._categoryItem.disabled = gViewController.currentViewId != "addons://updates/available" &&
                                     count == 0;
       self._categoryItem.badgeCount = count;
       if (aInitializing)
         notifyInitialized();
     });
   },
-  
+
   maybeDisableUpdateSelected: function gUpdatesView_maybeDisableUpdateSelected() {
     for (let item of this._listBox.childNodes) {
       if (item.includeUpdate) {
         this._updateSelected.disabled = false;
         return;
       }
     }
     this._updateSelected.disabled = true;
@@ -3406,16 +3430,21 @@ var gUpdatesView = {
   },
 
   onPropertyChanged: function gUpdatesView_onPropertyChanged(aAddon, aProperties) {
     if (aProperties.indexOf("applyBackgroundUpdates") != -1)
       this.updateAvailableCount();
   }
 };
 
+function debuggingPrefChanged() {
+  gViewController.updateState();
+  gViewController.updateCommands();
+  gViewController.notifyViewChanged();
+}
 
 var gDragDrop = {
   onDragOver: function gDragDrop_onDragOver(aEvent) {
     var types = aEvent.dataTransfer.types;
     if (types.contains("text/uri-list") ||
         types.contains("text/x-moz-url") ||
         types.contains("application/x-moz-file"))
       aEvent.preventDefault();
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -191,17 +191,17 @@
       <property name="status">
         <getter><![CDATA[
           return this._status.value;
         ]]></getter>
         <setter><![CDATA[
           this._status.value = val;
         ]]></setter>
       </property>
-      
+
       <method name="cancel">
         <body><![CDATA[
           this.mInstall.cancel();
         ]]></body>
       </method>
     </implementation>
   </binding>
 
@@ -862,16 +862,21 @@
 #ifdef XP_WIN
                             label="&cmd.showPreferencesWin.label;"
                             tooltiptext="&cmd.showPreferencesWin.tooltip;"
 #else
                             label="&cmd.showPreferencesUnix.label;"
                             tooltiptext="&cmd.showPreferencesUnix.tooltip;"
 #endif
                             oncommand="document.getBindingParent(this).showPreferences();"/>
+                            <!-- label="&cmd.debugAddon.label;" -->
+                <xul:button anonid="debug-btn" class="addon-control debug"
+                            label="&cmd.debugAddon.label;"
+                            oncommand="document.getBindingParent(this).debug();"/>
+
                 <xul:button anonid="enable-btn"  class="addon-control enable"
                             label="&cmd.enableAddon.label;"
                             oncommand="document.getBindingParent(this).userDisabled = false;"/>
                 <xul:button anonid="disable-btn" class="addon-control disable"
                             label="&cmd.disableAddon.label;"
                             oncommand="document.getBindingParent(this).userDisabled = true;"/>
                 <xul:button anonid="remove-btn" class="addon-control remove"
                             label="&cmd.uninstallAddon.label;"
@@ -998,16 +1003,20 @@
       <field name="_preferencesBtn">
         document.getAnonymousElementByAttribute(this, "anonid",
                                                 "preferences-btn");
       </field>
       <field name="_enableBtn">
         document.getAnonymousElementByAttribute(this, "anonid",
                                                 "enable-btn");
       </field>
+      <field name="_debugBtn">
+        document.getAnonymousElementByAttribute(this, "anonid",
+                                                "debug-btn");
+      </field>
       <field name="_disableBtn">
         document.getAnonymousElementByAttribute(this, "anonid",
                                                 "disable-btn");
       </field>
       <field name="_removeBtn">
         document.getAnonymousElementByAttribute(this, "anonid",
                                                 "remove-btn");
       </field>
@@ -1327,16 +1336,22 @@
             this._removeBtn.hidden = true;
           }
 
           this.setAttribute("active", this.mAddon.isActive);
 
           var showProgress = this.mAddon.purchaseURL || (this.mAddon.install &&
                              this.mAddon.install.state != AddonManager.STATE_INSTALLED);
           this._showStatus(showProgress ? "progress" : "none");
+
+          let debuggable = this.mAddon.isDebuggable &&
+                           Services.prefs.getBoolPref('devtools.debugger.addon-enabled') &&
+                           Services.prefs.getBoolPref('devtools.debugger.remote-enabled');
+
+          this._debugBtn.disabled = this._debugBtn.hidden = !debuggable
         ]]></body>
       </method>
 
       <method name="_updateUpgradeInfo">
         <body><![CDATA[
           // Only update the version string if we're displaying the upgrade info
           if (this.hasAttribute("upgrade") && shouldShowVersionNumber(this.mAddon))
             this._version.value = this.mManualUpdate.version;
@@ -1348,21 +1363,21 @@
         <body><![CDATA[
           var self = this;
           if (!aURI || this._relNotesLoaded) {
             sendToggleEvent();
             return;
           }
 
           var relNotesData = null, transformData = null;
- 
+
           this._relNotesLoaded = true;
           this._relNotesLoading.hidden = false;
           this._relNotesError.hidden = true;
-          
+
           function sendToggleEvent() {
             var event = document.createEvent("Events");
             event.initEvent("RelNotesToggle", true, true);
             self.dispatchEvent(event);
           }
 
           function showRelNotes() {
             if (!relNotesData || !transformData)
@@ -1487,16 +1502,22 @@
             // This won't update any other add-on manager views (bug 582002)
             this.setAttribute("pending", "uninstall");
           } else {
             this.mAddon.uninstall();
           }
         ]]></body>
       </method>
 
+      <method name="debug">
+        <body><![CDATA[
+          gViewController.doCommand("cmd_debugItem", this.mAddon);
+        ]]></body>
+      </method>
+
       <method name="showPreferences">
         <body><![CDATA[
           gViewController.doCommand("cmd_showItemPreferences", this.mAddon);
         ]]></body>
       </method>
 
       <method name="upgrade">
         <body><![CDATA[
@@ -1875,17 +1896,17 @@
 
       <method name="refreshInfo">
         <body><![CDATA[
           this.mAddon = this.mAddon || this.mInstall.addon;
           if (this.mAddon) {
             this._icon.src = this.mAddon.iconURL ||
                              (this.mInstall ? this.mInstall.iconURL : "");
             this._name.value = this.mAddon.name;
-            
+
             if (this.mAddon.version) {
               this._version.value = this.mAddon.version;
               this._version.hidden = false;
             } else {
               this._version.hidden = true;
             }
 
           } else {
@@ -1926,17 +1947,17 @@
             );
             this._warningLink.label = gStrings.ext.GetStringFromName("notification.installError.retry");
             this._warningLink.tooltipText = gStrings.ext.GetStringFromName("notification.downloadError.retry.tooltip");
           } else {
             this.removeAttribute("notification");
           }
         ]]></body>
       </method>
-      
+
       <method name="retryInstall">
         <body><![CDATA[
           this.mInstall.install();
         ]]></body>
       </method>
     </implementation>
   </binding>
 
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -47,16 +47,18 @@
                 label="&cmd.disableTheme.label;"
                 accesskey="&cmd.disableTheme.accesskey;"/>
       <menuitem id="menuitem_installItem" command="cmd_installItem"
                 label="&cmd.installAddon.label;"
                 accesskey="&cmd.installAddon.accesskey;"/>
       <menuitem id="menuitem_uninstallItem" command="cmd_uninstallItem"
                 label="&cmd.uninstallAddon.label;"
                 accesskey="&cmd.uninstallAddon.accesskey;"/>
+      <menuitem id="menuitem_debugItem" command="cmd_debugItem"
+                label="&cmd.debugAddon.label;"/>
       <menuseparator id="addonitem-menuseparator" />
       <menuitem id="menuitem_preferences" command="cmd_showItemPreferences"
 #ifdef XP_WIN
                 label="&cmd.preferencesWin.label;"
                 accesskey="&cmd.preferencesWin.accesskey;"/>
 #else
                 label="&cmd.preferencesUnix.label;"
                 accesskey="&cmd.preferencesUnix.accesskey;"/>
@@ -91,16 +93,17 @@
 
   <!-- view commands - these act on the selected addon -->
   <commandset id="viewCommandSet"
               events="richlistbox-select" commandupdater="true">
     <command id="cmd_showItemDetails"/>
     <command id="cmd_findItemUpdates"/>
     <command id="cmd_showItemPreferences"/>
     <command id="cmd_showItemAbout"/>
+    <command id="cmd_debugItem"/>
     <command id="cmd_enableItem"/>
     <command id="cmd_disableItem"/>
     <command id="cmd_installItem"/>
     <command id="cmd_purchaseItem"/>
     <command id="cmd_uninstallItem"/>
     <command id="cmd_cancelUninstallItem"/>
     <command id="cmd_cancelOperation"/>
     <command id="cmd_contribute"/>
@@ -592,16 +595,19 @@
                             tooltiptext="&detail.showPreferencesWin.tooltip;"
 #else
                             label="&detail.showPreferencesUnix.label;"
                             accesskey="&detail.showPreferencesUnix.accesskey;"
                             tooltiptext="&detail.showPreferencesUnix.tooltip;"
 #endif
                             command="cmd_showItemPreferences"/>
                     <spacer flex="1"/>
+                    <button id="detail-debug-btn" class="addon-control debug"
+                            label="Debug"
+                            command="cmd_debugItem" />
                     <button id="detail-enable-btn" class="addon-control enable"
                             label="&cmd.enableAddon.label;"
                             accesskey="&cmd.enableAddon.accesskey;"
                             command="cmd_enableItem"/>
                     <button id="detail-disable-btn" class="addon-control disable"
                             label="&cmd.disableAddon.label;"
                             accesskey="&cmd.disableAddon.accesskey;"
                             command="cmd_disableItem"/>
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -6474,16 +6474,20 @@ function AddonWrapper(aAddon) {
     if (XPIProvider.enableRequiresRestart(aAddon))
       ops |= AddonManager.OP_NEEDS_RESTART_ENABLE;
     if (XPIProvider.disableRequiresRestart(aAddon))
       ops |= AddonManager.OP_NEEDS_RESTART_DISABLE;
 
     return ops;
   });
 
+  this.__defineGetter__("isDebuggable", function AddonWrapper_isDebuggable() {
+    return this.isActive && aAddon.bootstrap;
+  });
+
   this.__defineGetter__("permissions", function AddonWrapper_permisionsGetter() {
     let permissions = 0;
 
     // Add-ons that aren't installed cannot be modified in any way
     if (!(aAddon.inDatabase))
       return permissions;
 
     if (!aAddon.appDisabled) {
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -1067,17 +1067,17 @@ this.XPIDatabase = {
   getAddonList: function(aFilter, aCallback) {
     this.asyncLoadDB().then(
       addonDB => {
         let addonList = _filterDB(addonDB, aFilter);
         asyncMap(addonList, getRepositoryAddon, makeSafe(aCallback));
       })
     .then(null,
         error => {
-          logger.error("getAddonList failed", e);
+          logger.error("getAddonList failed", error);
           makeSafe(aCallback)([]);
         });
   },
 
   /**
    * (Possibly asynchronously) get the first addon that matches the filter function
    * @param  aFilter
    *         Function that takes an addon instance and returns
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_jetpack/bootstrap.js
@@ -0,0 +1,17 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function install(data, reason) {
+  Services.prefs.setIntPref("jetpacktest.installed_version", 1);
+}
+
+function startup(data, reason) {
+  Services.prefs.setIntPref("jetpacktest.active_version", 1);
+}
+
+function shutdown(data, reason) {
+  Services.prefs.setIntPref("jetpacktest.active_version", 0);
+}
+
+function uninstall(data, reason) {
+  Services.prefs.setIntPref("jetpacktest.installed_version", 0);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_jetpack/harness-options.json
@@ -0,0 +1,1 @@
+{}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_jetpack/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>jetpack@tests.mozilla.org</em:id>
+    <em:version>1.0</em:version>
+    <em:bootstrap>true</em:bootstrap>
+
+    <!-- Front End MetaData -->
+    <em:name>Test jetpack</em:name>
+    <em:description>Test Description</em:description>
+
+    <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+    <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+    <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>1</em:minVersion>
+        <em:maxVersion>1</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
--- a/toolkit/mozapps/extensions/test/browser/browser-common.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser-common.ini
@@ -24,16 +24,17 @@ support-files =
 [browser_bug593535.js]
 [browser_bug596336.js]
 [browser_bug608316.js]
 [browser_bug610764.js]
 [browser_bug618502.js]
 [browser_bug679604.js]
 [browser_bug714593.js]
 [browser_bug590347.js]
+[browser_debug_button.js]
 [browser_details.js]
 [browser_discovery.js]
 [browser_dragdrop.js]
 [browser_list.js]
 [browser_metadataTimeout.js]
 [browser_searching.js]
 [browser_sorting.js]
 [browser_uninstalling.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_debug_button.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Tests debug button for addons in list view
+ */
+
+let { Promise } = Components.utils.import("resource://gre/modules/Promise.jsm", {});
+let { Task } = Components.utils.import("resource://gre/modules/Task.jsm", {});
+
+const getDebugButton = node =>
+    node.ownerDocument.getAnonymousElementByAttribute(node, "anonid", "debug-btn");
+const addonDebuggingEnabled = bool =>
+  Services.prefs.setBoolPref("devtools.debugger.addon-enabled", !!bool);
+const remoteDebuggingEnabled = bool =>
+  Services.prefs.setBoolPref("devtools.debugger.remote-enabled", !!bool);
+
+function test() {
+  requestLongerTimeout(2);
+
+  waitForExplicitFinish();
+
+
+  var gProvider = new MockProvider();
+  gProvider.createAddons([{
+    id: "non-debuggable@tests.mozilla.org",
+    name: "No debug",
+    description: "foo"
+  },
+  {
+    id: "debuggable@tests.mozilla.org",
+    name: "Debuggable",
+    description: "bar",
+    isDebuggable: true
+  }]);
+
+  Task.spawn(function* () {
+    addonDebuggingEnabled(false);
+    remoteDebuggingEnabled(false);
+
+    yield testDOM((nondebug, debuggable) => {
+      is(nondebug.disabled, true,
+        "addon:disabled::remote:disabled button is disabled for legacy addons");
+      is(nondebug.hidden, true,
+        "addon:disabled::remote:disabled button is hidden for legacy addons");
+      is(debuggable.disabled, true,
+        "addon:disabled::remote:disabled button is disabled for debuggable addons");
+      is(debuggable.hidden, true,
+        "addon:disabled::remote:disabled button is hidden for debuggable addons");
+    });
+    
+    addonDebuggingEnabled(true);
+    remoteDebuggingEnabled(false);
+
+    yield testDOM((nondebug, debuggable) => {
+      is(nondebug.disabled, true,
+        "addon:enabled::remote:disabled button is disabled for legacy addons");
+      is(nondebug.disabled, true,
+        "addon:enabled::remote:disabled button is hidden for legacy addons");
+      is(debuggable.disabled, true,
+        "addon:enabled::remote:disabled button is disabled for debuggable addons");
+      is(debuggable.disabled, true,
+        "addon:enabled::remote:disabled button is hidden for debuggable addons");
+    });
+    
+    addonDebuggingEnabled(false);
+    remoteDebuggingEnabled(true);
+
+    yield testDOM((nondebug, debuggable) => {
+      is(nondebug.disabled, true,
+        "addon:disabled::remote:enabled button is disabled for legacy addons");
+      is(nondebug.disabled, true,
+        "addon:disabled::remote:enabled button is hidden for legacy addons");
+      is(debuggable.disabled, true,
+        "addon:disabled::remote:enabled button is disabled for debuggable addons");
+      is(debuggable.disabled, true,
+        "addon:disabled::remote:enabled button is hidden for debuggable addons");
+    });
+    
+    addonDebuggingEnabled(true);
+    remoteDebuggingEnabled(true);
+
+    yield testDOM((nondebug, debuggable) => {
+      is(nondebug.disabled, true,
+        "addon:enabled::remote:enabled button is disabled for legacy addons");
+      is(nondebug.disabled, true,
+        "addon:enabled::remote:enabled button is hidden for legacy addons");
+      is(debuggable.disabled, false,
+        "addon:enabled::remote:enabled button is enabled for debuggable addons");
+      is(debuggable.hidden, false,
+        "addon:enabled::remote:enabled button is visible for debuggable addons");
+    });
+
+    finish();
+  });
+
+  function testDOM (testCallback) {
+    let deferred = Promise.defer();
+    open_manager("addons://list/extension", function(aManager) {
+      const {document} = aManager;
+      const addonList = document.getElementById("addon-list");
+      const nondebug = addonList.querySelector("[name='No debug']");
+      const debuggable = addonList.querySelector("[name='Debuggable']");
+
+      testCallback.apply(null, [nondebug, debuggable].map(getDebugButton));
+
+      close_manager(aManager, deferred.resolve);
+    });
+    return deferred.promise;
+  }
+}
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -63,16 +63,18 @@ var gRestorePrefs = [{name: PREF_LOGGING
                      {name: "extensions.webservice.discoverURL"},
                      {name: "extensions.update.url"},
                      {name: "extensions.update.background.url"},
                      {name: "extensions.getAddons.get.url"},
                      {name: "extensions.getAddons.getWithPerformance.url"},
                      {name: "extensions.getAddons.search.browseURL"},
                      {name: "extensions.getAddons.search.url"},
                      {name: "extensions.getAddons.cache.enabled"},
+                     {name: "devtools.debugger.addon-enabled"},
+                     {name: "devtools.debugger.remote-enabled"},
                      {name: PREF_SEARCH_MAXRESULTS},
                      {name: PREF_STRICT_COMPAT},
                      {name: PREF_CHECK_COMPATIBILITY}];
 
 for (let pref of gRestorePrefs) {
   if (!Services.prefs.prefHasUserValue(pref.name)) {
     pref.type = "clear";
     continue;
@@ -945,16 +947,17 @@ MockProvider.prototype = {
 
 function MockAddon(aId, aName, aType, aOperationsRequiringRestart) {
   // Only set required attributes.
   this.id = aId || "";
   this.name = aName || "";
   this.type = aType || "extension";
   this.version = "";
   this.isCompatible = true;
+  this.isDebuggable = false;
   this.providesUpdatesSecurely = true;
   this.blocklistState = 0;
   this._appDisabled = false;
   this._userDisabled = false;
   this._applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
   this.scope = AddonManager.SCOPE_PROFILE;
   this.isActive = true;
   this.creator = "";
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js
@@ -0,0 +1,36 @@
+/* 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/.
+ */
+
+var ADDONS = [
+  "test_bootstrap2_1", // restartless addon
+  "test_bootstrap1_4", // old-school addon
+  "test_jetpack"       // sdk addon
+];
+
+var IDS = [
+  "bootstrap1@tests.mozilla.org",
+  "bootstrap2@tests.mozilla.org",
+  "jetpack@tests.mozilla.org"
+];
+
+function run_test() {
+  do_test_pending();
+  
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+  startupManager();
+  AddonManager.checkCompatibility = false;
+
+  installAllFiles(ADDONS.map(do_get_addon), function () {
+    restartManager();
+
+    AddonManager.getAddonsByIDs(IDS, function([a1, a2, a3]) {
+      do_check_eq(a1.isDebuggable, false);
+      do_check_eq(a2.isDebuggable, true);
+      do_check_eq(a3.isDebuggable, true);
+      do_test_finished();
+    });
+  }, true);
+}
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -189,16 +189,17 @@ skip-if = os == "android"
 [test_install.js]
 [test_install_icons.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 [test_install_strictcompat.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 run-sequentially = Uses hardcoded ports in xpi files.
+[test_isDebuggable.js]
 [test_locale.js]
 [test_locked.js]
 [test_locked2.js]
 [test_locked_strictcompat.js]
 [test_manifest.js]
 [test_mapURIToAddonID.js]
 # Same as test_bootstrap.js
 skip-if = os == "android"