summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/update/UpdateServiceStub.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/update/UpdateServiceStub.sys.mjs')
-rw-r--r--toolkit/mozapps/update/UpdateServiceStub.sys.mjs425
1 files changed, 425 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/UpdateServiceStub.sys.mjs b/toolkit/mozapps/update/UpdateServiceStub.sys.mjs
new file mode 100644
index 0000000000..ae2d5b3f99
--- /dev/null
+++ b/toolkit/mozapps/update/UpdateServiceStub.sys.mjs
@@ -0,0 +1,425 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const DIR_UPDATES = "updates";
+const FILE_UPDATE_STATUS = "update.status";
+const FILE_UPDATE_MESSAGES = "update_messages.log";
+const FILE_BACKUP_MESSAGES = "update_messages_old.log";
+
+const KEY_UPDROOT = "UpdRootD";
+const KEY_OLD_UPDROOT = "OldUpdRootD";
+const KEY_PROFILE_DIR = "ProfD";
+
+// The pref prefix below should have the hash of the install path appended to
+// ensure that this is a per-installation pref (i.e. to ensure that migration
+// happens for every install rather than once per profile)
+const PREF_PREFIX_UPDATE_DIR_MIGRATED = "app.update.migrated.updateDir3.";
+const PREF_APP_UPDATE_ALTUPDATEDIRPATH = "app.update.altUpdateDirPath";
+const PREF_APP_UPDATE_LOG = "app.update.log";
+const PREF_APP_UPDATE_FILE_LOGGING = "app.update.log.file";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "gLogEnabled", function aus_gLogEnabled() {
+ return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false);
+});
+
+function getUpdateBaseDirNoCreate() {
+ if (Cu.isInAutomation) {
+ // This allows tests to use an alternate updates directory so they can test
+ // startup behavior.
+ const MAGIC_TEST_ROOT_PREFIX = "<test-root>";
+ const PREF_TEST_ROOT = "mochitest.testRoot";
+ let alternatePath = Services.prefs.getCharPref(
+ PREF_APP_UPDATE_ALTUPDATEDIRPATH,
+ null
+ );
+ if (alternatePath && alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
+ let testRoot = Services.prefs.getCharPref(PREF_TEST_ROOT);
+ let relativePath = alternatePath.substring(MAGIC_TEST_ROOT_PREFIX.length);
+ if (AppConstants.platform == "win") {
+ relativePath = relativePath.replace(/\//g, "\\");
+ }
+ alternatePath = testRoot + relativePath;
+ let updateDir = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ updateDir.initWithPath(alternatePath);
+ LOG(
+ "getUpdateBaseDirNoCreate returning test directory, path: " +
+ updateDir.path
+ );
+ return updateDir;
+ }
+ }
+
+ return FileUtils.getDir(KEY_UPDROOT, [], false);
+}
+
+export function UpdateServiceStub() {
+ let updateDir = getUpdateBaseDirNoCreate();
+ let prefUpdateDirMigrated =
+ PREF_PREFIX_UPDATE_DIR_MIGRATED + updateDir.leafName;
+
+ let statusFile = updateDir;
+ statusFile.append(DIR_UPDATES);
+ statusFile.append("0");
+ statusFile.append(FILE_UPDATE_STATUS);
+ updateDir = null; // We don't need updateDir anymore, plus now its nsIFile
+ // contains the status file's path
+
+ // We may need to migrate update data
+ if (
+ AppConstants.platform == "win" &&
+ !Services.prefs.getBoolPref(prefUpdateDirMigrated, false)
+ ) {
+ Services.prefs.setBoolPref(prefUpdateDirMigrated, true);
+ try {
+ migrateUpdateDirectory();
+ } catch (ex) {
+ // For the most part, migrateUpdateDirectory() catches its own errors.
+ // But there are technically things that could happen that might not be
+ // caught, like nsIFile.parent or nsIFile.append could unexpectedly fail.
+ // So we will catch any errors here, just in case.
+ LOG(
+ `UpdateServiceStub:UpdateServiceStub Failed to migrate update ` +
+ `directory. Exception: ${ex}`
+ );
+ }
+ }
+
+ // Prevent file logging from persisting for more than a session by disabling
+ // it on startup.
+ if (Services.prefs.getBoolPref(PREF_APP_UPDATE_FILE_LOGGING, false)) {
+ deactivateUpdateLogFile();
+ }
+
+ // If the update.status file exists then initiate post update processing.
+ if (statusFile.exists()) {
+ let aus = Cc["@mozilla.org/updates/update-service;1"]
+ .getService(Ci.nsIApplicationUpdateService)
+ .QueryInterface(Ci.nsIObserver);
+ aus.observe(null, "post-update-processing", "");
+ }
+}
+
+UpdateServiceStub.prototype = {
+ observe() {},
+ classID: Components.ID("{e43b0010-04ba-4da6-b523-1f92580bc150}"),
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+};
+
+function deactivateUpdateLogFile() {
+ LOG("Application update file logging being automatically turned off");
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_FILE_LOGGING, false);
+ let logFile = Services.dirsvc.get(KEY_PROFILE_DIR, Ci.nsIFile);
+ logFile.append(FILE_UPDATE_MESSAGES);
+
+ try {
+ logFile.moveTo(null, FILE_BACKUP_MESSAGES);
+ } catch (e) {
+ LOG(
+ "Failed to backup update messages log (" +
+ e +
+ "). Attempting to " +
+ "remove it."
+ );
+ try {
+ logFile.remove(false);
+ } catch (e) {
+ LOG("Also failed to remove the update messages log: " + e);
+ }
+ }
+}
+
+/**
+ * This function should be called when there are files in the old update
+ * directory that may need to be migrated to the new update directory.
+ */
+function migrateUpdateDirectory() {
+ LOG("UpdateServiceStub:migrateUpdateDirectory Performing migration");
+
+ let sourceRootDir = FileUtils.getDir(KEY_OLD_UPDROOT, [], false);
+ let destRootDir = FileUtils.getDir(KEY_UPDROOT, [], false);
+ let hash = destRootDir.leafName;
+
+ if (!sourceRootDir.exists()) {
+ // Nothing to migrate.
+ return;
+ }
+
+ // List of files to migrate. Each is specified as a list of path components.
+ const toMigrate = [
+ ["updates.xml"],
+ ["active-update.xml"],
+ ["update-config.json"],
+ ["updates", "last-update.log"],
+ ["updates", "backup-update.log"],
+ ["updates", "downloading", FILE_UPDATE_STATUS],
+ ["updates", "downloading", "update.mar"],
+ ["updates", "0", FILE_UPDATE_STATUS],
+ ["updates", "0", "update.mar"],
+ ["updates", "0", "update.version"],
+ ["updates", "0", "update.log"],
+ ["backgroundupdate", "datareporting", "glean", "db", "data.safe.bin"],
+ ];
+
+ // Before we copy anything, double check that a different profile hasn't
+ // already performed migration. If we don't have the necessary permissions to
+ // remove the pre-migration files, we don't want to copy any old files and
+ // potentially make the current update state inconsistent.
+ for (let pathComponents of toMigrate) {
+ // Assemble the destination nsIFile.
+ let destFile = destRootDir.clone();
+ for (let pathComponent of pathComponents) {
+ destFile.append(pathComponent);
+ }
+
+ if (destFile.exists()) {
+ LOG(
+ `UpdateServiceStub:migrateUpdateDirectory Aborting migration because ` +
+ `"${destFile.path}" already exists.`
+ );
+ return;
+ }
+ }
+
+ // Before we migrate everything in toMigrate, there are a few things that
+ // need special handling.
+ let sourceRootParent = sourceRootDir.parent.parent;
+ let destRootParent = destRootDir.parent.parent;
+
+ let profileCountFile = sourceRootParent.clone();
+ profileCountFile.append(`profile_count_${hash}.json`);
+ migrateFile(profileCountFile, destRootParent);
+
+ const updatePingPrefix = `uninstall_ping_${hash}_`;
+ const updatePingSuffix = ".json";
+ try {
+ for (let file of sourceRootParent.directoryEntries) {
+ if (
+ file.leafName.startsWith(updatePingPrefix) &&
+ file.leafName.endsWith(updatePingSuffix)
+ ) {
+ migrateFile(file, destRootParent);
+ }
+ }
+ } catch (ex) {
+ // migrateFile should catch its own errors, but it is possible that
+ // sourceRootParent.directoryEntries could throw.
+ LOG(
+ `UpdateServiceStub:migrateUpdateDirectory Failed to migrate uninstall ` +
+ `ping. Exception: ${ex}`
+ );
+ }
+
+ // Migrate "backgroundupdate.moz_log" and child process logs like
+ // "backgroundupdate.child-1.moz_log".
+ const backgroundLogPrefix = `backgroundupdate`;
+ const backgroundLogSuffix = ".moz_log";
+ try {
+ for (let file of sourceRootDir.directoryEntries) {
+ if (
+ file.leafName.startsWith(backgroundLogPrefix) &&
+ file.leafName.endsWith(backgroundLogSuffix)
+ ) {
+ migrateFile(file, destRootDir);
+ }
+ }
+ } catch (ex) {
+ LOG(
+ `UpdateServiceStub:migrateUpdateDirectory Failed to migrate background ` +
+ `log file. Exception: ${ex}`
+ );
+ }
+
+ const pendingPingRelDir =
+ "backgroundupdate\\datareporting\\glean\\pending_pings";
+ let pendingPingSourceDir = sourceRootDir.clone();
+ pendingPingSourceDir.appendRelativePath(pendingPingRelDir);
+ let pendingPingDestDir = destRootDir.clone();
+ pendingPingDestDir.appendRelativePath(pendingPingRelDir);
+ // Pending ping filenames are UUIDs.
+ const pendingPingFilenameRegex =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
+ if (pendingPingSourceDir.exists()) {
+ try {
+ for (let file of pendingPingSourceDir.directoryEntries) {
+ if (pendingPingFilenameRegex.test(file.leafName)) {
+ migrateFile(file, pendingPingDestDir);
+ }
+ }
+ } catch (ex) {
+ // migrateFile should catch its own errors, but it is possible that
+ // pendingPingSourceDir.directoryEntries could throw.
+ LOG(
+ `UpdateServiceStub:migrateUpdateDirectory Failed to migrate ` +
+ `pending pings. Exception: ${ex}`
+ );
+ }
+ }
+
+ // Migrate everything in toMigrate.
+ for (let pathComponents of toMigrate) {
+ let filename = pathComponents.pop();
+
+ // Assemble the source and destination nsIFile's.
+ let sourceFile = sourceRootDir.clone();
+ let destDir = destRootDir.clone();
+ for (let pathComponent of pathComponents) {
+ sourceFile.append(pathComponent);
+ destDir.append(pathComponent);
+ }
+ sourceFile.append(filename);
+
+ migrateFile(sourceFile, destDir);
+ }
+
+ // There is no reason to keep this file, and it often hangs around and could
+ // interfere with cleanup.
+ let updateLockFile = sourceRootParent.clone();
+ updateLockFile.append(`UpdateLock-${hash}`);
+ try {
+ updateLockFile.remove(false);
+ } catch (ex) {}
+
+ // We want to recursively remove empty directories out of the sourceRootDir.
+ // And if that was the only remaining update directory in sourceRootParent,
+ // we want to remove that too. But we don't want to recurse into other update
+ // directories in sourceRootParent.
+ //
+ // Potentially removes "C:\ProgramData\Mozilla\updates\<hash>" and
+ // subdirectories.
+ cleanupDir(sourceRootDir, true);
+ // Potentially removes "C:\ProgramData\Mozilla\updates"
+ cleanupDir(sourceRootDir.parent, false);
+ // Potentially removes "C:\ProgramData\Mozilla"
+ cleanupDir(sourceRootParent, false);
+}
+
+/**
+ * Attempts to move the source file to the destination directory. If the file
+ * cannot be moved, we attempt to copy it and remove the original. All errors
+ * are logged, but no exceptions are thrown. Both arguments must be of type
+ * nsIFile and are expected to be regular files.
+ *
+ * Non-existent files are silently ignored.
+ *
+ * The reason that we are migrating is to deal with problematic inherited
+ * permissions. But, luckily, neither nsIFile.moveTo nor nsIFile.copyTo preserve
+ * inherited permissions.
+ */
+function migrateFile(sourceFile, destDir) {
+ if (!sourceFile.exists()) {
+ return;
+ }
+
+ if (sourceFile.isDirectory()) {
+ LOG(
+ `UpdateServiceStub:migrateFile Aborting attempt to migrate ` +
+ `"${sourceFile.path}" because it is a directory.`
+ );
+ return;
+ }
+
+ // Create destination directory.
+ try {
+ // Pass an arbitrary value for permissions. Windows doesn't use octal
+ // permissions, so that value doesn't really do anything.
+ destDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0);
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ LOG(
+ `UpdateServiceStub:migrateFile Unable to create destination ` +
+ `directory "${destDir.path}": ${ex}`
+ );
+ }
+ }
+
+ try {
+ sourceFile.moveTo(destDir, null);
+ return;
+ } catch (ex) {}
+
+ try {
+ sourceFile.copyTo(destDir, null);
+ } catch (ex) {
+ LOG(
+ `UpdateServiceStub:migrateFile Failed to migrate file from ` +
+ `"${sourceFile.path}" to "${destDir.path}". Exception: ${ex}`
+ );
+ return;
+ }
+
+ try {
+ sourceFile.remove(false);
+ } catch (ex) {
+ LOG(
+ `UpdateServiceStub:migrateFile Successfully migrated file from ` +
+ `"${sourceFile.path}" to "${destDir.path}", but was unable to remove ` +
+ `the original. Exception: ${ex}`
+ );
+ }
+}
+
+/**
+ * If recurse is true, recurses through the directory's contents. Any empty
+ * directories are removed. Directories with remaining files are left behind.
+ *
+ * If recurse if false, we delete the directory passed as long as it is empty.
+ *
+ * All errors are silenced and not thrown.
+ *
+ * Returns true if the directory passed in was removed. Otherwise false.
+ */
+function cleanupDir(dir, recurse) {
+ let directoryEmpty = true;
+ try {
+ for (let file of dir.directoryEntries) {
+ if (!recurse) {
+ // If we aren't recursing, bail out after we find a single file. The
+ // directory isn't empty so we can't delete it, and we aren't going to
+ // clean out and remove any other directories.
+ return false;
+ }
+ if (file.isDirectory()) {
+ if (!cleanupDir(file, recurse)) {
+ directoryEmpty = false;
+ }
+ } else {
+ directoryEmpty = false;
+ }
+ }
+ } catch (ex) {
+ // If any of our nsIFile calls fail, just err on the side of caution and
+ // don't delete anything.
+ return false;
+ }
+
+ if (directoryEmpty) {
+ try {
+ dir.remove(false);
+ return true;
+ } catch (ex) {}
+ }
+ return false;
+}
+
+/**
+ * Logs a string to the error console.
+ * @param string
+ * The string to write to the error console.
+ */
+function LOG(string) {
+ if (lazy.gLogEnabled) {
+ dump("*** AUS:SVC " + string + "\n");
+ Services.console.logStringMessage("AUS:SVC " + string);
+ }
+}