/* 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 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) { 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 ssfi_readAuxSync(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 ssfi_syncRead() { // 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 ssfi_readAux(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 ssfi_read() { 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 ssfi_write(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 ssfi_createBackupCopy() { 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 ssfi_wipe() { 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 ssfi_isNoSuchFile(aReason) { return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile; } };