Files
binoc-central-mirror/mail/modules/sessionStoreManager.js
T
2020-05-10 13:52:36 -04:00

334 lines
10 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/. */
/**
* Session Storage and Restoration
*/
/* :::::::: Constants and Helpers ::::::::::::::: */
this.EXPORTED_SYMBOLS = ["sessionStoreManager"];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Services.jsm");
/**
* asuth arbitrarily chose this value to trade-off powersaving,
* processor usage, and recency of state in the face of the impossibility of
* our crashing; he also worded this.
*/
var SESSION_AUTO_SAVE_DEFAULT_MS = 300000; // 5 minutes
/* :::::::: The Module ::::::::::::::: */
var sessionStoreManager =
{
_initialized: false,
_sessionAutoSaveTimer: null,
_sessionAutoSaveTimerIntervalMS: SESSION_AUTO_SAVE_DEFAULT_MS,
/**
* The persisted state of the previous session. This is resurrected
* from disk when the module is initialized and cleared when all
* required windows have been restored.
*/
_initialState: null,
/**
* The string containing the JSON stringified representation of the last
* state we wrote to disk.
*/
_currentStateString: null,
/**
* A flag indicating whether the state "just before shutdown" of the current
* session has been persisted to disk. See |observe| and |unloadingWindow|
* for justification on why we need this.
*/
_shutdownStateSaved: false,
/**
* Cache the session file async writeAtomic Promise for AsyncShutdown.
*/
_promise: null,
/**
* Gets the nsIFile used for session storage.
*/
get sessionFile()
{
let sessionFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
sessionFile.append("session.json");
return sessionFile;
},
/**
* This is called on startup, and when a new 3 pane window is opened after
* the last 3 pane window was closed (e.g., on the mac, closing the last
* window doesn't shut down the app).
*/
_init: function ssm_init()
{
this._loadSessionFile();
// we listen for "quit-application-granted" instead of
// "quit-application-requested" because other observers of the
// latter can cancel the shutdown.
Services.obs.addObserver(this, "quit-application-granted", false);
this.startPeriodicSave();
this._initialized = true;
},
/**
* Loads the session file into _initialState. This should only be called by
* _init and a unit test.
*/
_loadSessionFile: function ssm_loadSessionFile()
{
if (!this.sessionFile.exists())
return;
// Read the session state data from file, synchronously.
let inStream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
inStream.init(this.sessionFile, -1, 0, 0);
let data = NetUtil.readInputStreamToString(inStream,
inStream.available(),
{ charset: "UTF-8" });
inStream.close();
// Clear the current state so that subsequent writes won't think
// the state hasn't changed.
this._currentStateString = null;
try {
// Parse the session state into a JSON object.
this._initialState = JSON.parse(data);
}
catch (ex) {
Cu.reportError("sessionStoreManager: error in session state data, " + ex);
}
if (!data || !this._initialState) {
// If the file exists but there is a data read or parse fail, save the
// bad file.
let errorFile = "session_error_" +
(new Date().toISOString()).replace(/\D/g, "") + ".json";
let errorFilePath = OS.Path.join(OS.Constants.Path.profileDir, errorFile);
OS.File.move(this.sessionFile.path, errorFilePath)
.then(null, error => Cu.reportError("sessionStoreManager: failed to rename " +
this.sessionFile.path + " to " +
errorFilePath + ": " + error));
}
},
/**
* Opens the windows that were open in the previous session.
*/
_openOtherRequiredWindows: function ssm_openOtherRequiredWindows(aWindow)
{
// XXX we might want to display a restore page and let the user decide
// whether to restore the other windows, just like Firefox does.
if (!this._initialState || !this._initialState.windows || !aWindow)
return;
for (var i = 0; i < this._initialState.windows.length; ++i)
aWindow.open(
"chrome://messenger/content/messenger.xul",
"_blank",
"chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar");
},
/**
* Writes the state object to disk.
*/
_saveStateObject: function ssm_saveStateObject(aStateObj)
{
let data = JSON.stringify(aStateObj);
// Write async to disk only if state changed since last write.
if (data == this._currentStateString)
return;
this._promise = OS.File
.writeAtomic(this.sessionFile.path, data,
{ tmpPath: this.sessionFile.path + ".tmp",
flush: true })
.then(() => sessionStoreManager._currentStateString = data,
error => Cu.reportError("sessionStoreManager: error " +
"storing session state data, " +
error))
},
/**
* @return an empty state object that can be populated with window states.
*/
_createStateObject: function ssm_createStateObject()
{
return {
rev: 0,
windows: []
};
},
/**
* Writes the state of all currently open 3pane windows to disk.
*/
_saveState: function ssm_saveState()
{
let state = this._createStateObject();
// XXX we'd like to support other window types in future, but for now
// only get the 3pane windows.
let enumerator = Services.wm.getEnumerator("mail:3pane");
while (enumerator.hasMoreElements()) {
let win = enumerator.getNext();
if (win && "complete" == win.document.readyState &&
win.getWindowStateForSessionPersistence)
state.windows.push(win.getWindowStateForSessionPersistence());
}
this._saveStateObject(state);
},
/* ........ Timer Callback ................*/
_sessionAutoSaveTimerCallback: function ssm_sessionAutoSaveTimerCallback()
{
sessionStoreManager._saveState();
},
/* ........ Observer Notification Handler ................*/
observe: function ssm_observe(aSubject, aTopic, aData) {
switch (aTopic) {
// This is observed before any windows start unloading if something other
// than the last 3pane window closing requested the application be
// shutdown. For example, when the user quits via the file menu.
case "quit-application-granted":
if (!this._shutdownStateSaved) {
this.stopPeriodicSave();
this._saveState();
// this is to ensure we don't clobber the saved state when the
// 3pane windows unload.
this._shutdownStateSaved = true;
}
break;
}
},
/* ........ Public API ................*/
/**
* Called by each 3pane window instance when it loads.
*
* @return a window state object if aWindow was opened as a result of a
* session restoration, null otherwise.
*/
loadingWindow: function ssm_loadingWindow(aWindow)
{
let firstWindow = !this._initialized || this._shutdownStateSaved;
if (firstWindow)
this._init();
// If we are seeing a new 3-pane, we are obviously not in a shutdown
// state anymore. (This would happen if all the 3panes got closed but
// we did not quit because another window was open and then a 3pane showed
// up again. This can happen in both unit tests and real life.)
// We treat this case like the first window case, and do a session restore.
this._shutdownStateSaved = false;
let windowState = null;
if (this._initialState && this._initialState.windows) {
windowState = this._initialState.windows.pop();
if (0 == this._initialState.windows.length)
this._initialState = null;
}
if (firstWindow)
this._openOtherRequiredWindows(aWindow);
return windowState;
},
/**
* Called by each 3pane window instance when it unloads. If aWindow is the
* last 3pane window, its state is persisted. The last 3pane window unloads
* first before the "quit-application-granted" event is generated.
*/
unloadingWindow: function ssm_unloadingWindow(aWindow)
{
if (!this._shutdownStateSaved) {
// determine whether aWindow is the last open window
let lastWindow = true;
let enumerator = Services.wm.getEnumerator("mail:3pane");
while (enumerator.hasMoreElements()) {
if (enumerator.getNext() != aWindow)
lastWindow = false;
}
if (lastWindow) {
// last chance to save any state for the current session since
// aWindow is the last 3pane window and the "quit-application-granted"
// event is observed AFTER this.
this.stopPeriodicSave();
let state = this._createStateObject();
state.windows.push(aWindow.getWindowStateForSessionPersistence());
this._saveStateObject(state);
// XXX this is to ensure we don't clobber the saved state when we
// observe the "quit-application-granted" event.
this._shutdownStateSaved = true;
}
}
},
/**
* Stops periodic session persistence.
*/
stopPeriodicSave: function ssm_stopPeriodicSave()
{
if (this._sessionAutoSaveTimer) {
this._sessionAutoSaveTimer.cancel();
delete this._sessionAutoSaveTimer;
this._sessionAutoSaveTimer = null;
}
},
/**
* Starts periodic session persistence.
*/
startPeriodicSave: function ssm_startPeriodicSave()
{
if (!this._sessionAutoSaveTimer) {
this._sessionAutoSaveTimer = Cc["@mozilla.org/timer;1"]
.createInstance(Ci.nsITimer);
this._sessionAutoSaveTimer.initWithCallback(
this._sessionAutoSaveTimerCallback,
this._sessionAutoSaveTimerIntervalMS,
Ci.nsITimer.TYPE_REPEATING_SLACK);
}
}
};
AsyncShutdown.profileBeforeChange.addBlocker(
"sessionStoreManager: session.json",
() => { return sessionStoreManager._promise; }
);