browser/components/sessionstore/src/SessionFile.jsm

changeset 0
6474c204b198
     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 +  });

mercurial