DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (27a812186ff4)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
/* 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";

const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;

Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Promise.jsm");

// Make it possible to mock out timers for testing
let MakeTimer = () => Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);

this.EXPORTED_SYMBOLS = ["DeferredSave"];

// If delay parameter is not provided, default is 50 milliseconds.
const DEFAULT_SAVE_DELAY_MS = 50;

Cu.import("resource://gre/modules/Log.jsm");
//Configure a logger at the parent 'DeferredSave' level to format
//messages for all the modules under DeferredSave.*
const DEFERREDSAVE_PARENT_LOGGER_ID = "DeferredSave";
let parentLogger = Log.repository.getLogger(DEFERREDSAVE_PARENT_LOGGER_ID);
parentLogger.level = Log.Level.Warn;
let formatter = new Log.BasicFormatter();
//Set parent logger (and its children) to append to
//the Javascript section of the Browser Console
parentLogger.addAppender(new Log.ConsoleAppender(formatter));
//Set parent logger (and its children) to
//also append to standard out
parentLogger.addAppender(new Log.DumpAppender(formatter));

//Provide the ability to enable/disable logging
//messages at runtime.
//If the "extensions.logging.enabled" preference is
//missing or 'false', messages at the WARNING and higher
//severity should be logged to the JS console and standard error.
//If "extensions.logging.enabled" is set to 'true', messages
//at DEBUG and higher should go to JS console and standard error.
Cu.import("resource://gre/modules/Services.jsm");

const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";

/**
* Preference listener which listens for a change in the
* "extensions.logging.enabled" preference and changes the logging level of the
* parent 'addons' level logger accordingly.
*/
var PrefObserver = {
 init: function PrefObserver_init() {
   Services.prefs.addObserver(PREF_LOGGING_ENABLED, this, false);
   Services.obs.addObserver(this, "xpcom-shutdown", false);
   this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED);
 },

 observe: function PrefObserver_observe(aSubject, aTopic, aData) {
   if (aTopic == "xpcom-shutdown") {
     Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this);
     Services.obs.removeObserver(this, "xpcom-shutdown");
   }
   else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) {
     let debugLogEnabled = false;
     try {
       debugLogEnabled = Services.prefs.getBoolPref(PREF_LOGGING_ENABLED);
     }
     catch (e) {
     }
     if (debugLogEnabled) {
       parentLogger.level = Log.Level.Debug;
     }
     else {
       parentLogger.level = Log.Level.Warn;
     }
   }
 }
};

PrefObserver.init();

/**
 * A module to manage deferred, asynchronous writing of data files
 * to disk. Writing is deferred by waiting for a specified delay after
 * a request to save the data, before beginning to write. If more than
 * one save request is received during the delay, all requests are
 * fulfilled by a single write.
 *
 * @constructor
 * @param aPath
 *        String representing the full path of the file where the data
 *        is to be written.
 * @param aDataProvider
 *        Callback function that takes no argument and returns the data to
 *        be written. If aDataProvider returns an ArrayBufferView, the
 *        bytes it contains are written to the file as is.
 *        If aDataProvider returns a String the data are UTF-8 encoded
 *        and then written to the file.
 * @param [optional] aDelay
 *        The delay in milliseconds between the first saveChanges() call
 *        that marks the data as needing to be saved, and when the DeferredSave
 *        begins writing the data to disk. Default 50 milliseconds.
 */
