forked from mirrors/gecko-dev
		
	 e6346b7f19
			
		
	
	
		e6346b7f19
		
	
	
	
	
		
			
			* Timestamped log files are written to a `sessionstore-logs` directory in the profile directory * Errors will always get written to disk, the logOnSuccess pref ensures all logs are saved even if no error has been captured * The prefs configure how much logging we do. We have more verbose logging by default in nightly/early-beta, and less (errors, warnings) in release. * Logging statements have been added to the early phases of session restore. We'll add more I'm sure as we troubleshoot specific bugs. Differential Revision: https://phabricator.services.mozilla.com/D208816
		
			
				
	
	
		
			547 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			547 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| /**
 | |
|  * Implementation of all the disk I/O required by the session store.
 | |
|  * This is a private API, meant to be used only by the session store.
 | |
|  * It will change. Do not use it for any other purpose.
 | |
|  *
 | |
|  * Note that this module depends on SessionWriter and that it enqueues its I/O
 | |
|  * requests and never attempts to simultaneously execute two I/O requests on
 | |
|  * the files used by this module from two distinct threads.
 | |
|  * Otherwise, we could encounter bugs, especially under Windows,
 | |
|  * e.g. if a request attempts to write sessionstore.js while
 | |
|  * another attempts to copy that file.
 | |
|  */
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
 | |
|   RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
 | |
|   SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
 | |
|   SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs",
 | |
| });
 | |
| 
 | |
| const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
 | |
| const PREF_MAX_UPGRADE_BACKUPS =
 | |
|   "browser.sessionstore.upgradeBackup.maxUpgradeBackups";
 | |
| 
 | |
| const PREF_MAX_SERIALIZE_BACK = "browser.sessionstore.max_serialize_back";
 | |
| const PREF_MAX_SERIALIZE_FWD = "browser.sessionstore.max_serialize_forward";
 | |
| 
 | |
