1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/sessionstore/src/SessionFile.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,258 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["SessionFile"]; 1.11 + 1.12 +/** 1.13 + * Implementation of all the disk I/O required by the session store. 1.14 + * This is a private API, meant to be used only by the session store. 1.15 + * It will change. Do not use it for any other purpose. 1.16 + * 1.17 + * Note that this module implicitly depends on one of two things: 1.18 + * 1. either the asynchronous file I/O system enqueues its requests 1.19 + * and never attempts to simultaneously execute two I/O requests on 1.20 + * the files used by this module from two distinct threads; or 1.21 + * 2. the clients of this API are well-behaved and do not place 1.22 + * concurrent requests to the files used by this module. 1.23 + * 1.24 + * Otherwise, we could encounter bugs, especially under Windows, 1.25 + * e.g. if a request attempts to write sessionstore.js while 1.26 + * another attempts to copy that file. 1.27 + * 1.28 + * This implementation uses OS.File, which guarantees property 1. 1.29 + */ 1.30 + 1.31 +const Cu = Components.utils; 1.32 +const Cc = Components.classes; 1.33 +const Ci = Components.interfaces; 1.34 +const Cr = Components.results; 1.35 + 1.36 +Cu.import("resource://gre/modules/Services.jsm"); 1.37 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.38 +Cu.import("resource://gre/modules/osfile.jsm"); 1.39 +Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this); 1.40 +Cu.import("resource://gre/modules/Promise.jsm"); 1.41 +Cu.import("resource://gre/modules/AsyncShutdown.jsm"); 1.42 + 1.43 +XPCOMUtils.defineLazyModuleGetter(this, "console", 1.44 + "resource://gre/modules/devtools/Console.jsm"); 1.45 +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", 1.46 + "resource://gre/modules/TelemetryStopwatch.jsm"); 1.47 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.48 + "resource://gre/modules/Task.jsm"); 1.49 +XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", 1.50 + "@mozilla.org/base/telemetry;1", "nsITelemetry"); 1.51 + 1.52 +this.SessionFile = { 1.53 + /** 1.54 + * Read the contents of the session file, asynchronously. 1.55 + */ 1.56 + read: function () { 1.57 + return SessionFileInternal.read(); 1.58 + }, 1.59 + /** 1.60 + * Write the contents of the session file, asynchronously. 1.61 + */ 1.62 + write: function (aData) { 1.63 + return SessionFileInternal.write(aData); 1.64 + }, 1.65 + /** 1.66 + * Gather telemetry statistics. 1.67 + * 1.68 + * 1.69 + * Most of the work is done off the main thread but there is a main 1.70 + * thread cost involved to send data to the worker thread. This method 1.71 + * should therefore be called only when we know that it will not disrupt 1.72 + * the user's experience, e.g. on idle-daily. 1.73 + * 1.74 + * @return {Promise} 1.75 + * @promise {object} An object holding all the information to be submitted 1.76 + * to Telemetry. 1.77 + */ 1.78 + gatherTelemetry: function(aData) { 1.79 + return SessionFileInternal.gatherTelemetry(aData); 1.80 + }, 1.81 + /** 1.82 + * Create a backup copy, asynchronously. 1.83 + * This is designed to perform backup on upgrade. 1.84 + */ 1.85 + createBackupCopy: function (ext) { 1.86 + return SessionFileInternal.createBackupCopy(ext); 1.87 + }, 1.88 + /** 1.89 + * Remove a backup copy, asynchronously. 1.90 + * This is designed to clean up a backup on upgrade. 1.91 + */ 1.92 + removeBackupCopy: function (ext) { 1.93 + return SessionFileInternal.removeBackupCopy(ext); 1.94 + }, 1.95 + /** 1.96 + * Wipe the contents of the session file, asynchronously. 1.97 + */ 1.98 + wipe: function () { 1.99 + SessionFileInternal.wipe(); 1.100 + } 1.101 +}; 1.102 + 1.103 +Object.freeze(SessionFile); 1.104 + 1.105 +/** 1.106 + * Utilities for dealing with promises and Task.jsm 1.107 + */ 1.108 +let SessionFileInternal = { 1.109 + /** 1.110 + * The path to sessionstore.js 1.111 + */ 1.112 + path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), 1.113 + 1.114 + /** 1.115 + * The path to sessionstore.bak 1.116 + */ 1.117 + backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"), 1.118 + 1.119 + /** 1.120 + * The promise returned by the latest call to |write|. 1.121 + * We use it to ensure that AsyncShutdown.profileBeforeChange cannot 1.122 + * interrupt a call to |write|. 1.123 + */ 1.124 + _latestWrite: null, 1.125 + 1.126 + /** 1.127 + * |true| once we have decided to stop receiving write instructiosn 1.128 + */ 1.129 + _isClosed: false, 1.130 + 1.131 + read: function () { 1.132 + // We must initialize the worker during startup so it will be ready to 1.133 + // perform the final write. If shutdown happens soon after startup and 1.134 + // the worker has not started yet we may not write. 1.135 + // See Bug 964531. 1.136 + SessionWorker.post("init"); 1.137 + 1.138 + return Task.spawn(function*() { 1.139 + for (let filename of [this.path, this.backupPath]) { 1.140 + try { 1.141 + let startMs = Date.now(); 1.142 + 1.143 + let data = yield OS.File.read(filename, { encoding: "utf-8" }); 1.144 + 1.145 + Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS") 1.146 + .add(Date.now() - startMs); 1.147 + 1.148 + return data; 1.149 + } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { 1.150 + // Ignore exceptions about non-existent files. 1.151 + } 1.152 + } 1.153 + 1.154 + return ""; 1.155 + }.bind(this)); 1.156 + }, 1.157 + 1.158 + gatherTelemetry: function(aStateString) { 1.159 + return Task.spawn(function() { 1.160 + let msg = yield SessionWorker.post("gatherTelemetry", [aStateString]); 1.161 + this._recordTelemetry(msg.telemetry); 1.162 + throw new Task.Result(msg.telemetry); 1.163 + }.bind(this)); 1.164 + }, 1.165 + 1.166 + write: function (aData) { 1.167 + if (this._isClosed) { 1.168 + return Promise.reject(new Error("SessionFile is closed")); 1.169 + } 1.170 + let refObj = {}; 1.171 + 1.172 + let isFinalWrite = false; 1.173 + if (Services.startup.shuttingDown) { 1.174 + // If shutdown has started, we will want to stop receiving 1.175 + // write instructions. 1.176 + isFinalWrite = this._isClosed = true; 1.177 + } 1.178 + 1.179 + return this._latestWrite = Task.spawn(function task() { 1.180 + TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); 1.181 + 1.182 + try { 1.183 + let promise = SessionWorker.post("write", [aData]); 1.184 + // At this point, we measure how long we stop the main thread 1.185 + TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); 1.186 + 1.187 + // Now wait for the result and record how long the write took 1.188 + let msg = yield promise; 1.189 + this._recordTelemetry(msg.telemetry); 1.190 + } catch (ex) { 1.191 + TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); 1.192 + console.error("Could not write session state file ", this.path, ex); 1.193 + } 1.194 + 1.195 + if (isFinalWrite) { 1.196 + Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", ""); 1.197 + } 1.198 + }.bind(this)); 1.199 + }, 1.200 + 1.201 + createBackupCopy: function (ext) { 1.202 + return SessionWorker.post("createBackupCopy", [ext]); 1.203 + }, 1.204 + 1.205 + removeBackupCopy: function (ext) { 1.206 + return SessionWorker.post("removeBackupCopy", [ext]); 1.207 + }, 1.208 + 1.209 + wipe: function () { 1.210 + SessionWorker.post("wipe"); 1.211 + }, 1.212 + 1.213 + _recordTelemetry: function(telemetry) { 1.214 + for (let id of Object.keys(telemetry)){ 1.215 + let value = telemetry[id]; 1.216 + let samples = []; 1.217 + if (Array.isArray(value)) { 1.218 + samples.push(...value); 1.219 + } else { 1.220 + samples.push(value); 1.221 + } 1.222 + let histogram = Telemetry.getHistogramById(id); 1.223 + for (let sample of samples) { 1.224 + histogram.add(sample); 1.225 + } 1.226 + } 1.227 + } 1.228 +}; 1.229 + 1.230 +// Interface to a dedicated thread handling I/O 1.231 +let SessionWorker = (function () { 1.232 + let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js", 1.233 + OS.Shared.LOG.bind("SessionWorker")); 1.234 + return { 1.235 + post: function post(...args) { 1.236 + let promise = worker.post.apply(worker, args); 1.237 + return promise.then( 1.238 + null, 1.239 + function onError(error) { 1.240 + // Decode any serialized error 1.241 + if (error instanceof PromiseWorker.WorkerError) { 1.242 + throw OS.File.Error.fromMsg(error.data); 1.243 + } 1.244 + // Extract something meaningful from ErrorEvent 1.245 + if (error instanceof ErrorEvent) { 1.246 + throw new Error(error.message, error.filename, error.lineno); 1.247 + } 1.248 + throw error; 1.249 + } 1.250 + ); 1.251 + } 1.252 + }; 1.253 +})(); 1.254 + 1.255 +// Ensure that we can write sessionstore.js cleanly before the profile 1.256 +// becomes unaccessible. 1.257 +AsyncShutdown.profileBeforeChange.addBlocker( 1.258 + "SessionFile: Finish writing the latest sessionstore.js", 1.259 + function() { 1.260 + return SessionFileInternal._latestWrite; 1.261 + });