Bug 925389: Cancel XPI update check if the add-on is uninstalled while the check is in progress; r=Unfocused

This commit is contained in:
Irving Reid 2013-11-26 10:21:32 -05:00
parent a083ecd946
commit 8ab0829977
8 changed files with 403 additions and 59 deletions

View File

@ -2279,6 +2279,8 @@ this.AddonManager = {
UPDATE_STATUS_UNKNOWN_FORMAT: -4, UPDATE_STATUS_UNKNOWN_FORMAT: -4,
// The update information was not correctly signed or there was an SSL error. // The update information was not correctly signed or there was an SSL error.
UPDATE_STATUS_SECURITY_ERROR: -5, UPDATE_STATUS_SECURITY_ERROR: -5,
// The update was cancelled.
UPDATE_STATUS_CANCELLED: -6,
// Constants to indicate why an update check is being performed // Constants to indicate why an update check is being performed
// Update check has been requested by the user. // Update check has been requested by the user.

View File

@ -490,8 +490,14 @@ UpdateParser.prototype = {
this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR); this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
return; return;
} }
if ("onUpdateCheckComplete" in this.observer) if ("onUpdateCheckComplete" in this.observer) {
this.observer.onUpdateCheckComplete(results); try {
this.observer.onUpdateCheckComplete(results);
}
catch (e) {
WARN("onUpdateCheckComplete notification failed", e);
}
}
return; return;
} }
@ -534,8 +540,14 @@ UpdateParser.prototype = {
* Helper method to notify the observer that an error occured. * Helper method to notify the observer that an error occured.
*/ */
notifyError: function UP_notifyError(aStatus) { notifyError: function UP_notifyError(aStatus) {
if ("onUpdateCheckError" in this.observer) if ("onUpdateCheckError" in this.observer) {
this.observer.onUpdateCheckError(aStatus); try {
this.observer.onUpdateCheckError(aStatus);
}
catch (e) {
WARN("onUpdateCheckError notification failed", e);
}
}
}, },
/** /**
@ -549,6 +561,17 @@ UpdateParser.prototype = {
WARN("Request timed out"); WARN("Request timed out");
this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT); this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
},
/**
* Called to cancel an in-progress update check.
*/
cancel: function UP_cancel() {
this.timer.cancel();
this.timer = null;
this.request.abort();
this.request = null;
this.notifyError(AddonUpdateChecker.ERROR_CANCELLED);
} }
}; };
@ -610,6 +633,8 @@ this.AddonUpdateChecker = {
ERROR_UNKNOWN_FORMAT: -4, ERROR_UNKNOWN_FORMAT: -4,
// The update information was not correctly signed or there was an SSL error. // The update information was not correctly signed or there was an SSL error.
ERROR_SECURITY_ERROR: -5, ERROR_SECURITY_ERROR: -5,
// The update was cancelled
ERROR_CANCELLED: -6,
/** /**
* Retrieves the best matching compatibility update for the application from * Retrieves the best matching compatibility update for the application from
@ -721,9 +746,11 @@ this.AddonUpdateChecker = {
* The URL of the add-on's update manifest * The URL of the add-on's update manifest
* @param aObserver * @param aObserver
* An observer to notify of results * An observer to notify of results
* @return UpdateParser so that the caller can use UpdateParser.cancel() to shut
* down in-progress update requests
*/ */
checkForUpdates: function AUC_checkForUpdates(aId, aUpdateKey, aUrl, checkForUpdates: function AUC_checkForUpdates(aId, aUpdateKey, aUrl,
aObserver) { aObserver) {
new UpdateParser(aId, aUpdateKey, aUrl, aObserver); return new UpdateParser(aId, aUpdateKey, aUrl, aObserver);
} }
}; };

View File

