Bug 587088: Rollback updates when file operations fail. r=robstrong, a=blocks-betaN

This commit is contained in:
Dave Townsend 2010-10-20 11:03:27 -07:00
parent 1c13959461
commit 1be9a480c3
10 changed files with 420 additions and 81 deletions

View File

@ -80,6 +80,7 @@ const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/upd
const DIR_EXTENSIONS = "extensions"; const DIR_EXTENSIONS = "extensions";
const DIR_STAGE = "staged"; const DIR_STAGE = "staged";
const DIR_XPI_STAGE = "staged-xpis"; const DIR_XPI_STAGE = "staged-xpis";
const DIR_TRASH = "trash";
const FILE_OLD_DATABASE = "extensions.rdf"; const FILE_OLD_DATABASE = "extensions.rdf";
const FILE_DATABASE = "extensions.sqlite"; const FILE_DATABASE = "extensions.sqlite";
@ -160,6 +161,125 @@ var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\
}) })
}, this); }, this);
/**
* A safe way to move a file or the contents of a directory to a new directory.
* The move is performed recursively and if anything fails an attempt is made to
* rollback the entire operation. The operation may also be rolled back to its
* original state after it has completed by calling the rollback method.
*
* Moves can be chained. Calling move multiple times will remember the whole set
* and if one fails all of the move operations will be rolled back.
*/
function SafeMoveOperation() {
this._movedFiles = [];
this._createdDirs = [];
}
SafeMoveOperation.prototype = {
_movedFiles: null,
_createdDirs: null,
_moveFile: function(aFile, aTargetDirectory) {
let oldFile = aFile.clone();
let newFile = aFile.clone();
try {
newFile.moveTo(aTargetDirectory, null);
}
catch (e) {
throw new Error("Failed to move file " + aFile.path + " to " +
aTargetDirectory.path + ": " + e);
}
this._movedFiles.push({ oldFile: oldFile, newFile: newFile });
},
_moveDirectory: function(aDirectory, aTargetDirectory) {
let newDir = aTargetDirectory.clone();
newDir.append(aDirectory.leafName);
try {
newDir.create(Ci.nsILocalFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
}
catch (e) {
throw new Error("Failed to create directory " + newDir.path + ": " + e);
}
this._createdDirs.push(newDir);
let entries = aDirectory.directoryEntries
.QueryInterface(Ci.nsIDirectoryEnumerator);
try {
let entry;
while (entry = entries.nextFile)
this._moveDirEntry(entry, newDir);
}
finally {
entries.close();
}
// The directory should be empty by this point. If it isn't this will throw
// and all of the operations will be rolled back
try {
aDirectory.permissions = FileUtils.PERMS_DIRECTORY;
aDirectory.remove(false);
}
catch (e) {
throw new Error("Failed to remove directory " + aDirectory.path + ": " + e);
}
// Note we put the directory move in after all the file moves so the
// directory is recreated before all the files are moved back
this._movedFiles.push({ oldFile: aDirectory, newFile: newDir });
},
_moveDirEntry: function(aDirEntry, aTargetDirectory) {
if (aDirEntry.isDirectory())
this._moveDirectory(aDirEntry, aTargetDirectory);
else
this._moveFile(aDirEntry, aTargetDirectory);
},
/**
* Moves a file or directory into a new directory. If an error occurs then all
* files that have been moved will be moved back to their original location.
*
* @param aFile
* The file or directory to be moved.
* @param aTargetDirectory
* The directory to move into, this is expected to be an empty
* directory.
*/
move: function(aFile, aTargetDirectory) {
try {
this._moveDirEntry(aFile, aTargetDirectory);
}
catch (e) {
ERROR("Failure moving " + aFile.path + " to " + aTargetDirectory.path + ": " + e);
this.rollback();
throw e;
}
},
/**
* Rolls back all the moves that this operation performed. If an exception
* occurs here then both old and new directories are left in an indeterminate
* state
*/
rollback: function() {
while (this._movedFiles.length > 0) {
let move = this._movedFiles.pop();
if (move.newFile.isDirectory()) {
let oldDir = move.oldFile.parent.clone();
oldDir.append(move.oldFile.leafName);
oldDir.create(Ci.nsILocalFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
}
else {
move.newFile.moveTo(move.oldFile.parent, null);
}
}
while (this._createdDirs.length > 0)
recursiveRemove(this._createdDirs.pop());
}
};
/** /**
* Gets the currently selected locale for display. * Gets the currently selected locale for display.
* @return the selected locale or "en-US" if none is selected * @return the selected locale or "en-US" if none is selected
@ -655,6 +775,7 @@ function buildJarURI(aJarfile, aPath) {
/** /**
* Creates and returns a new unique temporary file. The caller should delete * Creates and returns a new unique temporary file. The caller should delete
* the file when it is no longer needed. * the file when it is no longer needed.
*
* @return an nsIFile that points to a randomly named, initially empty file in * @return an nsIFile that points to a randomly named, initially empty file in
* the OS temporary files directory * the OS temporary files directory
*/ */
@ -889,6 +1010,42 @@ function resultRows(aStatement) {
} }
} }
/**
* Removes the specified files or directories in a staging directory and then if
* the staging directory is empty attempts to remove it.
*
* @param aDir
* nsIFile for the staging directory to clean up
* @param aLeafNames
* An array of file or directory to remove from the directory, the
* array may be empty
*/
function cleanStagingDir(aDir, aLeafNames) {
aLeafNames.forEach(function(aName) {
let file = aDir.clone();
file.append(aName);
if (file.exists())
recursiveRemove(file);
});
let dirEntries = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
try {
if (dirEntries.nextFile)
return;
}
finally {
dirEntries.close();
}
try {
aDir.permissions = FileUtils.PERMS_DIRECTORY;
aDir.remove(false);
}
catch (e) {
// Failing to remove the staging directory is ignorable
}
}
/** /**
* Recursively removes a directory or file fixing permissions when necessary. * Recursively removes a directory or file fixing permissions when necessary.
* *
@ -1267,17 +1424,11 @@ var XPIProvider = {
this.inactiveAddonIDs = []; this.inactiveAddonIDs = [];
// Get the list of IDs of add-ons that are pending update. // If there are pending operations then we must update the list of active
let updates = [i.addon.id for each (i in this.installs) // add-ons
if ((i.state == AddonManager.STATE_INSTALLED) && if (Prefs.getBoolPref(PREF_PENDING_OPERATIONS, false)) {
i.existingAddon)];
// If there are pending operations or installs waiting to complete then
// we must update the list of active add-ons
if (Prefs.getBoolPref(PREF_PENDING_OPERATIONS, false) ||
updates.length > 0) {
XPIDatabase.updateActiveAddons(); XPIDatabase.updateActiveAddons();
XPIDatabase.writeAddonsList(updates); XPIDatabase.writeAddonsList();
Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false); Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
} }
@ -1543,6 +1694,8 @@ var XPIProvider = {
continue; continue;
} }
changed = true;
if (stageDirEntry.isDirectory()) { if (stageDirEntry.isDirectory()) {
// Check if the directory contains an install manifest. // Check if the directory contains an install manifest.
let manifest = stageDirEntry.clone(); let manifest = stageDirEntry.clone();
@ -1552,9 +1705,13 @@ var XPIProvider = {
// install location. // install location.
if (!manifest.exists()) { if (!manifest.exists()) {
LOG("Processing uninstall of " + id + " in " + aLocation.name); LOG("Processing uninstall of " + id + " in " + aLocation.name);
aLocation.uninstallAddon(id); try {
aLocation.uninstallAddon(id);
}
catch (e) {
ERROR("Failed to uninstall add-on " + id + " in " + aLocation.name);
}
// The file check later will spot the removal and cleanup the database // The file check later will spot the removal and cleanup the database
changed = true;
continue; continue;
} }
} }
@ -1570,7 +1727,6 @@ var XPIProvider = {
} }
aManifests[aLocation.name][id] = null; aManifests[aLocation.name][id] = null;
changed = true;
// Check for a cached AddonInternal for this add-on, it may contain // Check for a cached AddonInternal for this add-on, it may contain
// updated compatibility information // updated compatibility information
@ -2151,7 +2307,7 @@ var XPIProvider = {
LOG("Updating database with changes to installed add-ons"); LOG("Updating database with changes to installed add-ons");
XPIDatabase.updateActiveAddons(); XPIDatabase.updateActiveAddons();
XPIDatabase.commitTransaction(); XPIDatabase.commitTransaction();
XPIDatabase.writeAddonsList([]); XPIDatabase.writeAddonsList();
Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false); Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
JSON.stringify(this.bootstrappedAddons)); JSON.stringify(this.bootstrappedAddons));
@ -2172,7 +2328,7 @@ var XPIProvider = {
true); true);
if (!addonsList.exists()) { if (!addonsList.exists()) {
LOG("Add-ons list is missing, recreating"); LOG("Add-ons list is missing, recreating");
XPIDatabase.writeAddonsList([]); XPIDatabase.writeAddonsList();
} }
}, },
@ -2899,10 +3055,7 @@ var XPIProvider = {
if (!(aAddon instanceof DBAddonInternal)) if (!(aAddon instanceof DBAddonInternal))
throw new Error("Can only cancel uninstall for installed addons."); throw new Error("Can only cancel uninstall for installed addons.");
let stagedAddon = aAddon._installLocation.getStagingDir(); cleanStagingDir(aAddon._installLocation.getStagingDir(), [aAddon.id]);
stagedAddon.append(aAddon.id);
if (stagedAddon.exists())
stagedAddon.remove(true);
XPIDatabase.setAddonProperties(aAddon, { XPIDatabase.setAddonProperties(aAddon, {
pendingUninstall: false pendingUninstall: false
@ -3116,10 +3269,9 @@ var XPIDatabase = {
makeAddonVisible: "UPDATE addon SET visible=1 WHERE internal_id=:internal_id", makeAddonVisible: "UPDATE addon SET visible=1 WHERE internal_id=:internal_id",
removeAddonMetadata: "DELETE FROM addon WHERE internal_id=:internal_id", removeAddonMetadata: "DELETE FROM addon WHERE internal_id=:internal_id",
// Equates to active = visible && !userDisabled && !appDisabled && // Equates to active = visible && !userDisabled && !appDisabled
// !pendingUninstall
setActiveAddons: "UPDATE addon SET active=MIN(visible, 1 - userDisabled, " + setActiveAddons: "UPDATE addon SET active=MIN(visible, 1 - userDisabled, " +
"1 - appDisabled, 1 - pendingUninstall)", "1 - appDisabled)",
setAddonProperties: "UPDATE addon SET userDisabled=:userDisabled, " + setAddonProperties: "UPDATE addon SET userDisabled=:userDisabled, " +
"appDisabled=:appDisabled, " + "appDisabled=:appDisabled, " +
"pendingUninstall=:pendingUninstall, " + "pendingUninstall=:pendingUninstall, " +
@ -4214,12 +4366,8 @@ var XPIDatabase = {
/** /**
* Writes out the XPI add-ons list for the platform to read. * Writes out the XPI add-ons list for the platform to read.
*
* @param aPendingUpdateIDs
* An array of IDs of add-ons that are pending update and so shouldn't
* be included in the add-ons list.
*/ */
writeAddonsList: function XPIDB_writeAddonsList(aPendingUpdateIDs) { writeAddonsList: function XPIDB_writeAddonsList() {
LOG("Writing add-ons list"); LOG("Writing add-ons list");
Services.appinfo.invalidateCachesOnRestart(); Services.appinfo.invalidateCachesOnRestart();
let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
@ -4232,9 +4380,6 @@ var XPIDatabase = {
let stmt = this.getStatement("getActiveAddons"); let stmt = this.getStatement("getActiveAddons");
for (let row in resultRows(stmt)) { for (let row in resultRows(stmt)) {
// Don't include add-ons that are waiting to be updated
if (aPendingUpdateIDs.indexOf(row.id) != -1)
continue;
text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
enabledAddons.push(row.id + ":" + row.version); enabledAddons.push(row.id + ":" + row.version);
} }
@ -4251,9 +4396,6 @@ var XPIDatabase = {
} }
count = 0; count = 0;
for (let row in resultRows(stmt)) { for (let row in resultRows(stmt)) {
// Don't include add-ons that are waiting to be updated
if (aPendingUpdateIDs.indexOf(row.id) != -1)
continue;
text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
enabledAddons.push(row.id + ":" + row.version); enabledAddons.push(row.id + ":" + row.version);
} }
@ -4489,20 +4631,9 @@ AddonInstall.prototype = {
break; break;
case AddonManager.STATE_INSTALLED: case AddonManager.STATE_INSTALLED:
LOG("Cancelling install of " + this.addon.id); LOG("Cancelling install of " + this.addon.id);
let stagedAddon = this.installLocation.getStagingDir(); cleanStagingDir(this.installLocation.getStagingDir(),
let stagedJSON = stagedAddon.clone(); [this.addon.id, this.addon.id + ".xpi",
stagedAddon.append(this.addon.id); this.addon.id + ".json"]);
stagedJSON.append(this.addon.id + ".json");
if (stagedAddon.exists()) {
recursiveRemove(stagedAddon);
}
else {
stagedAddon.leafName += ".xpi";
if (stagedAddon.exists())
stagedAddon.remove(false);
}
if (stagedJSON.exists())
stagedJSON.remove(true);
this.state = AddonManager.STATE_CANCELLED; this.state = AddonManager.STATE_CANCELLED;
XPIProvider.removeActiveInstall(this); XPIProvider.removeActiveInstall(this);
@ -5089,7 +5220,6 @@ AddonInstall.prototype = {
try { try {
// First stage the file regardless of whether restarting is necessary // First stage the file regardless of whether restarting is necessary
let stagedJSON = stagedAddon.clone();
if (this.addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) { if (this.addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) {
LOG("Addon " + this.addon.id + " will be installed as " + LOG("Addon " + this.addon.id + " will be installed as " +
"an unpacked directory"); "an unpacked directory");
@ -5114,7 +5244,8 @@ AddonInstall.prototype = {
this.addon._sourceBundle = stagedAddon; this.addon._sourceBundle = stagedAddon;
// Cache the AddonInternal as it may have updated compatibiltiy info // Cache the AddonInternal as it may have updated compatibiltiy info
stagedJSON.append(this.addon.id + ".json"); let stagedJSON = stagedAddon.clone();
stagedJSON.leafName = this.addon.id + ".json";
if (stagedJSON.exists()) if (stagedJSON.exists())
stagedJSON.remove(true); stagedJSON.remove(true);
let stream = Cc["@mozilla.org/network/file-output-stream;1"]. let stream = Cc["@mozilla.org/network/file-output-stream;1"].
@ -5174,10 +5305,7 @@ AddonInstall.prototype = {
XPIProvider.unloadBootstrapScope(this.existingAddon.id); XPIProvider.unloadBootstrapScope(this.existingAddon.id);
} }
if (isUpgrade) { if (!isUpgrade && this.existingAddon.active) {
this.installLocation.uninstallAddon(this.existingAddon.id);
}
else if (this.existingAddon.active) {
this.existingAddon.active = false; this.existingAddon.active = false;
XPIDatabase.updateAddonActive(this.existingAddon); XPIDatabase.updateAddonActive(this.existingAddon);
} }
@ -5185,6 +5313,7 @@ AddonInstall.prototype = {
// Install the new add-on into its final location // Install the new add-on into its final location
let file = this.installLocation.installAddon(this.addon.id, stagedAddon); let file = this.installLocation.installAddon(this.addon.id, stagedAddon);
cleanStagingDir(stagedAddon.parent, []);
// Update the metadata in the database // Update the metadata in the database
this.addon._installLocation = this.installLocation; this.addon._installLocation = this.installLocation;
@ -6230,7 +6359,7 @@ DirectoryInstallLocation.prototype = {
let id = entry.leafName; let id = entry.leafName;
if (id == DIR_STAGE || id == DIR_XPI_STAGE) if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH)
continue; continue;
let directLoad = false; let directLoad = false;
@ -6309,6 +6438,23 @@ DirectoryInstallLocation.prototype = {
return dir; return dir;
}, },
/**
* Returns a directory that is normally on the same filesystem as the rest of
* the install location and can be used for temporarily storing files during
* safe move operations. Calling this method will delete the existing trash
* directory and its contents.
*
* @return an nsIFile
*/
getTrashDir: function DirInstallLocation_getTrashDir() {
let trashDir = this._directory.clone();
trashDir.append(DIR_TRASH);
if (trashDir.exists())
recursiveRemove(trashDir);
trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
return trashDir;
},
/** /**
* Installs an add-on into the install location. * Installs an add-on into the install location.
* *
@ -6319,27 +6465,49 @@ DirectoryInstallLocation.prototype = {
* @return an nsIFile indicating where the add-on was installed to * @return an nsIFile indicating where the add-on was installed to
*/ */
installAddon: function DirInstallLocation_installAddon(aId, aSource) { installAddon: function DirInstallLocation_installAddon(aId, aSource) {
let trashDir = this.getTrashDir();
let transaction = new SafeMoveOperation();
let file = this._directory.clone().QueryInterface(Ci.nsILocalFile); let file = this._directory.clone().QueryInterface(Ci.nsILocalFile);
file.append(aId); file.append(aId);
if (file.exists())
recursiveRemove(file);
file = this._directory.clone().QueryInterface(Ci.nsILocalFile); // If any of these operations fails the finally block will clean up the
file.append(aId + ".xpi"); // temporary directory
if (file.exists()) { try {
Services.obs.notifyObservers(file, "flush-cache-entry", null); if (file.exists())
file.remove(true); transaction.move(file, trashDir);
file = this._directory.clone().QueryInterface(Ci.nsILocalFile);
file.append(aId + ".xpi");
if (file.exists()) {
Services.obs.notifyObservers(file, "flush-cache-entry", null);
transaction.move(file, trashDir);
}
if (aSource.isFile())
Services.obs.notifyObservers(aSource, "flush-cache-entry", null);
transaction.move(aSource, this._directory);
}
finally {
// It isn't ideal if this cleanup fails but it isn't worth rolling back
// the install because of it.
try {
recursiveRemove(trashDir);
}
catch (e) {
WARN("Failed to remove trash directory when installing " + aId);
}
} }
aSource = aSource.clone().QueryInterface(Ci.nsILocalFile); let newFile = this._directory.clone().QueryInterface(Ci.nsILocalFile);
if (aSource.isFile()) newFile.append(aSource.leafName);
Services.obs.notifyObservers(aSource, "flush-cache-entry", null); newFile.lastModifiedTime = Date.now();
aSource.moveTo(this._directory, aSource.leafName); this._FileToIDMap[newFile.path] = aId;
aSource.lastModifiedTime = Date.now(); this._IDToFileMap[aId] = newFile;
this._FileToIDMap[aSource.path] = aId;
this._IDToFileMap[aId] = aSource;
return aSource; return newFile;
}, },
/** /**
@ -6357,9 +6525,6 @@ DirectoryInstallLocation.prototype = {
return; return;
} }
delete this._FileToIDMap[file.path];
delete this._IDToFileMap[aId];
file = this._directory.clone(); file = this._directory.clone();
file.append(aId); file.append(aId);
if (!file.exists()) if (!file.exists())
@ -6368,12 +6533,35 @@ DirectoryInstallLocation.prototype = {
if (!file.exists()) { if (!file.exists()) {
WARN("Attempted to remove " + aId + " from " + WARN("Attempted to remove " + aId + " from " +
this._name + " but it was already gone"); this._name + " but it was already gone");
delete this._FileToIDMap[file.path];
delete this._IDToFileMap[aId];
return; return;
} }
let trashDir = this.getTrashDir();
if (file.leafName != aId) if (file.leafName != aId)
Services.obs.notifyObservers(file, "flush-cache-entry", null); Services.obs.notifyObservers(file, "flush-cache-entry", null);
recursiveRemove(file);
let transaction = new SafeMoveOperation();
try {
transaction.move(file, trashDir);
}
finally {
// It isn't ideal if this cleanup fails, but it is probably better than
// rolling back the uninstall at this point
try {
recursiveRemove(trashDir);
}
catch (e) {
WARN("Failed to remove trash directory when uninstalling " + aId);
}
}
delete this._FileToIDMap[file.path];
delete this._IDToFileMap[aId];
}, },
/** /**

View File

@ -0,0 +1,22 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>addon1@tests.mozilla.org</em:id>
<em:version>1.0</em:version>
<!-- Front End MetaData -->
<em:name>Bug 587088 Test</em:name>
<em:targetApplication>
<Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>1</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

View File

@ -0,0 +1 @@
Contents of add-on version 1

View File

@ -0,0 +1,22 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>addon1@tests.mozilla.org</em:id>
<em:version>2.0</em:version>
<!-- Front End MetaData -->
<em:name>Bug 587088 Test</em:name>
<em:targetApplication>
<Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>1</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

View File

@ -0,0 +1 @@
Contents of add-on version 2

View File

@ -1049,5 +1049,16 @@ do_register_cleanup(function() {
do_throw("Found unexpected file in temporary directory: " + entry.leafName); do_throw("Found unexpected file in temporary directory: " + entry.leafName);
} }
var testDir = gProfD.clone();
testDir.append("extensions");
testDir.append("trash");
do_check_false(testDir.exists());
testDir.leafName = "staged";
do_check_false(testDir.exists());
testDir.leafName = "staged-xpis";
do_check_false(testDir.exists());
shutdownManager(); shutdownManager();
}); });

View File

@ -0,0 +1,96 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
// Tests that trying to upgrade or uninstall an extension that has a file locked
// will roll back the upgrade
const profileDir = gProfD.clone();
profileDir.append("extensions");
function run_test() {
// This is only an issue on windows.
if (!("nsIWindowsRegKey" in AM_Ci))
return;
do_test_pending();
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
startupManager();
run_test_1();
}
function check_addon(aAddon) {
do_check_neq(aAddon, null);
do_check_eq(aAddon.version, "1.0");
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"));
}
function run_test_1() {
installAllFiles([do_get_addon("test_bug587088_1")], function() {
restartManager();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon(a1);
// Lock either install.rdf for unpacked add-ons or the xpi for packed add-ons.
let uri = a1.getResourceURI("install.rdf");
if (uri.schemeIs("jar"))
uri = a1.getResourceURI();
let fstream = AM_Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(AM_Ci.nsIFileInputStream);
fstream.init(uri.QueryInterface(AM_Ci.nsIFileURL).file, -1, 0, 0);
installAllFiles([do_get_addon("test_bug587088_2")], function() {
restartManager();
fstream.close();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon(a1);
a1.uninstall();
restartManager();
run_test_2();
});
});
});
});
}
// Test that a failed uninstall gets rolled back
function run_test_2() {
installAllFiles([do_get_addon("test_bug587088_1")], function() {
restartManager();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon(a1);
// Lock either install.rdf for unpacked add-ons or the xpi for packed add-ons.
let uri = a1.getResourceURI("install.rdf");
if (uri.schemeIs("jar"))
uri = a1.getResourceURI();
let fstream = AM_Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(AM_Ci.nsIFileInputStream);
fstream.init(uri.QueryInterface(AM_Ci.nsIFileURL).file, -1, 0, 0);
a1.uninstall();
restartManager();
fstream.close();
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
check_addon(a1);
do_test_finished();
});
});
});
}

View File

@ -204,8 +204,6 @@ function check_test_2() {
shutdownManager(); shutdownManager();
do_check_false(isExtensionInAddonsList(profileDir, olda1.id));
startupManager(); startupManager();
do_check_true(isExtensionInAddonsList(profileDir, olda1.id)); do_check_true(isExtensionInAddonsList(profileDir, olda1.id));