michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["SessionFile"]; michael@0: michael@0: /** michael@0: * Implementation of all the disk I/O required by the session store. michael@0: * This is a private API, meant to be used only by the session store. michael@0: * It will change. Do not use it for any other purpose. michael@0: * michael@0: * Note that this module implicitly depends on one of two things: michael@0: * 1. either the asynchronous file I/O system enqueues its requests michael@0: * and never attempts to simultaneously execute two I/O requests on michael@0: * the files used by this module from two distinct threads; or michael@0: * 2. the clients of this API are well-behaved and do not place michael@0: * concurrent requests to the files used by this module. michael@0: * michael@0: * Otherwise, we could encounter bugs, especially under Windows, michael@0: * e.g. if a request attempts to write sessionstore.js while michael@0: * another attempts to copy that file. michael@0: * michael@0: * This implementation uses OS.File, which guarantees property 1. michael@0: */ michael@0: michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/osfile.jsm"); michael@0: Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/AsyncShutdown.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "console", michael@0: "resource://gre/modules/devtools/Console.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", michael@0: "resource://gre/modules/TelemetryStopwatch.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", michael@0: "@mozilla.org/base/telemetry;1", "nsITelemetry"); michael@0: michael@0: this.SessionFile = { michael@0: /** michael@0: * Read the contents of the session file, asynchronously. michael@0: */ michael@0: read: function () { michael@0: return SessionFileInternal.read(); michael@0: }, michael@0: /** michael@0: * Write the contents of the session file, asynchronously. michael@0: */ michael@0: write: function (aData) { michael@0: return SessionFileInternal.write(aData); michael@0: }, michael@0: /** michael@0: * Gather telemetry statistics. michael@0: * michael@0: * michael@0: * Most of the work is done off the main thread but there is a main michael@0: * thread cost involved to send data to the worker thread. This method michael@0: * should therefore be called only when we know that it will not disrupt michael@0: * the user's experience, e.g. on idle-daily. michael@0: * michael@0: * @return {Promise} michael@0: * @promise {object} An object holding all the information to be submitted michael@0: * to Telemetry. michael@0: */ michael@0: gatherTelemetry: function(aData) { michael@0: return SessionFileInternal.gatherTelemetry(aData); michael@0: }, michael@0: /** michael@0: * Create a backup copy, asynchronously. michael@0: * This is designed to perform backup on upgrade. michael@0: */ michael@0: createBackupCopy: function (ext) { michael@0: return SessionFileInternal.createBackupCopy(ext); michael@0: }, michael@0: /** michael@0: * Remove a backup copy, asynchronously. michael@0: * This is designed to clean up a backup on upgrade. michael@0: */ michael@0: removeBackupCopy: function (ext) { michael@0: return SessionFileInternal.removeBackupCopy(ext); michael@0: }, michael@0: /** michael@0: * Wipe the contents of the session file, asynchronously. michael@0: */ michael@0: wipe: function () { michael@0: SessionFileInternal.wipe(); michael@0: } michael@0: }; michael@0: michael@0: Object.freeze(SessionFile); michael@0: michael@0: /** michael@0: * Utilities for dealing with promises and Task.jsm michael@0: */ michael@0: let SessionFileInternal = { michael@0: /** michael@0: * The path to sessionstore.js michael@0: */ michael@0: path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), michael@0: michael@0: /** michael@0: * The path to sessionstore.bak michael@0: */ michael@0: backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"), michael@0: michael@0: /** michael@0: * The promise returned by the latest call to |write|. michael@0: * We use it to ensure that AsyncShutdown.profileBeforeChange cannot michael@0: * interrupt a call to |write|. michael@0: */ michael@0: _latestWrite: null, michael@0: michael@0: /** michael@0: * |true| once we have decided to stop receiving write instructiosn michael@0: */ michael@0: _isClosed: false, michael@0: michael@0: read: function () { michael@0: // We must initialize the worker during startup so it will be ready to michael@0: // perform the final write. If shutdown happens soon after startup and michael@0: // the worker has not started yet we may not write. michael@0: // See Bug 964531. michael@0: SessionWorker.post("init"); michael@0: michael@0: return Task.spawn(function*() { michael@0: for (let filename of [this.path, this.backupPath]) { michael@0: try { michael@0: let startMs = Date.now(); michael@0: michael@0: let data = yield OS.File.read(filename, { encoding: "utf-8" }); michael@0: michael@0: Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS") michael@0: .add(Date.now() - startMs); michael@0: michael@0: return data; michael@0: } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { michael@0: // Ignore exceptions about non-existent files. michael@0: } michael@0: } michael@0: michael@0: return ""; michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: gatherTelemetry: function(aStateString) { michael@0: return Task.spawn(function() { michael@0: let msg = yield SessionWorker.post("gatherTelemetry", [aStateString]); michael@0: this._recordTelemetry(msg.telemetry); michael@0: throw new Task.Result(msg.telemetry); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: write: function (aData) { michael@0: if (this._isClosed) { michael@0: return Promise.reject(new Error("SessionFile is closed")); michael@0: } michael@0: let refObj = {}; michael@0: michael@0: let isFinalWrite = false; michael@0: if (Services.startup.shuttingDown) { michael@0: // If shutdown has started, we will want to stop receiving michael@0: // write instructions. michael@0: isFinalWrite = this._isClosed = true; michael@0: } michael@0: michael@0: return this._latestWrite = Task.spawn(function task() { michael@0: TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); michael@0: michael@0: try { michael@0: let promise = SessionWorker.post("write", [aData]); michael@0: // At this point, we measure how long we stop the main thread michael@0: TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); michael@0: michael@0: // Now wait for the result and record how long the write took michael@0: let msg = yield promise; michael@0: this._recordTelemetry(msg.telemetry); michael@0: } catch (ex) { michael@0: TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); michael@0: console.error("Could not write session state file ", this.path, ex); michael@0: } michael@0: michael@0: if (isFinalWrite) { michael@0: Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", ""); michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: createBackupCopy: function (ext) { michael@0: return SessionWorker.post("createBackupCopy", [ext]); michael@0: }, michael@0: michael@0: removeBackupCopy: function (ext) { michael@0: return SessionWorker.post("removeBackupCopy", [ext]); michael@0: }, michael@0: michael@0: wipe: function () { michael@0: SessionWorker.post("wipe"); michael@0: }, michael@0: michael@0: _recordTelemetry: function(telemetry) { michael@0: for (let id of Object.keys(telemetry)){ michael@0: let value = telemetry[id]; michael@0: let samples = []; michael@0: if (Array.isArray(value)) { michael@0: samples.push(...value); michael@0: } else { michael@0: samples.push(value); michael@0: } michael@0: let histogram = Telemetry.getHistogramById(id); michael@0: for (let sample of samples) { michael@0: histogram.add(sample); michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // Interface to a dedicated thread handling I/O michael@0: let SessionWorker = (function () { michael@0: let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js", michael@0: OS.Shared.LOG.bind("SessionWorker")); michael@0: return { michael@0: post: function post(...args) { michael@0: let promise = worker.post.apply(worker, args); michael@0: return promise.then( michael@0: null, michael@0: function onError(error) { michael@0: // Decode any serialized error michael@0: if (error instanceof PromiseWorker.WorkerError) { michael@0: throw OS.File.Error.fromMsg(error.data); michael@0: } michael@0: // Extract something meaningful from ErrorEvent michael@0: if (error instanceof ErrorEvent) { michael@0: throw new Error(error.message, error.filename, error.lineno); michael@0: } michael@0: throw error; michael@0: } michael@0: ); michael@0: } michael@0: }; michael@0: })(); michael@0: michael@0: // Ensure that we can write sessionstore.js cleanly before the profile michael@0: // becomes unaccessible. michael@0: AsyncShutdown.profileBeforeChange.addBlocker( michael@0: "SessionFile: Finish writing the latest sessionstore.js", michael@0: function() { michael@0: return SessionFileInternal._latestWrite; michael@0: });