1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * Implementation of all the disk I/O required by the session store.
7 * This is a private API, meant to be used only by the session store.
8 * It will change. Do not use it for any other purpose.
10 * Note that this module depends on SessionWriter and that it enqueues its I/O
11 * requests and never attempts to simultaneously execute two I/O requests on
12 * the files used by this module from two distinct threads.
13 * Otherwise, we could encounter bugs, especially under Windows,
14 * e.g. if a request attempts to write sessionstore.js while
15 * another attempts to copy that file.
20 ChromeUtils.defineESModuleGetters(lazy, {
21 RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
22 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
23 SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs",
26 const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
27 const PREF_MAX_UPGRADE_BACKUPS =
28 "browser.sessionstore.upgradeBackup.maxUpgradeBackups";
30 const PREF_MAX_SERIALIZE_BACK = "browser.sessionstore.max_serialize_back";
31 const PREF_MAX_SERIALIZE_FWD = "browser.sessionstore.max_serialize_forward";
33 export var SessionFile = {
35 * Read the contents of the session file, asynchronously.
38 return SessionFileInternal.read();
41 * Write the contents of the session file, asynchronously.
42 * @param aData - May get changed on shutdown.
45 return SessionFileInternal.write(aData);
48 * Wipe the contents of the session file, asynchronously.
51 return SessionFileInternal.wipe();
55 * Return the paths to the files used to store, backup, etc.
56 * the state of the file.
59 return SessionFileInternal.Paths;
63 Object.freeze(SessionFile);
65 const profileDir = PathUtils.profileDir;
67 var SessionFileInternal = {
68 Paths: Object.freeze({
69 // The path to the latest version of sessionstore written during a clean
70 // shutdown. After startup, it is renamed `cleanBackup`.
71 clean: PathUtils.join(profileDir, "sessionstore.jsonlz4"),
73 // The path at which we store the previous version of `clean`. Updated
74 // whenever we successfully load from `clean`.
75 cleanBackup: PathUtils.join(
77 "sessionstore-backups",
81 // The directory containing all sessionstore backups.
82 backups: PathUtils.join(profileDir, "sessionstore-backups"),
84 // The path to the latest version of the sessionstore written
85 // during runtime. Generally, this file contains more
86 // privacy-sensitive information than |clean|, and this file is
87 // therefore removed during clean shutdown. This file is designed to protect
88 // against crashes / sudden shutdown.
89 recovery: PathUtils.join(
91 "sessionstore-backups",
95 // The path to the previous version of the sessionstore written
96 // during runtime (e.g. 15 seconds before recovery). In case of a
97 // clean shutdown, this file is removed. Generally, this file
98 // contains more privacy-sensitive information than |clean|, and
99 // this file is therefore removed during clean shutdown. This
100 // file is designed to protect against crashes that are nasty
101 // enough to corrupt |recovery|.
102 recoveryBackup: PathUtils.join(
104 "sessionstore-backups",
108 // The path to a backup created during an upgrade of Firefox.
109 // Having this backup protects the user essentially from bugs in
110 // Firefox or add-ons, especially for users of Nightly. This file
111 // does not contain any information more sensitive than |clean|.
112 upgradeBackupPrefix: PathUtils.join(
114 "sessionstore-backups",
118 // The path to the backup of the version of the session store used
119 // during the latest upgrade of Firefox. During load/recovery,
120 // this file should be used if both |path|, |backupPath| and
121 // |latestStartPath| are absent/incorrect. May be "" if no
122 // upgrade backup has ever been performed. This file does not
123 // contain any information more sensitive than |clean|.
124 get upgradeBackup() {
125 let latestBackupID = SessionFileInternal.latestUpgradeBackupID;
126 if (!latestBackupID) {
129 return this.upgradeBackupPrefix + latestBackupID;
132 // The path to a backup created during an upgrade of Firefox.
133 // Having this backup protects the user essentially from bugs in
134 // Firefox, especially for users of Nightly.
135 get nextUpgradeBackup() {
136 return this.upgradeBackupPrefix + Services.appinfo.platformBuildID;
140 * The order in which to search for a valid sessionstore file.
143 // If `clean` exists and has been written without corruption during
144 // the latest shutdown, we need to use it.
146 // Otherwise, `recovery` and `recoveryBackup` represent the most
147 // recent state of the session store.
149 // Finally, if nothing works, fall back to the last known state
150 // that can be loaded (`cleanBackup`) or, if available, to the
151 // backup performed during the latest upgrade.
152 let order = ["clean", "recovery", "recoveryBackup", "cleanBackup"];
153 if (SessionFileInternal.latestUpgradeBackupID) {
154 // We have an upgradeBackup
155 order.push("upgradeBackup");
161 // Number of attempted calls to `write`.
162 // Note that we may have _attempts > _successes + _failures,
163 // if attempts never complete.
164 // Used for error reporting.
167 // Number of successful calls to `write`.
168 // Used for error reporting.
171 // Number of failed calls to `write`.
172 // Used for error reporting.
175 // `true` once we have initialized SessionWriter.
178 // A string that will be set to the session file name part that was read from
179 // disk. It will be available _after_ a session file read() is done.
182 // `true` if the old, uncompressed, file format was used to read from disk, as
183 // a fallback mechanism.
184 _usingOldExtension: false,
186 // The ID of the latest version of Gecko for which we have an upgrade backup
187 // or |undefined| if no upgrade backup was ever written.
188 get latestUpgradeBackupID() {
190 return Services.prefs.getCharPref(PREF_UPGRADE_BACKUP);
196 async _readInternal(useOldExtension) {
198 let noFilesFound = true;
199 this._usingOldExtension = useOldExtension;
201 // Attempt to load by order of priority from the various backups
202 for (let key of this.Paths.loadOrder) {
203 let corrupted = false;
207 let startMs = Date.now();
210 if (useOldExtension) {
211 path = this.Paths[key]
212 .replace("jsonlz4", "js")
213 .replace("baklz4", "bak");
215 path = this.Paths[key];
216 options.decompress = true;
218 let source = await IOUtils.readUTF8(path, options);
219 let parsed = JSON.parse(source);
221 if (parsed._cachedObjs) {
223 let cacheMap = new Map(parsed._cachedObjs);
224 for (let win of parsed.windows.concat(
225 parsed._closedWindows || []
227 for (let tab of win.tabs.concat(win._closedTabs || [])) {
228 tab.image = cacheMap.get(tab.image) || tab.image;
232 // This is temporary code to clean up after the backout of bug
233 // 1546847. Just in case there are problems in the format of
234 // the parsed data, continue on. Favicons might be broken, but
235 // the session will at least be recovered
241 !lazy.SessionStore.isFormatVersionCompatible(
245 ] /* fallback for old versions*/
248 // Skip sessionstore files that we don't understand.
250 "Cannot extract data from Session Restore file ",
252 ". Wrong format/version: " + JSON.stringify(parsed.version) + "."
263 .getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE")
266 .getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS")
267 .add(Date.now() - startMs);
270 if (DOMException.isInstance(ex) && ex.name == "NotFoundError") {
273 DOMException.isInstance(ex) &&
274 ex.name == "NotAllowedError"
276 // The file might be inaccessible due to wrong permissions
277 // or similar failures. We'll just count it as "corrupted".
278 console.error("Could not read session file ", ex);
280 } else if (ex instanceof SyntaxError) {
282 "Corrupt session file (invalid JSON found) ",
286 // File is corrupted, try next file
291 noFilesFound = false;
293 .getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE")
298 return { result, noFilesFound };
301 // Find the correct session file and read it.
303 // Load session files with lz4 compression.
304 let { result, noFilesFound } = await this._readInternal(false);
306 // No result? Probably because of migration, let's
307 // load uncompressed session files.
308 let r = await this._readInternal(true);
312 // All files are corrupted if files found but none could deliver a result.
313 let allCorrupt = !noFilesFound && !result;
315 .getHistogramById("FX_SESSION_RESTORE_ALL_FILES_CORRUPT")
319 // If everything fails, start with an empty session.
324 useOldExtension: false,
327 this._readOrigin = result.origin;
329 result.noFilesFound = noFilesFound;
334 // Initialize SessionWriter and return it as a resolved promise.
336 if (!this._initialized) {
337 if (!this._readOrigin) {
338 return Promise.reject(
339 "SessionFileInternal.getWriter() called too early! Please read the session file from disk first."
343 this._initialized = true;
344 lazy.SessionWriter.init(
346 this._usingOldExtension,
349 maxUpgradeBackups: Services.prefs.getIntPref(
350 PREF_MAX_UPGRADE_BACKUPS,
353 maxSerializeBack: Services.prefs.getIntPref(
354 PREF_MAX_SERIALIZE_BACK,
357 maxSerializeForward: Services.prefs.getIntPref(
358 PREF_MAX_SERIALIZE_FWD,
365 return Promise.resolve(lazy.SessionWriter);
369 if (lazy.RunState.isClosed) {
370 return Promise.reject(new Error("SessionFile is closed"));
373 let isFinalWrite = false;
374 if (lazy.RunState.isClosing) {
375 // If shutdown has started, we will want to stop receiving
376 // write instructions.
378 lazy.RunState.setClosed();
381 let performShutdownCleanup =
382 isFinalWrite && !lazy.SessionStore.willAutoRestore;
385 let options = { isFinalWrite, performShutdownCleanup };
386 let promise = this.getWriter().then(writer => writer.write(aData, options));
388 // Wait until the write is done.
389 promise = promise.then(
391 // Record how long the write took.
392 this._recordTelemetry(msg.telemetry);
394 if (msg.result.upgradeBackup) {
395 // We have just completed a backup-on-upgrade, store the information
397 Services.prefs.setCharPref(
399 Services.appinfo.platformBuildID
404 // Catch and report any errors.
405 console.error("Could not write session state file ", err, err.stack);
407 // By not doing anything special here we ensure that |promise| cannot
408 // be rejected anymore. The shutdown/cleanup code at the end of the
409 // function will thus always be executed.
413 // Ensure that we can write sessionstore.js cleanly before the profile
414 // becomes unaccessible.
415 IOUtils.profileBeforeChange.addBlocker(
416 "SessionFile: Finish writing Session Restore data",
421 attempts: this._attempts,
422 successes: this._successes,
423 failures: this._failures,
428 // This code will always be executed because |promise| can't fail anymore.
429 // We ensured that by having a reject handler that reports the failure but
430 // doesn't forward the rejection.
431 return promise.then(() => {
432 // Remove the blocker, no matter if writing failed or not.
433 IOUtils.profileBeforeChange.removeBlocker(promise);
436 Services.obs.notifyObservers(
438 "sessionstore-final-state-write-complete"
445 const writer = await this.getWriter();
447 // After a wipe, we need to make sure to re-initialize upon the next read(),
448 // because the state variables as sent to the writer have changed.
449 this._initialized = false;
452 _recordTelemetry(telemetry) {
453 for (let id of Object.keys(telemetry)) {
454 let value = telemetry[id];
456 if (Array.isArray(value)) {
457 samples.push(...value);
461 let histogram = Services.telemetry.getHistogramById(id);
462 for (let sample of samples) {
463 histogram.add(sample);