@ -1683,6 +1683,24 @@ function directoryStateDiffers(aState, aCache)
return false; return false;
} }
/**
* Wraps a function in an exception handler to protect against exceptions inside callbacks
* @param aFunction function(args...)
* @return function(args...), a function that takes the same arguments as aFunction
* and returns the same result unless aFunction throws, in which case it logs
* a warning and returns undefined.
*/
function makeSafe(aFunction) {
return function(...aArgs) {
try {
return aFunction(...aArgs);
}
catch(ex) {
WARN("XPIProvider callback failed", ex);
}
return undefined;
}
}
var XPIProvider = { var XPIProvider = {
// An array of known install locations // An array of known install locations
@ -1734,6 +1752,32 @@ var XPIProvider = {
this._telemetryDetails[aId][aName] = aValue; this._telemetryDetails[aId][aName] = aValue;
}, },
// Keep track of in-progress operations that support cancel()
_inProgress: new Set(),
doing: function XPI_doing(aCancellable) {
this._inProgress.add(aCancellable);
},
done: function XPI_done(aCancellable) {
return this._inProgress.delete(aCancellable);
},
cancelAll: function XPI_cancelAll() {
// Cancelling one may alter _inProgress, so restart the iterator after each
while (this._inProgress.size > 0) {
for (let c of this._inProgress) {
try {
c.cancel();
}
catch (e) {
WARN("Cancel failed", e);
}
this._inProgress.delete(c);
}
}
},
/** /**
* Adds or updates a URI mapping for an Addon.id. * Adds or updates a URI mapping for an Addon.id.
* *
@ -2076,6 +2120,9 @@ var XPIProvider = {
shutdown: function XPI_shutdown() { shutdown: function XPI_shutdown() {
LOG("shutdown"); LOG("shutdown");
// Stop anything we were doing asynchronously
this.cancelAll();
this.bootstrappedAddons = {}; this.bootstrappedAddons = {};
this.bootstrapScopes = {}; this.bootstrapScopes = {};
this.enabledAddons = null; this.enabledAddons = null;
@ -3308,7 +3355,7 @@ var XPIProvider = {
locMigrateData = XPIDatabase.migrateData[installLocation.name]; locMigrateData = XPIDatabase.migrateData[installLocation.name];
for (let id in addonStates) { for (let id in addonStates) {
changed = addMetadata(installLocation, id, addonStates[id], changed = addMetadata(installLocation, id, addonStates[id],
locMigrateData[id] || null) || changed; (locMigrateData[id] || null)) || changed;
} }
} }
@ -3642,10 +3689,7 @@ var XPIProvider = {
*/ */
getAddonByID: function XPI_getAddonByID(aId, aCallback) { getAddonByID: function XPI_getAddonByID(aId, aCallback) {
XPIDatabase.getVisibleAddonForID (aId, function getAddonByID_getVisibleAddonForID(aAddon) { XPIDatabase.getVisibleAddonForID (aId, function getAddonByID_getVisibleAddonForID(aAddon) {
if (aAddon) aCallback(createWrapper(aAddon));
aCallback(createWrapper(aAddon));
else
aCallback(null);
}); });
}, },
@ -3673,10 +3717,7 @@ var XPIProvider = {
*/ */
getAddonBySyncGUID: function XPI_getAddonBySyncGUID(aGUID, aCallback) { getAddonBySyncGUID: function XPI_getAddonBySyncGUID(aGUID, aCallback) {
XPIDatabase.getAddonBySyncGUID(aGUID, function getAddonBySyncGUID_getAddonBySyncGUID(aAddon) { XPIDatabase.getAddonBySyncGUID(aGUID, function getAddonBySyncGUID_getAddonBySyncGUID(aAddon) {
if (aAddon) aCallback(createWrapper(aAddon));
aCallback(createWrapper(aAddon));
else
aCallback(null);
}); });
}, },
@ -4382,6 +4423,11 @@ var XPIProvider = {
if ("_hasResourceCache" in aAddon) if ("_hasResourceCache" in aAddon)
aAddon._hasResourceCache = new Map(); aAddon._hasResourceCache = new Map();
if (aAddon._updateCheck) {
LOG("Cancel in-progress update check for " + aAddon.id);
aAddon._updateCheck.cancel();
}
// Inactive add-ons don't require a restart to uninstall // Inactive add-ons don't require a restart to uninstall
let requiresRestart = this.uninstallRequiresRestart(aAddon); let requiresRestart = this.uninstallRequiresRestart(aAddon);
@ -4631,6 +4677,7 @@ AddonInstall.prototype = {
* The callback to pass the initialised AddonInstall to * The callback to pass the initialised AddonInstall to
*/ */
initLocalInstall: function AI_initLocalInstall(aCallback) { initLocalInstall: function AI_initLocalInstall(aCallback) {
aCallback = makeSafe(aCallback);
this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file; this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file;
if (!this.file.exists()) { if (!this.file.exists()) {
@ -4746,7 +4793,7 @@ AddonInstall.prototype = {
AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners, AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
this.wrapper); this.wrapper);
aCallback(this); makeSafe(aCallback)(this);
}, },
/** /**
@ -4946,7 +4993,7 @@ AddonInstall.prototype = {
if (!addon) { if (!addon) {
// No valid add-on was found // No valid add-on was found
aCallback(); makeSafe(aCallback)();
return; return;
} }
@ -4995,7 +5042,7 @@ AddonInstall.prototype = {
}, this); }, this);
} }
else { else {
aCallback(); makeSafe(aCallback)();
} }
}, },
@ -5009,6 +5056,7 @@ AddonInstall.prototype = {
* XPI is incorrectly signed * XPI is incorrectly signed
*/ */
loadManifest: function AI_loadManifest(aCallback) { loadManifest: function AI_loadManifest(aCallback) {
aCallback = makeSafe(aCallback);
let self = this; let self = this;
function addRepositoryData(aAddon) { function addRepositoryData(aAddon) {
// Try to load from the existing cache first // Try to load from the existing cache first
@ -5680,7 +5728,7 @@ AddonInstall.createInstall = function AI_createInstall(aCallback, aFile) {
} }
catch(e) { catch(e) {
ERROR("Error creating install", e); ERROR("Error creating install", e);
aCallback(null); makeSafe(aCallback)(null);
} }
}; };
@ -5819,6 +5867,8 @@ function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion
Components.utils.import("resource://gre/modules/AddonUpdateChecker.jsm"); Components.utils.import("resource://gre/modules/AddonUpdateChecker.jsm");
this.addon = aAddon; this.addon = aAddon;
aAddon._updateCheck = this;
XPIProvider.doing(this);
this.listener = aListener; this.listener = aListener;
this.appVersion = aAppVersion; this.appVersion = aAppVersion;
this.platformVersion = aPlatformVersion; this.platformVersion = aPlatformVersion;
@ -5842,8 +5892,8 @@ function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion
aReason |= UPDATE_TYPE_NEWVERSION; aReason |= UPDATE_TYPE_NEWVERSION;
let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion); let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion);
AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey, this._parser = AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey,
url, this); url, this);
} }
UpdateChecker.prototype = { UpdateChecker.prototype = {
@ -5868,7 +5918,7 @@ UpdateChecker.prototype = {
this.listener[aMethod].apply(this.listener, aArgs); this.listener[aMethod].apply(this.listener, aArgs);
} }
catch (e) { catch (e) {
LOG("Exception calling UpdateListener method " + aMethod + ": " + e); WARN("Exception calling UpdateListener method " + aMethod, e);
} }
}, },
@ -5879,6 +5929,8 @@ UpdateChecker.prototype = {
* The list of update details for the add-on * The list of update details for the add-on
*/ */
onUpdateCheckComplete: function UC_onUpdateCheckComplete(aUpdates) { onUpdateCheckComplete: function UC_onUpdateCheckComplete(aUpdates) {
XPIProvider.done(this.addon._updateCheck);
this.addon._updateCheck = null;
let AUC = AddonUpdateChecker; let AUC = AddonUpdateChecker;
let ignoreMaxVersion = false; let ignoreMaxVersion = false;
@ -5979,9 +6031,23 @@ UpdateChecker.prototype = {
* An error status * An error status
*/ */
onUpdateCheckError: function UC_onUpdateCheckError(aError) { onUpdateCheckError: function UC_onUpdateCheckError(aError) {
XPIProvider.done(this.addon._updateCheck);
this.addon._updateCheck = null;
this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon)); this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
this.callListener("onNoUpdateAvailable", createWrapper(this.addon)); this.callListener("onNoUpdateAvailable", createWrapper(this.addon));
this.callListener("onUpdateFinished", createWrapper(this.addon), aError); this.callListener("onUpdateFinished", createWrapper(this.addon), aError);
},
/**
* Called to cancel an in-progress update check
*/
cancel: function UC_cancel() {
let parser = this._parser;
if (parser) {
this._parser = null;
// This will call back to onUpdateCheckError with a CANCELLED error
parser.cancel();
}
} }
}; };
@ -6635,6 +6701,15 @@ function AddonWrapper(aAddon) {
new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion); new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion);
}; };
// Returns true if there was an update in progress, false if there was no update to cancel
this.cancelUpdate = function AddonWrapper_cancelUpdate() {
if (aAddon._updateCheck) {
aAddon._updateCheck.cancel();
return true;
}
return false;
};
this.hasResource = function AddonWrapper_hasResource(aPath) { this.hasResource = function AddonWrapper_hasResource(aPath) {
if (aAddon._hasResourceCache.has(aPath)) if (aAddon._hasResourceCache.has(aPath))
return aAddon._hasResourceCache.get(aPath); return aAddon._hasResourceCache.get(aPath);

View File

@ -146,10 +146,10 @@ function getRepositoryAddon(aAddon, aCallback) {
/** /**
* Wrap an API-supplied function in an exception handler to make it safe to call * Wrap an API-supplied function in an exception handler to make it safe to call
*/ */
function safeCallback(aCallback) { function makeSafe(aCallback) {
return function(...aArgs) { return function(...aArgs) {
try { try {
aCallback.apply(null, aArgs); aCallback(...aArgs);
} }
catch(ex) { catch(ex) {
WARN("XPI Database callback failed", ex); WARN("XPI Database callback failed", ex);
@ -1057,12 +1057,12 @@ this.XPIDatabase = {
this.asyncLoadDB().then( this.asyncLoadDB().then(
addonDB => { addonDB => {
let addonList = _filterDB(addonDB, aFilter); let addonList = _filterDB(addonDB, aFilter);
asyncMap(addonList, getRepositoryAddon, safeCallback(aCallback)); asyncMap(addonList, getRepositoryAddon, makeSafe(aCallback));
}) })
.then(null, .then(null,
error => { error => {
ERROR("getAddonList failed", e); ERROR("getAddonList failed", e);
safeCallback(aCallback)([]); makeSafe(aCallback)([]);
}); });
}, },
@ -1077,12 +1077,12 @@ this.XPIDatabase = {
getAddon: function(aFilter, aCallback) { getAddon: function(aFilter, aCallback) {
return this.asyncLoadDB().then( return this.asyncLoadDB().then(
addonDB => { addonDB => {
getRepositoryAddon(_findAddon(addonDB, aFilter), safeCallback(aCallback)); getRepositoryAddon(_findAddon(addonDB, aFilter), makeSafe(aCallback));
}) })
.then(null, .then(null,
error => { error => {
ERROR("getAddon failed", e); ERROR("getAddon failed", e);
safeCallback(aCallback)(null); makeSafe(aCallback)(null);
}); });
}, },
@ -1112,7 +1112,7 @@ this.XPIDatabase = {
getAddonInLocation: function XPIDB_getAddonInLocation(aId, aLocation, aCallback) { getAddonInLocation: function XPIDB_getAddonInLocation(aId, aLocation, aCallback) {
this.asyncLoadDB().then( this.asyncLoadDB().then(
addonDB => getRepositoryAddon(addonDB.get(aLocation + ":" + aId), addonDB => getRepositoryAddon(addonDB.get(aLocation + ":" + aId),
safeCallback(aCallback))); makeSafe(aCallback)));
}, },
/** /**

View File

@ -16,13 +16,29 @@ const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVer
// Forcibly end the test if it runs longer than 15 minutes // Forcibly end the test if it runs longer than 15 minutes
const TIMEOUT_MS = 900000; const TIMEOUT_MS = 900000;
Components.utils.import("resource://gre/modules/AddonManager.jsm");
Components.utils.import("resource://gre/modules/AddonRepository.jsm"); Components.utils.import("resource://gre/modules/AddonRepository.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/FileUtils.jsm"); Components.utils.import("resource://gre/modules/FileUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/NetUtil.jsm"); Components.utils.import("resource://gre/modules/NetUtil.jsm");
// We need some internal bits of AddonManager
let AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm");
let AddonManager = AMscope.AddonManager;
let AddonManagerInternal = AMscope.AddonManagerInternal;
// Mock out AddonManager's reference to the AsyncShutdown module so we can shut
// down AddonManager from the test
let MockAsyncShutdown = {
hook: null,
profileBeforeChange: {
addBlocker: function(aName, aBlocker) {
do_print("Mock profileBeforeChange blocker for '" + aName + "'");
MockAsyncShutdown.hook = aBlocker;
}
}
};
AMscope.AsyncShutdown = MockAsyncShutdown;
var gInternalManager = null; var gInternalManager = null;
var gAppInfo = null; var gAppInfo = null;
var gAddonsList; var gAddonsList;
@ -403,11 +419,16 @@ function shutdownManager() {
let shutdownDone = false; let shutdownDone = false;
Services.obs.notifyObservers(null, "quit-application-granted", null); Services.obs.notifyObservers(null, "quit-application-granted", null);
let scope = Components.utils.import("resource://gre/modules/AddonManager.jsm"); MockAsyncShutdown.hook().then(
scope.AddonManagerInternal.shutdown() () => shutdownDone = true,
.then( err => shutdownDone = true);
() => shutdownDone = true,
err => shutdownDone = true); let thr = Services.tm.mainThread;
// Wait until we observe the shutdown notifications
while (!shutdownDone) {
thr.processNextEvent(true);
}
gInternalManager = null; gInternalManager = null;
@ -417,20 +438,13 @@ function shutdownManager() {
// Clear any crash report annotations // Clear any crash report annotations
gAppInfo.annotations = {}; gAppInfo.annotations = {};
let thr = Services.tm.mainThread;
// Wait until we observe the shutdown notifications
while (!shutdownDone) {
thr.processNextEvent(true);
}
// Force the XPIProvider provider to reload to better // Force the XPIProvider provider to reload to better
// simulate real-world usage. // simulate real-world usage.
scope = Components.utils.import("resource://gre/modules/XPIProvider.jsm"); let XPIscope = Components.utils.import("resource://gre/modules/XPIProvider.jsm");
// This would be cleaner if I could get it as the rejection reason from // This would be cleaner if I could get it as the rejection reason from
// the AddonManagerInternal.shutdown() promise // the AddonManagerInternal.shutdown() promise
gXPISaveError = scope.XPIProvider._shutdownError; gXPISaveError = XPIscope.XPIProvider._shutdownError;
AddonManagerPrivate.unregisterProvider(scope.XPIProvider); AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
Components.utils.unload("resource://gre/modules/XPIProvider.jsm"); Components.utils.unload("resource://gre/modules/XPIProvider.jsm");
} }
@ -1090,16 +1104,24 @@ if ("nsIWindowsRegKey" in AM_Ci) {
var MockRegistry = { var MockRegistry = {
LOCAL_MACHINE: {}, LOCAL_MACHINE: {},
CURRENT_USER: {}, CURRENT_USER: {},
CLASSES_ROOT: {},
setValue: function(aRoot, aPath, aName, aValue) { getRoot: function(aRoot) {
switch (aRoot) { switch (aRoot) {
case AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE: case AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE:
var rootKey = MockRegistry.LOCAL_MACHINE; return MockRegistry.LOCAL_MACHINE;
break
case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER: case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER:
rootKey = MockRegistry.CURRENT_USER; return MockRegistry.CURRENT_USER;
break case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT:
return MockRegistry.CLASSES_ROOT;
default:
do_throw("Unknown root " + aRootKey);
return null;
} }
},
setValue: function(aRoot, aPath, aName, aValue) {
let rootKey = MockRegistry.getRoot(aRoot);
if (!(aPath in rootKey)) { if (!(aPath in rootKey)) {
rootKey[aPath] = []; rootKey[aPath] = [];
@ -1141,14 +1163,7 @@ if ("nsIWindowsRegKey" in AM_Ci) {
// --- Overridden nsIWindowsRegKey interface functions --- // --- Overridden nsIWindowsRegKey interface functions ---
open: function(aRootKey, aRelPath, aMode) { open: function(aRootKey, aRelPath, aMode) {
switch (aRootKey) { let rootKey = MockRegistry.getRoot(aRootKey);
case AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE:
var rootKey = MockRegistry.LOCAL_MACHINE;
break
case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER:
rootKey = MockRegistry.CURRENT_USER;
break
}
if (!(aRelPath in rootKey)) if (!(aRelPath in rootKey))
rootKey[aRelPath] = []; rootKey[aRelPath] = [];
@ -1398,9 +1413,9 @@ function changeXPIDBVersion(aNewVersion) {
} }
/** /**
* Raw load of a JSON file * Load a file into a string
*/ */
function loadJSON(aFile) { function loadFile(aFile) {
let data = ""; let data = "";
let fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]. let fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
createInstance(Components.interfaces.nsIFileInputStream); createInstance(Components.interfaces.nsIFileInputStream);
@ -1416,6 +1431,14 @@ function loadJSON(aFile) {
} while (read != 0); } while (read != 0);
} }
cstream.close(); cstream.close();
return data;
}
/**
* Raw load of a JSON file
*/
function loadJSON(aFile) {
let data = loadFile(aFile);
do_print("Loaded JSON file " + aFile.path); do_print("Loaded JSON file " + aFile.path);
return(JSON.parse(data)); return(JSON.parse(data));
} }

View File

@ -0,0 +1,66 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
// Test the cancellable doing/done/cancelAll API in XPIProvider
let scope = Components.utils.import("resource://gre/modules/XPIProvider.jsm");
let XPIProvider = scope.XPIProvider;
function run_test() {
// Check that cancelling with nothing in progress doesn't blow up
XPIProvider.cancelAll();
// Check that a basic object gets cancelled
let getsCancelled = {
isCancelled: false,
cancel: function () {
if (this.isCancelled)
do_throw("Already cancelled");
this.isCancelled = true;
}
};
XPIProvider.doing(getsCancelled);
XPIProvider.cancelAll();
do_check_true(getsCancelled.isCancelled);
// Check that if we complete a cancellable, it doesn't get cancelled
let doesntGetCancelled = {
cancel: () => do_throw("This should not have been cancelled")
};
XPIProvider.doing(doesntGetCancelled);
do_check_true(XPIProvider.done(doesntGetCancelled));
XPIProvider.cancelAll();
// A cancellable that adds a cancellable
getsCancelled.isCancelled = false;
let addsAnother = {
isCancelled: false,
cancel: function () {
if (this.isCancelled)
do_throw("Already cancelled");
this.isCancelled = true;
XPIProvider.doing(getsCancelled);
}
}
XPIProvider.doing(addsAnother);
XPIProvider.cancelAll();
do_check_true(addsAnother.isCancelled);
do_check_true(getsCancelled.isCancelled);
// A cancellable that removes another. This assumes that Set() iterates in the
// order that members were added
let removesAnother = {
isCancelled: false,
cancel: function () {
if (this.isCancelled)
do_throw("Already cancelled");
this.isCancelled = true;
XPIProvider.done(doesntGetCancelled);
}
}
XPIProvider.doing(removesAnother);
XPIProvider.doing(doesntGetCancelled);
XPIProvider.cancelAll();
do_check_true(removesAnother.isCancelled);
}

View File

@ -0,0 +1,149 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
// Test cancelling add-on update checks while in progress (bug 925389)
Components.utils.import("resource://gre/modules/Promise.jsm");
// The test extension uses an insecure update url.
Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
// Set up an HTTP server to respond to update requests
Components.utils.import("resource://testing-common/httpd.js");
const profileDir = gProfD.clone();
profileDir.append("extensions");
// Return a promise that resolves with an addon retrieved by
// AddonManager.getAddonByID()
function promiseGetAddon(aID) {
let p = Promise.defer();
AddonManager.getAddonByID(aID, p.resolve);
return p.promise;
}
function run_test() {
// Kick off the task-based tests...
run_next_test();
}
// Install one extension
// Start download of update check (but delay HTTP response)
// Cancel update check
// - ensure we get cancel notification
// complete HTTP response
// - ensure no callbacks after cancel
// - ensure update is gone
// Create an addon update listener containing a promise
// that resolves when the update is cancelled
function makeCancelListener() {
let updated = Promise.defer();
return {
onUpdateAvailable: function(addon, install) {
updated.reject("Should not have seen onUpdateAvailable notification");
},
onUpdateFinished: function(aAddon, aError) {
do_print("onUpdateCheckFinished: " + aAddon.id + " " + aError);
updated.resolve(aError);
},
promise: updated.promise
};
}
// Set up the HTTP server so that we can control when it responds
let httpReceived = Promise.defer();
function dataHandler(aRequest, aResponse) {
asyncResponse = aResponse;
aResponse.processAsync();
httpReceived.resolve([aRequest, aResponse]);
}
var testserver = new HttpServer();
testserver.registerDirectory("/addons/", do_get_file("addons"));
testserver.registerPathHandler("/data/test_update.rdf", dataHandler);
testserver.start(-1);
gPort = testserver.identity.primaryPort;
// Set up an add-on for update check
writeInstallRDFForExtension({
id: "addon1@tests.mozilla.org",
version: "1.0",
updateURL: "http://localhost:" + gPort + "/data/test_update.rdf",
targetApplications: [{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1"
}],
name: "Test Addon 1",
}, profileDir);
add_task(function cancel_during_check() {
startupManager();
let a1 = yield promiseGetAddon("addon1@tests.mozilla.org");
do_check_neq(a1, null);
let listener = makeCancelListener();
a1.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
// Wait for the http request to arrive
let [request, response] = yield httpReceived.promise;
// cancelUpdate returns true if there is an update check in progress
do_check_true(a1.cancelUpdate());
let updateResult = yield listener.promise;
do_check_eq(AddonManager.UPDATE_STATUS_CANCELLED, updateResult);
// Now complete the HTTP request
let file = do_get_cwd();
file.append("data");
file.append("test_update.rdf");
let data = loadFile(file);
response.write(data);
response.finish();
// trying to cancel again should return false, i.e. nothing to cancel
do_check_false(a1.cancelUpdate());
yield true;
});
// Test that update check is cancelled if the XPI provider shuts down while
// the update check is in progress
add_task(function shutdown_during_check() {
// Reset our HTTP listener
httpReceived = Promise.defer();
let a1 = yield promiseGetAddon("addon1@tests.mozilla.org");
do_check_neq(a1, null);
let listener = makeCancelListener();
a1.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
// Wait for the http request to arrive
let [request, response] = yield httpReceived.promise;
shutdownManager();
let updateResult = yield listener.promise;
do_check_eq(AddonManager.UPDATE_STATUS_CANCELLED, updateResult);
// Now complete the HTTP request
let file = do_get_cwd();
file.append("data");
file.append("test_update.rdf");
let data = loadFile(file);
response.write(data);
response.finish();
// trying to cancel again should return false, i.e. nothing to cancel
do_check_false(a1.cancelUpdate());
yield testserver.stop(Promise.defer().resolve);
});

View File

@ -11,6 +11,7 @@ run-sequentially = Uses hardcoded ports in xpi files.
skip-if = os == "android" skip-if = os == "android"
[test_DeferredSave.js] [test_DeferredSave.js]
[test_LightweightThemeManager.js] [test_LightweightThemeManager.js]
[test_XPIcancel.js]
[test_backgroundupdate.js] [test_backgroundupdate.js]
[test_bad_json.js] [test_bad_json.js]
[test_badschema.js] [test_badschema.js]
@ -230,6 +231,7 @@ fail-if = os == "android"
[test_update.js] [test_update.js]
# Bug 676992: test consistently hangs on Android # Bug 676992: test consistently hangs on Android
skip-if = os == "android" skip-if = os == "android"
[test_updateCancel.js]
[test_update_strictcompat.js] [test_update_strictcompat.js]
# Bug 676992: test consistently hangs on Android # Bug 676992: test consistently hangs on Android
skip-if = os == "android" skip-if = os == "android"