Revision control

Copy as Markdown

Other Tools

/* 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/. */
import { JSONFile } from "resource://gre/modules/JSONFile.sys.mjs";
/**
* 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
export var SessionStoreManager = {
_initialized: false,
/**
* Session restored successfully on startup; use this to test for an early
* failed startup which does not restore user tab state to ensure a session
* save on close will not overwrite the last good session state.
*/
_restored: 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,
/**
* 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,
/**
* The JSONFile store object.
*/
get store() {
if (this._store) {
return this._store;
}
return (this._store = new JSONFile({
path: this.sessionFile.path,
backupTo: this.sessionFile.path + ".backup",
}));
},
/**
* Gets the nsIFile used for session storage.
*/
get sessionFile() {
const 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).
*/
async _init() {
await 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");
this.startPeriodicSave();
this._initialized = true;
},
/**
* Loads the session file into _initialState. This should only be called by
* _init and a unit test.
*/
async _loadSessionFile() {
if (!this.sessionFile.exists()) {
return;
}
// Read the session state data from file, asynchronously.
// An error on the json file returns an empty object which corresponds
// to a null |_initialState|.
await this.store.load();
this._initialState =
this.store.data.toSource() == {}.toSource() ? null : this.store.data;
},
/**
* Opens the windows that were open in the previous session.
*/
_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(
"_blank",
"chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar"
);
}
},
/**
* Writes the state object to disk.
*/
_saveStateObject(aStateObj) {
if (!this.store) {
console.error(
"SessionStoreManager: could not create data store from file"
);
return;
}
const currentStateString = JSON.stringify(aStateObj);
const storedStateString =
this.store.dataReady && this.store.data
? JSON.stringify(this.store.data)
: null;
// Do not save state (overwrite last good state) in case of a failed startup.
// Write async to disk only if state changed since last write.
if (!this._restored || currentStateString == storedStateString) {
return;
}
this.store.data = aStateObj;
this.store.saveSoon();
},
/**
* @returns an empty state object that can be populated with window states.
*/
_createStateObject() {
return {
rev: 0,
windows: [],
};
},
/**
* Writes the state of all currently open 3pane windows to disk.
*/
_saveState() {
const state = this._createStateObject();
// XXX we'd like to support other window types in future, but for now
// only get the 3pane windows.
for (const win of Services.wm.getEnumerator("mail:3pane")) {
if (
win &&
"complete" == win.document.readyState &&
win.getWindowStateForSessionPersistence
) {
state.windows.push(win.getWindowStateForSessionPersistence());
}
}
this._saveStateObject(state);
},
// Timer Callback
_sessionAutoSaveTimerCallback() {
SessionStoreManager._saveState();
},
// Observer Notification Handler
observe(aSubject, aTopic) {
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.
*
* @returns a window state object if aWindow was opened as a result of a
* session restoration, null otherwise.
*/
async loadingWindow(aWindow) {
const firstWindow = !this._initialized || this._shutdownStateSaved;
if (firstWindow) {
await 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(aWindow) {
if (!this._shutdownStateSaved) {
// determine whether aWindow is the last open window
let lastWindow = true;
for (const win of Services.wm.getEnumerator("mail:3pane")) {
if (win != 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();
const 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() {
if (this._sessionAutoSaveTimer) {
this._sessionAutoSaveTimer.cancel();
delete this._sessionAutoSaveTimer;
this._sessionAutoSaveTimer = null;
}
},
/**
* Starts periodic session persistence.
*/
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
);
}
},
};