Bug 1378397 - Introduce shim for managing upcoming DevTools addon. r=jdescottes draft
authorAlexandre Poirot <poirot.alex@gmail.com>
Wed, 09 Aug 2017 11:24:22 +0200
changeset 643099 fa9f4d05a7511ce857a324199b1da1e42cce380b
parent 643096 d896496d6468f63ecf24b8bbb313b517f84cf796
child 643112 1ca9566b7681bb757418f54704c9858ce6242881
push id72995
push userbmo:poirot.alex@gmail.com
push dateWed, 09 Aug 2017 09:28:47 +0000
reviewersjdescottes
bugs1378397
milestone57.0a1
Bug 1378397 - Introduce shim for managing upcoming DevTools addon. r=jdescottes MozReview-Commit-ID: GUJEwwR2rd6
devtools/shim/DevToolsShim.jsm
devtools/shim/devtools-startup-prefs.js
devtools/shim/devtools-startup.js
--- a/devtools/shim/DevToolsShim.jsm
+++ b/devtools/shim/DevToolsShim.jsm
@@ -1,22 +1,37 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cu = Components.utils;
 const Ci = Components.interfaces;
+
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
 const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "console", () => {
+  let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
+  return new ConsoleAPI({
+    maxLogLevelPref: "devtools.addon.install-log",
+    prefix: "Devtools-Addon-Install",
+  });
+});
+
 this.EXPORTED_SYMBOLS = [
   "DevToolsShim",
 ];
 
+// ID defined in add-on's install.rdf file
+const ADDON_ID = "devtools@mozilla.org";
+
 function removeItem(array, callback) {
   let index = array.findIndex(callback);
   if (index >= 0) {
     array.splice(index, 1);
   }
 }
 
 /**
@@ -255,16 +270,93 @@ this.DevToolsShim = {
     for (let theme of this.themes) {
       this._gDevTools.registerTheme(theme);
     }
 
     this.listeners = [];
     this.tools = [];
     this.themes = [];
   },
+
+  async install(onProgress = function () {}) {
+    // First check if the add-on is disabled,
+    // and only re-enable it if that's the case.
+    // Also, do not do anything if the add-on is already insatlled.
+    let addon = await new Promise(resolve => {
+      AddonManager.getAddonByID(ADDON_ID, resolve);
+    });
+    if (addon) {
+      if (addon.userDisabled) {
+        addon.userDisabled = false;
+      }
+      onProgress(1);
+      return Promise.resolve();
+    }
+    return new Promise((resolve, reject) => {
+      let installFailureHandler = (install, message) => {
+        console.error("Install failure: " + message);
+        reject(message);
+      };
+      let listener = {
+        onDownloadStarted() {
+          this.status = "downloading";
+          console.log("Downloading");
+          onProgress(0);
+        },
+
+        onInstallStarted() {
+          this.status = "installing";
+          console.log("Installing");
+        },
+
+        onDownloadProgress(install) {
+          let progress = install.maxProgress == -1 ? -1 :
+            install.progress / install.maxProgress;
+          console.log("Progress: " + progress);
+          onProgress(progress);
+        },
+
+        onInstallEnded(event) {
+          event.addon.userDisabled = false;
+          console.log("Installed!");
+          resolve();
+          onProgress(1);
+        },
+
+        onDownloadEnded(install) {
+          // Assert that the addon we are trying to install has the right ID
+          if (install.addon.id != ADDON_ID) {
+            console.error("Install cancelled as addon id doesn't match devtools one");
+            install.cancel();
+          }
+        },
+        onDownloadCancelled(install) {
+          installFailureHandler(install, "Download cancelled");
+        },
+        onDownloadFailed(install) {
+          installFailureHandler(install, "Download failed");
+        },
+        onInstallCancelled(install) {
+          installFailureHandler(install, "Install cancelled");
+        },
+        onInstallFailed(install) {
+          installFailureHandler(install, "Install failed");
+        },
+      };
+      AddonManager.getInstallForURL(this.addonURL, install => {
+        install.addListener(listener);
+        install.install();
+      }, "application/x-xpinstall");
+    });
+  },
+
+  // Returns the xpi file URL
+  get addonURL() {
+    return Services.prefs.getCharPref("devtools.addon.install-url");
+  },
 };
 
 /**
  * Compatibility layer for addon-sdk. Remove when Firefox 57 hits release.
  *
  * The methods below are used by classes and tests from addon-sdk/
  * If DevTools are not installed when calling one of them, the call will throw.
  */
