services/sync/modules/addonsreconciler.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 /**
michael@0 6 * This file contains middleware to reconcile state of AddonManager for
michael@0 7 * purposes of tracking events for Sync. The content in this file exists
michael@0 8 * because AddonManager does not have a getChangesSinceX() API and adding
michael@0 9 * that functionality properly was deemed too time-consuming at the time
michael@0 10 * add-on sync was originally written. If/when AddonManager adds this API,
michael@0 11 * this file can go away and the add-ons engine can be rewritten to use it.
michael@0 12 *
michael@0 13 * It was decided to have this tracking functionality exist in a separate
michael@0 14 * standalone file so it could be more easily understood, tested, and
michael@0 15 * hopefully ported.
michael@0 16 */
michael@0 17
michael@0 18 "use strict";
michael@0 19
michael@0 20 const Cu = Components.utils;
michael@0 21
michael@0 22 Cu.import("resource://gre/modules/Log.jsm");
michael@0 23 Cu.import("resource://services-sync/util.js");
michael@0 24 Cu.import("resource://gre/modules/AddonManager.jsm");
michael@0 25
michael@0 26 const DEFAULT_STATE_FILE = "addonsreconciler";
michael@0 27
michael@0 28 this.CHANGE_INSTALLED = 1;
michael@0 29 this.CHANGE_UNINSTALLED = 2;
michael@0 30 this.CHANGE_ENABLED = 3;
michael@0 31 this.CHANGE_DISABLED = 4;
michael@0 32
michael@0 33 this.EXPORTED_SYMBOLS = ["AddonsReconciler", "CHANGE_INSTALLED",
michael@0 34 "CHANGE_UNINSTALLED", "CHANGE_ENABLED",
michael@0 35 "CHANGE_DISABLED"];
michael@0 36 /**
michael@0 37 * Maintains state of add-ons.
michael@0 38 *
michael@0 39 * State is maintained in 2 data structures, an object mapping add-on IDs
michael@0 40 * to metadata and an array of changes over time. The object mapping can be
michael@0 41 * thought of as a minimal copy of data from AddonManager which is needed for
michael@0 42 * Sync. The array is effectively a log of changes over time.
michael@0 43 *
michael@0 44 * The data structures are persisted to disk by serializing to a JSON file in
michael@0 45 * the current profile. The data structures are updated by 2 mechanisms. First,
michael@0 46 * they can be refreshed from the global state of the AddonManager. This is a
michael@0 47 * sure-fire way of ensuring the reconciler is up to date. Second, the
michael@0 48 * reconciler adds itself as an AddonManager listener. When it receives change
michael@0 49 * notifications, it updates its internal state incrementally.
michael@0 50 *
michael@0 51 * The internal state is persisted to a JSON file in the profile directory.
michael@0 52 *
michael@0 53 * An instance of this is bound to an AddonsEngine instance. In reality, it
michael@0 54 * likely exists as a singleton. To AddonsEngine, it functions as a store and
michael@0 55 * an entity which emits events for tracking.
michael@0 56 *
michael@0 57 * The usage pattern for instances of this class is:
michael@0 58 *
michael@0 59 * let reconciler = new AddonsReconciler();
michael@0 60 * reconciler.loadState(null, function(error) { ... });
michael@0 61 *
michael@0 62 * // At this point, your instance should be ready to use.
michael@0 63 *
michael@0 64 * When you are finished with the instance, please call:
michael@0 65 *
michael@0 66 * reconciler.stopListening();
michael@0 67 * reconciler.saveState(...);
michael@0 68 *
michael@0 69 * There are 2 classes of listeners in the AddonManager: AddonListener and
michael@0 70 * InstallListener. This class is a listener for both (member functions just
michael@0 71 * get called directly).
michael@0 72 *
michael@0 73 * When an add-on is installed, listeners are called in the following order:
michael@0 74 *
michael@0 75 * IL.onInstallStarted, AL.onInstalling, IL.onInstallEnded, AL.onInstalled
michael@0 76 *
michael@0 77 * For non-restartless add-ons, an application restart may occur between
michael@0 78 * IL.onInstallEnded and AL.onInstalled. Unfortunately, Sync likely will
michael@0 79 * not be loaded when AL.onInstalled is fired shortly after application
michael@0 80 * start, so it won't see this event. Therefore, for add-ons requiring a
michael@0 81 * restart, Sync treats the IL.onInstallEnded event as good enough to
michael@0 82 * indicate an install. For restartless add-ons, Sync assumes AL.onInstalled
michael@0 83 * will follow shortly after IL.onInstallEnded and thus it ignores
michael@0 84 * IL.onInstallEnded.
michael@0 85 *
michael@0 86 * The listeners can also see events related to the download of the add-on.
michael@0 87 * This class isn't interested in those. However, there are failure events,
michael@0 88 * IL.onDownloadFailed and IL.onDownloadCanceled which get called if a
michael@0 89 * download doesn't complete successfully.
michael@0 90 *
michael@0 91 * For uninstalls, we see AL.onUninstalling then AL.onUninstalled. Like
michael@0 92 * installs, the events could be separated by an application restart and Sync
michael@0 93 * may not see the onUninstalled event. Again, if we require a restart, we
michael@0 94 * react to onUninstalling. If not, we assume we'll get onUninstalled.
michael@0 95 *
michael@0 96 * Enabling and disabling work by sending:
michael@0 97 *
michael@0 98 * AL.onEnabling, AL.onEnabled
michael@0 99 * AL.onDisabling, AL.onDisabled
michael@0 100 *
michael@0 101 * Again, they may be separated by a restart, so we heed the requiresRestart
michael@0 102 * flag.
michael@0 103 *
michael@0 104 * Actions can be undone. All undoable actions notify the same
michael@0 105 * AL.onOperationCancelled event. We treat this event like any other.
michael@0 106 *
michael@0 107 * Restartless add-ons have interesting behavior during uninstall. These
michael@0 108 * add-ons are first disabled then they are actually uninstalled. So, we will
michael@0 109 * see AL.onDisabling and AL.onDisabled. The onUninstalling and onUninstalled
michael@0 110 * events only come after the Addon Manager is closed or another view is
michael@0 111 * switched to. In the case of Sync performing the uninstall, the uninstall
michael@0 112 * events will occur immediately. However, we still see disabling events and
michael@0 113 * heed them like they were normal. In the end, the state is proper.
michael@0 114 */
michael@0 115 this.AddonsReconciler = function AddonsReconciler() {
michael@0 116 this._log = Log.repository.getLogger("Sync.AddonsReconciler");
michael@0 117 let level = Svc.Prefs.get("log.logger.addonsreconciler", "Debug");
michael@0 118 this._log.level = Log.Level[level];
michael@0 119
michael@0 120 Svc.Obs.add("xpcom-shutdown", this.stopListening, this);
michael@0 121 };
michael@0 122 AddonsReconciler.prototype = {
michael@0 123 /** Flag indicating whether we are listening to AddonManager events. */
michael@0 124 _listening: false,
michael@0 125
michael@0 126 /**
michael@0 127 * Whether state has been loaded from a file.
michael@0 128 *
michael@0 129 * State is loaded on demand if an operation requires it.
michael@0 130 */
michael@0 131 _stateLoaded: false,
michael@0 132
michael@0 133 /**
michael@0 134 * Define this as false if the reconciler should not persist state
michael@0 135 * to disk when handling events.
michael@0 136 *
michael@0 137 * This allows test code to avoid spinning to write during observer
michael@0 138 * notifications and xpcom shutdown, which appears to cause hangs on WinXP
michael@0 139 * (Bug 873861).
michael@0 140 */
michael@0 141 _shouldPersist: true,
michael@0 142
michael@0 143 /** Log logger instance */
michael@0 144 _log: null,
michael@0 145
michael@0 146 /**
michael@0 147 * Container for add-on metadata.
michael@0 148 *
michael@0 149 * Keys are add-on IDs. Values are objects which describe the state of the
michael@0 150 * add-on. This is a minimal mirror of data that can be queried from
michael@0 151 * AddonManager. In some cases, we retain data longer than AddonManager.
michael@0 152 */
michael@0 153 _addons: {},
michael@0 154
michael@0 155 /**
michael@0 156 * List of add-on changes over time.
michael@0 157 *
michael@0 158 * Each element is an array of [time, change, id].
michael@0 159 */
michael@0 160 _changes: [],
michael@0 161
michael@0 162 /**
michael@0 163 * Objects subscribed to changes made to this instance.
michael@0 164 */
michael@0 165 _listeners: [],
michael@0 166
michael@0 167 /**
michael@0 168 * Accessor for add-ons in this object.
michael@0 169 *
michael@0 170 * Returns an object mapping add-on IDs to objects containing metadata.
michael@0 171 */
michael@0 172 get addons() {
michael@0 173 this._ensureStateLoaded();
michael@0 174 return this._addons;
michael@0 175 },
michael@0 176
michael@0 177 /**
michael@0 178 * Load reconciler state from a file.
michael@0 179 *
michael@0 180 * The path is relative to the weave directory in the profile. If no
michael@0 181 * path is given, the default one is used.
michael@0 182 *
michael@0 183 * If the file does not exist or there was an error parsing the file, the
michael@0 184 * state will be transparently defined as empty.
michael@0 185 *
michael@0 186 * @param path
michael@0 187 * Path to load. ".json" is appended automatically. If not defined,
michael@0 188 * a default path will be consulted.
michael@0 189 * @param callback
michael@0 190 * Callback to be executed upon file load. The callback receives a
michael@0 191 * truthy error argument signifying whether an error occurred and a
michael@0 192 * boolean indicating whether data was loaded.
michael@0 193 */
michael@0 194 loadState: function loadState(path, callback) {
michael@0 195 let file = path || DEFAULT_STATE_FILE;
michael@0 196 Utils.jsonLoad(file, this, function(json) {
michael@0 197 this._addons = {};
michael@0 198 this._changes = [];
michael@0 199
michael@0 200 if (!json) {
michael@0 201 this._log.debug("No data seen in loaded file: " + file);
michael@0 202 if (callback) {
michael@0 203 callback(null, false);
michael@0 204 }
michael@0 205
michael@0 206 return;
michael@0 207 }
michael@0 208
michael@0 209 let version = json.version;
michael@0 210 if (!version || version != 1) {
michael@0 211 this._log.error("Could not load JSON file because version not " +
michael@0 212 "supported: " + version);
michael@0 213 if (callback) {
michael@0 214 callback(null, false);
michael@0 215 }
michael@0 216
michael@0 217 return;
michael@0 218 }
michael@0 219
michael@0 220 this._addons = json.addons;
michael@0 221 for each (let record in this._addons) {
michael@0 222 record.modified = new Date(record.modified);
michael@0 223 }
michael@0 224
michael@0 225 for each (let [time, change, id] in json.changes) {
michael@0 226 this._changes.push([new Date(time), change, id]);
michael@0 227 }
michael@0 228
michael@0 229 if (callback) {
michael@0 230 callback(null, true);
michael@0 231 }
michael@0 232 });
michael@0 233 },
michael@0 234
michael@0 235 /**
michael@0 236 * Saves the current state to a file in the local profile.
michael@0 237 *
michael@0 238 * @param path
michael@0 239 * String path in profile to save to. If not defined, the default
michael@0 240 * will be used.
michael@0 241 * @param callback
michael@0 242 * Function to be invoked on save completion. No parameters will be
michael@0 243 * passed to callback.
michael@0 244 */
michael@0 245 saveState: function saveState(path, callback) {
michael@0 246 let file = path || DEFAULT_STATE_FILE;
michael@0 247 let state = {version: 1, addons: {}, changes: []};
michael@0 248
michael@0 249 for (let [id, record] in Iterator(this._addons)) {
michael@0 250 state.addons[id] = {};
michael@0 251 for (let [k, v] in Iterator(record)) {
michael@0 252 if (k == "modified") {
michael@0 253 state.addons[id][k] = v.getTime();
michael@0 254 }
michael@0 255 else {
michael@0 256 state.addons[id][k] = v;
michael@0 257 }
michael@0 258 }
michael@0 259 }
michael@0 260
michael@0 261 for each (let [time, change, id] in this._changes) {
michael@0 262 state.changes.push([time.getTime(), change, id]);
michael@0 263 }
michael@0 264
michael@0 265 this._log.info("Saving reconciler state to file: " + file);
michael@0 266 Utils.jsonSave(file, this, state, callback);
michael@0 267 },
michael@0 268
michael@0 269 /**
michael@0 270 * Registers a change listener with this instance.
michael@0 271 *
michael@0 272 * Change listeners are called every time a change is recorded. The listener
michael@0 273 * is an object with the function "changeListener" that takes 3 arguments,
michael@0 274 * the Date at which the change happened, the type of change (a CHANGE_*
michael@0 275 * constant), and the add-on state object reflecting the current state of
michael@0 276 * the add-on at the time of the change.
michael@0 277 *
michael@0 278 * @param listener
michael@0 279 * Object containing changeListener function.
michael@0 280 */
michael@0 281 addChangeListener: function addChangeListener(listener) {
michael@0 282 if (this._listeners.indexOf(listener) == -1) {
michael@0 283 this._log.debug("Adding change listener.");
michael@0 284 this._listeners.push(listener);
michael@0 285 }
michael@0 286 },
michael@0 287
michael@0 288 /**
michael@0 289 * Removes a previously-installed change listener from the instance.
michael@0 290 *
michael@0 291 * @param listener
michael@0 292 * Listener instance to remove.
michael@0 293 */
michael@0 294 removeChangeListener: function removeChangeListener(listener) {
michael@0 295 this._listeners = this._listeners.filter(function(element) {
michael@0 296 if (element == listener) {
michael@0 297 this._log.debug("Removing change listener.");
michael@0 298 return false;
michael@0 299 } else {
michael@0 300 return true;
michael@0 301 }
michael@0 302 }.bind(this));
michael@0 303 },
michael@0 304
michael@0 305 /**
michael@0 306 * Tells the instance to start listening for AddonManager changes.
michael@0 307 *
michael@0 308 * This is typically called automatically when Sync is loaded.
michael@0 309 */
michael@0 310 startListening: function startListening() {
michael@0 311 if (this._listening) {
michael@0 312 return;
michael@0 313 }
michael@0 314
michael@0 315 this._log.info("Registering as Add-on Manager listener.");
michael@0 316 AddonManager.addAddonListener(this);
michael@0 317 AddonManager.addInstallListener(this);
michael@0 318 this._listening = true;
michael@0 319 },
michael@0 320
michael@0 321 /**
michael@0 322 * Tells the instance to stop listening for AddonManager changes.
michael@0 323 *
michael@0 324 * The reconciler should always be listening. This should only be called when
michael@0 325 * the instance is being destroyed.
michael@0 326 *
michael@0 327 * This function will get called automatically on XPCOM shutdown. However, it
michael@0 328 * is a best practice to call it yourself.
michael@0 329 */
michael@0 330 stopListening: function stopListening() {
michael@0 331 if (!this._listening) {
michael@0 332 return;
michael@0 333 }
michael@0 334
michael@0 335 this._log.debug("Stopping listening and removing AddonManager listeners.");
michael@0 336 AddonManager.removeInstallListener(this);
michael@0 337 AddonManager.removeAddonListener(this);
michael@0 338 this._listening = false;
michael@0 339 },
michael@0 340
michael@0 341 /**
michael@0 342 * Refreshes the global state of add-ons by querying the AddonManager.
michael@0 343 */
michael@0 344 refreshGlobalState: function refreshGlobalState(callback) {
michael@0 345 this._log.info("Refreshing global state from AddonManager.");
michael@0 346 this._ensureStateLoaded();
michael@0 347
michael@0 348 let installs;
michael@0 349
michael@0 350 AddonManager.getAllAddons(function (addons) {
michael@0 351 let ids = {};
michael@0 352
michael@0 353 for each (let addon in addons) {
michael@0 354 ids[addon.id] = true;
michael@0 355 this.rectifyStateFromAddon(addon);
michael@0 356 }
michael@0 357
michael@0 358 // Look for locally-defined add-ons that no longer exist and update their
michael@0 359 // record.
michael@0 360 for (let [id, addon] in Iterator(this._addons)) {
michael@0 361 if (id in ids) {
michael@0 362 continue;
michael@0 363 }
michael@0 364
michael@0 365 // If the id isn't in ids, it means that the add-on has been deleted or
michael@0 366 // the add-on is in the process of being installed. We detect the
michael@0 367 // latter by seeing if an AddonInstall is found for this add-on.
michael@0 368
michael@0 369 if (!installs) {
michael@0 370 let cb = Async.makeSyncCallback();
michael@0 371 AddonManager.getAllInstalls(cb);
michael@0 372 installs = Async.waitForSyncCallback(cb);
michael@0 373 }
michael@0 374
michael@0 375 let installFound = false;
michael@0 376 for each (let install in installs) {
michael@0 377 if (install.addon && install.addon.id == id &&
michael@0 378 install.state == AddonManager.STATE_INSTALLED) {
michael@0 379
michael@0 380 installFound = true;
michael@0 381 break;
michael@0 382 }
michael@0 383 }
michael@0 384
michael@0 385 if (installFound) {
michael@0 386 continue;
michael@0 387 }
michael@0 388
michael@0 389 if (addon.installed) {
michael@0 390 addon.installed = false;
michael@0 391 this._log.debug("Adding change because add-on not present in " +
michael@0 392 "Add-on Manager: " + id);
michael@0 393 this._addChange(new Date(), CHANGE_UNINSTALLED, addon);
michael@0 394 }
michael@0 395 }
michael@0 396
michael@0 397 // See note for _shouldPersist.
michael@0 398 if (this._shouldPersist) {
michael@0 399 this.saveState(null, callback);
michael@0 400 } else {
michael@0 401 callback();
michael@0 402 }
michael@0 403 }.bind(this));
michael@0 404 },
michael@0 405
michael@0 406 /**
michael@0 407 * Rectifies the state of an add-on from an Addon instance.
michael@0 408 *
michael@0 409 * This basically says "given an Addon instance, assume it is truth and
michael@0 410 * apply changes to the local state to reflect it."
michael@0 411 *
michael@0 412 * This function could result in change listeners being called if the local
michael@0 413 * state differs from the passed add-on's state.
michael@0 414 *
michael@0 415 * @param addon
michael@0 416 * Addon instance being updated.
michael@0 417 */
michael@0 418 rectifyStateFromAddon: function rectifyStateFromAddon(addon) {
michael@0 419 this._log.debug("Rectifying state for addon: " + addon.id);
michael@0 420 this._ensureStateLoaded();
michael@0 421
michael@0 422 let id = addon.id;
michael@0 423 let enabled = !addon.userDisabled;
michael@0 424 let guid = addon.syncGUID;
michael@0 425 let now = new Date();
michael@0 426
michael@0 427 if (!(id in this._addons)) {
michael@0 428 let record = {
michael@0 429 id: id,
michael@0 430 guid: guid,
michael@0 431 enabled: enabled,
michael@0 432 installed: true,
michael@0 433 modified: now,
michael@0 434 type: addon.type,
michael@0 435 scope: addon.scope,
michael@0 436 foreignInstall: addon.foreignInstall
michael@0 437 };
michael@0 438 this._addons[id] = record;
michael@0 439 this._log.debug("Adding change because add-on not present locally: " +
michael@0 440 id);
michael@0 441 this._addChange(now, CHANGE_INSTALLED, record);
michael@0 442 return;
michael@0 443 }
michael@0 444
michael@0 445 let record = this._addons[id];
michael@0 446
michael@0 447 if (!record.installed) {
michael@0 448 // It is possible the record is marked as uninstalled because an
michael@0 449 // uninstall is pending.
michael@0 450 if (!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL)) {
michael@0 451 record.installed = true;
michael@0 452 record.modified = now;
michael@0 453 }
michael@0 454 }
michael@0 455
michael@0 456 if (record.enabled != enabled) {
michael@0 457 record.enabled = enabled;
michael@0 458 record.modified = now;
michael@0 459 let change = enabled ? CHANGE_ENABLED : CHANGE_DISABLED;
michael@0 460 this._log.debug("Adding change because enabled state changed: " + id);
michael@0 461 this._addChange(new Date(), change, record);
michael@0 462 }
michael@0 463
michael@0 464 if (record.guid != guid) {
michael@0 465 record.guid = guid;
michael@0 466 // We don't record a change because the Sync engine rectifies this on its
michael@0 467 // own. This is tightly coupled with Sync. If this code is ever lifted
michael@0 468 // outside of Sync, this exception should likely be removed.
michael@0 469 }
michael@0 470 },
michael@0 471
michael@0 472 /**
michael@0 473 * Record a change in add-on state.
michael@0 474 *
michael@0 475 * @param date
michael@0 476 * Date at which the change occurred.
michael@0 477 * @param change
michael@0 478 * The type of the change. A CHANGE_* constant.
michael@0 479 * @param state
michael@0 480 * The new state of the add-on. From this.addons.
michael@0 481 */
michael@0 482 _addChange: function _addChange(date, change, state) {
michael@0 483 this._log.info("Change recorded for " + state.id);
michael@0 484 this._changes.push([date, change, state.id]);
michael@0 485
michael@0 486 for each (let listener in this._listeners) {
michael@0 487 try {
michael@0 488 listener.changeListener.call(listener, date, change, state);
michael@0 489 } catch (ex) {
michael@0 490 this._log.warn("Exception calling change listener: " +
michael@0 491 Utils.exceptionStr(ex));
michael@0 492 }
michael@0 493 }
michael@0 494 },
michael@0 495
michael@0 496 /**
michael@0 497 * Obtain the set of changes to add-ons since the date passed.
michael@0 498 *
michael@0 499 * This will return an array of arrays. Each entry in the array has the
michael@0 500 * elements [date, change_type, id], where
michael@0 501 *
michael@0 502 * date - Date instance representing when the change occurred.
michael@0 503 * change_type - One of CHANGE_* constants.
michael@0 504 * id - ID of add-on that changed.
michael@0 505 */
michael@0 506 getChangesSinceDate: function getChangesSinceDate(date) {
michael@0 507 this._ensureStateLoaded();
michael@0 508
michael@0 509 let length = this._changes.length;
michael@0 510 for (let i = 0; i < length; i++) {
michael@0 511 if (this._changes[i][0] >= date) {
michael@0 512 return this._changes.slice(i);
michael@0 513 }
michael@0 514 }
michael@0 515
michael@0 516 return [];
michael@0 517 },
michael@0 518
michael@0 519 /**
michael@0 520 * Prunes all recorded changes from before the specified Date.
michael@0 521 *
michael@0 522 * @param date
michael@0 523 * Entries older than this Date will be removed.
michael@0 524 */
michael@0 525 pruneChangesBeforeDate: function pruneChangesBeforeDate(date) {
michael@0 526 this._ensureStateLoaded();
michael@0 527
michael@0 528 this._changes = this._changes.filter(function test_age(change) {
michael@0 529 return change[0] >= date;
michael@0 530 });
michael@0 531 },
michael@0 532
michael@0 533 /**
michael@0 534 * Obtains the set of all known Sync GUIDs for add-ons.
michael@0 535 *
michael@0 536 * @return Object with guids as keys and values of true.
michael@0 537 */
michael@0 538 getAllSyncGUIDs: function getAllSyncGUIDs() {
michael@0 539 let result = {};
michael@0 540 for (let id in this.addons) {
michael@0 541 result[id] = true;
michael@0 542 }
michael@0 543
michael@0 544 return result;
michael@0 545 },
michael@0 546
michael@0 547 /**
michael@0 548 * Obtain the add-on state record for an add-on by Sync GUID.
michael@0 549 *
michael@0 550 * If the add-on could not be found, returns null.
michael@0 551 *
michael@0 552 * @param guid
michael@0 553 * Sync GUID of add-on to retrieve.
michael@0 554 * @return Object on success on null on failure.
michael@0 555 */
michael@0 556 getAddonStateFromSyncGUID: function getAddonStateFromSyncGUID(guid) {
michael@0 557 for each (let addon in this.addons) {
michael@0 558 if (addon.guid == guid) {
michael@0 559 return addon;
michael@0 560 }
michael@0 561 }
michael@0 562
michael@0 563 return null;
michael@0 564 },
michael@0 565
michael@0 566 /**
michael@0 567 * Ensures that state is loaded before continuing.
michael@0 568 *
michael@0 569 * This is called internally by anything that accesses the internal data
michael@0 570 * structures. It effectively just-in-time loads serialized state.
michael@0 571 */
michael@0 572 _ensureStateLoaded: function _ensureStateLoaded() {
michael@0 573 if (this._stateLoaded) {
michael@0 574 return;
michael@0 575 }
michael@0 576
michael@0 577 let cb = Async.makeSpinningCallback();
michael@0 578 this.loadState(null, cb);
michael@0 579 cb.wait();
michael@0 580 this._stateLoaded = true;
michael@0 581 },
michael@0 582
michael@0 583 /**
michael@0 584 * Handler that is invoked as part of the AddonManager listeners.
michael@0 585 */
michael@0 586 _handleListener: function _handlerListener(action, addon, requiresRestart) {
michael@0 587 // Since this is called as an observer, we explicitly trap errors and
michael@0 588 // log them to ourselves so we don't see errors reported elsewhere.
michael@0 589 try {
michael@0 590 let id = addon.id;
michael@0 591 this._log.debug("Add-on change: " + action + " to " + id);
michael@0 592
michael@0 593 // We assume that every event for non-restartless add-ons is
michael@0 594 // followed by another event and that this follow-up event is the most
michael@0 595 // appropriate to react to. Currently we ignore onEnabling, onDisabling,
michael@0 596 // and onUninstalling for non-restartless add-ons.
michael@0 597 if (requiresRestart === false) {
michael@0 598 this._log.debug("Ignoring " + action + " for restartless add-on.");
michael@0 599 return;
michael@0 600 }
michael@0 601
michael@0 602 switch (action) {
michael@0 603 case "onEnabling":
michael@0 604 case "onEnabled":
michael@0 605 case "onDisabling":
michael@0 606 case "onDisabled":
michael@0 607 case "onInstalled":
michael@0 608 case "onInstallEnded":
michael@0 609 case "onOperationCancelled":
michael@0 610 this.rectifyStateFromAddon(addon);
michael@0 611 break;
michael@0 612
michael@0 613 case "onUninstalling":
michael@0 614 case "onUninstalled":
michael@0 615 let id = addon.id;
michael@0 616 let addons = this.addons;
michael@0 617 if (id in addons) {
michael@0 618 let now = new Date();
michael@0 619 let record = addons[id];
michael@0 620 record.installed = false;
michael@0 621 record.modified = now;
michael@0 622 this._log.debug("Adding change because of uninstall listener: " +
michael@0 623 id);
michael@0 624 this._addChange(now, CHANGE_UNINSTALLED, record);
michael@0 625 }
michael@0 626 }
michael@0 627
michael@0 628 // See note for _shouldPersist.
michael@0 629 if (this._shouldPersist) {
michael@0 630 let cb = Async.makeSpinningCallback();
michael@0 631 this.saveState(null, cb);
michael@0 632 cb.wait();
michael@0 633 }
michael@0 634 }
michael@0 635 catch (ex) {
michael@0 636 this._log.warn("Exception: " + Utils.exceptionStr(ex));
michael@0 637 }
michael@0 638 },
michael@0 639
michael@0 640 // AddonListeners
michael@0 641 onEnabling: function onEnabling(addon, requiresRestart) {
michael@0 642 this._handleListener("onEnabling", addon, requiresRestart);
michael@0 643 },
michael@0 644 onEnabled: function onEnabled(addon) {
michael@0 645 this._handleListener("onEnabled", addon);
michael@0 646 },
michael@0 647 onDisabling: function onDisabling(addon, requiresRestart) {
michael@0 648 this._handleListener("onDisabling", addon, requiresRestart);
michael@0 649 },
michael@0 650 onDisabled: function onDisabled(addon) {
michael@0 651 this._handleListener("onDisabled", addon);
michael@0 652 },
michael@0 653 onInstalling: function onInstalling(addon, requiresRestart) {
michael@0 654 this._handleListener("onInstalling", addon, requiresRestart);
michael@0 655 },
michael@0 656 onInstalled: function onInstalled(addon) {
michael@0 657 this._handleListener("onInstalled", addon);
michael@0 658 },
michael@0 659 onUninstalling: function onUninstalling(addon, requiresRestart) {
michael@0 660 this._handleListener("onUninstalling", addon, requiresRestart);
michael@0 661 },
michael@0 662 onUninstalled: function onUninstalled(addon) {
michael@0 663 this._handleListener("onUninstalled", addon);
michael@0 664 },
michael@0 665 onOperationCancelled: function onOperationCancelled(addon) {
michael@0 666 this._handleListener("onOperationCancelled", addon);
michael@0 667 },
michael@0 668
michael@0 669 // InstallListeners
michael@0 670 onInstallEnded: function onInstallEnded(install, addon) {
michael@0 671 this._handleListener("onInstallEnded", addon);
michael@0 672 }
michael@0 673 };

mercurial