Bug 1668117 - [devtools] do not load prefsHelper from devtools-modules in debugger r=nchevobbe
authorJulian Descottes <jdescottes@mozilla.com>
Fri, 02 Oct 2020 15:39:44 +0000
changeset 551361 87389dede385c168717b46d17357699f5180c2e5
parent 551360 06e10dd4a6c42274800f957048457577c3788ce1
child 551362 26b597ec35bb6a9e0da25cfb58a70d2504e48d18
push id127820
push userjdescottes@mozilla.com
push dateFri, 02 Oct 2020 22:08:32 +0000
treeherderautoland@ba4685a82eea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnchevobbe
bugs1668117
milestone83.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 1668117 - [devtools] do not load prefsHelper from devtools-modules in debugger r=nchevobbe Depends on D91858 Differential Revision: https://phabricator.services.mozilla.com/D91859
devtools/client/debugger/jest-test.config.js
devtools/client/debugger/src/test/fixtures/Services.js
devtools/client/debugger/src/utils/prefs.js
devtools/client/shared/prefs.js
devtools/shared/event-emitter.js
--- a/devtools/client/debugger/jest-test.config.js
+++ b/devtools/client/debugger/jest-test.config.js
@@ -35,16 +35,17 @@ module.exports = {
   setupFiles: ["<rootDir>/src/test/shim.js", "jest-localstorage-mock"],
   snapshotSerializers: [
     "jest-serializer-babel-ast",
     "enzyme-to-json/serializer",
   ],
   moduleNameMapper: {
     "\\.css$": "<rootDir>/src/test/__mocks__/styleMock.js",
     "\\.svg$": "<rootDir>/src/test/__mocks__/svgMock.js",
+    "devtools-services": "<rootDir>/src/test/fixtures/Services",
     "^Services": "<rootDir>/src/test/fixtures/Services",
     "^chrome": "<rootDir>/src/test/fixtures/Chrome",
     "^ChromeUtils": "<rootDir>/src/test/fixtures/ChromeUtils",
     "\\/plural-form$": "<rootDir>/src/test/fixtures/plural-form",
     "shared\\/telemetry$": "<rootDir>/src/test/fixtures/telemetry",
     "\\/unicode-url$": "<rootDir>/src/test/fixtures/unicode-url",
     // Map all require("devtools/...") to the real devtools root.
     "^devtools\\/(.*)": "<rootDir>/../../$1",
--- a/devtools/client/debugger/src/test/fixtures/Services.js
+++ b/devtools/client/debugger/src/test/fixtures/Services.js
@@ -1,11 +1,533 @@
 /* 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";
 
-module.exports = {
+/* globals localStorage, window, document, NodeFilter */
+
+// XXX: This file is a copy of the Services shim from devtools-services.
+// See https://github.com/firefox-devtools/devtools-core/blob/a9263b4c3f88ea42879a36cdc3ca8217b4a528ea/packages/devtools-services/index.js
+// Many Jest tests in the debugger rely on preferences, but can't use Services.
+// This fixture is probably doing too much and should be reduced to the minimum
+// needed to pass the tests.
+
+// Some constants from nsIPrefBranch.idl.
+const PREF_INVALID = 0;
+const PREF_STRING = 32;
+const PREF_INT = 64;
+const PREF_BOOL = 128;
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+// We prefix all our local storage items with this.
+const PREFIX = "Services.prefs:";
+
+/**
+ * Create a new preference branch.  This object conforms largely to
+ * nsIPrefBranch and nsIPrefService, though it only implements the
+ * subset needed by devtools.  A preference branch can hold child
+ * preferences while also holding a preference value itself.
+ *
+ * @param {PrefBranch} parent the parent branch, or null for the root
+ *        branch.
+ * @param {String} name the base name of this branch
+ * @param {String} fullName the fully-qualified name of this branch
+ */
+function PrefBranch(parent, name, fullName) {
+  this._parent = parent;
+  this._name = name;
+  this._fullName = fullName;
+  this._observers = {};
+  this._children = {};
+
+  // Properties used when this branch has a value as well.
+  this._defaultValue = null;
+  this._hasUserValue = false;
+  this._userValue = null;
+  this._type = PREF_INVALID;
+}
+
+PrefBranch.prototype = {
+  PREF_INVALID: PREF_INVALID,
+  PREF_STRING: PREF_STRING,
+  PREF_INT: PREF_INT,
+  PREF_BOOL: PREF_BOOL,
+
+  /** @see nsIPrefBranch.root.  */
+  get root() {
+    return this._fullName;
+  },
+
+  /** @see nsIPrefBranch.getPrefType.  */
+  getPrefType: function(prefName) {
+    return this._findPref(prefName)._type;
+  },
+
+  /** @see nsIPrefBranch.getBoolPref.  */
+  getBoolPref: function(prefName, defaultValue) {
+    try {
+      let thePref = this._findPref(prefName);
+      if (thePref._type !== PREF_BOOL) {
+        throw new Error(`${prefName} does not have bool type`);
+      }
+      return thePref._get();
+    } catch (e) {
+      if (typeof defaultValue !== "undefined") {
+        return defaultValue;
+      }
+      throw e;
+    }
+  },
+
+  /** @see nsIPrefBranch.setBoolPref.  */
+  setBoolPref: function(prefName, value) {
+    if (typeof value !== "boolean") {
+      throw new Error("non-bool passed to setBoolPref");
+    }
+    let thePref = this._findOrCreatePref(prefName, value, true, value);
+    if (thePref._type !== PREF_BOOL) {
+      throw new Error(`${prefName} does not have bool type`);
+    }
+    thePref._set(value);
+  },
+
+  /** @see nsIPrefBranch.getCharPref.  */
+  getCharPref: function(prefName, defaultValue) {
+    try {
+      let thePref = this._findPref(prefName);
+      if (thePref._type !== PREF_STRING) {
+        throw new Error(`${prefName} does not have string type`);
+      }
+      return thePref._get();
+    } catch (e) {
+      if (typeof defaultValue !== "undefined") {
+        return defaultValue;
+      }
+      throw e;
+    }
+  },
+
+  /** @see nsIPrefBranch.getStringPref.  */
+  getStringPref: function() {
+    return this.getCharPref.apply(this, arguments);
+  },
+
+  /** @see nsIPrefBranch.setCharPref.  */
+  setCharPref: function(prefName, value) {
+    if (typeof value !== "string") {
+      throw new Error("non-string passed to setCharPref");
+    }
+    let thePref = this._findOrCreatePref(prefName, value, true, value);
+    if (thePref._type !== PREF_STRING) {
+      throw new Error(`${prefName} does not have string type`);
+    }
+    thePref._set(value);
+  },
+
+  /** @see nsIPrefBranch.setStringPref.  */
+  setStringPref: function() {
+    return this.setCharPref.apply(this, arguments);
+  },
+
+  /** @see nsIPrefBranch.getIntPref.  */
+  getIntPref: function(prefName, defaultValue) {
+    try {
+      let thePref = this._findPref(prefName);
+      if (thePref._type !== PREF_INT) {
+        throw new Error(`${prefName} does not have int type`);
+      }
+      return thePref._get();
+    } catch (e) {
+      if (typeof defaultValue !== "undefined") {
+        return defaultValue;
+      }
+      throw e;
+    }
+  },
+
+  /** @see nsIPrefBranch.setIntPref.  */
+  setIntPref: function(prefName, value) {
+    if (typeof value !== "number") {
+      throw new Error("non-number passed to setIntPref");
+    }
+    let thePref = this._findOrCreatePref(prefName, value, true, value);
+    if (thePref._type !== PREF_INT) {
+      throw new Error(`${prefName} does not have int type`);
+    }
+    thePref._set(value);
+  },
+
+  /** @see nsIPrefBranch.clearUserPref */
+  clearUserPref: function(prefName) {
+    let thePref = this._findPref(prefName);
+    thePref._clearUserValue();
+  },
+
+  /** @see nsIPrefBranch.prefHasUserValue */
+  prefHasUserValue: function(prefName) {
+    let thePref = this._findPref(prefName);
+    return thePref._hasUserValue;
+  },
+
+  /** @see nsIPrefBranch.addObserver */
+  addObserver: function(domain, observer, holdWeak) {
+    if (holdWeak) {
+      throw new Error("shim prefs only supports strong observers");
+    }
+
+    if (!(domain in this._observers)) {
+      this._observers[domain] = [];
+    }
+    this._observers[domain].push(observer);
+  },
+
+  /** @see nsIPrefBranch.removeObserver */
+  removeObserver: function(domain, observer) {
+    if (!(domain in this._observers)) {
+      return;
+    }
+    let index = this._observers[domain].indexOf(observer);
+    if (index >= 0) {
+      this._observers[domain].splice(index, 1);
+    }
+  },
+
+  /** @see nsIPrefService.savePrefFile */
+  savePrefFile: function(file) {
+    if (file) {
+      throw new Error("shim prefs only supports null file in savePrefFile");
+    }
+    // Nothing to do - this implementation always writes back.
+  },
+
+  /** @see nsIPrefService.getBranch */
+  getBranch: function(prefRoot) {
+    if (!prefRoot) {
+      return this;
+    }
+    if (prefRoot.endsWith(".")) {
+      prefRoot = prefRoot.slice(0, -1);
+    }
+    // This is a bit weird since it could erroneously return a pref,
+    // not a pref branch.
+    return this._findPref(prefRoot);
+  },
+
+  /**
+   * Return this preference's current value.
+   *
+   * @return {Any} The current value of this preference.  This may
+   *         return a string, a number, or a boolean depending on the
+   *         preference's type.
+   */
+  _get: function() {
+    if (this._hasUserValue) {
+      return this._userValue;
+    }
+    return this._defaultValue;
+  },
+
+  /**
+   * Set the preference's value.  The new value is assumed to be a
+   * user value.  After setting the value, this function emits a
+   * change notification.
+   *
+   * @param {Any} value the new value
+   */
+  _set: function(value) {
+    if (!this._hasUserValue || value !== this._userValue) {
+      this._userValue = value;
+      this._hasUserValue = true;
+      this._saveAndNotify();
+    }
+  },
+
+  /**
+   * Set the default value for this preference, and emit a
+   * notification if this results in a visible change.
+   *
+   * @param {Any} value the new default value
+   */
+  _setDefault: function(value) {
+    if (this._defaultValue !== value) {
+      this._defaultValue = value;
+      if (!this._hasUserValue) {
+        this._saveAndNotify();
+      }
+    }
+  },
+
+  /**
+   * If this preference has a user value, clear it.  If a change was
+   * made, emit a change notification.
+   */
+  _clearUserValue: function() {
+    if (this._hasUserValue) {
+      this._userValue = null;
+      this._hasUserValue = false;
+      this._saveAndNotify();
+    }
+  },
+
+  /**
+   * Helper function to write the preference's value to local storage
+   * and then emit a change notification.
+   */
+  _saveAndNotify: function() {
+    let store = {
+      type: this._type,
+      defaultValue: this._defaultValue,
+      hasUserValue: this._hasUserValue,
+      userValue: this._userValue,
+    };
+
+    localStorage.setItem(PREFIX + this._fullName, JSON.stringify(store));
+    this._parent._notify(this._name);
+  },
+
+  /**
+   * Change this preference's value without writing it back to local
+   * storage.  This is used to handle changes to local storage that
+   * were made externally.
+   *
+   * @param {Number} type one of the PREF_* values
+   * @param {Any} userValue the user value to use if the pref does not exist
+   * @param {Any} defaultValue the default value to use if the pref
+   *        does not exist
+   * @param {Boolean} hasUserValue if a new pref is created, whether
+   *        the default value is also a user value
+   * @param {Object} store the new value of the preference.  It should
+   *        be of the form {type, defaultValue, hasUserValue, userValue};
+   *        where |type| is one of the PREF_* type constants; |defaultValue|
+   *        and |userValue| are the default and user values, respectively;
+   *        and |hasUserValue| is a boolean indicating whether the user value
+   *        is valid
+   */
+  _storageUpdated: function(type, userValue, hasUserValue, defaultValue) {
+    this._type = type;
+    this._defaultValue = defaultValue;
+    this._hasUserValue = hasUserValue;
+    this._userValue = userValue;
+    // There's no need to write this back to local storage, since it
+    // came from there; and this avoids infinite event loops.
+    this._parent._notify(this._name);
+  },
+
+  /**
+   * Helper function to find either a Preference or PrefBranch object
+   * given its name.  If the name is not found, throws an exception.
+   *
+   * @param {String} prefName the fully-qualified preference name
+   * @return {Object} Either a Preference or PrefBranch object
+   */
+  _findPref: function(prefName) {
+    let branchNames = prefName.split(".");
+    let branch = this;
+
+    for (let branchName of branchNames) {
+      branch = branch._children[branchName];
+      if (!branch) {
+        // throw new Error(`could not find pref branch ${ prefName}`);
+        return false;
+
+      }
+    }
+
+    return branch;
+  },
+
+  /**
+   * Helper function to notify any observers when a preference has
+   * changed.  This will also notify the parent branch for further
+   * reporting.
+   *
+   * @param {String} relativeName the name of the updated pref,
+   *        relative to this branch
+   */
+  _notify: function(relativeName) {
+    for (let domain in this._observers) {
+      if (relativeName === domain || domain === "" ||
+          (domain.endsWith(".") && relativeName.startsWith(domain))) {
+        // Allow mutation while walking.
+        let localList = this._observers[domain].slice();
+        for (let observer of localList) {
+          try {
+            if ("observe" in observer) {
+              observer.observe(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID,
+                               relativeName);
+            } else {
+              // Function-style observer -- these aren't mentioned in
+              // the IDL, but they're accepted and devtools uses them.
+              observer(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, relativeName);
+            }
+          } catch (e) {
+            console.error(e);
+          }
+        }
+      }
+    }
+
+    if (this._parent) {
+      this._parent._notify(`${this._name }.${ relativeName}`);
+    }
+  },
+
+  /**
+   * Helper function to create a branch given an array of branch names
+   * representing the path of the new branch.
+   *
+   * @param {Array} branchList an array of strings, one per component
+   *        of the branch to be created
+   * @return {PrefBranch} the new branch
+   */
+  _createBranch: function(branchList) {
+    let parent = this;
+    for (let branch of branchList) {
+      if (!parent._children[branch]) {
+        let isParentRoot = !parent._parent;
+        let branchName = (isParentRoot ? "" : `${parent.root }.`) + branch;
+        parent._children[branch] = new PrefBranch(parent, branch, branchName);
+      }
+      parent = parent._children[branch];
+    }
+    return parent;
+  },
+
+  /**
+   * Create a new preference.  The new preference is assumed to be in
+   * local storage already, and the new value is taken from there.
+   *
+   * @param {String} keyName the full-qualified name of the preference.
+   *        This is also the name of the key in local storage.
+   * @param {Any} userValue the user value to use if the pref does not exist
+   * @param {Boolean} hasUserValue if a new pref is created, whether
+   *        the default value is also a user value
+   * @param {Any} defaultValue the default value to use if the pref
+   *        does not exist
+   * @param {Boolean} init if true, then this call is initialization
+   *        from local storage and should override the default prefs
+   */
+  _findOrCreatePref: function(keyName, userValue, hasUserValue, defaultValue,
+                               init = false) {
+    let branch = this._createBranch(keyName.split("."));
+
+    if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) {
+      throw new Error(`inconsistent values when creating ${ keyName}`);
+    }
+
+    let type;
+    switch (typeof (defaultValue)) {
+      case "boolean":
+        type = PREF_BOOL;
+        break;
+      case "number":
+        type = PREF_INT;
+        break;
+      case "string":
+        type = PREF_STRING;
+        break;
+      default:
+        throw new Error(`unhandled argument type: ${ typeof (defaultValue)}`);
+    }
+
+    if (init || branch._type === PREF_INVALID) {
+      branch._storageUpdated(type, userValue, hasUserValue, defaultValue);
+    } else if (branch._type !== type) {
+      throw new Error(`attempt to change type of pref ${ keyName}`);
+    }
+
+    return branch;
+  },
+
+  getKeyName: function(keyName) {
+    if (keyName.startsWith(PREFIX)) {
+      return keyName.slice(PREFIX.length);
+    }
+
+    return keyName;
+  },
+
+  /**
+   * Helper function that is called when local storage changes.  This
+   * updates the preferences and notifies pref observers as needed.
+   *
+   * @param {StorageEvent} event the event representing the local
+   *        storage change
+   */
+  _onStorageChange: function(event) {
+    if (event.storageArea !== localStorage) {
+      return;
+    }
+
+    const key = this.getKeyName(event.key);
+
+    // Ignore delete events.  Not clear what's correct.
+    if (key === null || event.newValue === null) {
+      return;
+    }
+
+    let { type, userValue, hasUserValue, defaultValue } =
+        JSON.parse(event.newValue);
+    if (event.oldValue === null) {
+      this._findOrCreatePref(key, userValue, hasUserValue, defaultValue);
+    } else {
+      let thePref = this._findPref(key);
+      thePref._storageUpdated(type, userValue, hasUserValue, defaultValue);
+    }
+  },
+
+  /**
+   * Helper function to initialize the root PrefBranch.
+   */
+  _initializeRoot: function() {
+    if (Services._defaultPrefsEnabled) {
+      /* eslint-disable no-eval */
+      // let devtools = require("raw!prefs!devtools/client/preferences/devtools");
+      // eval(devtools);
+      // let all = require("raw!prefs!modules/libpref/init/all");
+      // eval(all);
+      /* eslint-enable no-eval */
+    }
+
+    // Read the prefs from local storage and create the local
+    // representations.
+    for (let i = 0; i < localStorage.length; ++i) {
+      let keyName = localStorage.key(i);
+      if (keyName.startsWith(PREFIX)) {
+        let { userValue, hasUserValue, defaultValue } =
+            JSON.parse(localStorage.getItem(keyName));
+        this._findOrCreatePref(keyName.slice(PREFIX.length), userValue,
+                               hasUserValue, defaultValue, true);
+      }
+    }
+
+    this._onStorageChange = this._onStorageChange.bind(this);
+    window.addEventListener("storage", this._onStorageChange);
+  },
+};
+
+
+const Services = {
+  _prefs: null,
+
+  _defaultPrefsEnabled: true,
+
+  get prefs() {
+    if (!this._prefs) {
+      this._prefs = new PrefBranch(null, "", "");
+      this._prefs._initializeRoot();
+    }
+    return this._prefs;
+  },
+
   appinfo: "",
-  prefs: { getBoolPref: () => {}, addObserver: () => {} },
-  obs: { addObserver: () => {} },
+  obs: { addObserver: () => {} }
 };
+
+function pref(name, value) {
+  let thePref = Services.prefs._findOrCreatePref(name, value, true, value);
+  thePref._setDefault(value);
+}
+
+module.exports = Services;
+Services.pref = pref;
--- a/devtools/client/debugger/src/utils/prefs.js
+++ b/devtools/client/debugger/src/utils/prefs.js
@@ -1,15 +1,16 @@
 /* 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/>. */
 
 // @flow
 
-import { PrefsHelper } from "devtools-modules";
+// $FlowIgnore
+const { PrefsHelper } = require("devtools/client/shared/prefs");
 
 import { isDevelopment } from "devtools-environment";
 import Services from "devtools-services";
 
 // Schema version to bump when the async store format has changed incompatibly
 // and old stores should be cleared.
 const prefsSchemaVersion = 11;
 const { pref } = Services;
--- a/devtools/client/shared/prefs.js
+++ b/devtools/client/shared/prefs.js
@@ -33,41 +33,51 @@ const EventEmitter = require("devtools/s
  *        An object containing { accessorName: [prefType, prefName] } keys.
  */
 function PrefsHelper(prefsRoot = "", prefsBlueprint = {}) {
   EventEmitter.decorate(this);
 
   const cache = new Map();
 
   for (const accessorName in prefsBlueprint) {
-    const [prefType, prefName] = prefsBlueprint[accessorName];
-    map(this, cache, accessorName, prefType, prefsRoot, prefName);
+    const [prefType, prefName, fallbackValue] = prefsBlueprint[accessorName];
+    map(
+      this,
+      cache,
+      accessorName,
+      prefType,
+      prefsRoot,
+      prefName,
+      fallbackValue
+    );
   }
 
   const observer = makeObserver(this, cache, prefsRoot, prefsBlueprint);
   this.registerObserver = () => observer.register();
   this.unregisterObserver = () => observer.unregister();
 }
 
 /**
  * Helper method for getting a pref value.
  *
  * @param Map cache
  * @param string prefType
  * @param string prefsRoot
  * @param string prefName
+ * @param string|int|boolean fallbackValue
  * @return any
  */
-function get(cache, prefType, prefsRoot, prefName) {
+function get(cache, prefType, prefsRoot, prefName, fallbackValue) {
   const cachedPref = cache.get(prefName);
   if (cachedPref !== undefined) {
     return cachedPref;
   }
   const value = Services.prefs["get" + prefType + "Pref"](
-    [prefsRoot, prefName].join(".")
+    [prefsRoot, prefName].join("."),
+    fallbackValue
   );
   cache.set(prefName, value);
   return value;
 }
 
 /**
  * Helper method for setting a pref value.
  *
@@ -92,50 +102,53 @@ function set(cache, prefType, prefsRoot,
  * using the standard JSON serializer).
  *
  * @param PrefsHelper self
  * @param Map cache
  * @param string accessorName
  * @param string prefType
  * @param string prefsRoot
  * @param string prefName
+ * @param string|int|boolean fallbackValue
  * @param array serializer [optional]
  */
 function map(
   self,
   cache,
   accessorName,
   prefType,
   prefsRoot,
   prefName,
+  fallbackValue,
   serializer = { in: e => e, out: e => e }
 ) {
   if (prefName in self) {
     throw new Error(
       `Can't use ${prefName} because it overrides a property` +
         "on the instance."
     );
   }
   if (prefType == "Json") {
-    map(self, cache, accessorName, "Char", prefsRoot, prefName, {
+    map(self, cache, accessorName, "Char", prefsRoot, prefName, fallbackValue, {
       in: JSON.parse,
       out: JSON.stringify,
     });
     return;
   }
   if (prefType == "Float") {
-    map(self, cache, accessorName, "Char", prefsRoot, prefName, {
+    map(self, cache, accessorName, "Char", prefsRoot, prefName, fallbackValue, {
       in: Number.parseFloat,
       out: n => n + "",
     });
     return;
   }
 
   Object.defineProperty(self, accessorName, {
-    get: () => serializer.in(get(cache, prefType, prefsRoot, prefName)),
+    get: () =>
+      serializer.in(get(cache, prefType, prefsRoot, prefName, fallbackValue)),
     set: e => set(cache, prefType, prefsRoot, prefName, serializer.out(e)),
   });
 }
 
 /**
  * Finds the accessor for the provided pref, based on the blueprint object
  * used in the constructor.
  *
--- a/devtools/shared/event-emitter.js
+++ b/devtools/shared/event-emitter.js
@@ -344,17 +344,17 @@ module.exports = EventEmitter;
 const isEventHandler = listener =>
   listener && handler in listener && typeof listener[handler] === "function";
 
 const Services = require("Services");
 const { getNthPathExcluding } = require("devtools/shared/platform/stack");
 let loggingEnabled = false;
 
 if (!isWorker) {
-  loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
+  loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit", false);
   const observer = {
     observe: () => {
       loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
     },
   };
   Services.prefs.addObserver("devtools.dump.emit", observer);
 
   // Also listen for Loader unload to unregister the pref observer and