--- a/devtools/shim/devtools-startup-prefs.js
+++ b/devtools/shim/devtools-startup-prefs.js
@@ -12,8 +12,21 @@
 pref("devtools.jsonview.enabled", true);
 
 // Default theme ("dark" or "light")
 #ifdef MOZ_DEV_EDITION
 sticky_pref("devtools.theme", "dark");
 #else
 sticky_pref("devtools.theme", "light");
 #endif
+
+// DevTools add-on is automatically installed on startup if:
+// - they were opened once,
+// - any DevTools command line argument is passed,
+// This pref allow to disable that.
+pref("devtools.addon.auto-install", true);
+
+// DevTools add-on install URL.
+pref("devtools.addon.install-url", "https://archive.mozilla.org/pub/labs/devtools/master/devtools.xpi");
+
+// Log level of /devtools/shim/devtools-startup.js managing addon installation
+// (Use "log" to see everything)
+pref("devtools.addon.install-log", "error");
--- a/devtools/shim/devtools-startup.js
+++ b/devtools/shim/devtools-startup.js
@@ -16,27 +16,29 @@
  *
  * Only once any of these entry point is fired, this module ensures starting
  * core modules like 'devtools-browser.js' that hooks the browser windows
  * and ensure setting up tools.
  **/
 
 "use strict";
 
-const { interfaces: Ci, utils: Cu } = Components;
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableWidgets",
                                   "resource:///modules/CustomizableWidgets.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DevToolsShim",
+                                  "chrome://devtools-shim/content/DevToolsShim.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "Bundle", function () {
   const kUrl = "chrome://devtools/locale/key-shortcuts.properties";
   return Services.strings.createBundle(kUrl);
 });
 
 XPCOMUtils.defineLazyGetter(this, "KeyShortcuts", function () {
   const isMac = AppConstants.platform == "macosx";
@@ -170,17 +172,49 @@ DevToolsStartup.prototype = {
    */
   developerToggleCreated: false,
 
   // Check if any command line argument specific to DevTools has been given
   hasAnyDevToolsFlag(cmdLine) {
     return DevToolsFlags.some(flag => cmdLine.findFlag(flag, false) != -1);
   },
 
-  handle: function (cmdLine) {
+  // Returns true if DevTools toolbox has been opened at least once for this profile
+  get hasDevToolsEverBeenOpened() {
+    // This telemetry pref is updated whenever we open the toolbox
+    return Services.prefs.prefHasUserValue("devtools.telemetry.tools.opened.version");
+  },
+
+  // Returns true if we should try to automatically install the add-on
+  get autoInstallEnabled() {
+    return Services.prefs.getBoolPref("devtools.addon.auto-install");
+  },
+
+  // Check if DevTools is packaged in this profile, either builtin in the omni.jar,
+  // or via a system-addon or via a regular add-on.
+  // No matter which packaging method is used, it will automatically register
+  // resource://devtools/ to expose Loader.jsm
+  get isInstalled() {
+    let resProto = Cc["@mozilla.org/network/protocol;1?name=resource"]
+      .getService(Ci.nsIResProtocolHandler);
+    return resProto.hasSubstitution("devtools");
+  },
+
+  async handle(cmdLine) {
+    // Firefox builds that still ship DevTools (Nightly, Dev-edition) will have
+    // isInstalled=true and skip that path.
+    if (!this.isInstalled &&
+         this.autoInstallEnabled && (this.hasAnyDevToolsFlag(cmdLine) ||
+                                     this.hasDevToolsEverBeenOpened)) {
+      await DevToolsShim.install();
+
+      // Only try to auto-install the add-on once
+      Services.prefs.setBoolPref("devtools.addon.auto-install", false);
+    }
+
     // Defer command line implementation to a dedicated module to reduce
     // this module size
     if (this.hasAnyDevToolsFlag(cmdLine)) {
       let require = this.initDevTools("CommandLine");
       let CommandLine = require("devtools/client/framework/command-line");
       CommandLine.handle(cmdLine);
     }