gecko/browser/components/sessionstore/SessionFile.jsm

358 lines
12 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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["SessionFile"];
/**
* 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 implicitly depends on one of two things:
* 1. either the asynchronous file I/O system enqueues its requests
* and never attempts to simultaneously execute two I/O requests on
* the files used by this module from two distinct threads; or
* 2. the clients of this API are well-behaved and do not place
* concurrent requests to the files used by this module.
*
* 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.
*
* This implementation uses OS.File, which guarantees property 1.
*/
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RunState",
"resource:///modules/sessionstore/RunState.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
"resource://gre/modules/TelemetryStopwatch.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
"@mozilla.org/base/telemetry;1", "nsITelemetry");
XPCOMUtils.defineLazyServiceGetter(this, "sessionStartup",
"@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
XPCOMUtils.defineLazyModuleGetter(this, "SessionWorker",
"resource:///modules/sessionstore/SessionWorker.jsm");
const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
this.SessionFile = {
/**
* Read the contents of the session file, asynchronously.
*/
read: function () {
return SessionFileInternal.read();
},
/**
* Write the contents of the session file, asynchronously.
*/
write: function (aData) {
return SessionFileInternal.write(aData);
},
/**
* Gather telemetry statistics.
*
*
* Most of the work is done off the main thread but there is a main
* thread cost involved to send data to the worker thread. This method
* should therefore be called only when we know that it will not disrupt
* the user's experience, e.g. on idle-daily.
*
* @return {Promise}
* @promise {object} An object holding all the information to be submitted
* to Telemetry.
*/
gatherTelemetry: function(aData) {
return SessionFileInternal.gatherTelemetry(aData);
},
/**
* Wipe the contents of the session file, asynchronously.
*/
wipe: function () {
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);
let Path = OS.Path;
let profileDir = OS.Constants.Path.profileDir;
let SessionFileInternal = {
Paths: Object.freeze({
// The path to the latest version of sessionstore written during a clean
// shutdown. After startup, it is renamed `cleanBackup`.
clean: Path.join(profileDir, "sessionstore.js"),
// The path at which we store the previous version of `clean`. Updated
// whenever we successfully load from `clean`.
cleanBackup: Path.join(profileDir, "sessionstore-backups", "previous.js"),
// The directory containing all sessionstore backups.
backups: Path.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: Path.join(profileDir, "sessionstore-backups", "recovery.js"),
// 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: Path.join(profileDir, "sessionstore-backups", "recovery.bak"),
// 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: Path.join(profileDir, "sessionstore-backups", "upgrade.js-"),
// 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;
},
}),
// 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;
}
},
read: Task.async(function* () {
let result;
let noFilesFound = true;
// 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 = this.Paths[key];
let startMs = Date.now();
let source = yield OS.File.read(path, { encoding: "utf-8" });
let parsed = JSON.parse(source);
result = {
origin: key,
source: source,
parsed: parsed
};
Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").
add(false);
Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS").
add(Date.now() - startMs);
break;
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
exists = false;
} catch (ex if ex instanceof SyntaxError) {
// File is corrupted, try next file
corrupted = true;
} finally {
if (exists) {
noFilesFound = false;
Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").
add(corrupted);
}
}
}
// All files are corrupted if files found but none could deliver a result.
let allCorrupt = !noFilesFound && !result;
Telemetry.getHistogramById("FX_SESSION_RESTORE_ALL_FILES_CORRUPT").
add(allCorrupt);
if (!result) {
// If everything fails, start with an empty session.
result = {
origin: "empty",
source: "",
parsed: null
};
}
result.noFilesFound = noFilesFound;
// Initialize the worker to let it handle backups and also
// as a workaround for bug 964531.
SessionWorker.post("init", [
result.origin,
this.Paths,
]);
return result;
}),
gatherTelemetry: function(aStateString) {
return Task.spawn(function() {
let msg = yield SessionWorker.post("gatherTelemetry", [aStateString]);
this._recordTelemetry(msg.telemetry);
throw new Task.Result(msg.telemetry);
}.bind(this));
},
write: function (aData) {
if (RunState.isClosed) {
return Promise.reject(new Error("SessionFile is closed"));
}
let isFinalWrite = false;
if (RunState.isClosing) {
// If shutdown has started, we will want to stop receiving
// write instructions.
isFinalWrite = true;
RunState.setClosed();
}
let refObj = {};
let name = "FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS";
let promise = new Promise(resolve => {
// Start measuring main thread impact.
TelemetryStopwatch.start(name, refObj);
let performShutdownCleanup = isFinalWrite &&
!sessionStartup.isAutomaticRestoreEnabled();
let options = {isFinalWrite, performShutdownCleanup};
try {
resolve(SessionWorker.post("write", [aData, options]));
} finally {
// Record how long we stopped the main thread.
TelemetryStopwatch.finish(name, refObj);
}
});
// Wait until the write is done.
promise = promise.then(msg => {
// Record how long the write took.
this._recordTelemetry(msg.telemetry);
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.
TelemetryStopwatch.cancel(name, refObj);
console.error("Could not write session state file ", err, err.stack);
// 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.
AsyncShutdown.profileBeforeChange.addBlocker(
"SessionFile: Finish writing Session Restore data", promise);
// 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.
AsyncShutdown.profileBeforeChange.removeBlocker(promise);
if (isFinalWrite) {
Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", "");
}
});
},
wipe: function () {
return SessionWorker.post("wipe");
},
_recordTelemetry: function(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 = Telemetry.getHistogramById(id);
for (let sample of samples) {
histogram.add(sample);
}
}
}
};