Bug 439672: Add-on uninstall fails when one of the add-ons files is locked. r=robstrong

This commit is contained in:
Dave Townsend 2011-06-23 09:13:46 -07:00
parent 5529fb9651
commit 7506247545
5 changed files with 264 additions and 88 deletions

View File

@ -1240,6 +1240,7 @@ function cleanStagingDir(aDir, aLeafNames) {
aDir.remove(false);
}
catch (e) {
WARN("Failed to remove staging dir", e);
// Failing to remove the staging directory is ignorable
}
}
@ -1270,8 +1271,14 @@ function recursiveRemove(aFile) {
try {
while (entry = dirEntries.nextFile)
recursiveRemove(entry);
try {
aFile.remove(true);
}
catch (e) {
ERROR("Failed to remove empty directory " + aFile.path, e);
throw e;
}
}
finally {
dirEntries.close();
}
@ -1914,6 +1921,7 @@ var XPIProvider = {
if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory())
return;
let seenFiles = [];
entries = stagingDir.directoryEntries
.QueryInterface(Ci.nsIDirectoryEnumerator);
while (entries.hasMoreElements()) {
@ -1925,8 +1933,10 @@ var XPIProvider = {
id = id.substring(0, id.length - 4);
}
else {
if (id.substring(id.length - 5).toLowerCase() != ".json")
if (id.substring(id.length - 5).toLowerCase() != ".json") {
WARN("Ignoring file: " + stageDirEntry.path);
seenFiles.push(stageDirEntry.leafName);
}
continue;
}
}
@ -1935,6 +1945,7 @@ var XPIProvider = {
if (!gIDTest.test(id)) {
WARN("Ignoring directory whose name is not a valid add-on ID: " +
stageDirEntry.path);
seenFiles.push(stageDirEntry.leafName);
continue;
}
@ -1951,6 +1962,7 @@ var XPIProvider = {
LOG("Processing uninstall of " + id + " in " + aLocation.name);
try {
aLocation.uninstallAddon(id);
seenFiles.push(stageDirEntry.leafName);
}
catch (e) {
ERROR("Failed to uninstall add-on " + id + " in " + aLocation.name, e);
@ -1988,6 +2000,7 @@ var XPIProvider = {
fis.close();
}
}
seenFiles.push(jsonfile.leafName);
// If there was no cached AddonInternal then load it directly
if (!aManifests[aLocation.name][id]) {
@ -1996,9 +2009,10 @@ var XPIProvider = {
existingAddonID = aManifests[aLocation.name][id].existingAddonID || id;
}
catch (e) {
// This add-on can't be installed so just remove it now
stageDirEntry.remove(true);
ERROR("Unable to read add-on manifest from " + stageDirEntry.path, e);
// This add-on can't be installed so just remove it now
seenFiles.push(stageDirEntry.leafName);
continue;
}
}
@ -2038,6 +2052,12 @@ var XPIProvider = {
catch (e) {
ERROR("Failed to install staged add-on " + id + " in " + aLocation.name,
e);
// Re-create the staged install
AddonInstall.createStagedInstall(aLocation, stageDirEntry,
aManifests[aLocation.name][id]);
// Make sure not to delete the cached manifest json file
seenFiles.pop();
delete aManifests[aLocation.name][id];
if (oldBootstrap) {
@ -2052,11 +2072,11 @@ var XPIProvider = {
entries.close();
try {
recursiveRemove(stagingDir);
cleanStagingDir(stagingDir, seenFiles);
}
catch (e) {
// Non-critical, just saves some perf on startup if we clean this up.
LOG("Error removing staging dir " + stagingDir.path, e);
LOG("Error cleaning staging dir " + stagingDir.path, e);
}
}, this);
return changed;
@ -3937,9 +3957,10 @@ var XPIDatabase = {
makeAddonVisible: "UPDATE addon SET visible=1 WHERE internal_id=:internal_id",
removeAddonMetadata: "DELETE FROM addon WHERE internal_id=:internal_id",
// Equates to active = visible && !userDisabled && !softDisabled && !appDisabled
// Equates to active = visible && !userDisabled && !softDisabled &&
// !appDisabled && !pendingUninstall
setActiveAddons: "UPDATE addon SET active=MIN(visible, 1 - userDisabled, " +
"1 - softDisabled, 1 - appDisabled)",
"1 - softDisabled, 1 - appDisabled, 1 - pendingUninstall)",
setAddonProperties: "UPDATE addon SET userDisabled=:userDisabled, " +
"appDisabled=:appDisabled, " +
"softDisabled=:softDisabled, " +
@ -5295,11 +5316,8 @@ function getHashStringForCrypto(aCrypto) {
}
/**
* Instantiates an AddonInstall and passes the new object to a callback when
* it is complete.
* Instantiates an AddonInstall.
*
* @param aCallback
* The callback to pass the AddonInstall to
* @param aInstallLocation
* The install location the add-on will be installed into
* @param aUrl
@ -5307,14 +5325,6 @@ function getHashStringForCrypto(aCrypto) {
* the add-on will not need to be downloaded
* @param aHash
* An optional hash for the add-on
* @param aName
* An optional name for the add-on
* @param aType
* An optional type for the add-on
* @param aIconURL
* An optional icon for the add-on
* @param aVersion
* An optional version for the add-on
* @param aReleaseNotesURI
* An optional nsIURI of release notes for the add-on
* @param aExistingAddon
@ -5324,9 +5334,8 @@ function getHashStringForCrypto(aCrypto) {
* @throws if the url is the url of a local file and the hash does not match
* or the add-on does not contain an valid install manifest
*/
function AddonInstall(aCallback, aInstallLocation, aUrl, aHash, aName, aType,
aIconURL, aVersion, aReleaseNotesURI, aExistingAddon,
aLoadGroup) {
function AddonInstall(aInstallLocation, aUrl, aHash, aReleaseNotesURI,
aExistingAddon, aLoadGroup) {
this.wrapper = new AddonInstallWrapper(this);
this.installLocation = aInstallLocation;
this.sourceURI = aUrl;
@ -5348,9 +5357,74 @@ function AddonInstall(aCallback, aInstallLocation, aUrl, aHash, aName, aType,
.getInterface(Ci.nsIDOMWindow);
else
this.window = null;
}
if (aUrl instanceof Ci.nsIFileURL) {
this.file = aUrl.file.QueryInterface(Ci.nsILocalFile);
AddonInstall.prototype = {
installLocation: null,
wrapper: null,
stream: null,
crypto: null,
originalHash: null,
hash: null,
loadGroup: null,
badCertHandler: null,
listeners: null,
restartDownload: false,
name: null,
type: null,
version: null,
iconURL: null,
releaseNotesURI: null,
sourceURI: null,
file: null,
ownsTempFile: false,
certificate: null,
certName: null,
linkedInstalls: null,
existingAddon: null,
addon: null,
state: null,
error: null,
progress: null,
maxProgress: null,
/**
* Initialises this install to be a staged install waiting to be applied
*
* @param aManifest
* The cached manifest for the staged install
*/
initStagedInstall: function(aManifest) {
this.name = aManifest.name;
this.type = aManifest.type;
this.version = aManifest.version;
this.iconURL = aManifest.iconURL;
this.releaseNotesURI = aManifest.releaseNotesURI ?
NetUtil.newURI(aManifest.releaseNotesURI) :
null
this.sourceURI = aManifest.sourceURI ?
NetUtil.newURI(aManifest.sourceURI) :
null;
this.file = null;
this.addon = aManifest;
this.state = AddonManager.STATE_INSTALLED;
XPIProvider.installs.push(this);
},
/**
* Initialises this install to be an install from a local file.
*
* @param aCallback
* The callback to pass the initialised AddonInstall to
*/
initLocalInstall: function(aCallback) {
this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL)
.file.QueryInterface(Ci.nsILocalFile);
if (!this.file.exists()) {
WARN("XPI file " + this.file.path + " does not exist");
@ -5436,8 +5510,23 @@ function AddonInstall(aCallback, aInstallLocation, aUrl, aHash, aName, aType,
aCallback(this);
return;
}
}
else {
},
/**
* Initialises this install to be a download from a remote url.
*
* @param aCallback
* The callback to pass the initialised AddonInstall to
* @param aName
* An optional name for the add-on
* @param aType
* An optional type for the add-on
* @param aIconURL
* An optional icon for the add-on
* @param aVersion
* An optional version for the add-on
*/
initAvailableDownload: function(aName, aType, aIconURL, aVersion, aCallback) {
this.state = AddonManager.STATE_AVAILABLE;
this.name = aName;
this.type = aType;
@ -5451,40 +5540,7 @@ function AddonInstall(aCallback, aInstallLocation, aUrl, aHash, aName, aType,
this.wrapper);
aCallback(this);
}
}
AddonInstall.prototype = {
installLocation: null,
wrapper: null,
stream: null,
crypto: null,
originalHash: null,
hash: null,
loadGroup: null,
badCertHandler: null,
listeners: null,
restartDownload: false,
name: null,
type: null,
version: null,
iconURL: null,
releaseNotesURI: null,
sourceURI: null,
file: null,
ownsTempFile: false,
certificate: null,
certName: null,
linkedInstalls: null,
existingAddon: null,
addon: null,
state: null,
error: null,
progress: null,
maxProgress: null,
},
/**
* Starts installation of this add-on from whatever state it is currently at
@ -6338,6 +6394,22 @@ AddonInstall.prototype = {
}
}
/**
* Creates a new AddonInstall for an already staged install. Used when
* installing the staged install failed for some reason.
*
* @param aDir
* The directory holding the staged install
* @param aManifest
* The cached manifest for the install
*/
AddonInstall.createStagedInstall = function(aInstallLocation, aDir, aManifest) {
let url = Services.io.newFileURI(aDir);
let install = new AddonInstall(aInstallLocation, aDir);
install.initStagedInstall(aManifest);
};
/**
* Creates a new AddonInstall to install an add-on from a local file. Installs
* always go into the profile install location.
@ -6352,7 +6424,8 @@ AddonInstall.createInstall = function(aCallback, aFile) {
let url = Services.io.newFileURI(aFile);
try {
new AddonInstall(aCallback, location, url);
let install = new AddonInstall(location, url);
install.initLocalInstall(aCallback);
}
catch(e) {
ERROR("Error creating install", e);
@ -6382,8 +6455,12 @@ AddonInstall.createDownload = function(aCallback, aUri, aHash, aName, aIconURL,
aVersion, aLoadGroup) {
let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
let url = NetUtil.newURI(aUri);
new AddonInstall(aCallback, location, url, aHash, aName, null,
aIconURL, aVersion, null, null, aLoadGroup);
let install = new AddonInstall(location, url, aHash, null, null, aLoadGroup);
if (url instanceof Ci.nsIFileURL)
install.initLocalInstall(aCallback);
else
install.initAvailableDownload(aName, null, aIconURL, aVersion, aCallback);
};
/**
@ -6406,9 +6483,16 @@ AddonInstall.createUpdate = function(aCallback, aAddon, aUpdate) {
catch (e) {
// If the releaseNotesURI cannot be parsed then just ignore it.
}
new AddonInstall(aCallback, aAddon._installLocation, url, aUpdate.updateHash,
aAddon.selectedLocale.name, aAddon.type,
aAddon.iconURL, aUpdate.version, releaseNotesURI, aAddon);
let install = new AddonInstall(aAddon._installLocation, url,
aUpdate.updateHash, releaseNotesURI, aAddon);
if (url instanceof Ci.nsIFileURL) {
install.initLocalInstall(aCallback);
}
else {
install.initAvailableDownload(aAddon.selectedLocale.name, aAddon.type,
aAddon.iconURL, aUpdate.version, aCallback);
}
};
/**
@ -7093,13 +7177,16 @@ function AddonWrapper(aAddon) {
if (!(aAddon instanceof DBAddonInternal)) {
// Add-on is pending install if there is no associated install (shouldn't
// happen here) or if the install is in the process of or has successfully
// completed the install.
// completed the install. If an add-on is pending install then we ignore
// any other pending operations.
if (!aAddon._install || aAddon._install.state == AddonManager.STATE_INSTALLING ||
aAddon._install.state == AddonManager.STATE_INSTALLED)
pending |= AddonManager.PENDING_INSTALL;
return AddonManager.PENDING_INSTALL;
}
else if (aAddon.pendingUninstall) {
pending |= AddonManager.PENDING_UNINSTALL;
// If an add-on is pending uninstall then we ignore any other pending
// operations
return AddonManager.PENDING_UNINSTALL;
}
if (aAddon.active && isAddonDisabled(aAddon))

View File

@ -1105,6 +1105,7 @@ do_register_cleanup(function() {
while (entry = dirEntries.nextFile) {
do_throw("Found unexpected file in temporary directory: " + entry.leafName);
}
dirEntries.close();
var testDir = gProfD.clone();
testDir.append("extensions");

View File

@ -3,7 +3,7 @@
*/
// Tests that trying to upgrade or uninstall an extension that has a file locked
// will roll back the upgrade
// will roll back the upgrade or uninstall and retry at the next restart
const profileDir = gProfD.clone();
profileDir.append("extensions");
@ -20,14 +20,58 @@ function run_test() {
run_test_1();
}
function check_addon(aAddon) {
function check_addon(aAddon, aVersion) {
do_check_neq(aAddon, null);
do_check_eq(aAddon.version, aVersion);
do_check_true(aAddon.isActive);
do_check_true(isExtensionInAddonsList(profileDir, aAddon.id));
do_check_true(aAddon.hasResource("testfile"));
if (aVersion == "1.0") {
do_check_true(aAddon.hasResource("testfile1"));
do_check_false(aAddon.hasResource("testfile2"));
}
else {
do_check_false(aAddon.hasResource("testfile1"));
do_check_true(aAddon.hasResource("testfile2"));
}
do_check_eq(aAddon.pendingOperations, AddonManager.PENDING_NONE);
}
function check_addon_upgrading(aAddon) {
do_check_neq(aAddon, null);
do_check_eq(aAddon.version, "1.0");
do_check_true(aAddon.isActive);
do_check_true(isExtensionInAddonsList(profileDir, aAddon.id));
do_check_true(aAddon.hasResource("testfile"));
do_check_true(aAddon.hasResource("testfile1"));
do_check_false(aAddon.hasResource("testfile2"));
do_check_eq(aAddon.pendingOperations, AddonManager.PENDING_UPGRADE);
do_check_eq(aAddon.pendingUpgrade.version, "2.0");
}
function check_addon_uninstalling(aAddon, aAfterRestart) {
do_check_neq(aAddon, null);
do_check_eq(aAddon.version, "1.0");
if (aAfterRestart) {
do_check_false(aAddon.isActive);
do_check_false(isExtensionInAddonsList(profileDir, aAddon.id));
}
else {
do_check_true(aAddon.isActive);
do_check_true(isExtensionInAddonsList(profileDir, aAddon.id));
}
do_check_true(aAddon.hasResource("testfile"));
do_check_true(aAddon.hasResource("testfile1"));
do_check_false(aAddon.hasResource("testfile2"));
do_check_eq(aAddon.pendingOperations, AddonManager.PENDING_UNINSTALL);
}
function run_test_1() {
@ -35,7 +79,7 @@ function run_test_1() {
restartManager();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon(a1);
check_addon(a1, "1.0");
// Lock either install.rdf for unpacked add-ons or the xpi for packed add-ons.
let uri = a1.getResourceURI("install.rdf");
@ -47,11 +91,25 @@ function run_test_1() {
fstream.init(uri.QueryInterface(AM_Ci.nsIFileURL).file, -1, 0, 0);
installAllFiles([do_get_addon("test_bug587088_2")], function() {
check_addon_upgrading(a1);
restartManager();
fstream.close();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon(a1);
check_addon_upgrading(a1);
restartManager();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon_upgrading(a1);
fstream.close();
restartManager();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon(a1, "2.0");
a1.uninstall();
restartManager();
@ -61,6 +119,8 @@ function run_test_1() {
});
});
});
});
});
}
// Test that a failed uninstall gets rolled back
@ -69,7 +129,7 @@ function run_test_2() {
restartManager();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon(a1);
check_addon(a1, "1.0");
// Lock either install.rdf for unpacked add-ons or the xpi for packed add-ons.
let uri = a1.getResourceURI("install.rdf");
@ -82,15 +142,33 @@ function run_test_2() {
a1.uninstall();
check_addon_uninstalling(a1);
restartManager();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon_uninstalling(a1, true);
restartManager();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon_uninstalling(a1, true);
fstream.close();
restartManager();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon(a1);
do_check_eq(a1, null);
var dir = profileDir.clone();
dir.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
do_check_false(dir.exists());
do_check_false(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
do_test_finished();
});
});
});
});
});
}

View File

@ -169,7 +169,6 @@ function check_test_3() {
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
do_check_neq(a1, null);
do_check_true(hasFlag(AddonManager.PENDING_DISABLE, a1.pendingOperations));
do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
prepare_test({
@ -179,6 +178,7 @@ function check_test_3() {
});
a1.cancelUninstall();
ensure_test_completed();
do_check_true(hasFlag(AddonManager.PENDING_DISABLE, a1.pendingOperations));
restartManager();
run_test_4();

View File

@ -239,11 +239,20 @@ function check_test_3(install) {
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
"addon2@tests.mozilla.org"], function([a1, a2]) {
// Should not have installed the new add-on
// Should not have installed the new add-on but it should still be
// pending install
do_check_neq(a1, null);
do_check_eq(a2, null);
a1.uninstall();
restartManager();
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
"addon2@tests.mozilla.org"], function([a1, a2]) {
// Should have installed the new add-on
do_check_eq(a1, null);
do_check_neq(a2, null);
a2.uninstall();
restartManager();
shutdownManager();
@ -251,6 +260,7 @@ function check_test_3(install) {
run_test_4();
});
});
});
}
// Tests that upgrading to a bootstrapped add-on works but requires a restart