this.DeferredSave = function (aPath, aDataProvider, aDelay) {
  // Create a new logger (child of 'DeferredSave' logger)
  // for use by this particular instance of DeferredSave object
  let leafName = OS.Path.basename(aPath);
  let logger_id = DEFERREDSAVE_PARENT_LOGGER_ID + "." + leafName;
  this.logger = Log.repository.getLogger(logger_id);

  // @type {Deferred|null}, null when no data needs to be written
  // @resolves with the result of OS.File.writeAtomic when all writes complete
  // @rejects with the error from OS.File.writeAtomic if the write fails,
  //          or with the error from aDataProvider() if that throws.
  this._pending = null;

  // @type {Promise}, completes when the in-progress write (if any) completes,
  //       kept as a resolved promise at other times to simplify logic.
  //       Because _deferredSave() always uses _writing.then() to execute
  //       its next action, we don't need a special case for whether a write
  //       is in progress - if the previous write is complete (and the _writing
  //       promise is already resolved/rejected), _writing.then() starts
  //       the next action immediately.
  //
  // @resolves with the result of OS.File.writeAtomic
  // @rejects with the error from OS.File.writeAtomic
  this._writing = Promise.resolve(0);

  // Are we currently waiting for a write to complete
  this.writeInProgress = false;

  this._path = aPath;
  this._dataProvider = aDataProvider;

  this._timer = null;

  // Some counters for telemetry
  // The total number of times the file was written
  this.totalSaves = 0;

  // The number of times the data became dirty while
  // another save was in progress
  this.overlappedSaves = 0;

  // Error returned by the most recent write (if any)
  this._lastError = null;

  if (aDelay && (aDelay > 0))
    this._delay = aDelay;
  else
    this._delay = DEFAULT_SAVE_DELAY_MS;
}

this.DeferredSave.prototype = {
  get dirty() {
    return this._pending || this.writeInProgress;
  },

  get lastError() {
    return this._lastError;
  },

  // Start the pending timer if data is dirty
  _startTimer: function() {
    if (!this._pending) {
      return;
    }

      this.logger.debug("Starting timer");
    if (!this._timer)
      this._timer = MakeTimer();
    this._timer.initWithCallback(() => this._deferredSave(),
                                 this._delay, Ci.nsITimer.TYPE_ONE_SHOT);
  },

  /**
   * Mark the current stored data dirty, and schedule a flush to disk
   * @return A Promise<integer> that will be resolved after the data is written to disk;
   *         the promise is resolved with the number of bytes written.
   */
  saveChanges: function() {
      this.logger.debug("Save changes");
    if (!this._pending) {
      if (this.writeInProgress) {
          this.logger.debug("Data changed while write in progress");
        this.overlappedSaves++;
      }
      this._pending = Promise.defer();
      // Wait until the most recent write completes or fails (if it hasn't already)
      // and then restart our timer
      this._writing.then(count => this._startTimer(), error => this._startTimer());
    }
    return this._pending.promise;
  },

  _deferredSave: function() {
    let pending = this._pending;
    this._pending = null;
    let writing = this._writing;
    this._writing = pending.promise;

    // In either the success or the exception handling case, we don't need to handle
    // the error from _writing here; it's already being handled in another then()
    let toSave = null;
    try {
      toSave = this._dataProvider();
    }
    catch(e) {
        this.logger.error("Deferred save dataProvider failed", e);
      writing.then(null, error => {})
        .then(count => {
          pending.reject(e);
        });
      return;
    }

    writing.then(null, error => {return 0;})
    .then(count => {
        this.logger.debug("Starting write");
      this.totalSaves++;
      this.writeInProgress = true;

      OS.File.writeAtomic(this._path, toSave, {tmpPath: this._path + ".tmp"})
      .then(
        result => {
          this._lastError = null;
          this.writeInProgress = false;
              this.logger.debug("Write succeeded");
          pending.resolve(result);
        },
        error => {
          this._lastError = error;
          this.writeInProgress = false;
              this.logger.warn("Write failed", error);
          pending.reject(error);
        });
    });
  },

  /**
   * Immediately save the dirty data to disk, skipping
   * the delay of normal operation. Note that the write
   * still happens asynchronously in the worker
   * thread from OS.File.
   *
   * There are four possible situations:
   * 1) Nothing to flush
   * 2) Data is not currently being written, in-memory copy is dirty
   * 3) Data is currently being written, in-memory copy is clean
   * 4) Data is being written and in-memory copy is dirty
   *
   * @return Promise<integer> that will resolve when all in-memory data
   *         has finished being flushed, returning the number of bytes
   *         written. If all in-memory data is clean, completes with the
   *         result of the most recent write.
   */
  flush: function() {
    // If we have pending changes, cancel our timer and set up the write
    // immediately (_deferredSave queues the write for after the most
    // recent write completes, if it hasn't already)
    if (this._pending) {
        this.logger.debug("Flush called while data is dirty");
      if (this._timer) {
        this._timer.cancel();
        this._timer = null;
      }
      this._deferredSave();
    }

    return this._writing;
  }
};