Bug 1132203 - Load JSON viewer dynamically on demand; r=jryans
☠☠ backed out by 288d12e9236b ☠ ☠
authorJan Odvarko <odvarko@gmail.com>
Mon, 28 Sep 2015 13:34:03 +0200
changeset 265026 f6fa722a86362c4f2ef00f54bbbf1c2304b30bb0
parent 265025 ce3ca3b43597abde28b5a37dd4e2ec2f1d0645be
child 265027 ed8ce0300161aef4d0b061c5fddb69b2afced8bc
push id65832
push userkwierso@gmail.com
push dateTue, 29 Sep 2015 23:14:05 +0000
treeherdermozilla-inbound@00ac696cdc86 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1132203
milestone44.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 1132203 - Load JSON viewer dynamically on demand; r=jryans
devtools/client/jsonview/converter-child.js
devtools/client/jsonview/converter-observer.js
devtools/client/jsonview/main.js
devtools/client/jsonview/moz.build
devtools/client/jsonview/test/head.js
testing/profiles/prefs_general.js
--- a/devtools/client/jsonview/converter-child.js
+++ b/devtools/client/jsonview/converter-child.js
@@ -1,36 +1,33 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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 Cc = Components.classes;
-
-const {devtools} = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
-const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = devtools.require;
-
+const {Cu, Cc, Ci, components} = require("chrome");
 const {Class} = require("sdk/core/heritage");
 const {Unknown} = require("sdk/platform/xpcom");
 const xpcom = require("sdk/platform/xpcom");
 const Events = require("sdk/dom/events");
 const Clipboard = require("sdk/clipboard");
 
-const NetworkHelper = require("devtools/shared/webconsole/network-helper");
-const JsonViewUtils = require("devtools/client/jsonview/utils");
+loader.lazyRequireGetter(this, "NetworkHelper",
+                               "devtools/shared/webconsole/network-helper");
+loader.lazyRequireGetter(this, "JsonViewUtils",
+                               "devtools/client/jsonview/utils");
 
-let childProcessMessageManager = Cc["@mozilla.org/childprocessmessagemanager;1"].
-  getService(Ci.nsISyncMessageSender);
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+const childProcessMessageManager =
+  Cc["@mozilla.org/childprocessmessagemanager;1"].
+    getService(Ci.nsISyncMessageSender);
 
 // Amount of space that will be allocated for the stream's backing-store.
 // Must be power of 2. Used to copy the data stream in onStopRequest.
 const SEGMENT_SIZE = Math.pow(2, 17);
 
 // Localization
 var jsonViewStrings = Services.strings.createBundle(
   "chrome://browser/locale/devtools/jsonview.properties");