| export var SessionFile = {
 | |
|   /**
 | |
|    * Read the contents of the session file, asynchronously.
 | |
|    */
 | |
|   read() {
 | |
|     return SessionFileInternal.read();
 | |
|   },
 | |
|   /**
 | |
|    * Write the contents of the session file, asynchronously.
 | |
|    * @param aData - May get changed on shutdown.
 | |
|    */
 | |
|   write(aData) {
 | |
|     return SessionFileInternal.write(aData);
 | |
|   },
 | |
|   /**
 | |
|    * Wipe the contents of the session file, asynchronously.
 | |
|    */
 | |
|   wipe() {
 | |
|     return SessionFileInternal.wipe();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Return the paths to the files used to store, backup, etc.
 | |
|    * the state of the file.
 | |
|    */
 | |
|   get Paths() {
 | |
|     return SessionFileInternal.Paths;
 | |
|   },
 | |
| };
 | |
| 
 | |
| Object.freeze(SessionFile);
 | |
| 
 | |
| const profileDir = PathUtils.profileDir;
 | |
| 
 | |
| var SessionFileInternal = {
 | |
|   Paths: Object.freeze({
 | |
|     // The path to the latest version of sessionstore written during a clean
 | |
|     // shutdown. After startup, it is renamed `cleanBackup`.
 | |
|     clean: PathUtils.join(profileDir, "sessionstore.jsonlz4"),
 | |
| 
 | |
|     // The path at which we store the previous version of `clean`. Updated
 | |
|     // whenever we successfully load from `clean`.
 | |
|     cleanBackup: PathUtils.join(
 | |
|       profileDir,
 | |
|       "sessionstore-backups",
 | |
|       "previous.jsonlz4"
 | |
|     ),
 | |
| 
 | |
|     // The directory containing all sessionstore backups.
 | |
|     backups: PathUtils.join(profileDir, "sessionstore-backups"),
 | |
| 
 | |
|     // The path to the latest version of the sessionstore written
 | |
|     // during runtime. Generally, this file contains more
 | |
|     // privacy-sensitive information than |clean|, and this file is
 | |
|     // therefore removed during clean shutdown. This file is designed to protect
 | |
|     // against crashes / sudden shutdown.
 | |
|     recovery: PathUtils.join(
 | |
|       profileDir,
 | |
|       "sessionstore-backups",
 | |
|       "recovery.jsonlz4"
 | |
|     ),
 | |
| 
 | |
|     // The path to the previous version of the sessionstore written
 | |
|     // during runtime (e.g. 15 seconds before recovery). In case of a
 | |
|     // clean shutdown, this file is removed.  Generally, this file
 | |
|     // contains more privacy-sensitive information than |clean|, and
 | |
|     // this file is therefore removed during clean shutdown.  This
 | |
|     // file is designed to protect against crashes that are nasty
 | |
|     // enough to corrupt |recovery|.
 | |
|     recoveryBackup: PathUtils.join(
 | |
|       profileDir,
 | |
|       "sessionstore-backups",
 | |
|       "recovery.baklz4"
 | |
|     ),
 | |
| 
 | |
|     // The path to a backup created during an upgrade of Firefox.
 | |
|     // Having this backup protects the user essentially from bugs in
 | |
|     // Firefox or add-ons, especially for users of Nightly. This file
 | |
|     // does not contain any information more sensitive than |clean|.
 | |
|     upgradeBackupPrefix: PathUtils.join(
 | |
|       profileDir,
 | |
|       "sessionstore-backups",
 | |
|       "upgrade.jsonlz4-"
 | |
|     ),
 | |
| 
 | |
|     // The path to the backup of the version of the session store used
 | |
|     // during the latest upgrade of Firefox. During load/recovery,
 | |
|     // this file should be used if both |path|, |backupPath| and
 | |
|     // |latestStartPath| are absent/incorrect.  May be "" if no
 | |
|     // upgrade backup has ever been performed. This file does not
 | |
|     // contain any information more sensitive than |clean|.
 | |
|     get upgradeBackup() {
 | |
|       let latestBackupID = SessionFileInternal.latestUpgradeBackupID;
 | |
|       if (!latestBackupID) {
 | |
|         return "";
 | |
|       }
 | |
|       return this.upgradeBackupPrefix + latestBackupID;
 | |
|     },
 | |
| 
 | |
|     // The path to a backup created during an upgrade of Firefox.
 | |
|     // Having this backup protects the user essentially from bugs in
 | |
|     // Firefox, especially for users of Nightly.
 | |
|     get nextUpgradeBackup() {
 | |
|       return this.upgradeBackupPrefix + Services.appinfo.platformBuildID;
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * The order in which to search for a valid sessionstore file.
 | |
|      */
 | |
|     get loadOrder() {
 | |
|       // If `clean` exists and has been written without corruption during
 | |
|       // the latest shutdown, we need to use it.
 | |
|       //
 | |
|       // Otherwise, `recovery` and `recoveryBackup` represent the most
 | |
|       // recent state of the session store.
 | |
|       //
 | |
|       // Finally, if nothing works, fall back to the last known state
 | |
|       // that can be loaded (`cleanBackup`) or, if available, to the
 | |
|       // backup performed during the latest upgrade.
 | |
|       let order = ["clean", "recovery", "recoveryBackup", "cleanBackup"];
 | |
|       if (SessionFileInternal.latestUpgradeBackupID) {
 | |
|         // We have an upgradeBackup
 | |
|         order.push("upgradeBackup");
 | |
|       }
 | |
|       return order;
 | |
|     },
 | |
|   }),
 | |
| 
 | |
|   // Number of attempted calls to `write`.
 | |
|   // Note that we may have _attempts > _successes + _failures,
 | |
|   // if attempts never complete.
 | |
|   // Used for error reporting.
 | |
|   _attempts: 0,
 | |
| 
 | |
|   // Number of successful calls to `write`.
 | |
|   // Used for error reporting.
 | |
|   _successes: 0,
 | |
| 
 | |
|   // Number of failed calls to `write`.
 | |
|   // Used for error reporting.
 | |
|   _failures: 0,
 | |
| 
 | |
|   // `true` once we have initialized SessionWriter.
 | |
|   _initialized: false,
 | |
| 
 | |
|   // A string that will be set to the session file name part that was read from
 | |
|   // disk. It will be available _after_ a session file read() is done.
 | |
|   _readOrigin: null,
 | |
| 
 | |
|   // `true` if the old, uncompressed, file format was used to read from disk, as
 | |
|   // a fallback mechanism.
 | |
|   _usingOldExtension: false,
 | |
| 
 | |
|   // The ID of the latest version of Gecko for which we have an upgrade backup
 | |
|   // or |undefined| if no upgrade backup was ever written.
 | |
|   get latestUpgradeBackupID() {
 | |
|     try {
 | |
|       return Services.prefs.getCharPref(PREF_UPGRADE_BACKUP);
 | |
|     } catch (ex) {
 | |
|       return undefined;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   async _readInternal(useOldExtension) {
 | |
|     Services.telemetry.setEventRecordingEnabled("session_restore", true);
 | |
|     let result;
 | |
|     let noFilesFound = true;
 | |
|     this._usingOldExtension = useOldExtension;
 | |
| 
 | |
|     // Attempt to load by order of priority from the various backups
 | |
|     for (let key of this.Paths.loadOrder) {
 | |
|       let corrupted = false;
 | |
|       let exists = true;
 | |
|       try {
 | |
|         let path;
 | |
|         let startMs = Date.now();
 | |
| 
 | |
|         let options = {};
 | |
|         if (useOldExtension) {
 | |
|           path = this.Paths[key]
 | |
|             .replace("jsonlz4", "js")
 | |
|             .replace("baklz4", "bak");
 | |
|         } else {
 | |
|           path = this.Paths[key];
 | |
|           options.decompress = true;
 | |
|         }
 | |
|         let source = await IOUtils.readUTF8(path, options);
 | |
|         let parsed = JSON.parse(source);
 | |
| 
 | |
|         if (parsed._cachedObjs) {
 | |
|           try {
 | |
|             let cacheMap = new Map(parsed._cachedObjs);
 | |
|             for (let win of parsed.windows.concat(
 | |
|               parsed._closedWindows || []
 | |
|             )) {
 | |
|               for (let tab of win.tabs.concat(win._closedTabs || [])) {
 | |
|                 tab.image = cacheMap.get(tab.image) || tab.image;
 | |
|               }
 | |
|             }
 | |
|           } catch (e) {
 | |
|             // This is temporary code to clean up after the backout of bug
 | |
|             // 1546847. Just in case there are problems in the format of
 | |
|             // the parsed data, continue on. Favicons might be broken, but
 | |
|             // the session will at least be recovered
 | |
|             lazy.sessionStoreLogger.error(e);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (
 | |
|           !lazy.SessionStore.isFormatVersionCompatible(
 | |
|             parsed.version || [
 | |
|               "sessionrestore",
 | |
|               0,
 | |
|             ] /* fallback for old versions*/
 | |
|           )
 | |
|         ) {
 | |
|           // Skip sessionstore files that we don't understand.
 | |
|           lazy.sessionStoreLogger.warn(
 | |
|             "Cannot extract data from Session Restore file ",
 | |
|             path,
 | |
|             ". Wrong format/version: " + JSON.stringify(parsed.version) + "."
 | |
|           );
 | |
|           Services.telemetry.recordEvent(
 | |
|             "session_restore",
 | |
|             "backup_can_be_loaded",
 | |
|             "session_file",
 | |
|             null,
 | |
|             {
 | |
|               can_load: "false",
 | |
|               path_key: key,
 | |
|               loadfail_reason:
 | |
|                 "Wrong format/version: " + JSON.stringify(parsed.version) + ".",
 | |
|             }
 | |
|           );
 | |
|           continue;
 | |
|         }
 | |
|         result = {
 | |
|           origin: key,
 | |
|           source,
 | |
|           parsed,
 | |
|           useOldExtension,
 | |
|         };
 | |
|         Services.telemetry.recordEvent(
 | |
|           "session_restore",
 | |
|           "backup_can_be_loaded",
 | |
|           "session_file",
 | |
|           null,
 | |
|           {
 | |
|             can_load: "true",
 | |
|             path_key: key,
 | |
|             loadfail_reason: "N/A",
 | |
|           }
 | |
|         );
 | |
|         Services.telemetry
 | |
|           .getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE")
 | |
|           .add(false);
 | |
|         Services.telemetry
 | |
|           .getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS")
 | |
|           .add(Date.now() - startMs);
 | |
|         lazy.sessionStoreLogger.debug(`Successful file read of ${key} file`);
 | |
|         break;
 | |
|       } catch (ex) {
 | |
|         if (DOMException.isInstance(ex) && ex.name == "NotFoundError") {
 | |
|           exists = false;
 | |
|           Services.telemetry.recordEvent(
 | |
|             "session_restore",
 | |
|             "backup_can_be_loaded",
 | |
|             "session_file",
 | |
|             null,
 | |
|             {
 | |
|               can_load: "false",
 | |
|               path_key: key,
 | |
|               loadfail_reason: "File doesn't exist.",
 | |
|             }
 | |
|           );
 | |
|           // A file not existing can be normal and expected.
 | |
|           lazy.sessionStoreLogger.debug(
 | |
|             `Can't read session file which doesn't exist: ${key}`
 | |
|           );
 | |
|         } else if (
 | |
|           DOMException.isInstance(ex) &&
 | |
|           ex.name == "NotAllowedError"
 | |
|         ) {
 | |
|           // The file might be inaccessible due to wrong permissions
 | |
|           // or similar failures. We'll just count it as "corrupted".
 | |
|           lazy.sessionStoreLogger.error(
 | |
|             `NotAllowedError when reading session file: ${key}`,
 | |
|             ex
 | |
|           );
 | |
|           corrupted = true;
 | |
|           Services.telemetry.recordEvent(
 | |
|             "session_restore",
 | |
|             "backup_can_be_loaded",
 | |
|             "session_file",
 | |
|             null,
 | |
|             {
 | |
|               can_load: "false",
 | |
|               path_key: key,
 | |
|               loadfail_reason: ` ${ex.name}: Could not read session file`,
 | |
|             }
 | |
|           );
 | |
|         } else if (ex instanceof SyntaxError) {
 | |
|           lazy.sessionStoreLogger.error(
 | |
|             "Corrupt session file (invalid JSON found) ",
 | |
|             ex,
 | |
|             ex.stack
 | |
|           );
 | |
|           // File is corrupted, try next file
 | |
|           corrupted = true;
 | |
|           Services.telemetry.recordEvent(
 | |
|             "session_restore",
 | |
|             "backup_can_be_loaded",
 | |
|             "session_file",
 | |
|             null,
 | |
|             {
 | |
|               can_load: "false",
 | |
|               path_key: key,
 | |
|               loadfail_reason: ` ${ex.name}: Corrupt session file (invalid JSON found)`,
 | |
|             }
 | |
|           );
 | |
|         }
 | |
|       } finally {
 | |
|         if (exists) {
 | |
|           noFilesFound = false;
 | |
|           Services.telemetry
 | |
|             .getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE")
 | |
|             .add(corrupted);
 | |
|           Services.telemetry.recordEvent(
 | |
|             "session_restore",
 | |
|             "backup_can_be_loaded",
 | |
|             "session_file",
 | |
|             null,
 | |
|             {
 | |
|               can_load: (!corrupted).toString(),
 | |
|               path_key: key,
 | |
|               loadfail_reason: "N/A",
 | |
|             }
 | |
|           );
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     return { result, noFilesFound };
 | |
|   },
 | |
| 
 | |
|   // Find the correct session file and read it.
 | |
|   async read() {
 | |
|     // Load session files with lz4 compression.
 | |
|     let { result, noFilesFound } = await this._readInternal(false);
 | |
|     if (!result) {
 | |
|       // No result? Probably because of migration, let's
 | |
|       // load uncompressed session files.
 | |
|       let r = await this._readInternal(true);
 | |
|       result = r.result;
 | |
|     }
 | |
| 
 | |
|     // All files are corrupted if files found but none could deliver a result.
 | |
|     let allCorrupt = !noFilesFound && !result;
 | |
|     Services.telemetry
 | |
|       .getHistogramById("FX_SESSION_RESTORE_ALL_FILES_CORRUPT")
 | |
|       .add(allCorrupt);
 | |
| 
 | |
|     if (!result) {
 | |
|       // If everything fails, start with an empty session.
 | |
|       lazy.sessionStoreLogger.warn(
 | |
|         "No readable session files found to restore, starting with empty session"
 | |
|       );
 | |
|       result = {
 | |
|         origin: "empty",
 | |
|         source: "",
 | |
|         parsed: null,
 | |
|         useOldExtension: false,
 | |
|       };
 | |
|     }
 | |
|     this._readOrigin = result.origin;
 | |
| 
 | |
|     result.noFilesFound = noFilesFound;
 | |
| 
 | |
|     return result;
 | |
|   },
 | |
| 
 | |
|   // Initialize SessionWriter and return it as a resolved promise.
 | |
|   getWriter() {
 | |
|     if (!this._initialized) {
 | |
|       if (!this._readOrigin) {
 | |
|         return Promise.reject(
 | |
|           "SessionFileInternal.getWriter() called too early! Please read the session file from disk first."
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       this._initialized = true;
 | |
|       lazy.SessionWriter.init(
 | |
|         this._readOrigin,
 | |
|         this._usingOldExtension,
 | |
|         this.Paths,
 | |
|         {
 | |
|           maxUpgradeBackups: Services.prefs.getIntPref(
 | |
|             PREF_MAX_UPGRADE_BACKUPS,
 | |
|             3
 | |
|           ),
 | |
|           maxSerializeBack: Services.prefs.getIntPref(
 | |
|             PREF_MAX_SERIALIZE_BACK,
 | |
|             10
 | |
|           ),
 | |
|           maxSerializeForward: Services.prefs.getIntPref(
 | |
|             PREF_MAX_SERIALIZE_FWD,
 | |
|             -1
 | |
|           ),
 | |
|         }
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return Promise.resolve(lazy.SessionWriter);
 | |
|   },
 | |
| 
 | |
|   write(aData) {
 | |
|     if (lazy.RunState.isClosed) {
 | |
|       return Promise.reject(new Error("SessionFile is closed"));
 | |
|     }
 | |
| 
 | |
|     let isFinalWrite = false;
 | |
|     if (lazy.RunState.isClosing) {
 | |
|       // If shutdown has started, we will want to stop receiving
 | |
|       // write instructions.
 | |
|       isFinalWrite = true;
 | |
|       lazy.RunState.setClosed();
 | |
|     }
 | |
| 
 | |
|     let performShutdownCleanup =
 | |
|       isFinalWrite && !lazy.SessionStore.willAutoRestore;
 | |
| 
 | |
|     this._attempts++;
 | |
|     let options = { isFinalWrite, performShutdownCleanup };
 | |
|     let promise = this.getWriter().then(writer => writer.write(aData, options));
 | |
| 
 | |
|     // Wait until the write is done.
 | |
|     promise = promise.then(
 | |
|       msg => {
 | |
|         // Record how long the write took.
 | |
|         this._recordTelemetry(msg.telemetry);
 | |
|         this._successes++;
 | |
|         if (msg.result.upgradeBackup) {
 | |
|           // We have just completed a backup-on-upgrade, store the information
 | |
|           // in preferences.
 | |
|           Services.prefs.setCharPref(
 | |
|             PREF_UPGRADE_BACKUP,
 | |
|             Services.appinfo.platformBuildID
 | |
|           );
 | |
|         }
 | |
|       },
 | |
|       err => {
 | |
|         // Catch and report any errors.
 | |
|         console.error("Could not write session state file ", err, err.stack);
 | |
|         this._failures++;
 | |
|         // By not doing anything special here we ensure that |promise| cannot
 | |
|         // be rejected anymore. The shutdown/cleanup code at the end of the
 | |
|         // function will thus always be executed.
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     // Ensure that we can write sessionstore.js cleanly before the profile
 | |
|     // becomes unaccessible.
 | |
|     IOUtils.profileBeforeChange.addBlocker(
 | |
|       "SessionFile: Finish writing Session Restore data",
 | |
|       promise,
 | |
|       {
 | |
|         fetchState: () => ({
 | |
|           options,
 | |
|           attempts: this._attempts,
 | |
|           successes: this._successes,
 | |
|           failures: this._failures,
 | |
|         }),
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     // This code will always be executed because |promise| can't fail anymore.
 | |
|     // We ensured that by having a reject handler that reports the failure but
 | |
|     // doesn't forward the rejection.
 | |
|     return promise.then(() => {
 | |
|       // Remove the blocker, no matter if writing failed or not.
 | |
|       IOUtils.profileBeforeChange.removeBlocker(promise);
 | |
| 
 | |
|       if (isFinalWrite) {
 | |
|         Services.obs.notifyObservers(
 | |
|           null,
 | |
|           "sessionstore-final-state-write-complete"
 | |
|         );
 | |
|       }
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   async wipe() {
 | |
|     const writer = await this.getWriter();
 | |
|     await writer.wipe();
 | |
|     // After a wipe, we need to make sure to re-initialize upon the next read(),
 | |
|     // because the state variables as sent to the writer have changed.
 | |
|     this._initialized = false;
 | |
|   },
 | |
| 
 | |
|   _recordTelemetry(telemetry) {
 | |
|     for (let id of Object.keys(telemetry)) {
 | |
|       let value = telemetry[id];
 | |
|       let samples = [];
 | |
|       if (Array.isArray(value)) {
 | |
|         samples.push(...value);
 | |
|       } else {
 | |
|         samples.push(value);
 | |
|       }
 | |
|       let histogram = Services.telemetry.getHistogramById(id);
 | |
|       for (let sample of samples) {
 | |
|         histogram.add(sample);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| };
 |