diff options
Diffstat (limited to 'browser/components/sessionstore/_SessionFile.jsm')
-rw-r--r-- | browser/components/sessionstore/_SessionFile.jsm | 314 |
1 files changed, 314 insertions, 0 deletions
diff --git a/browser/components/sessionstore/_SessionFile.jsm b/browser/components/sessionstore/_SessionFile.jsm new file mode 100644 index 000000000..173f6035d --- /dev/null +++ b/browser/components/sessionstore/_SessionFile.jsm @@ -0,0 +1,314 @@ +/* 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/Promise.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"); +XPCOMUtils.defineLazyModuleGetter(this, "console", + "resource://gre/modules/Console.jsm"); + +// An encoder to UTF-8. +XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { + return new TextEncoder(); +}); +// A decoder. +XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { + return new TextDecoder(); +}); + +this._SessionFile = { + /** + * A promise fulfilled once initialization (either synchronous or + * asynchronous) is complete. + */ + promiseInitialized: function() { + return SessionFileInternal.promiseInitialized; + }, + /** + * Read the contents of the session file, asynchronously. + */ + read: function() { + return SessionFileInternal.read(); + }, + /** + * Read the contents of the session file, synchronously. + */ + syncRead: function() { + return SessionFileInternal.syncRead(); + }, + /** + * Write the contents of the session file, asynchronously. + */ + write: function(aData) { + return SessionFileInternal.write(aData); + }, + /** + * Create a backup copy, asynchronously. + */ + createBackupCopy: function() { + return SessionFileInternal.createBackupCopy(); + }, + /** + * Wipe the contents of the session file, asynchronously. + */ + wipe: function() { + 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(promise) { + return promise.then( + null, + function onError(reason) { + console.error("Uncaught asynchronous error:", reason); + 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)); + } +}; + +var 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"), + + /** + * Utility function to safely read a file synchronously. + * @param aPath + * A path to read the file from. + * @returns string if successful, undefined otherwise. + */ + readAuxSync: function(aPath) { + let text; + try { + let file = new FileUtils.File(aPath); + let chan = NetUtil.newChannel({ + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true + }); + let stream = chan.open(); + text = NetUtil.readInputStreamToString(stream, stream.available(), + {charset: "utf-8"}); + } catch (e if e.result == Components.results.NS_ERROR_FILE_NOT_FOUND) { + // Ignore exceptions about non-existent files. + } catch (ex) { + // Any other error. + console.error("Uncaught error:", ex); + } finally { + return text; + } + }, + + /** + * 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. + * + * In case if sessionstore.js file does not exist or is corrupted (something + * happened between backup and write), attempt to read the sessionstore.bak + * instead. + */ + syncRead: function() { + // First read the sessionstore.js. + let text = this.readAuxSync(this.path); + if (typeof text === "undefined") { + // If sessionstore.js does not exist or is corrupted, read sessionstore.bak. + text = this.readAuxSync(this.backupPath); + } + return text || ""; + }, + + /** + * Utility function to safely read a file asynchronously. + * @param aPath + * A path to read the file from. + * @param aReadOptions + * Read operation options. + * |outExecutionDuration| option will be reused and can be + * incrementally updated by the worker process. + * @returns string if successful, undefined otherwise. + */ + readAux: function(aPath, aReadOptions) { + let self = this; + return TaskUtils.spawn(function() { + let text; + try { + let bytes = yield OS.File.read(aPath, undefined, aReadOptions); + text = gDecoder.decode(bytes); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + // Any other error. + console.error("Uncaught error - with the file: " + self.path, ex); + } + throw new Task.Result(text); + }); + }, + + /** + * Read the sessionstore file asynchronously. + * + * In case sessionstore.js file does not exist or is corrupted (something + * happened between backup and write), attempt to read the sessionstore.bak + * instead. + */ + read: function() { + let self = this; + return TaskUtils.spawn(function task() { + // Specify |outExecutionDuration| option to hold the combined duration of + // the asynchronous reads off the main thread (of both sessionstore.js and + // sessionstore.bak, if necessary). If sessionstore.js does not exist or + // is corrupted, |outExecutionDuration| will register the time it took to + // attempt to read the file. It will then be subsequently incremented by + // the read time of sessionsore.bak. + let readOptions = { + outExecutionDuration: null + }; + // First read the sessionstore.js. + let text = yield self.readAux(self.path, readOptions); + if (typeof text === "undefined") { + // If sessionstore.js does not exist or is corrupted, read the + // sessionstore.bak. + text = yield self.readAux(self.backupPath, readOptions); + } + // Return either the content of the sessionstore.bak if it was read + // successfully or an empty string otherwise. + throw new Task.Result(text || ""); + }); + }, + + write: function(aData) { + let refObj = {}; + let self = this; + return TaskUtils.spawn(function task() { + let bytes = gEncoder.encode(aData); + + try { + let promise = OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"}); + yield promise; + } catch (ex) { + console.error("Could not write session state file: " + self.path, ex); + } + }); + }, + + createBackupCopy: function() { + let backupCopyOptions = { + outExecutionDuration: null + }; + let self = this; + return TaskUtils.spawn(function task() { + try { + yield OS.File.move(self.path, self.backupPath, backupCopyOptions); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + console.error("Could not backup session state file: " + self.path, ex); + throw ex; + } + }); + }, + + wipe: function() { + let self = this; + return TaskUtils.spawn(function task() { + try { + yield OS.File.remove(self.path); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + console.error("Could not remove session state file: " + self.path, ex); + throw ex; + } + + try { + yield OS.File.remove(self.backupPath); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + console.error("Could not remove session state backup file: " + self.path, ex); + throw ex; + } + }); + }, + + _isNoSuchFile: function(aReason) { + return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile; + } +}; |