1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/extensions/DeferredSave.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,274 @@ 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 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const Cu = Components.utils; 1.11 +const Cc = Components.classes; 1.12 +const Ci = Components.interfaces; 1.13 + 1.14 +Cu.import("resource://gre/modules/osfile.jsm"); 1.15 +Cu.import("resource://gre/modules/Promise.jsm"); 1.16 + 1.17 +// Make it possible to mock out timers for testing 1.18 +let MakeTimer = () => Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.19 + 1.20 +this.EXPORTED_SYMBOLS = ["DeferredSave"]; 1.21 + 1.22 +// If delay parameter is not provided, default is 50 milliseconds. 1.23 +const DEFAULT_SAVE_DELAY_MS = 50; 1.24 + 1.25 +Cu.import("resource://gre/modules/Log.jsm"); 1.26 +//Configure a logger at the parent 'DeferredSave' level to format 1.27 +//messages for all the modules under DeferredSave.* 1.28 +const DEFERREDSAVE_PARENT_LOGGER_ID = "DeferredSave"; 1.29 +let parentLogger = Log.repository.getLogger(DEFERREDSAVE_PARENT_LOGGER_ID); 1.30 +parentLogger.level = Log.Level.Warn; 1.31 +let formatter = new Log.BasicFormatter(); 1.32 +//Set parent logger (and its children) to append to 1.33 +//the Javascript section of the Browser Console 1.34 +parentLogger.addAppender(new Log.ConsoleAppender(formatter)); 1.35 +//Set parent logger (and its children) to 1.36 +//also append to standard out 1.37 +parentLogger.addAppender(new Log.DumpAppender(formatter)); 1.38 + 1.39 +//Provide the ability to enable/disable logging 1.40 +//messages at runtime. 1.41 +//If the "extensions.logging.enabled" preference is 1.42 +//missing or 'false', messages at the WARNING and higher 1.43 +//severity should be logged to the JS console and standard error. 1.44 +//If "extensions.logging.enabled" is set to 'true', messages 1.45 +//at DEBUG and higher should go to JS console and standard error. 1.46 +Cu.import("resource://gre/modules/Services.jsm"); 1.47 + 1.48 +const PREF_LOGGING_ENABLED = "extensions.logging.enabled"; 1.49 +const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed"; 1.50 + 1.51 +/** 1.52 +* Preference listener which listens for a change in the 1.53 +* "extensions.logging.enabled" preference and changes the logging level of the 1.54 +* parent 'addons' level logger accordingly. 1.55 +*/ 1.56 +var PrefObserver = { 1.57 + init: function PrefObserver_init() { 1.58 + Services.prefs.addObserver(PREF_LOGGING_ENABLED, this, false); 1.59 + Services.obs.addObserver(this, "xpcom-shutdown", false); 1.60 + this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED); 1.61 + }, 1.62 + 1.63 + observe: function PrefObserver_observe(aSubject, aTopic, aData) { 1.64 + if (aTopic == "xpcom-shutdown") { 1.65 + Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this); 1.66 + Services.obs.removeObserver(this, "xpcom-shutdown"); 1.67 + } 1.68 + else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) { 1.69 + let debugLogEnabled = false; 1.70 + try { 1.71 + debugLogEnabled = Services.prefs.getBoolPref(PREF_LOGGING_ENABLED); 1.72 + } 1.73 + catch (e) { 1.74 + } 1.75 + if (debugLogEnabled) { 1.76 + parentLogger.level = Log.Level.Debug; 1.77 + } 1.78 + else { 1.79 + parentLogger.level = Log.Level.Warn; 1.80 + } 1.81 + } 1.82 + } 1.83 +}; 1.84 + 1.85 +PrefObserver.init(); 1.86 + 1.87 +/** 1.88 + * A module to manage deferred, asynchronous writing of data files 1.89 + * to disk. Writing is deferred by waiting for a specified delay after 1.90 + * a request to save the data, before beginning to write. If more than 1.91 + * one save request is received during the delay, all requests are 1.92 + * fulfilled by a single write. 1.93 + * 1.94 + * @constructor 1.95 + * @param aPath 1.96 + * String representing the full path of the file where the data 1.97 + * is to be written. 1.98 + * @param aDataProvider 1.99 + * Callback function that takes no argument and returns the data to 1.100 + * be written. If aDataProvider returns an ArrayBufferView, the 1.101 + * bytes it contains are written to the file as is. 1.102 + * If aDataProvider returns a String the data are UTF-8 encoded 1.103 + * and then written to the file. 1.104 + * @param [optional] aDelay 1.105 + * The delay in milliseconds between the first saveChanges() call 1.106 + * that marks the data as needing to be saved, and when the DeferredSave 1.107 + * begins writing the data to disk. Default 50 milliseconds. 1.108 + */ 1.109 +this.DeferredSave = function (aPath, aDataProvider, aDelay) { 1.110 + // Create a new logger (child of 'DeferredSave' logger) 1.111 + // for use by this particular instance of DeferredSave object 1.112 + let leafName = OS.Path.basename(aPath); 1.113 + let logger_id = DEFERREDSAVE_PARENT_LOGGER_ID + "." + leafName; 1.114 + this.logger = Log.repository.getLogger(logger_id); 1.115 + 1.116 + // @type {Deferred|null}, null when no data needs to be written 1.117 + // @resolves with the result of OS.File.writeAtomic when all writes complete 1.118 + // @rejects with the error from OS.File.writeAtomic if the write fails, 1.119 + // or with the error from aDataProvider() if that throws. 1.120 + this._pending = null; 1.121 + 1.122 + // @type {Promise}, completes when the in-progress write (if any) completes, 1.123 + // kept as a resolved promise at other times to simplify logic. 1.124 + // Because _deferredSave() always uses _writing.then() to execute 1.125 + // its next action, we don't need a special case for whether a write 1.126 + // is in progress - if the previous write is complete (and the _writing 1.127 + // promise is already resolved/rejected), _writing.then() starts 1.128 + // the next action immediately. 1.129 + // 1.130 + // @resolves with the result of OS.File.writeAtomic 1.131 + // @rejects with the error from OS.File.writeAtomic 1.132 + this._writing = Promise.resolve(0); 1.133 + 1.134 + // Are we currently waiting for a write to complete 1.135 + this.writeInProgress = false; 1.136 + 1.137 + this._path = aPath; 1.138 + this._dataProvider = aDataProvider; 1.139 + 1.140 + this._timer = null; 1.141 + 1.142 + // Some counters for telemetry 1.143 + // The total number of times the file was written 1.144 + this.totalSaves = 0; 1.145 + 1.146 + // The number of times the data became dirty while 1.147 + // another save was in progress 1.148 + this.overlappedSaves = 0; 1.149 + 1.150 + // Error returned by the most recent write (if any) 1.151 + this._lastError = null; 1.152 + 1.153 + if (aDelay && (aDelay > 0)) 1.154 + this._delay = aDelay; 1.155 + else 1.156 + this._delay = DEFAULT_SAVE_DELAY_MS; 1.157 +} 1.158 + 1.159 +this.DeferredSave.prototype = { 1.160 + get dirty() { 1.161 + return this._pending || this.writeInProgress; 1.162 + }, 1.163 + 1.164 + get lastError() { 1.165 + return this._lastError; 1.166 + }, 1.167 + 1.168 + // Start the pending timer if data is dirty 1.169 + _startTimer: function() { 1.170 + if (!this._pending) { 1.171 + return; 1.172 + } 1.173 + 1.174 + this.logger.debug("Starting timer"); 1.175 + if (!this._timer) 1.176 + this._timer = MakeTimer(); 1.177 + this._timer.initWithCallback(() => this._deferredSave(), 1.178 + this._delay, Ci.nsITimer.TYPE_ONE_SHOT); 1.179 + }, 1.180 + 1.181 + /** 1.182 + * Mark the current stored data dirty, and schedule a flush to disk 1.183 + * @return A Promise<integer> that will be resolved after the data is written to disk; 1.184 + * the promise is resolved with the number of bytes written. 1.185 + */ 1.186 + saveChanges: function() { 1.187 + this.logger.debug("Save changes"); 1.188 + if (!this._pending) { 1.189 + if (this.writeInProgress) { 1.190 + this.logger.debug("Data changed while write in progress"); 1.191 + this.overlappedSaves++; 1.192 + } 1.193 + this._pending = Promise.defer(); 1.194 + // Wait until the most recent write completes or fails (if it hasn't already) 1.195 + // and then restart our timer 1.196 + this._writing.then(count => this._startTimer(), error => this._startTimer()); 1.197 + } 1.198 + return this._pending.promise; 1.199 + }, 1.200 + 1.201 + _deferredSave: function() { 1.202 + let pending = this._pending; 1.203 + this._pending = null; 1.204 + let writing = this._writing; 1.205 + this._writing = pending.promise; 1.206 + 1.207 + // In either the success or the exception handling case, we don't need to handle 1.208 + // the error from _writing here; it's already being handled in another then() 1.209 + let toSave = null; 1.210 + try { 1.211 + toSave = this._dataProvider(); 1.212 + } 1.213 + catch(e) { 1.214 + this.logger.error("Deferred save dataProvider failed", e); 1.215 + writing.then(null, error => {}) 1.216 + .then(count => { 1.217 + pending.reject(e); 1.218 + }); 1.219 + return; 1.220 + } 1.221 + 1.222 + writing.then(null, error => {return 0;}) 1.223 + .then(count => { 1.224 + this.logger.debug("Starting write"); 1.225 + this.totalSaves++; 1.226 + this.writeInProgress = true; 1.227 + 1.228 + OS.File.writeAtomic(this._path, toSave, {tmpPath: this._path + ".tmp"}) 1.229 + .then( 1.230 + result => { 1.231 + this._lastError = null; 1.232 + this.writeInProgress = false; 1.233 + this.logger.debug("Write succeeded"); 1.234 + pending.resolve(result); 1.235 + }, 1.236 + error => { 1.237 + this._lastError = error; 1.238 + this.writeInProgress = false; 1.239 + this.logger.warn("Write failed", error); 1.240 + pending.reject(error); 1.241 + }); 1.242 + }); 1.243 + }, 1.244 + 1.245 + /** 1.246 + * Immediately save the dirty data to disk, skipping 1.247 + * the delay of normal operation. Note that the write 1.248 + * still happens asynchronously in the worker 1.249 + * thread from OS.File. 1.250 + * 1.251 + * There are four possible situations: 1.252 + * 1) Nothing to flush 1.253 + * 2) Data is not currently being written, in-memory copy is dirty 1.254 + * 3) Data is currently being written, in-memory copy is clean 1.255 + * 4) Data is being written and in-memory copy is dirty 1.256 + * 1.257 + * @return Promise<integer> that will resolve when all in-memory data 1.258 + * has finished being flushed, returning the number of bytes 1.259 + * written. If all in-memory data is clean, completes with the 1.260 + * result of the most recent write. 1.261 + */ 1.262 + flush: function() { 1.263 + // If we have pending changes, cancel our timer and set up the write 1.264 + // immediately (_deferredSave queues the write for after the most 1.265 + // recent write completes, if it hasn't already) 1.266 + if (this._pending) { 1.267 + this.logger.debug("Flush called while data is dirty"); 1.268 + if (this._timer) { 1.269 + this._timer.cancel(); 1.270 + this._timer = null; 1.271 + } 1.272 + this._deferredSave(); 1.273 + } 1.274 + 1.275 + return this._writing; 1.276 + } 1.277 +};