browser/components/sessionstore/src/SessionFile.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 this.EXPORTED_SYMBOLS = ["SessionFile"];
     9 /**
    10  * Implementation of all the disk I/O required by the session store.
    11  * This is a private API, meant to be used only by the session store.
    12  * It will change. Do not use it for any other purpose.
    13  *
    14  * Note that this module implicitly depends on one of two things:
    15  * 1. either the asynchronous file I/O system enqueues its requests
    16  *   and never attempts to simultaneously execute two I/O requests on
    17  *   the files used by this module from two distinct threads; or
    18  * 2. the clients of this API are well-behaved and do not place
    19  *   concurrent requests to the files used by this module.
    20  *
    21  * Otherwise, we could encounter bugs, especially under Windows,
    22  *   e.g. if a request attempts to write sessionstore.js while
    23  *   another attempts to copy that file.
    24  *
    25  * This implementation uses OS.File, which guarantees property 1.
    26  */
    28 const Cu = Components.utils;
    29 const Cc = Components.classes;
    30 const Ci = Components.interfaces;
    31 const Cr = Components.results;
    33 Cu.import("resource://gre/modules/Services.jsm");
    34 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    35 Cu.import("resource://gre/modules/osfile.jsm");
    36 Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
    37 Cu.import("resource://gre/modules/Promise.jsm");
    38 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
    40 XPCOMUtils.defineLazyModuleGetter(this, "console",
    41   "resource://gre/modules/devtools/Console.jsm");
    42 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
    43   "resource://gre/modules/TelemetryStopwatch.jsm");
    44 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    45   "resource://gre/modules/Task.jsm");
    46 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
    47   "@mozilla.org/base/telemetry;1", "nsITelemetry");
    49 this.SessionFile = {
    50   /**
    51    * Read the contents of the session file, asynchronously.
    52    */
    53   read: function () {
    54     return SessionFileInternal.read();
    55   },
    56   /**
    57    * Write the contents of the session file, asynchronously.
    58    */
    59   write: function (aData) {
    60     return SessionFileInternal.write(aData);
    61   },
    62   /**
    63    * Gather telemetry statistics.
    64    *
    65    *
    66    * Most of the work is done off the main thread but there is a main
    67    * thread cost involved to send data to the worker thread. This method
    68    * should therefore be called only when we know that it will not disrupt
    69    * the user's experience, e.g. on idle-daily.
    70    *
    71    * @return {Promise}
    72    * @promise {object} An object holding all the information to be submitted
    73    * to Telemetry.
    74    */
    75   gatherTelemetry: function(aData) {
    76     return SessionFileInternal.gatherTelemetry(aData);
    77   },
    78   /**
    79    * Create a backup copy, asynchronously.
    80    * This is designed to perform backup on upgrade.
    81    */
    82   createBackupCopy: function (ext) {
    83     return SessionFileInternal.createBackupCopy(ext);
    84   },
    85   /**
    86    * Remove a backup copy, asynchronously.
    87    * This is designed to clean up a backup on upgrade.
    88    */
    89   removeBackupCopy: function (ext) {
    90     return SessionFileInternal.removeBackupCopy(ext);
    91   },
    92   /**
    93    * Wipe the contents of the session file, asynchronously.
    94    */
    95   wipe: function () {
    96     SessionFileInternal.wipe();
    97   }
    98 };
   100 Object.freeze(SessionFile);
   102 /**
   103  * Utilities for dealing with promises and Task.jsm
   104  */
   105 let SessionFileInternal = {
   106   /**
   107    * The path to sessionstore.js
   108    */
   109   path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
   111   /**
   112    * The path to sessionstore.bak
   113    */
   114   backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
   116   /**
   117    * The promise returned by the latest call to |write|.
   118    * We use it to ensure that AsyncShutdown.profileBeforeChange cannot
   119    * interrupt a call to |write|.
   120    */
   121   _latestWrite: null,
   123   /**
   124    * |true| once we have decided to stop receiving write instructiosn
   125    */
   126   _isClosed: false,
   128   read: function () {
   129     // We must initialize the worker during startup so it will be ready to
   130     // perform the final write. If shutdown happens soon after startup and
   131     // the worker has not started yet we may not write.
   132     // See Bug 964531.
   133     SessionWorker.post("init");
   135     return Task.spawn(function*() {
   136       for (let filename of [this.path, this.backupPath]) {
   137         try {
   138           let startMs = Date.now();
   140           let data = yield OS.File.read(filename, { encoding: "utf-8" });
   142           Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS")
   143                    .add(Date.now() - startMs);
   145           return data;
   146         } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
   147           // Ignore exceptions about non-existent files.
   148         }
   149       }
   151       return "";
   152     }.bind(this));
   153   },
   155   gatherTelemetry: function(aStateString) {
   156     return Task.spawn(function() {
   157       let msg = yield SessionWorker.post("gatherTelemetry", [aStateString]);
   158       this._recordTelemetry(msg.telemetry);
   159       throw new Task.Result(msg.telemetry);
   160     }.bind(this));
   161   },
   163   write: function (aData) {
   164     if (this._isClosed) {
   165       return Promise.reject(new Error("SessionFile is closed"));
   166     }
   167     let refObj = {};
   169     let isFinalWrite = false;
   170     if (Services.startup.shuttingDown) {
   171       // If shutdown has started, we will want to stop receiving
   172       // write instructions.
   173       isFinalWrite = this._isClosed = true;
   174     }
   176     return this._latestWrite = Task.spawn(function task() {
   177       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
   179       try {
   180         let promise = SessionWorker.post("write", [aData]);
   181         // At this point, we measure how long we stop the main thread
   182         TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
   184         // Now wait for the result and record how long the write took
   185         let msg = yield promise;
   186         this._recordTelemetry(msg.telemetry);
   187       } catch (ex) {
   188         TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
   189         console.error("Could not write session state file ", this.path, ex);
   190       }
   192       if (isFinalWrite) {
   193         Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", "");
   194       }
   195     }.bind(this));
   196   },
   198   createBackupCopy: function (ext) {
   199     return SessionWorker.post("createBackupCopy", [ext]);
   200   },
   202   removeBackupCopy: function (ext) {
   203     return SessionWorker.post("removeBackupCopy", [ext]);
   204   },
   206   wipe: function () {
   207     SessionWorker.post("wipe");
   208   },
   210   _recordTelemetry: function(telemetry) {
   211     for (let id of Object.keys(telemetry)){
   212       let value = telemetry[id];
   213       let samples = [];
   214       if (Array.isArray(value)) {
   215         samples.push(...value);
   216       } else {
   217         samples.push(value);
   218       }
   219       let histogram = Telemetry.getHistogramById(id);
   220       for (let sample of samples) {
   221         histogram.add(sample);
   222       }
   223     }
   224   }
   225 };
   227 // Interface to a dedicated thread handling I/O
   228 let SessionWorker = (function () {
   229   let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js",
   230     OS.Shared.LOG.bind("SessionWorker"));
   231   return {
   232     post: function post(...args) {
   233       let promise = worker.post.apply(worker, args);
   234       return promise.then(
   235         null,
   236         function onError(error) {
   237           // Decode any serialized error
   238           if (error instanceof PromiseWorker.WorkerError) {
   239             throw OS.File.Error.fromMsg(error.data);
   240           }
   241           // Extract something meaningful from ErrorEvent
   242           if (error instanceof ErrorEvent) {
   243             throw new Error(error.message, error.filename, error.lineno);
   244           }
   245           throw error;
   246         }
   247       );
   248     }
   249   };
   250 })();
   252 // Ensure that we can write sessionstore.js cleanly before the profile
   253 // becomes unaccessible.
   254 AsyncShutdown.profileBeforeChange.addBlocker(
   255   "SessionFile: Finish writing the latest sessionstore.js",
   256   function() {
   257     return SessionFileInternal._latestWrite;
   258   });

mercurial