Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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 | }; |