/* -*- 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { UpdateLog: "resource://gre/modules/UpdateLog.sys.mjs", }); if (AppConstants.ENABLE_WEBDRIVER) { XPCOMUtils.defineLazyServiceGetter( lazy, "Marionette", "@mozilla.org/remote/marionette;1", "nsIMarionette" ); XPCOMUtils.defineLazyServiceGetter( lazy, "RemoteAgent", "@mozilla.org/remote/agent;1", "nsIRemoteAgent" ); } else { lazy.Marionette = { running: false }; lazy.RemoteAgent = { running: false }; } const DIR_UPDATES = "updates"; const FILE_UPDATE_STATUS = "update.status"; const KEY_UPDROOT = "UpdRootD"; const KEY_OLD_UPDROOT = "OldUpdRootD"; // 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_DISABLEDFORTESTING = "app.update.disabledForTesting"; 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 = ""; 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, []); } export class UpdateServiceStub { #initUpdatePromise; #initStubHasRun = false; /** * See nsIUpdateService.idl */ async init() { await this.#init(false); } async initUpdate() { await this.#init(true); } #initOnlyStub(updateDir) { // We may need to migrate update data let prefUpdateDirMigrated = PREF_PREFIX_UPDATE_DIR_MIGRATED + updateDir.leafName; 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}` ); } } } async #initUpdate() { // Ensure that the constructors for the update services have run. const aus = Cc["@mozilla.org/updates/update-service;1"].getService( Ci.nsIApplicationUpdateService ); await Cc["@mozilla.org/updates/update-manager;1"] .getService(Ci.nsIUpdateManager) .internal.reload(false); Cc["@mozilla.org/updates/update-checker;1"].getService(Ci.nsIUpdateChecker); // Run update service initialization await aus.internal.init(false); } async #init(force_update_init) { if (this.updateDisabled) { return; } // We call into this from many places to ensure that initialization is done, // so we want to optimize for the case where initialization is already // finished. if (this.#initUpdatePromise) { try { await this.#initUpdatePromise; } catch (ex) { // This will already have been logged when the error first happened, we // don't need to log it again now. } return; } LOG(`UpdateServiceStub - Begin (force_update_init=${force_update_init})`); let initUpdate = force_update_init; try { const updateDir = getUpdateBaseDirNoCreate(); if (!this.#initStubHasRun) { this.#initStubHasRun = true; try { this.#initOnlyStub(updateDir); } catch (ex) { LOG( `UpdateServiceStub - Stub initialization failed: ${ex}: ${ex.stack}` ); } } try { if (!initUpdate) { const statusFile = updateDir.clone(); statusFile.append(DIR_UPDATES); statusFile.append("0"); statusFile.append(FILE_UPDATE_STATUS); initUpdate = statusFile.exists(); } } catch (ex) { LOG( `UpdateServiceStub - Failed to generate the status file: ${ex}: ${ex.stack}` ); } } catch (e) { LOG( `UpdateServiceStub - Failed to get update directory: ${e}: ${e.stack}` ); } if (initUpdate) { this.#initUpdatePromise = this.#initUpdate(); try { await this.#initUpdatePromise; } catch (ex) { LOG(`UpdateServiceStub - Init failed: ${ex}: ${ex.stack}`); } } } /** * See nsIUpdateService.idl */ get updateDisabledForTesting() { return ( (Cu.isInAutomation || lazy.Marionette.running || lazy.RemoteAgent.running) && Services.prefs.getBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, false) ); } /** * See nsIUpdateService.idl */ get updateDisabled() { return ( (Services.policies && !Services.policies.isAllowed("appUpdate")) || this.updateDisabledForTesting || Services.sysinfo.getProperty("isPackagedApp") ); } async observe(_subject, topic, _data) { switch (topic) { // This is sort of the "default" way of being initialized. The // "@mozilla.org/updates/update-service-stub;1" contract definition in // `components.conf` registers us for this notification, which we use to // trigger initialization. Note, however, that this calls only `init` and // not `initUpdate`. case "profile-after-change": await this.init(); break; } } classID = Components.ID("{e43b0010-04ba-4da6-b523-1f92580bc150}"); QueryInterface = ChromeUtils.generateQI([ Ci.nsIApplicationUpdateServiceStub, Ci.nsIObserver, ]); } /** * 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, []); let destRootDir = FileUtils.getDir(KEY_UPDROOT, []); 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\" 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) { lazy.UpdateLog.logPrefixedString("AUS:STB", string); }