@@ -112,20 +109,16 @@ var Converter = Class({
    * 4. Spit it back out at the listener
    */
   onStopRequest: function(aRequest, aContext, aStatusCode) {
     let headers = {
       response: [],
       request: []
     }
 
-    if (!(aRequest instanceof Ci.nsIHttpChannel)) {
-      return;
-    }
-
     let win = NetworkHelper.getWindowForRequest(aRequest);
 
     let Locale = {
       $STR: key => {
         try {
           return jsonViewStrings.GetStringFromName(key);
         } catch (err) {
           Cu.reportError(err);
@@ -136,27 +129,31 @@ var Converter = Class({
     JsonViewUtils.exportIntoContentScope(win, Locale, "Locale");
 
     Events.once(win, "DOMContentLoaded", event => {
       Cu.exportFunction(this.postChromeMessage.bind(this), win, {
         defineAs: "postChromeMessage"
       });
     })
 
-    aRequest.visitResponseHeaders({
-      visitHeader: function(name, value) {
-        headers.response.push({name: name, value: value});
-      }
-    });
+    // The request doesn't have to be always nsIHttpChannel
+    // (e.g. in case of data: URLs)
+    if (aRequest instanceof Ci.nsIHttpChannel) {
+      aRequest.visitResponseHeaders({
+        visitHeader: function(name, value) {
+          headers.response.push({name: name, value: value});
+        }
+      });
 
-    aRequest.visitRequestHeaders({
-      visitHeader: function(name, value) {
-        headers.request.push({name: name, value: value});
-      }
-    });
+      aRequest.visitRequestHeaders({
+        visitHeader: function(name, value) {
+          headers.request.push({name: name, value: value});
+        }
+      });
+    }
 
     let outputDoc = "";
 
     try {
       headers = JSON.stringify(headers);
       outputDoc = this.toHTML(this.data, headers, this.uri);
     } catch (e) {
       Cu.reportError("JSON Viewer ERROR " + e);
@@ -271,33 +268,42 @@ var Converter = Class({
       var header = requestHeaders[i];
       value += header.name + ": " + header.value + eol;
     }
 
     Clipboard.set(value, "text");
   }
 });
 
-// Stream converter component registration
-const JSON_TYPE = "application/json";
-const CONTRACT_ID = "@mozilla.org/streamconv;1?from=" + JSON_TYPE + "&to=*/*";
+// Stream converter component definition
+const CONTRACT_ID = "@mozilla.org/streamconv;1?from=application/json&to=*/*";
 const CLASS_ID = "{d8c9acee-dec5-11e4-8c75-1681e6b88ec1}";
-const GECKO_VIEWER = "Gecko-Content-Viewers";
 
 var service = xpcom.Service({
-  id: Components.ID(CLASS_ID),
+  id: components.ID(CLASS_ID),
   contract: CONTRACT_ID,
   Component: Converter,
   register: false,
   unregister: false
 });
 
-if (!xpcom.isRegistered(service)) {
-  xpcom.register(service);
+function register() {
+  if (!xpcom.isRegistered(service)) {
+    xpcom.register(service);
+    return true;
+  }
+
+  return false;
 }
 
-// Remove native Gecko viewer
-var categoryManager = Cc["@mozilla.org/categorymanager;1"].
-  getService(Ci.nsICategoryManager);
-categoryManager.deleteCategoryEntry(GECKO_VIEWER, JSON_TYPE, false);
+function unregister() {
+  if (xpcom.isRegistered(service)) {
+    xpcom.unregister(service);
+    return true;
+  }
 
-categoryManager.addCategoryEntry("ext-to-type-mapping", "json",
-  JSON_TYPE, false, true);
+  return false;
+}
+
+exports.JsonViewService = {
+  register: register,
+  unregister: unregister
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/jsonview/converter-observer.js
@@ -0,0 +1,111 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+const categoryManager = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
+
+// Load JsonView service module (converter-child.js) as soon as required.
+XPCOMUtils.defineLazyGetter(this, "JsonViewService", function() {
+  const {devtools} = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
+  const {JsonViewService} = devtools.require("devtools/client/jsonview/converter-child");
+  return JsonViewService;
+});
+
+const JSON_TYPE = "application/json";
+const GECKO_VIEWER = "Gecko-Content-Viewers";
+const JSON_VIEW_PREF = "devtools.jsonview.enabled";
+const GECKO_TYPE_MAPPING = "ext-to-type-mapping";
+
+/**
+ * Listen for 'devtools.jsonview.enabled' preference changes and
+ * register/unregister the JSON View XPCOM service as appropriate.
+ */
+function ConverterObserver() {
+}
+
+ConverterObserver.prototype = {
+  initialize: function() {
+    this.geckoViewer = categoryManager.getCategoryEntry(GECKO_VIEWER, JSON_TYPE);
+
+    // Only the DevEdition has this feature available by default.
+    // Users need to manually flip 'devtools.jsonview.enabled' preference
+    // to have it available in other distributions.
+    if (this.isEnabled()) {
+      this.register();
+    }
+
+    Services.prefs.addObserver(JSON_VIEW_PREF, this, false);
+    Services.obs.addObserver(this, "xpcom-shutdown", false);
+  },
+
+  observe: function(subject, topic, data) {
+    switch (topic) {
+      case "xpcom-shutdown":
+        this.onShutdown();
+        break;
+      case "nsPref:changed":
+        this.onPrefChanged();
+        break;
+    };
+  },
+
+  onShutdown: function() {
+    Services.prefs.removeObserver(JSON_VIEW_PREF, observer);
+    Services.obs.removeObserver(observer, "xpcom-shutdown");
+  },
+
+  onPrefChanged: function() {
+    if (this.isEnabled()) {
+      this.register();
+    } else {
+      this.unregister();
+    }
+  },
+
+  register: function() {
+    if (JsonViewService.register()) {
+      // Delete default JSON viewer (text)
+      categoryManager.deleteCategoryEntry(GECKO_VIEWER, JSON_TYPE, false);
+
+      // Append new *.json -> application/json type mapping
+      this.geckoMapping = categoryManager.addCategoryEntry(GECKO_TYPE_MAPPING,
+        "json", JSON_TYPE, false, true);
+    }
+  },
+
+  unregister: function() {
+    if (JsonViewService.unregister()) {
+      categoryManager.addCategoryEntry(GECKO_VIEWER, JSON_TYPE,
+        this.geckoViewer, false, false);
+
+      if (this.geckoMapping) {
+        categoryManager.addCategoryEntry(GECKO_TYPE_MAPPING, "json",
+          this.geckoMapping, false, true);
+      } else {
+        categoryManager.deleteCategoryEntry(GECKO_TYPE_MAPPING,
+          JSON_TYPE, false)
+      }
+    }
+  },
+
+  isEnabled: function() {
+    return Services.prefs.getBoolPref(JSON_VIEW_PREF);
+  },
+};
+
+// Listen to JSON View 'enable' pref and perform dynamic
+// registration or unregistration of the main application
+// component.
+var observer = new ConverterObserver();
+observer.initialize();
--- a/devtools/client/jsonview/main.js
+++ b/devtools/client/jsonview/main.js
@@ -1,63 +1,48 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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, Ci, Cc } = require("chrome");
+const {Cu, Ci, Cc} = require("chrome");
 const JsonViewUtils = require("devtools/client/jsonview/utils");
 
-Cu.import("resource://gre/modules/Services.jsm");
-
-const { makeInfallible } = require("devtools/shared/DevToolsUtils");
-
-const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"].
-  getService(Ci.nsIMessageListenerManager);
-const parentProcessMessageManager = Cc["@mozilla.org/parentprocessmessagemanager;1"].
-  getService(Ci.nsIMessageBroadcaster);
-
 // Constants
-const JSON_VIEW_PREF = "devtools.jsonview.enabled";
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+const {makeInfallible} = require("devtools/shared/DevToolsUtils");
 
 /**
  * Singleton object that represents the JSON View in-content tool.
  * It has the same lifetime as the browser. Initialization done by
  * DevTools() object from gDevTools.jsm
  */
 var JsonView = {
   initialize: makeInfallible(function() {
-    // Only the DevEdition has this feature available by default.
-    // Users need to manually flip 'devtools.jsonview.enabled' preference
-    // to have it available in other distributions.
-    if (Services.prefs.getBoolPref(JSON_VIEW_PREF)) {
-      // Load JSON converter module. This converter is responsible
-      // for handling 'application/json' documents and converting
-      // them into a simple web-app that allows easy inspection
-      // of the JSON data.
-      globalMessageManager.loadFrameScript(
-        "resource:///modules/devtools/client/jsonview/converter-child.js",
-        true);
+    // Load JSON converter module. This converter is responsible
+    // for handling 'application/json' documents and converting
+    // them into a simple web-app that allows easy inspection
+    // of the JSON data.
+    Services.ppmm.loadProcessScript(
+      "resource:///modules/devtools/client/jsonview/converter-observer.js",
+      true);
 
-      this.onSaveListener = this.onSave.bind(this);
+    this.onSaveListener = this.onSave.bind(this);
 
-      // Register for messages coming from the child process.
-      parentProcessMessageManager.addMessageListener(
-        "devtools:jsonview:save", this.onSaveListener);
-    }
+    // Register for messages coming from the child process.
+    Services.ppmm.addMessageListener(
+      "devtools:jsonview:save", this.onSaveListener);
   }),
 
   destroy: makeInfallible(function() {
-    if (this.onSaveListener) {
-      parentProcessMessageManager.removeMessageListener(
-        "devtools:jsonview:save", this.onSaveListener);
-    }
+    Services.ppmm.removeMessageListener(
+      "devtools:jsonview:save", this.onSaveListener);
   }),
 
   // Message handlers for events from child processes
 
   /**
    * Save JSON to a file needs to be implemented here
    * in the parent process.
    */
--- a/devtools/client/jsonview/moz.build
+++ b/devtools/client/jsonview/moz.build
@@ -7,15 +7,16 @@
 DIRS += [
     'components',
     'css',
     'lib'
 ]
 
 DevToolsModules(
     'converter-child.js',
+    'converter-observer.js',
     'json-viewer.js',
     'main.js',
     'utils.js',
     'viewer-config.js'
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/jsonview/test/head.js
+++ b/devtools/client/jsonview/test/head.js
@@ -3,16 +3,25 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // shared-head.js handles imports, constants, and utility functions
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/head.js", this);
 
+const JSON_VIEW_PREF = "devtools.jsonview.enabled";
+
+// Enable JSON View for the test
+Services.prefs.setBoolPref(JSON_VIEW_PREF, true);
+
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref(JSON_VIEW_PREF);
+});
+
 // XXX move some API into devtools/framework/test/shared-head.js
 
 /**
  * Add a new test tab in the browser and load the given url.
  * @param {String} url The url to be loaded in the new tab
  * @return a promise that resolves to the tab object when the url is loaded
  */
 function addJsonViewTab(url) {
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -23,17 +23,16 @@ user_pref("signed.applets.codebase_princ
 user_pref("browser.shell.checkDefaultBrowser", false);
 user_pref("shell.checkDefaultClient", false);
 user_pref("browser.warnOnQuit", false);
 user_pref("accessibility.typeaheadfind.autostart", false);
 user_pref("javascript.options.showInConsole", true);
 user_pref("devtools.browsertoolbox.panel", "jsdebugger");
 user_pref("devtools.errorconsole.enabled", true);
 user_pref("devtools.debugger.remote-port", 6023);
-user_pref("devtools.jsonview.enabled", true);
 user_pref("browser.EULA.override", true);
 user_pref("gfx.color_management.force_srgb", true);
 user_pref("network.manage-offline-status", false);
 // Disable speculative connections so they aren't reported as leaking when they're hanging around.
 user_pref("network.http.speculative-parallel-limit", 0);
 user_pref("dom.min_background_timeout_value", 1000);
 user_pref("test.mousescroll", true);
 user_pref("security.default_personal_cert", "Select Automatically"); // Need to client auth test be w/o any dialogs