mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-14 02:31:59 +00:00
Bug 532150 - Reading and writing session file off the main thread; r=felipe
--HG-- extra : rebase_source : f9ceb62680c932621b876a39f007e74a4f4e4c40
This commit is contained in:
parent
d0e5e16471
commit
8704499fcc
@ -10,9 +10,15 @@
|
||||
* - and allows to restore everything into one window.
|
||||
*/
|
||||
|
||||
[scriptable, uuid(170c6857-7f71-46ce-bc9b-185723b1c3a8)]
|
||||
[scriptable, uuid(35235b39-7098-4b3b-8e28-cd004a88b06f)]
|
||||
interface nsISessionStartup: nsISupports
|
||||
{
|
||||
/**
|
||||
* Return a promise that is resolved once initialization
|
||||
* is complete.
|
||||
*/
|
||||
readonly attribute jsval onceInitialized;
|
||||
|
||||
// Get session state
|
||||
readonly attribute jsval state;
|
||||
|
||||
|
@ -12,8 +12,8 @@ include $(topsrcdir)/config/config.mk
|
||||
|
||||
EXTRA_COMPONENTS = \
|
||||
nsSessionStore.manifest \
|
||||
nsSessionStore.js \
|
||||
nsSessionStartup.js \
|
||||
nsSessionStore.js \
|
||||
nsSessionStartup.js \
|
||||
$(NULL)
|
||||
|
||||
JS_MODULES_PATH := $(FINAL_TARGET)/modules/sessionstore
|
||||
@ -22,6 +22,7 @@ EXTRA_JS_MODULES := \
|
||||
DocumentUtils.jsm \
|
||||
SessionStorage.jsm \
|
||||
XPathGenerator.jsm \
|
||||
_SessionFile.jsm \
|
||||
$(NULL)
|
||||
|
||||
EXTRA_PP_JS_MODULES := \
|
||||
|
@ -75,14 +75,18 @@ const TAB_EVENTS = [
|
||||
#define BROKEN_WM_Z_ORDER
|
||||
#endif
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm", this);
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
||||
// debug.js adds NS_ASSERT. cf. bug 669196
|
||||
Cu.import("resource://gre/modules/debug.js");
|
||||
Cu.import("resource:///modules/TelemetryTimestamps.jsm");
|
||||
Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
Cu.import("resource://gre/modules/debug.js", this);
|
||||
Cu.import("resource:///modules/TelemetryTimestamps.jsm", this);
|
||||
Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
|
||||
Cu.import("resource://gre/modules/osfile.jsm", this);
|
||||
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this);
|
||||
Cu.import("resource://gre/modules/commonjs/promise/core.js", this);
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
|
||||
"@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
"resource://gre/modules/NetUtil.jsm");
|
||||
@ -92,6 +96,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils",
|
||||
"resource:///modules/sessionstore/DocumentUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
|
||||
"resource:///modules/sessionstore/SessionStorage.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
|
||||
"resource:///modules/sessionstore/_SessionFile.jsm");
|
||||
|
||||
#ifdef MOZ_CRASHREPORTER
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
|
||||
@ -290,8 +296,11 @@ let SessionStoreInternal = {
|
||||
// session
|
||||
_lastSessionState: null,
|
||||
|
||||
// Whether we've been initialized
|
||||
_initialized: false,
|
||||
// A promise resolved once initialization is complete
|
||||
_promiseInitialization: Promise.defer(),
|
||||
|
||||
// Whether session has been initialized
|
||||
_sessionInitialized: false,
|
||||
|
||||
// The original "sessionstore.resume_session_once" preference value before it
|
||||
// was modified by saveState. saveState will set the
|
||||
@ -326,6 +335,9 @@ let SessionStoreInternal = {
|
||||
* Initialize the component
|
||||
*/
|
||||
initService: function ssi_initService() {
|
||||
if (this._sessionInitialized) {
|
||||
return;
|
||||
}
|
||||
TelemetryTimestamps.add("sessionRestoreInitialized");
|
||||
OBSERVING.forEach(function(aTopic) {
|
||||
Services.obs.addObserver(this, aTopic, true);
|
||||
@ -358,15 +370,13 @@ let SessionStoreInternal = {
|
||||
this._prefBranch.getBoolPref("sessionstore.restore_pinned_tabs_on_demand");
|
||||
this._prefBranch.addObserver("sessionstore.restore_pinned_tabs_on_demand", this, true);
|
||||
|
||||
// get file references
|
||||
this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
|
||||
this._sessionFileBackup = this._sessionFile.clone();
|
||||
this._sessionFile.append("sessionstore.js");
|
||||
this._sessionFileBackup.append("sessionstore.bak");
|
||||
gSessionStartup.onceInitialized.then(
|
||||
this.initSession.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
// get string containing session state
|
||||
var ss = Cc["@mozilla.org/browser/sessionstartup;1"].
|
||||
getService(Ci.nsISessionStartup);
|
||||
initSession: function ssi_initSession() {
|
||||
let ss = gSessionStartup;
|
||||
try {
|
||||
if (ss.doRestore() ||
|
||||
ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION)
|
||||
@ -437,14 +447,11 @@ let SessionStoreInternal = {
|
||||
}
|
||||
|
||||
if (this._resume_from_crash) {
|
||||
// create a backup if the session data file exists
|
||||
try {
|
||||
if (this._sessionFileBackup.exists())
|
||||
this._sessionFileBackup.remove(false);
|
||||
if (this._sessionFile.exists())
|
||||
this._sessionFile.copyTo(null, this._sessionFileBackup.leafName);
|
||||
}
|
||||
catch (ex) { Cu.reportError(ex); } // file was write-locked?
|
||||
// Launch background copy of the session file. Note that we do
|
||||
// not have race conditions here as _SessionFile guarantees
|
||||
// that any I/O operation is completed before proceeding to
|
||||
// the next I/O operation.
|
||||
_SessionFile.createBackupCopy();
|
||||
}
|
||||
|
||||
// at this point, we've as good as resumed the session, so we can
|
||||
@ -455,7 +462,9 @@ let SessionStoreInternal = {
|
||||
|
||||
this._initEncoding();
|
||||
|
||||
this._initialized = true;
|
||||
// Session is ready.
|
||||
this._sessionInitialized = true;
|
||||
this._promiseInitialization.resolve();
|
||||
},
|
||||
|
||||
_initEncoding : function ssi_initEncoding() {
|
||||
@ -495,16 +504,7 @@ let SessionStoreInternal = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Start tracking a window.
|
||||
* This function also initializes the component if it's not already
|
||||
* initialized.
|
||||
*/
|
||||
init: function ssi_init(aWindow) {
|
||||
// Initialize the service if needed.
|
||||
if (!this._initialized)
|
||||
this.initService();
|
||||
|
||||
_initWindow: function ssi_initWindow(aWindow) {
|
||||
if (!aWindow || this._loadState == STATE_RUNNING) {
|
||||
// make sure that all browser windows which try to initialize
|
||||
// SessionStore are really tracked by it
|
||||
@ -524,13 +524,30 @@ let SessionStoreInternal = {
|
||||
this.onLoad(aWindow);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start tracking a window.
|
||||
*
|
||||
* This function also initializes the component if it is not
|
||||
* initialized yet.
|
||||
*/
|
||||
init: function ssi_init(aWindow) {
|
||||
let self = this;
|
||||
this.initService();
|
||||
this._promiseInitialization.promise.then(
|
||||
function onSuccess() {
|
||||
self._initWindow(aWindow);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called on application shutdown, after notifications:
|
||||
* quit-application-granted, quit-application
|
||||
*/
|
||||
_uninit: function ssi_uninit() {
|
||||
// save all data for session resuming
|
||||
this.saveState(true);
|
||||
if (this._sessionInitialized)
|
||||
this.saveState(true);
|
||||
|
||||
// clear out _tabsToRestore in case it's still holding refs
|
||||
this._tabsToRestore.priority = null;
|
||||
@ -998,7 +1015,7 @@ let SessionStoreInternal = {
|
||||
*/
|
||||
onPurgeSessionHistory: function ssi_onPurgeSessionHistory() {
|
||||
var _this = this;
|
||||
this._clearDisk();
|
||||
_SessionFile.wipe();
|
||||
// If the browser is shutting down, simply return after clearing the
|
||||
// session data on disk as this notification fires after the
|
||||
// quit-application notification so the browser is about to exit.
|
||||
@ -1139,7 +1156,7 @@ let SessionStoreInternal = {
|
||||
// either create the file with crash recovery information or remove it
|
||||
// (when _loadState is not STATE_RUNNING, that file is used for session resuming instead)
|
||||
if (!this._resume_from_crash)
|
||||
this._clearDisk();
|
||||
_SessionFile.wipe();
|
||||
this.saveState(true);
|
||||
break;
|
||||
case "sessionstore.restore_on_demand":
|
||||
@ -3767,40 +3784,37 @@ let SessionStoreInternal = {
|
||||
*/
|
||||
_saveStateObject: function ssi_saveStateObject(aStateObj) {
|
||||
TelemetryStopwatch.start("FX_SESSION_RESTORE_SERIALIZE_DATA_MS");
|
||||
var stateString = Cc["@mozilla.org/supports-string;1"].
|
||||
createInstance(Ci.nsISupportsString);
|
||||
stateString.data = this._toJSONString(aStateObj);
|
||||
let data = this._toJSONString(aStateObj);
|
||||
TelemetryStopwatch.finish("FX_SESSION_RESTORE_SERIALIZE_DATA_MS");
|
||||
|
||||
let stateString = this._createSupportsString(data);
|
||||
Services.obs.notifyObservers(stateString, "sessionstore-state-write", "");
|
||||
data = stateString.data;
|
||||
|
||||
// don't touch the file if an observer has deleted all state data
|
||||
if (stateString.data)
|
||||
this._writeFile(this._sessionFile, stateString.data);
|
||||
|
||||
this._lastSaveTime = Date.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* delete session datafile and backup
|
||||
*/
|
||||
_clearDisk: function ssi_clearDisk() {
|
||||
if (this._sessionFile.exists()) {
|
||||
try {
|
||||
this._sessionFile.remove(false);
|
||||
}
|
||||
catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
|
||||
// Don't touch the file if an observer has deleted all state data.
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (this._sessionFileBackup.exists()) {
|
||||
try {
|
||||
this._sessionFileBackup.remove(false);
|
||||
|
||||
let self = this;
|
||||
_SessionFile.write(data).then(
|
||||
function onSuccess() {
|
||||
self._lastSaveTime = Date.now();
|
||||
Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
|
||||
}
|
||||
catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/* ........ Auxiliary Functions .............. */
|
||||
|
||||
// Wrap a string as a nsISupports
|
||||
_createSupportsString: function ssi_createSupportsString(aData) {
|
||||
let string = Cc["@mozilla.org/supports-string;1"]
|
||||
.createInstance(Ci.nsISupportsString);
|
||||
string.data = aData;
|
||||
return string;
|
||||
},
|
||||
|
||||
/**
|
||||
* call a callback for all currently opened browser windows
|
||||
* (might miss the most recent one)
|
||||
@ -4494,33 +4508,6 @@ let SessionStoreInternal = {
|
||||
removeSHistoryListener(browser.__SS_shistoryListener);
|
||||
delete browser.__SS_shistoryListener;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* write file to disk
|
||||
* @param aFile
|
||||
* nsIFile
|
||||
* @param aData
|
||||
* String data
|
||||
*/
|
||||
_writeFile: function ssi_writeFile(aFile, aData) {
|
||||
let refObj = {};
|
||||
TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
||||
let path = aFile.path;
|
||||
let encoded = this._writeFileEncoder.encode(aData);
|
||||
let promise = OS.File.writeAtomic(path, encoded, {tmpPath: path + ".tmp"});
|
||||
|
||||
promise.then(
|
||||
function onSuccess() {
|
||||
TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
||||
Services.obs.notifyObservers(null,
|
||||
"sessionstore-state-write-complete",
|
||||
"");
|
||||
},
|
||||
function onFailure(reason) {
|
||||
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
||||
Components.reportError("ssi_writeFile failure " + reason);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
255
browser/components/sessionstore/src/_SessionFile.jsm
Normal file
255
browser/components/sessionstore/src/_SessionFile.jsm
Normal file
@ -0,0 +1,255 @@
|
||||
/* 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;
|
||||
|
||||
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/commonjs/promise/core.js");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
|
||||
"resource://gre/modules/TelemetryStopwatch.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
"resource://gre/modules/NetUtil.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
// An encoder to UTF-8.
|
||||
XPCOMUtils.defineLazyGetter(this, "gEncoder", function () {
|
||||
return new TextEncoder();
|
||||
});
|
||||
|
||||
this._SessionFile = {
|
||||
/**
|
||||
* A promise fulfilled once initialization (either synchronous or
|
||||
* asynchronous) is complete.
|
||||
*/
|
||||
promiseInitialized: function SessionFile_initialized() {
|
||||
return SessionFileInternal.promiseInitialized;
|
||||
},
|
||||
/**
|
||||
* Read the contents of the session file, asynchronously.
|
||||
*/
|
||||
read: function SessionFile_read() {
|
||||
return SessionFileInternal.read();
|
||||
},
|
||||
/**
|
||||
* Read the contents of the session file, synchronously.
|
||||
*/
|
||||
syncRead: function SessionFile_syncRead() {
|
||||
return SessionFileInternal.syncRead();
|
||||
},
|
||||
/**
|
||||
* Write the contents of the session file, asynchronously.
|
||||
*/
|
||||
write: function SessionFile_write(aData) {
|
||||
return SessionFileInternal.write(aData);
|
||||
},
|
||||
/**
|
||||
* Create a backup copy, asynchronously.
|
||||
*/
|
||||
createBackupCopy: function SessionFile_createBackupCopy() {
|
||||
return SessionFileInternal.createBackupCopy();
|
||||
},
|
||||
/**
|
||||
* Wipe the contents of the session file, asynchronously.
|
||||
*/
|
||||
wipe: function SessionFile_wipe() {
|
||||
return SessionFileInternal.wipe();
|
||||
}
|
||||
};
|
||||
|
||||
Object.freeze(_SessionFile);
|
||||
|
||||
/**
|
||||
* Utilities for dealing with promises and Task.jsm
|
||||
*/
|
||||
const TaskUtils = {
|
||||
/**
|
||||
* Add logging to a promise.
|
||||
*
|
||||
* @param {Promise} promise
|
||||
* @return {Promise} A promise behaving as |promise|, but with additional
|
||||
* logging in case of uncaught error.
|
||||
*/
|
||||
captureErrors: function captureErrors(promise) {
|
||||
return promise.then(
|
||||
null,
|
||||
function onError(reason) {
|
||||
Cu.reportError("Uncaught asynchronous error: " + reason + " at\n" + reason.stack);
|
||||
throw reason;
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Spawn a new Task from a generator.
|
||||
*
|
||||
* This function behaves as |Task.spawn|, with the exception that it
|
||||
* adds logging in case of uncaught error. For more information, see
|
||||
* the documentation of |Task.jsm|.
|
||||
*
|
||||
* @param {generator} gen Some generator.
|
||||
* @return {Promise} A promise built from |gen|, with the same semantics
|
||||
* as |Task.spawn(gen)|.
|
||||
*/
|
||||
spawn: function spawn(gen) {
|
||||
return this.captureErrors(Task.spawn(gen));
|
||||
}
|
||||
};
|
||||
|
||||
let SessionFileInternal = {
|
||||
/**
|
||||
* A promise fulfilled once initialization is complete
|
||||
*/
|
||||
promiseInitialized: Promise.defer(),
|
||||
|
||||
/**
|
||||
* The path to sessionstore.js
|
||||
*/
|
||||
path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
|
||||
|
||||
/**
|
||||
* The path to sessionstore.bak
|
||||
*/
|
||||
backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
|
||||
|
||||
/**
|
||||
* Read the sessionstore file synchronously.
|
||||
*
|
||||
* This function is meant to serve as a fallback in case of race
|
||||
* between a synchronous usage of the API and asynchronous
|
||||
* initialization.
|
||||
*/
|
||||
syncRead: function ssfi_syncRead() {
|
||||
let text;
|
||||
let exn;
|
||||
TelemetryStopwatch.start("FX_SESSION_RESTORE_SYNC_READ_FILE_MS");
|
||||
try {
|
||||
let file = new FileUtils.File(this.path);
|
||||
if (!file.exists()) {
|
||||
return null;
|
||||
}
|
||||
let chan = NetUtil.newChannel(file);
|
||||
let stream = chan.open();
|
||||
text = NetUtil.readInputStreamToString(stream, stream.available(), {charset: "utf-8"});
|
||||
} catch(ex) {
|
||||
exn = ex;
|
||||
} finally {
|
||||
TelemetryStopwatch.finish("FX_SESSION_RESTORE_SYNC_READ_FILE_MS");
|
||||
}
|
||||
if (exn) {
|
||||
Cu.reportError(exn);
|
||||
return "";
|
||||
}
|
||||
return text;
|
||||
},
|
||||
|
||||
read: function ssfi_read() {
|
||||
let refObj = {};
|
||||
let self = this;
|
||||
return TaskUtils.spawn(function task() {
|
||||
TelemetryStopwatch.start("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
|
||||
let text;
|
||||
try {
|
||||
let bytes = yield OS.File.read(self.path);
|
||||
text = new TextDecoder().decode(bytes);
|
||||
TelemetryStopwatch.finish("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
|
||||
} catch (ex) {
|
||||
if (self._isNoSuchFile(ex)) {
|
||||
// The file does not exist, this is perfectly valid
|
||||
TelemetryStopwatch.finish("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
|
||||
} else {
|
||||
// Any other error: let's not measure with telemetry
|
||||
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
text = "";
|
||||
}
|
||||
throw new Task.Result(text);
|
||||
});
|
||||
},
|
||||
|
||||
write: function ssfi_write(aData) {
|
||||
let refObj = {};
|
||||
let self = this;
|
||||
return TaskUtils.spawn(function task() {
|
||||
TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
||||
|
||||
let bytes = gEncoder.encode(aData);
|
||||
|
||||
try {
|
||||
yield OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"});
|
||||
TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
||||
} catch (ex) {
|
||||
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
||||
Cu.reportError("Could not write session state file " + self.path
|
||||
+ ": " + aReason);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createBackupCopy: function ssfi_createBackupCopy() {
|
||||
let self = this;
|
||||
return TaskUtils.spawn(function task() {
|
||||
try {
|
||||
yield OS.File.copy(self.path, self.backupPath);
|
||||
} catch (ex) {
|
||||
if (!self._isNoSuchFile(ex)) {
|
||||
Cu.reportError("Could not backup session state file: " + ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
wipe: function ssfi_wipe() {
|
||||
let self = this;
|
||||
return TaskUtils.spawn(function task() {
|
||||
try {
|
||||
yield OS.File.remove(self.path);
|
||||
} catch (ex) {
|
||||
Cu.reportError("Could not remove session state file: " + ex);
|
||||
throw ex;
|
||||
}
|
||||
try {
|
||||
yield OS.File.remove(self.backupPath);
|
||||
} catch (ex) {
|
||||
Cu.reportError("Could not remove session state backup file: " + ex);
|
||||
throw ex;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_isNoSuchFile: function ssfi_isNoSuchFile(aReason) {
|
||||
return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile;
|
||||
}
|
||||
};
|
@ -39,15 +39,20 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
|
||||
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
Cu.import("resource://gre/modules/commonjs/promise/core.js");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
|
||||
"resource:///modules/sessionstore/_SessionFile.jsm");
|
||||
|
||||
const STATE_RUNNING_STR = "running";
|
||||
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 megabytes
|
||||
|
||||
function debug(aMsg) {
|
||||
aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n");
|
||||
Services.console.logStringMessage(aMsg);
|
||||
}
|
||||
|
||||
let gOnceInitializedDeferred = Promise.defer();
|
||||
|
||||
/* :::::::: The Service ::::::::::::::: */
|
||||
|
||||
function SessionStartup() {
|
||||
@ -58,6 +63,7 @@ SessionStartup.prototype = {
|
||||
// the state to restore at startup
|
||||
_initialState: null,
|
||||
_sessionType: Ci.nsISessionStartup.NO_SESSION,
|
||||
_initialized: false,
|
||||
|
||||
/* ........ Global Event Handlers .............. */
|
||||
|
||||
@ -65,95 +71,121 @@ SessionStartup.prototype = {
|
||||
* Initialize the component
|
||||
*/
|
||||
init: function sss_init() {
|
||||
debug("init starting");
|
||||
// do not need to initialize anything in auto-started private browsing sessions
|
||||
let pbs = Cc["@mozilla.org/privatebrowsing;1"].
|
||||
getService(Ci.nsIPrivateBrowsingService);
|
||||
if (PrivateBrowsingUtils.permanentPrivateBrowsing ||
|
||||
pbs.lastChangedByCommandLine)
|
||||
return;
|
||||
// Session state is unknown until we read the file.
|
||||
this._sessionType = null;
|
||||
_SessionFile.read().then(
|
||||
this._onSessionFileRead.bind(this)
|
||||
);
|
||||
debug("init launched");
|
||||
},
|
||||
|
||||
let prefBranch = Cc["@mozilla.org/preferences-service;1"].
|
||||
getService(Ci.nsIPrefService).getBranch("browser.");
|
||||
// Wrap a string as a nsISupports
|
||||
_createSupportsString: function ssfi_createSupportsString(aData) {
|
||||
let string = Cc["@mozilla.org/supports-string;1"]
|
||||
.createInstance(Ci.nsISupportsString);
|
||||
string.data = aData;
|
||||
return string;
|
||||
},
|
||||
|
||||
// get file references
|
||||
var dirService = Cc["@mozilla.org/file/directory_service;1"].
|
||||
getService(Ci.nsIProperties);
|
||||
let sessionFile = dirService.get("ProfD", Ci.nsILocalFile);
|
||||
sessionFile.append("sessionstore.js");
|
||||
|
||||
let doResumeSessionOnce = prefBranch.getBoolPref("sessionstore.resume_session_once");
|
||||
let doResumeSession = doResumeSessionOnce ||
|
||||
prefBranch.getIntPref("startup.page") == 3;
|
||||
|
||||
// only continue if the session file exists
|
||||
if (!sessionFile.exists())
|
||||
_onSessionFileRead: function sss_onSessionFileRead(aStateString) {
|
||||
debug("onSessionFileRead ");
|
||||
if (this._initialized) {
|
||||
debug("onSessionFileRead: Initialization is already complete");
|
||||
// Initialization is complete, nothing else to do
|
||||
return;
|
||||
|
||||
// get string containing session state
|
||||
let iniString = this._readStateFile(sessionFile);
|
||||
if (!iniString)
|
||||
return;
|
||||
|
||||
// parse the session state into a JS object
|
||||
// remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0)
|
||||
if (iniString.charAt(0) == '(')
|
||||
iniString = iniString.slice(1, -1);
|
||||
let corruptFile = false;
|
||||
}
|
||||
try {
|
||||
this._initialState = JSON.parse(iniString);
|
||||
}
|
||||
catch (ex) {
|
||||
debug("The session file contained un-parse-able JSON: " + ex);
|
||||
// Try to eval.
|
||||
// evalInSandbox will throw if iniString is not parse-able.
|
||||
try {
|
||||
var s = new Cu.Sandbox("about:blank", {sandboxName: 'nsSessionStartup'});
|
||||
this._initialState = Cu.evalInSandbox("(" + iniString + ")", s);
|
||||
} catch(ex) {
|
||||
debug("The session file contained un-eval-able JSON: " + ex);
|
||||
corruptFile = true;
|
||||
this._initialized = true;
|
||||
|
||||
// Let observers modify the state before it is used
|
||||
let supportsStateString = this._createSupportsString(aStateString);
|
||||
Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", "");
|
||||
aStateString = supportsStateString.data;
|
||||
|
||||
// No valid session found.
|
||||
if (!aStateString) {
|
||||
this._sessionType = Ci.nsISessionStartup.NO_SESSION;
|
||||
return;
|
||||
}
|
||||
|
||||
// parse the session state into a JS object
|
||||
// remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0)
|
||||
if (aStateString.charAt(0) == '(')
|
||||
aStateString = aStateString.slice(1, -1);
|
||||
let corruptFile = false;
|
||||
try {
|
||||
this._initialState = JSON.parse(aStateString);
|
||||
}
|
||||
catch (ex) {
|
||||
debug("The session file contained un-parse-able JSON: " + ex);
|
||||
// This is not valid JSON, but this might still be valid JavaScript,
|
||||
// as used in FF2/FF3, so we need to eval.
|
||||
// evalInSandbox will throw if aStateString is not parse-able.
|
||||
try {
|
||||
var s = new Cu.Sandbox("about:blank", {sandboxName: 'nsSessionStartup'});
|
||||
this._initialState = Cu.evalInSandbox("(" + aStateString + ")", s);
|
||||
} catch(ex) {
|
||||
debug("The session file contained un-eval-able JSON: " + ex);
|
||||
corruptFile = true;
|
||||
}
|
||||
}
|
||||
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile);
|
||||
|
||||
let doResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
|
||||
let doResumeSession = doResumeSessionOnce ||
|
||||
Services.prefs.getIntPref("browser.startup.page") == 3;
|
||||
|
||||
// If this is a normal restore then throw away any previous session
|
||||
if (!doResumeSessionOnce)
|
||||
delete this._initialState.lastSessionState;
|
||||
|
||||
let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash");
|
||||
let lastSessionCrashed =
|
||||
this._initialState && this._initialState.session &&
|
||||
this._initialState.session.state &&
|
||||
this._initialState.session.state == STATE_RUNNING_STR;
|
||||
|
||||
// Report shutdown success via telemetry. Shortcoming here are
|
||||
// being-killed-by-OS-shutdown-logic, shutdown freezing after
|
||||
// session restore was written, etc.
|
||||
Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!lastSessionCrashed);
|
||||
|
||||
// set the startup type
|
||||
if (lastSessionCrashed && resumeFromCrash)
|
||||
this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
|
||||
else if (!lastSessionCrashed && doResumeSession)
|
||||
this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
|
||||
else if (this._initialState)
|
||||
this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
|
||||
else
|
||||
this._initialState = null; // reset the state
|
||||
|
||||
// wait for the first browser window to open
|
||||
// Don't reset the initial window's default args (i.e. the home page(s))
|
||||
// if all stored tabs are pinned.
|
||||
if (this.doRestore() &&
|
||||
(!this._initialState.windows ||
|
||||
!this._initialState.windows.every(function (win)
|
||||
win.tabs.every(function (tab) tab.pinned))))
|
||||
Services.obs.addObserver(this, "domwindowopened", true);
|
||||
|
||||
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
|
||||
|
||||
if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
|
||||
Services.obs.addObserver(this, "browser:purge-session-history", true);
|
||||
|
||||
} finally {
|
||||
// We're ready. Notify everyone else.
|
||||
Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
|
||||
gOnceInitializedDeferred.resolve();
|
||||
}
|
||||
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile);
|
||||
|
||||
// If this is a normal restore then throw away any previous session
|
||||
if (!doResumeSessionOnce)
|
||||
delete this._initialState.lastSessionState;
|
||||
|
||||
let resumeFromCrash = prefBranch.getBoolPref("sessionstore.resume_from_crash");
|
||||
let lastSessionCrashed =
|
||||
this._initialState && this._initialState.session &&
|
||||
this._initialState.session.state &&
|
||||
this._initialState.session.state == STATE_RUNNING_STR;
|
||||
|
||||
// Report shutdown success via telemetry. Shortcoming here are
|
||||
// being-killed-by-OS-shutdown-logic, shutdown freezing after
|
||||
// session restore was written, etc.
|
||||
Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!lastSessionCrashed);
|
||||
|
||||
// set the startup type
|
||||
if (lastSessionCrashed && resumeFromCrash)
|
||||
this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
|
||||
else if (!lastSessionCrashed && doResumeSession)
|
||||
this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
|
||||
else if (this._initialState)
|
||||
this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
|
||||
else
|
||||
this._initialState = null; // reset the state
|
||||
|
||||
// wait for the first browser window to open
|
||||
// Don't reset the initial window's default args (i.e. the home page(s))
|
||||
// if all stored tabs are pinned.
|
||||
if (this.doRestore() &&
|
||||
(!this._initialState.windows ||
|
||||
!this._initialState.windows.every(function (win)
|
||||
win.tabs.every(function (tab) tab.pinned))))
|
||||
Services.obs.addObserver(this, "domwindowopened", true);
|
||||
|
||||
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
|
||||
|
||||
if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
|
||||
Services.obs.addObserver(this, "browser:purge-session-history", true);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -237,10 +269,15 @@ SessionStartup.prototype = {
|
||||
|
||||
/* ........ Public API ................*/
|
||||
|
||||
get onceInitialized() {
|
||||
return gOnceInitializedDeferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the session state as a jsval
|
||||
*/
|
||||
get state() {
|
||||
this._ensureInitialized();
|
||||
return this._initialState;
|
||||
},
|
||||
|
||||
@ -249,6 +286,7 @@ SessionStartup.prototype = {
|
||||
* @returns bool
|
||||
*/
|
||||
doRestore: function sss_doRestore() {
|
||||
this._ensureInitialized();
|
||||
return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION ||
|
||||
this._sessionType == Ci.nsISessionStartup.RESUME_SESSION;
|
||||
},
|
||||
@ -257,59 +295,26 @@ SessionStartup.prototype = {
|
||||
* Get the type of pending session store, if any.
|
||||
*/
|
||||
get sessionType() {
|
||||
this._ensureInitialized();
|
||||
return this._sessionType;
|
||||
},
|
||||
|
||||
/* ........ Storage API .............. */
|
||||
|
||||
/**
|
||||
* Reads a session state file into a string and lets
|
||||
* observers modify the state before it's being used
|
||||
*
|
||||
* @param aFile is any nsIFile
|
||||
* @returns a session state string
|
||||
*/
|
||||
_readStateFile: function sss_readStateFile(aFile) {
|
||||
TelemetryStopwatch.start("FX_SESSION_RESTORE_READ_FILE_MS");
|
||||
var stateString = Cc["@mozilla.org/supports-string;1"].
|
||||
createInstance(Ci.nsISupportsString);
|
||||
stateString.data = this._readFile(aFile) || "";
|
||||
TelemetryStopwatch.finish("FX_SESSION_RESTORE_READ_FILE_MS");
|
||||
|
||||
Services.obs.notifyObservers(stateString, "sessionstore-state-read", "");
|
||||
|
||||
return stateString.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* reads a file into a string
|
||||
* @param aFile
|
||||
* nsIFile
|
||||
* @returns string
|
||||
*/
|
||||
_readFile: function sss_readFile(aFile) {
|
||||
// Ensure that initialization is complete.
|
||||
// If initialization is not complete yet, fall back to a synchronous
|
||||
// initialization and kill ongoing asynchronous initialization
|
||||
_ensureInitialized: function sss__ensureInitialized() {
|
||||
try {
|
||||
var stream = Cc["@mozilla.org/network/file-input-stream;1"].
|
||||
createInstance(Ci.nsIFileInputStream);
|
||||
stream.init(aFile, 0x01, 0, 0);
|
||||
var cvstream = Cc["@mozilla.org/intl/converter-input-stream;1"].
|
||||
createInstance(Ci.nsIConverterInputStream);
|
||||
|
||||
var fileSize = stream.available();
|
||||
if (fileSize > MAX_FILE_SIZE)
|
||||
throw "SessionStartup: sessionstore.js was not processed because it was too large.";
|
||||
|
||||
cvstream.init(stream, "UTF-8", fileSize, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
||||
var data = {};
|
||||
cvstream.readString(fileSize, data);
|
||||
var content = data.value;
|
||||
cvstream.close();
|
||||
|
||||
return content.replace(/\r\n?/g, "\n");
|
||||
debug("_ensureInitialized: " + this._initialState);
|
||||
if (this._initialized) {
|
||||
// Initialization is complete, nothing else to do
|
||||
return;
|
||||
}
|
||||
let contents = _SessionFile.syncRead();
|
||||
this._onSessionFileRead(contents);
|
||||
} catch(ex) {
|
||||
debug("ensureInitialized: could not read session " + ex + ", " + ex.stack);
|
||||
throw ex;
|
||||
}
|
||||
catch (ex) { Cu.reportError(ex); }
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/* ........ QueryInterface .............. */
|
||||
|
@ -2113,6 +2113,12 @@
|
||||
"n_buckets": 10,
|
||||
"description": "Session restore: Time to read the session data from the file on disk (ms)"
|
||||
},
|
||||
"FX_SESSION_RESTORE_SYNC_READ_FILE_MS": {
|
||||
"kind": "exponential",
|
||||
"high": "3000",
|
||||
"n_buckets": 10,
|
||||
"description": "Session restore: Time to read the session data from the file on disk, using the synchronous fallback (ms)"
|
||||
},
|
||||
"FX_SESSION_RESTORE_WRITE_FILE_MS": {
|
||||
"kind": "exponential",
|
||||
"high": "3000",
|
||||
|
Loading…
Reference in New Issue
Block a user