1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/osfile/modules/osfile_async_front.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1586 @@ 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 +/** 1.9 + * Asynchronous front-end for OS.File. 1.10 + * 1.11 + * This front-end is meant to be imported from the main thread. In turn, 1.12 + * it spawns one worker (perhaps more in the future) and delegates all 1.13 + * disk I/O to this worker. 1.14 + * 1.15 + * Documentation note: most of the functions and methods in this module 1.16 + * return promises. For clarity, we denote as follows a promise that may resolve 1.17 + * with type |A| and some value |value| or reject with type |B| and some 1.18 + * reason |reason| 1.19 + * @resolves {A} value 1.20 + * @rejects {B} reason 1.21 + */ 1.22 + 1.23 +"use strict"; 1.24 + 1.25 +this.EXPORTED_SYMBOLS = ["OS"]; 1.26 + 1.27 +const Cu = Components.utils; 1.28 +const Ci = Components.interfaces; 1.29 + 1.30 +let SharedAll = {}; 1.31 +Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll); 1.32 +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); 1.33 +Cu.import("resource://gre/modules/Timer.jsm", this); 1.34 + 1.35 +XPCOMUtils.defineLazyModuleGetter(this, 'Deprecated', 1.36 + 'resource://gre/modules/Deprecated.jsm'); 1.37 + 1.38 +// Boilerplate, to simplify the transition to require() 1.39 +let LOG = SharedAll.LOG.bind(SharedAll, "Controller"); 1.40 +let isTypedArray = SharedAll.isTypedArray; 1.41 + 1.42 +// The constructor for file errors. 1.43 +let SysAll = {}; 1.44 +if (SharedAll.Constants.Win) { 1.45 + Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll); 1.46 +} else if (SharedAll.Constants.libc) { 1.47 + Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll); 1.48 +} else { 1.49 + throw new Error("I am neither under Windows nor under a Posix system"); 1.50 +} 1.51 +let OSError = SysAll.Error; 1.52 +let Type = SysAll.Type; 1.53 + 1.54 +let Path = {}; 1.55 +Cu.import("resource://gre/modules/osfile/ospath.jsm", Path); 1.56 + 1.57 +// The library of promises. 1.58 +Cu.import("resource://gre/modules/Promise.jsm", this); 1.59 +Cu.import("resource://gre/modules/Task.jsm", this); 1.60 + 1.61 +// The implementation of communications 1.62 +Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this); 1.63 + 1.64 +Cu.import("resource://gre/modules/Services.jsm", this); 1.65 +Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); 1.66 +Cu.import("resource://gre/modules/AsyncShutdown.jsm", this); 1.67 +let Native = Cu.import("resource://gre/modules/osfile/osfile_native.jsm", {}); 1.68 + 1.69 +/** 1.70 + * Constructors for decoding standard exceptions 1.71 + * received from the worker. 1.72 + */ 1.73 +const EXCEPTION_CONSTRUCTORS = { 1.74 + EvalError: function(error) { 1.75 + return new EvalError(error.message, error.fileName, error.lineNumber); 1.76 + }, 1.77 + InternalError: function(error) { 1.78 + return new InternalError(error.message, error.fileName, error.lineNumber); 1.79 + }, 1.80 + RangeError: function(error) { 1.81 + return new RangeError(error.message, error.fileName, error.lineNumber); 1.82 + }, 1.83 + ReferenceError: function(error) { 1.84 + return new ReferenceError(error.message, error.fileName, error.lineNumber); 1.85 + }, 1.86 + SyntaxError: function(error) { 1.87 + return new SyntaxError(error.message, error.fileName, error.lineNumber); 1.88 + }, 1.89 + TypeError: function(error) { 1.90 + return new TypeError(error.message, error.fileName, error.lineNumber); 1.91 + }, 1.92 + URIError: function(error) { 1.93 + return new URIError(error.message, error.fileName, error.lineNumber); 1.94 + }, 1.95 + OSError: function(error) { 1.96 + return OS.File.Error.fromMsg(error); 1.97 + } 1.98 +}; 1.99 + 1.100 +// It's possible for osfile.jsm to get imported before the profile is 1.101 +// set up. In this case, some path constants aren't yet available. 1.102 +// Here, we make them lazy loaders. 1.103 + 1.104 +function lazyPathGetter(constProp, dirKey) { 1.105 + return function () { 1.106 + let path; 1.107 + try { 1.108 + path = Services.dirsvc.get(dirKey, Ci.nsIFile).path; 1.109 + delete SharedAll.Constants.Path[constProp]; 1.110 + SharedAll.Constants.Path[constProp] = path; 1.111 + } catch (ex) { 1.112 + // Ignore errors if the value still isn't available. Hopefully 1.113 + // the next access will return it. 1.114 + } 1.115 + 1.116 + return path; 1.117 + }; 1.118 +} 1.119 + 1.120 +for (let [constProp, dirKey] of [ 1.121 + ["localProfileDir", "ProfLD"], 1.122 + ["profileDir", "ProfD"], 1.123 + ["userApplicationDataDir", "UAppData"], 1.124 + ["winAppDataDir", "AppData"], 1.125 + ["winStartMenuProgsDir", "Progs"], 1.126 + ]) { 1.127 + 1.128 + if (constProp in SharedAll.Constants.Path) { 1.129 + continue; 1.130 + } 1.131 + 1.132 + LOG("Installing lazy getter for OS.Constants.Path." + constProp + 1.133 + " because it isn't defined and profile may not be loaded."); 1.134 + Object.defineProperty(SharedAll.Constants.Path, constProp, { 1.135 + get: lazyPathGetter(constProp, dirKey), 1.136 + }); 1.137 +} 1.138 + 1.139 +/** 1.140 + * Return a shallow clone of the enumerable properties of an object. 1.141 + */ 1.142 +let clone = SharedAll.clone; 1.143 + 1.144 +/** 1.145 + * Extract a shortened version of an object, fit for logging. 1.146 + * 1.147 + * This function returns a copy of the original object in which all 1.148 + * long strings, Arrays, TypedArrays, ArrayBuffers are removed and 1.149 + * replaced with placeholders. Use this function to sanitize objects 1.150 + * if you wish to log them or to keep them in memory. 1.151 + * 1.152 + * @param {*} obj The obj to shorten. 1.153 + * @return {*} array A shorter object, fit for logging. 1.154 + */ 1.155 +function summarizeObject(obj) { 1.156 + if (!obj) { 1.157 + return null; 1.158 + } 1.159 + if (typeof obj == "string") { 1.160 + if (obj.length > 1024) { 1.161 + return {"Long string": obj.length}; 1.162 + } 1.163 + return obj; 1.164 + } 1.165 + if (typeof obj == "object") { 1.166 + if (Array.isArray(obj)) { 1.167 + if (obj.length > 32) { 1.168 + return {"Long array": obj.length}; 1.169 + } 1.170 + return [summarizeObject(k) for (k of obj)]; 1.171 + } 1.172 + if ("byteLength" in obj) { 1.173 + // Assume TypedArray or ArrayBuffer 1.174 + return {"Binary Data": obj.byteLength}; 1.175 + } 1.176 + let result = {}; 1.177 + for (let k of Object.keys(obj)) { 1.178 + result[k] = summarizeObject(obj[k]); 1.179 + } 1.180 + return result; 1.181 + } 1.182 + return obj; 1.183 +} 1.184 + 1.185 +let Scheduler = { 1.186 + 1.187 + /** 1.188 + * |true| once we have sent at least one message to the worker. 1.189 + * This field is unaffected by resetting the worker. 1.190 + */ 1.191 + launched: false, 1.192 + 1.193 + /** 1.194 + * |true| once shutdown has begun i.e. we should reject any 1.195 + * message, including resets. 1.196 + */ 1.197 + shutdown: false, 1.198 + 1.199 + /** 1.200 + * A promise resolved once all operations are complete. 1.201 + * 1.202 + * This promise is never rejected and the result is always undefined. 1.203 + */ 1.204 + queue: Promise.resolve(), 1.205 + 1.206 + /** 1.207 + * Miscellaneous debugging information 1.208 + */ 1.209 + Debugging: { 1.210 + /** 1.211 + * The latest message sent and still waiting for a reply. 1.212 + */ 1.213 + latestSent: undefined, 1.214 + 1.215 + /** 1.216 + * The latest reply received, or null if we are waiting for a reply. 1.217 + */ 1.218 + latestReceived: undefined, 1.219 + 1.220 + /** 1.221 + * Number of messages sent to the worker. This includes the 1.222 + * initial SET_DEBUG, if applicable. 1.223 + */ 1.224 + messagesSent: 0, 1.225 + 1.226 + /** 1.227 + * Total number of messages ever queued, including the messages 1.228 + * sent. 1.229 + */ 1.230 + messagesQueued: 0, 1.231 + 1.232 + /** 1.233 + * Number of messages received from the worker. 1.234 + */ 1.235 + messagesReceived: 0, 1.236 + }, 1.237 + 1.238 + /** 1.239 + * A timer used to automatically shut down the worker after some time. 1.240 + */ 1.241 + resetTimer: null, 1.242 + 1.243 + /** 1.244 + * The worker to which to send requests. 1.245 + * 1.246 + * If the worker has never been created or has been reset, this is a 1.247 + * fresh worker, initialized with osfile_async_worker.js. 1.248 + * 1.249 + * @type {PromiseWorker} 1.250 + */ 1.251 + get worker() { 1.252 + if (!this._worker) { 1.253 + // Either the worker has never been created or it has been reset 1.254 + this._worker = new PromiseWorker( 1.255 + "resource://gre/modules/osfile/osfile_async_worker.js", LOG); 1.256 + } 1.257 + return this._worker; 1.258 + }, 1.259 + 1.260 + _worker: null, 1.261 + 1.262 + /** 1.263 + * Prepare to kill the OS.File worker after a few seconds. 1.264 + */ 1.265 + restartTimer: function(arg) { 1.266 + let delay; 1.267 + try { 1.268 + delay = Services.prefs.getIntPref("osfile.reset_worker_delay"); 1.269 + } catch(e) { 1.270 + // Don't auto-shutdown if we don't have a delay preference set. 1.271 + return; 1.272 + } 1.273 + 1.274 + if (this.resetTimer) { 1.275 + clearTimeout(this.resetTimer); 1.276 + } 1.277 + this.resetTimer = setTimeout( 1.278 + () => Scheduler.kill({reset: true, shutdown: false}), 1.279 + delay 1.280 + ); 1.281 + }, 1.282 + 1.283 + /** 1.284 + * Shutdown OS.File. 1.285 + * 1.286 + * @param {*} options 1.287 + * - {boolean} shutdown If |true|, reject any further request. Otherwise, 1.288 + * further requests will resurrect the worker. 1.289 + * - {boolean} reset If |true|, instruct the worker to shutdown if this 1.290 + * would not cause leaks. Otherwise, assume that the worker will be shutdown 1.291 + * through some other mean. 1.292 + */ 1.293 + kill: function({shutdown, reset}) { 1.294 + return Task.spawn(function*() { 1.295 + 1.296 + yield this.queue; 1.297 + 1.298 + // Enter critical section: no yield in this block 1.299 + // (we want to make sure that we remain the only 1.300 + // request in the queue). 1.301 + 1.302 + if (!this.launched || this.shutdown || !this._worker) { 1.303 + // Nothing to kill 1.304 + this.shutdown = this.shutdown || shutdown; 1.305 + this._worker = null; 1.306 + return null; 1.307 + } 1.308 + 1.309 + // Deactivate the queue, to ensure that no message is sent 1.310 + // to an obsolete worker (we reactivate it in the |finally|). 1.311 + let deferred = Promise.defer(); 1.312 + this.queue = deferred.promise; 1.313 + 1.314 + 1.315 + // Exit critical section 1.316 + 1.317 + let message = ["Meta_shutdown", [reset]]; 1.318 + 1.319 + try { 1.320 + Scheduler.latestReceived = []; 1.321 + Scheduler.latestSent = [Date.now(), ...message]; 1.322 + let promise = this._worker.post(...message); 1.323 + 1.324 + // Wait for result 1.325 + let resources; 1.326 + try { 1.327 + resources = (yield promise).ok; 1.328 + 1.329 + Scheduler.latestReceived = [Date.now(), message]; 1.330 + } catch (ex) { 1.331 + LOG("Could not dispatch Meta_reset", ex); 1.332 + // It's most likely a programmer error, but we'll assume that 1.333 + // the worker has been shutdown, as it's less risky than the 1.334 + // opposite stance. 1.335 + resources = {openedFiles: [], openedDirectoryIterators: [], killed: true}; 1.336 + 1.337 + Scheduler.latestReceived = [Date.now(), message, ex]; 1.338 + } 1.339 + 1.340 + let {openedFiles, openedDirectoryIterators, killed} = resources; 1.341 + if (!reset 1.342 + && (openedFiles && openedFiles.length 1.343 + || ( openedDirectoryIterators && openedDirectoryIterators.length))) { 1.344 + // The worker still holds resources. Report them. 1.345 + 1.346 + let msg = ""; 1.347 + if (openedFiles.length > 0) { 1.348 + msg += "The following files are still open:\n" + 1.349 + openedFiles.join("\n"); 1.350 + } 1.351 + if (openedDirectoryIterators.length > 0) { 1.352 + msg += "The following directory iterators are still open:\n" + 1.353 + openedDirectoryIterators.join("\n"); 1.354 + } 1.355 + 1.356 + LOG("WARNING: File descriptors leaks detected.\n" + msg); 1.357 + } 1.358 + 1.359 + // Make sure that we do not leave an invalid |worker| around. 1.360 + if (killed || shutdown) { 1.361 + this._worker = null; 1.362 + } 1.363 + 1.364 + this.shutdown = shutdown; 1.365 + 1.366 + return resources; 1.367 + 1.368 + } finally { 1.369 + // Resume accepting messages. If we have set |shutdown| to |true|, 1.370 + // any pending/future request will be rejected. Otherwise, any 1.371 + // pending/future request will spawn a new worker if necessary. 1.372 + deferred.resolve(); 1.373 + } 1.374 + 1.375 + }.bind(this)); 1.376 + }, 1.377 + 1.378 + /** 1.379 + * Push a task at the end of the queue. 1.380 + * 1.381 + * @param {function} code A function returning a Promise. 1.382 + * This function will be executed once all the previously 1.383 + * pushed tasks have completed. 1.384 + * @return {Promise} A promise with the same behavior as 1.385 + * the promise returned by |code|. 1.386 + */ 1.387 + push: function(code) { 1.388 + let promise = this.queue.then(code); 1.389 + // By definition, |this.queue| can never reject. 1.390 + this.queue = promise.then(null, () => undefined); 1.391 + // Fork |promise| to ensure that uncaught errors are reported 1.392 + return promise.then(null, null); 1.393 + }, 1.394 + 1.395 + /** 1.396 + * Post a message to the worker thread. 1.397 + * 1.398 + * @param {string} method The name of the method to call. 1.399 + * @param {...} args The arguments to pass to the method. These arguments 1.400 + * must be clonable. 1.401 + * @return {Promise} A promise conveying the result/error caused by 1.402 + * calling |method| with arguments |args|. 1.403 + */ 1.404 + post: function post(method, ...args) { 1.405 + if (this.shutdown) { 1.406 + LOG("OS.File is not available anymore. The following request has been rejected.", 1.407 + method, args); 1.408 + return Promise.reject(new Error("OS.File has been shut down. Rejecting post to " + method)); 1.409 + } 1.410 + let firstLaunch = !this.launched; 1.411 + this.launched = true; 1.412 + 1.413 + if (firstLaunch && SharedAll.Config.DEBUG) { 1.414 + // If we have delayed sending SET_DEBUG, do it now. 1.415 + this.worker.post("SET_DEBUG", [true]); 1.416 + Scheduler.Debugging.messagesSent++; 1.417 + } 1.418 + 1.419 + // By convention, the last argument of any message may be an |options| object. 1.420 + let options; 1.421 + let methodArgs = args[0]; 1.422 + if (methodArgs) { 1.423 + options = methodArgs[methodArgs.length - 1]; 1.424 + } 1.425 + Scheduler.Debugging.messagesQueued++; 1.426 + return this.push(Task.async(function*() { 1.427 + if (this.shutdown) { 1.428 + LOG("OS.File is not available anymore. The following request has been rejected.", 1.429 + method, args); 1.430 + throw new Error("OS.File has been shut down. Rejecting request to " + method); 1.431 + } 1.432 + 1.433 + // Update debugging information. As |args| may be quite 1.434 + // expensive, we only keep a shortened version of it. 1.435 + Scheduler.Debugging.latestReceived = null; 1.436 + Scheduler.Debugging.latestSent = [Date.now(), method, summarizeObject(methodArgs)]; 1.437 + 1.438 + // Don't kill the worker just yet 1.439 + Scheduler.restartTimer(); 1.440 + 1.441 + 1.442 + let data; 1.443 + let reply; 1.444 + let isError = false; 1.445 + try { 1.446 + try { 1.447 + data = yield this.worker.post(method, ...args); 1.448 + } finally { 1.449 + Scheduler.Debugging.messagesReceived++; 1.450 + } 1.451 + reply = data; 1.452 + } catch (error) { 1.453 + reply = error; 1.454 + isError = true; 1.455 + if (error instanceof PromiseWorker.WorkerError) { 1.456 + throw EXCEPTION_CONSTRUCTORS[error.data.exn || "OSError"](error.data); 1.457 + } 1.458 + if (error instanceof ErrorEvent) { 1.459 + let message = error.message; 1.460 + if (message == "uncaught exception: [object StopIteration]") { 1.461 + isError = false; 1.462 + throw StopIteration; 1.463 + } 1.464 + throw new Error(message, error.filename, error.lineno); 1.465 + } 1.466 + throw error; 1.467 + } finally { 1.468 + Scheduler.Debugging.latestSent = Scheduler.Debugging.latestSent.slice(0, 2); 1.469 + if (isError) { 1.470 + Scheduler.Debugging.latestReceived = [Date.now(), reply.message, reply.fileName, reply.lineNumber]; 1.471 + } else { 1.472 + Scheduler.Debugging.latestReceived = [Date.now(), summarizeObject(reply)]; 1.473 + } 1.474 + if (firstLaunch) { 1.475 + Scheduler._updateTelemetry(); 1.476 + } 1.477 + 1.478 + Scheduler.restartTimer(); 1.479 + } 1.480 + 1.481 + // Check for duration and return result. 1.482 + if (!options) { 1.483 + return data.ok; 1.484 + } 1.485 + // Check for options.outExecutionDuration. 1.486 + if (typeof options !== "object" || 1.487 + !("outExecutionDuration" in options)) { 1.488 + return data.ok; 1.489 + } 1.490 + // If data.durationMs is not present, return data.ok (there was an 1.491 + // exception applying the method). 1.492 + if (!("durationMs" in data)) { 1.493 + return data.ok; 1.494 + } 1.495 + // Bug 874425 demonstrates that two successive calls to Date.now() 1.496 + // can actually produce an interval with negative duration. 1.497 + // We assume that this is due to an operation that is so short 1.498 + // that Date.now() is not monotonic, so we round this up to 0. 1.499 + let durationMs = Math.max(0, data.durationMs); 1.500 + // Accumulate (or initialize) outExecutionDuration 1.501 + if (typeof options.outExecutionDuration == "number") { 1.502 + options.outExecutionDuration += durationMs; 1.503 + } else { 1.504 + options.outExecutionDuration = durationMs; 1.505 + } 1.506 + return data.ok; 1.507 + }.bind(this))); 1.508 + }, 1.509 + 1.510 + /** 1.511 + * Post Telemetry statistics. 1.512 + * 1.513 + * This is only useful on first launch. 1.514 + */ 1.515 + _updateTelemetry: function() { 1.516 + let worker = this.worker; 1.517 + let workerTimeStamps = worker.workerTimeStamps; 1.518 + if (!workerTimeStamps) { 1.519 + // If the first call to OS.File results in an uncaught errors, 1.520 + // the timestamps are absent. As this case is a developer error, 1.521 + // let's not waste time attempting to extract telemetry from it. 1.522 + return; 1.523 + } 1.524 + let HISTOGRAM_LAUNCH = Services.telemetry.getHistogramById("OSFILE_WORKER_LAUNCH_MS"); 1.525 + HISTOGRAM_LAUNCH.add(worker.workerTimeStamps.entered - worker.launchTimeStamp); 1.526 + 1.527 + let HISTOGRAM_READY = Services.telemetry.getHistogramById("OSFILE_WORKER_READY_MS"); 1.528 + HISTOGRAM_READY.add(worker.workerTimeStamps.loaded - worker.launchTimeStamp); 1.529 + } 1.530 +}; 1.531 + 1.532 +const PREF_OSFILE_LOG = "toolkit.osfile.log"; 1.533 +const PREF_OSFILE_LOG_REDIRECT = "toolkit.osfile.log.redirect"; 1.534 + 1.535 +/** 1.536 + * Safely read a PREF_OSFILE_LOG preference. 1.537 + * Returns a value read or, in case of an error, oldPref or false. 1.538 + * 1.539 + * @param bool oldPref 1.540 + * An optional value that the DEBUG flag was set to previously. 1.541 + */ 1.542 +function readDebugPref(prefName, oldPref = false) { 1.543 + let pref = oldPref; 1.544 + try { 1.545 + pref = Services.prefs.getBoolPref(prefName); 1.546 + } catch (x) { 1.547 + // In case of an error when reading a pref keep it as is. 1.548 + } 1.549 + // If neither pref nor oldPref were set, default it to false. 1.550 + return pref; 1.551 +}; 1.552 + 1.553 +/** 1.554 + * Listen to PREF_OSFILE_LOG changes and update gShouldLog flag 1.555 + * appropriately. 1.556 + */ 1.557 +Services.prefs.addObserver(PREF_OSFILE_LOG, 1.558 + function prefObserver(aSubject, aTopic, aData) { 1.559 + SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, SharedAll.Config.DEBUG); 1.560 + if (Scheduler.launched) { 1.561 + // Don't start the worker just to set this preference. 1.562 + Scheduler.post("SET_DEBUG", [SharedAll.Config.DEBUG]); 1.563 + } 1.564 + }, false); 1.565 +SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, false); 1.566 + 1.567 +Services.prefs.addObserver(PREF_OSFILE_LOG_REDIRECT, 1.568 + function prefObserver(aSubject, aTopic, aData) { 1.569 + SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, OS.Shared.TEST); 1.570 + }, false); 1.571 +SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, false); 1.572 + 1.573 + 1.574 +/** 1.575 + * If |true|, use the native implementaiton of OS.File methods 1.576 + * whenever possible. Otherwise, force the use of the JS version. 1.577 + */ 1.578 +let nativeWheneverAvailable = true; 1.579 +const PREF_OSFILE_NATIVE = "toolkit.osfile.native"; 1.580 +Services.prefs.addObserver(PREF_OSFILE_NATIVE, 1.581 + function prefObserver(aSubject, aTopic, aData) { 1.582 + nativeWheneverAvailable = readDebugPref(PREF_OSFILE_NATIVE, nativeWheneverAvailable); 1.583 + }, false); 1.584 + 1.585 + 1.586 +// Update worker's DEBUG flag if it's true. 1.587 +// Don't start the worker just for this, though. 1.588 +if (SharedAll.Config.DEBUG && Scheduler.launched) { 1.589 + Scheduler.post("SET_DEBUG", [true]); 1.590 +} 1.591 + 1.592 +// Observer topics used for monitoring shutdown 1.593 +const WEB_WORKERS_SHUTDOWN_TOPIC = "web-workers-shutdown"; 1.594 + 1.595 +// Preference used to configure test shutdown observer. 1.596 +const PREF_OSFILE_TEST_SHUTDOWN_OBSERVER = 1.597 + "toolkit.osfile.test.shutdown.observer"; 1.598 + 1.599 +AsyncShutdown.webWorkersShutdown.addBlocker( 1.600 + "OS.File: flush pending requests, warn about unclosed files, shut down service.", 1.601 + () => Scheduler.kill({reset: false, shutdown: true}) 1.602 +); 1.603 + 1.604 + 1.605 +// Attaching an observer for PREF_OSFILE_TEST_SHUTDOWN_OBSERVER to enable or 1.606 +// disable the test shutdown event observer. 1.607 +// Note: By default the PREF_OSFILE_TEST_SHUTDOWN_OBSERVER is unset. 1.608 +// Note: This is meant to be used for testing purposes only. 1.609 +Services.prefs.addObserver(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER, 1.610 + function prefObserver() { 1.611 + // The temporary phase topic used to trigger the unclosed 1.612 + // phase warning. 1.613 + let TOPIC = null; 1.614 + try { 1.615 + TOPIC = Services.prefs.getCharPref( 1.616 + PREF_OSFILE_TEST_SHUTDOWN_OBSERVER); 1.617 + } catch (x) { 1.618 + } 1.619 + if (TOPIC) { 1.620 + // Generate a phase, add a blocker. 1.621 + // Note that this can work only if AsyncShutdown itself has been 1.622 + // configured for testing by the testsuite. 1.623 + let phase = AsyncShutdown._getPhase(TOPIC); 1.624 + phase.addBlocker( 1.625 + "(for testing purposes) OS.File: warn about unclosed files", 1.626 + () => Scheduler.kill({shutdown: false, reset: false}) 1.627 + ); 1.628 + } 1.629 + }, false); 1.630 + 1.631 +/** 1.632 + * Representation of a file, with asynchronous methods. 1.633 + * 1.634 + * @param {*} fdmsg The _message_ representing the platform-specific file 1.635 + * handle. 1.636 + * 1.637 + * @constructor 1.638 + */ 1.639 +let File = function File(fdmsg) { 1.640 + // FIXME: At the moment, |File| does not close on finalize 1.641 + // (see bug 777715) 1.642 + this._fdmsg = fdmsg; 1.643 + this._closeResult = null; 1.644 + this._closed = null; 1.645 +}; 1.646 + 1.647 + 1.648 +File.prototype = { 1.649 + /** 1.650 + * Close a file asynchronously. 1.651 + * 1.652 + * This method is idempotent. 1.653 + * 1.654 + * @return {promise} 1.655 + * @resolves {null} 1.656 + * @rejects {OS.File.Error} 1.657 + */ 1.658 + close: function close() { 1.659 + if (this._fdmsg != null) { 1.660 + let msg = this._fdmsg; 1.661 + this._fdmsg = null; 1.662 + return this._closeResult = 1.663 + Scheduler.post("File_prototype_close", [msg], this); 1.664 + } 1.665 + return this._closeResult; 1.666 + }, 1.667 + 1.668 + /** 1.669 + * Fetch information about the file. 1.670 + * 1.671 + * @return {promise} 1.672 + * @resolves {OS.File.Info} The latest information about the file. 1.673 + * @rejects {OS.File.Error} 1.674 + */ 1.675 + stat: function stat() { 1.676 + return Scheduler.post("File_prototype_stat", [this._fdmsg], this).then( 1.677 + File.Info.fromMsg 1.678 + ); 1.679 + }, 1.680 + 1.681 + /** 1.682 + * Set the last access and modification date of the file. 1.683 + * The time stamp resolution is 1 second at best, but might be worse 1.684 + * depending on the platform. 1.685 + * 1.686 + * @return {promise} 1.687 + * @rejects {TypeError} 1.688 + * @rejects {OS.File.Error} 1.689 + */ 1.690 + setDates: function setDates(accessDate, modificationDate) { 1.691 + return Scheduler.post("File_prototype_setDates", 1.692 + [this._fdmsg, accessDate, modificationDate], this); 1.693 + }, 1.694 + 1.695 + /** 1.696 + * Read a number of bytes from the file and into a buffer. 1.697 + * 1.698 + * @param {Typed array | C pointer} buffer This buffer will be 1.699 + * modified by another thread. Using this buffer before the |read| 1.700 + * operation has completed is a BAD IDEA. 1.701 + * @param {JSON} options 1.702 + * 1.703 + * @return {promise} 1.704 + * @resolves {number} The number of bytes effectively read. 1.705 + * @rejects {OS.File.Error} 1.706 + */ 1.707 + readTo: function readTo(buffer, options = {}) { 1.708 + // If |buffer| is a typed array and there is no |bytes| options, we 1.709 + // need to extract the |byteLength| now, as it will be lost by 1.710 + // communication. 1.711 + // Options might be a nullish value, so better check for that before using 1.712 + // the |in| operator. 1.713 + if (isTypedArray(buffer) && !(options && "bytes" in options)) { 1.714 + // Preserve reference to option |outExecutionDuration|, if it is passed. 1.715 + options = clone(options, ["outExecutionDuration"]); 1.716 + options.bytes = buffer.byteLength; 1.717 + } 1.718 + // Note: Type.void_t.out_ptr.toMsg ensures that 1.719 + // - the buffer is effectively shared (not neutered) between both 1.720 + // threads; 1.721 + // - we take care of any |byteOffset|. 1.722 + return Scheduler.post("File_prototype_readTo", 1.723 + [this._fdmsg, 1.724 + Type.void_t.out_ptr.toMsg(buffer), 1.725 + options], 1.726 + buffer/*Ensure that |buffer| is not gc-ed*/); 1.727 + }, 1.728 + /** 1.729 + * Write bytes from a buffer to this file. 1.730 + * 1.731 + * Note that, by default, this function may perform several I/O 1.732 + * operations to ensure that the buffer is fully written. 1.733 + * 1.734 + * @param {Typed array | C pointer} buffer The buffer in which the 1.735 + * the bytes are stored. The buffer must be large enough to 1.736 + * accomodate |bytes| bytes. Using the buffer before the operation 1.737 + * is complete is a BAD IDEA. 1.738 + * @param {*=} options Optionally, an object that may contain the 1.739 + * following fields: 1.740 + * - {number} bytes The number of |bytes| to write from the buffer. If 1.741 + * unspecified, this is |buffer.byteLength|. Note that |bytes| is required 1.742 + * if |buffer| is a C pointer. 1.743 + * 1.744 + * @return {number} The number of bytes actually written. 1.745 + */ 1.746 + write: function write(buffer, options = {}) { 1.747 + // If |buffer| is a typed array and there is no |bytes| options, 1.748 + // we need to extract the |byteLength| now, as it will be lost 1.749 + // by communication. 1.750 + // Options might be a nullish value, so better check for that before using 1.751 + // the |in| operator. 1.752 + if (isTypedArray(buffer) && !(options && "bytes" in options)) { 1.753 + // Preserve reference to option |outExecutionDuration|, if it is passed. 1.754 + options = clone(options, ["outExecutionDuration"]); 1.755 + options.bytes = buffer.byteLength; 1.756 + } 1.757 + // Note: Type.void_t.out_ptr.toMsg ensures that 1.758 + // - the buffer is effectively shared (not neutered) between both 1.759 + // threads; 1.760 + // - we take care of any |byteOffset|. 1.761 + return Scheduler.post("File_prototype_write", 1.762 + [this._fdmsg, 1.763 + Type.void_t.in_ptr.toMsg(buffer), 1.764 + options], 1.765 + buffer/*Ensure that |buffer| is not gc-ed*/); 1.766 + }, 1.767 + 1.768 + /** 1.769 + * Read bytes from this file to a new buffer. 1.770 + * 1.771 + * @param {number=} bytes If unspecified, read all the remaining bytes from 1.772 + * this file. If specified, read |bytes| bytes, or less if the file does not 1.773 + * contain that many bytes. 1.774 + * @param {JSON} options 1.775 + * @return {promise} 1.776 + * @resolves {Uint8Array} An array containing the bytes read. 1.777 + */ 1.778 + read: function read(nbytes, options = {}) { 1.779 + let promise = Scheduler.post("File_prototype_read", 1.780 + [this._fdmsg, 1.781 + nbytes, options]); 1.782 + return promise.then( 1.783 + function onSuccess(data) { 1.784 + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); 1.785 + }); 1.786 + }, 1.787 + 1.788 + /** 1.789 + * Return the current position in the file, as bytes. 1.790 + * 1.791 + * @return {promise} 1.792 + * @resolves {number} The current position in the file, 1.793 + * as a number of bytes since the start of the file. 1.794 + */ 1.795 + getPosition: function getPosition() { 1.796 + return Scheduler.post("File_prototype_getPosition", 1.797 + [this._fdmsg]); 1.798 + }, 1.799 + 1.800 + /** 1.801 + * Set the current position in the file, as bytes. 1.802 + * 1.803 + * @param {number} pos A number of bytes. 1.804 + * @param {number} whence The reference position in the file, 1.805 + * which may be either POS_START (from the start of the file), 1.806 + * POS_END (from the end of the file) or POS_CUR (from the 1.807 + * current position in the file). 1.808 + * 1.809 + * @return {promise} 1.810 + */ 1.811 + setPosition: function setPosition(pos, whence) { 1.812 + return Scheduler.post("File_prototype_setPosition", 1.813 + [this._fdmsg, pos, whence]); 1.814 + }, 1.815 + 1.816 + /** 1.817 + * Flushes the file's buffers and causes all buffered data 1.818 + * to be written. 1.819 + * Disk flushes are very expensive and therefore should be used carefully, 1.820 + * sparingly and only in scenarios where it is vital that data survives 1.821 + * system crashes. Even though the function will be executed off the 1.822 + * main-thread, it might still affect the overall performance of any running 1.823 + * application. 1.824 + * 1.825 + * @return {promise} 1.826 + */ 1.827 + flush: function flush() { 1.828 + return Scheduler.post("File_prototype_flush", 1.829 + [this._fdmsg]); 1.830 + }, 1.831 + 1.832 + /** 1.833 + * Set the file's access permissions. Without any options, the 1.834 + * permissions are set to an approximation of what they would have 1.835 + * been if the file had been created in its current directory in the 1.836 + * "most typical" fashion for the operating system. In the current 1.837 + * implementation, this means that on Unix-like systems (including 1.838 + * Android, B2G, etc) we set the POSIX file mode to (0666 & ~umask), 1.839 + * and on Windows, we do nothing. 1.840 + * 1.841 + * This operation is likely to fail if applied to a file that was 1.842 + * not created by the currently running program (more precisely, 1.843 + * if it was created by a program running under a different OS-level 1.844 + * user account). It may also fail, or silently do nothing, if the 1.845 + * filesystem containing the file does not support access permissions. 1.846 + * 1.847 + * @param {*=} options 1.848 + * - {number} unixMode If present, the POSIX file mode is set to exactly 1.849 + * this value, unless |unixHonorUmask| is also 1.850 + * present. 1.851 + * - {bool} unixHonorUmask If true, any |unixMode| value is modified by the 1.852 + * process umask, as open() would have done. 1.853 + */ 1.854 + setPermissions: function setPermissions(options = {}) { 1.855 + return Scheduler.post("File_prototype_setPermissions", 1.856 + [this._fdmsg, options]); 1.857 + } 1.858 +}; 1.859 + 1.860 +/** 1.861 + * Open a file asynchronously. 1.862 + * 1.863 + * @return {promise} 1.864 + * @resolves {OS.File} 1.865 + * @rejects {OS.Error} 1.866 + */ 1.867 +File.open = function open(path, mode, options) { 1.868 + return Scheduler.post( 1.869 + "open", [Type.path.toMsg(path), mode, options], 1.870 + path 1.871 + ).then( 1.872 + function onSuccess(msg) { 1.873 + return new File(msg); 1.874 + } 1.875 + ); 1.876 +}; 1.877 + 1.878 +/** 1.879 + * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name. 1.880 + * 1.881 + * @param {string} path The path to the file. 1.882 + * @param {*=} options Additional options for file opening. This 1.883 + * implementation interprets the following fields: 1.884 + * 1.885 + * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext. 1.886 + * If |false| use HEX numbers ie: filename-A65BC0.ext 1.887 + * - {number} maxReadableNumber Used to limit the amount of tries after a failed 1.888 + * file creation. Default is 20. 1.889 + * 1.890 + * @return {Object} contains A file object{file} and the path{path}. 1.891 + * @throws {OS.File.Error} If the file could not be opened. 1.892 + */ 1.893 +File.openUnique = function openUnique(path, options) { 1.894 + return Scheduler.post( 1.895 + "openUnique", [Type.path.toMsg(path), options], 1.896 + path 1.897 + ).then( 1.898 + function onSuccess(msg) { 1.899 + return { 1.900 + path: msg.path, 1.901 + file: new File(msg.file) 1.902 + }; 1.903 + } 1.904 + ); 1.905 +}; 1.906 + 1.907 +/** 1.908 + * Get the information on the file. 1.909 + * 1.910 + * @return {promise} 1.911 + * @resolves {OS.File.Info} 1.912 + * @rejects {OS.Error} 1.913 + */ 1.914 +File.stat = function stat(path, options) { 1.915 + return Scheduler.post( 1.916 + "stat", [Type.path.toMsg(path), options], 1.917 + path).then(File.Info.fromMsg); 1.918 +}; 1.919 + 1.920 + 1.921 +/** 1.922 + * Set the last access and modification date of the file. 1.923 + * The time stamp resolution is 1 second at best, but might be worse 1.924 + * depending on the platform. 1.925 + * 1.926 + * @return {promise} 1.927 + * @rejects {TypeError} 1.928 + * @rejects {OS.File.Error} 1.929 + */ 1.930 +File.setDates = function setDates(path, accessDate, modificationDate) { 1.931 + return Scheduler.post("setDates", 1.932 + [Type.path.toMsg(path), accessDate, modificationDate], 1.933 + this); 1.934 +}; 1.935 + 1.936 +/** 1.937 + * Set the file's access permissions. Without any options, the 1.938 + * permissions are set to an approximation of what they would have 1.939 + * been if the file had been created in its current directory in the 1.940 + * "most typical" fashion for the operating system. In the current 1.941 + * implementation, this means that on Unix-like systems (including 1.942 + * Android, B2G, etc) we set the POSIX file mode to (0666 & ~umask), 1.943 + * and on Windows, we do nothing. 1.944 + * 1.945 + * This operation is likely to fail if applied to a file that was 1.946 + * not created by the currently running program (more precisely, 1.947 + * if it was created by a program running under a different OS-level 1.948 + * user account). It may also fail, or silently do nothing, if the 1.949 + * filesystem containing the file does not support access permissions. 1.950 + * 1.951 + * @param {string} path The path to the file. 1.952 + * 1.953 + * @param {*=} options 1.954 + * - {number} unixMode If present, the POSIX file mode is set to exactly 1.955 + * this value, unless |unixHonorUmask| is also 1.956 + * present. 1.957 + * - {bool} unixHonorUmask If true, any |unixMode| value is modified by the 1.958 + * process umask, as open() would have done. 1.959 + */ 1.960 +File.setPermissions = function setPermissions(path, options = {}) { 1.961 + return Scheduler.post("setPermissions", 1.962 + [Type.path.toMsg(path), options]); 1.963 +}; 1.964 + 1.965 +/** 1.966 + * Fetch the current directory 1.967 + * 1.968 + * @return {promise} 1.969 + * @resolves {string} The current directory, as a path usable with OS.Path 1.970 + * @rejects {OS.Error} 1.971 + */ 1.972 +File.getCurrentDirectory = function getCurrentDirectory() { 1.973 + return Scheduler.post( 1.974 + "getCurrentDirectory" 1.975 + ).then(Type.path.fromMsg); 1.976 +}; 1.977 + 1.978 +/** 1.979 + * Change the current directory 1.980 + * 1.981 + * @param {string} path The OS-specific path to the current directory. 1.982 + * You should use the methods of OS.Path and the constants of OS.Constants.Path 1.983 + * to build OS-specific paths in a portable manner. 1.984 + * 1.985 + * @return {promise} 1.986 + * @resolves {null} 1.987 + * @rejects {OS.Error} 1.988 + */ 1.989 +File.setCurrentDirectory = function setCurrentDirectory(path) { 1.990 + return Scheduler.post( 1.991 + "setCurrentDirectory", [Type.path.toMsg(path)], path 1.992 + ); 1.993 +}; 1.994 + 1.995 +/** 1.996 + * Copy a file to a destination. 1.997 + * 1.998 + * @param {string} sourcePath The platform-specific path at which 1.999 + * the file may currently be found. 1.1000 + * @param {string} destPath The platform-specific path at which the 1.1001 + * file should be copied. 1.1002 + * @param {*=} options An object which may contain the following fields: 1.1003 + * 1.1004 + * @option {bool} noOverwrite - If true, this function will fail if 1.1005 + * a file already exists at |destPath|. Otherwise, if this file exists, 1.1006 + * it will be erased silently. 1.1007 + * 1.1008 + * @rejects {OS.File.Error} In case of any error. 1.1009 + * 1.1010 + * General note: The behavior of this function is defined only when 1.1011 + * it is called on a single file. If it is called on a directory, the 1.1012 + * behavior is undefined and may not be the same across all platforms. 1.1013 + * 1.1014 + * General note: The behavior of this function with respect to metadata 1.1015 + * is unspecified. Metadata may or may not be copied with the file. The 1.1016 + * behavior may not be the same across all platforms. 1.1017 +*/ 1.1018 +File.copy = function copy(sourcePath, destPath, options) { 1.1019 + return Scheduler.post("copy", [Type.path.toMsg(sourcePath), 1.1020 + Type.path.toMsg(destPath), options], [sourcePath, destPath]); 1.1021 +}; 1.1022 + 1.1023 +/** 1.1024 + * Move a file to a destination. 1.1025 + * 1.1026 + * @param {string} sourcePath The platform-specific path at which 1.1027 + * the file may currently be found. 1.1028 + * @param {string} destPath The platform-specific path at which the 1.1029 + * file should be moved. 1.1030 + * @param {*=} options An object which may contain the following fields: 1.1031 + * 1.1032 + * @option {bool} noOverwrite - If set, this function will fail if 1.1033 + * a file already exists at |destPath|. Otherwise, if this file exists, 1.1034 + * it will be erased silently. 1.1035 + * 1.1036 + * @returns {Promise} 1.1037 + * @rejects {OS.File.Error} In case of any error. 1.1038 + * 1.1039 + * General note: The behavior of this function is defined only when 1.1040 + * it is called on a single file. If it is called on a directory, the 1.1041 + * behavior is undefined and may not be the same across all platforms. 1.1042 + * 1.1043 + * General note: The behavior of this function with respect to metadata 1.1044 + * is unspecified. Metadata may or may not be moved with the file. The 1.1045 + * behavior may not be the same across all platforms. 1.1046 + */ 1.1047 +File.move = function move(sourcePath, destPath, options) { 1.1048 + return Scheduler.post("move", [Type.path.toMsg(sourcePath), 1.1049 + Type.path.toMsg(destPath), options], [sourcePath, destPath]); 1.1050 +}; 1.1051 + 1.1052 +/** 1.1053 + * Create a symbolic link to a source. 1.1054 + * 1.1055 + * @param {string} sourcePath The platform-specific path to which 1.1056 + * the symbolic link should point. 1.1057 + * @param {string} destPath The platform-specific path at which the 1.1058 + * symbolic link should be created. 1.1059 + * 1.1060 + * @returns {Promise} 1.1061 + * @rejects {OS.File.Error} In case of any error. 1.1062 + */ 1.1063 +if (!SharedAll.Constants.Win) { 1.1064 + File.unixSymLink = function unixSymLink(sourcePath, destPath) { 1.1065 + return Scheduler.post("unixSymLink", [Type.path.toMsg(sourcePath), 1.1066 + Type.path.toMsg(destPath)], [sourcePath, destPath]); 1.1067 + }; 1.1068 +} 1.1069 + 1.1070 +/** 1.1071 + * Gets the number of bytes available on disk to the current user. 1.1072 + * 1.1073 + * @param {string} Platform-specific path to a directory on the disk to 1.1074 + * query for free available bytes. 1.1075 + * 1.1076 + * @return {number} The number of bytes available for the current user. 1.1077 + * @throws {OS.File.Error} In case of any error. 1.1078 + */ 1.1079 +File.getAvailableFreeSpace = function getAvailableFreeSpace(sourcePath) { 1.1080 + return Scheduler.post("getAvailableFreeSpace", 1.1081 + [Type.path.toMsg(sourcePath)], sourcePath 1.1082 + ).then(Type.uint64_t.fromMsg); 1.1083 +}; 1.1084 + 1.1085 +/** 1.1086 + * Remove an empty directory. 1.1087 + * 1.1088 + * @param {string} path The name of the directory to remove. 1.1089 + * @param {*=} options Additional options. 1.1090 + * - {bool} ignoreAbsent If |true|, do not fail if the 1.1091 + * directory does not exist yet. 1.1092 + */ 1.1093 +File.removeEmptyDir = function removeEmptyDir(path, options) { 1.1094 + return Scheduler.post("removeEmptyDir", 1.1095 + [Type.path.toMsg(path), options], path); 1.1096 +}; 1.1097 + 1.1098 +/** 1.1099 + * Remove an existing file. 1.1100 + * 1.1101 + * @param {string} path The name of the file. 1.1102 + */ 1.1103 +File.remove = function remove(path) { 1.1104 + return Scheduler.post("remove", 1.1105 + [Type.path.toMsg(path)]); 1.1106 +}; 1.1107 + 1.1108 + 1.1109 + 1.1110 +/** 1.1111 + * Create a directory and, optionally, its parent directories. 1.1112 + * 1.1113 + * @param {string} path The name of the directory. 1.1114 + * @param {*=} options Additional options. 1.1115 + * 1.1116 + * - {string} from If specified, the call to |makeDir| creates all the 1.1117 + * ancestors of |path| that are descendants of |from|. Note that |path| 1.1118 + * must be a descendant of |from|, and that |from| and its existing 1.1119 + * subdirectories present in |path| must be user-writeable. 1.1120 + * Example: 1.1121 + * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir }); 1.1122 + * creates directories profileDir/foo, profileDir/foo/bar 1.1123 + * - {bool} ignoreExisting If |false|, throw an error if the directory 1.1124 + * already exists. |true| by default. Ignored if |from| is specified. 1.1125 + * - {number} unixMode Under Unix, if specified, a file creation mode, 1.1126 + * as per libc function |mkdir|. If unspecified, dirs are 1.1127 + * created with a default mode of 0700 (dir is private to 1.1128 + * the user, the user can read, write and execute). Ignored under Windows 1.1129 + * or if the file system does not support file creation modes. 1.1130 + * - {C pointer} winSecurity Under Windows, if specified, security 1.1131 + * attributes as per winapi function |CreateDirectory|. If 1.1132 + * unspecified, use the default security descriptor, inherited from 1.1133 + * the parent directory. Ignored under Unix or if the file system 1.1134 + * does not support security descriptors. 1.1135 + */ 1.1136 +File.makeDir = function makeDir(path, options) { 1.1137 + return Scheduler.post("makeDir", 1.1138 + [Type.path.toMsg(path), options], path); 1.1139 +}; 1.1140 + 1.1141 +/** 1.1142 + * Return the contents of a file 1.1143 + * 1.1144 + * @param {string} path The path to the file. 1.1145 + * @param {number=} bytes Optionally, an upper bound to the number of bytes 1.1146 + * to read. DEPRECATED - please use options.bytes instead. 1.1147 + * @param {JSON} options Additional options. 1.1148 + * - {boolean} sequential A flag that triggers a population of the page cache 1.1149 + * with data from a file so that subsequent reads from that file would not 1.1150 + * block on disk I/O. If |true| or unspecified, inform the system that the 1.1151 + * contents of the file will be read in order. Otherwise, make no such 1.1152 + * assumption. |true| by default. 1.1153 + * - {number} bytes An upper bound to the number of bytes to read. 1.1154 + * - {string} compression If "lz4" and if the file is compressed using the lz4 1.1155 + * compression algorithm, decompress the file contents on the fly. 1.1156 + * 1.1157 + * @resolves {Uint8Array} A buffer holding the bytes 1.1158 + * read from the file. 1.1159 + */ 1.1160 +File.read = function read(path, bytes, options = {}) { 1.1161 + if (typeof bytes == "object") { 1.1162 + // Passing |bytes| as an argument is deprecated. 1.1163 + // We should now be passing it as a field of |options|. 1.1164 + options = bytes || {}; 1.1165 + } else { 1.1166 + options = clone(options, ["outExecutionDuration"]); 1.1167 + if (typeof bytes != "undefined") { 1.1168 + options.bytes = bytes; 1.1169 + } 1.1170 + } 1.1171 + 1.1172 + if (options.compression || !nativeWheneverAvailable) { 1.1173 + // We need to use the JS implementation. 1.1174 + let promise = Scheduler.post("read", 1.1175 + [Type.path.toMsg(path), bytes, options], path); 1.1176 + return promise.then( 1.1177 + function onSuccess(data) { 1.1178 + if (typeof data == "string") { 1.1179 + return data; 1.1180 + } 1.1181 + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); 1.1182 + }); 1.1183 + } 1.1184 + 1.1185 + // Otherwise, use the native implementation. 1.1186 + return Scheduler.push(() => Native.read(path, options)); 1.1187 +}; 1.1188 + 1.1189 +/** 1.1190 + * Find outs if a file exists. 1.1191 + * 1.1192 + * @param {string} path The path to the file. 1.1193 + * 1.1194 + * @return {bool} true if the file exists, false otherwise. 1.1195 + */ 1.1196 +File.exists = function exists(path) { 1.1197 + return Scheduler.post("exists", 1.1198 + [Type.path.toMsg(path)], path); 1.1199 +}; 1.1200 + 1.1201 +/** 1.1202 + * Write a file, atomically. 1.1203 + * 1.1204 + * By opposition to a regular |write|, this operation ensures that, 1.1205 + * until the contents are fully written, the destination file is 1.1206 + * not modified. 1.1207 + * 1.1208 + * Limitation: In a few extreme cases (hardware failure during the 1.1209 + * write, user unplugging disk during the write, etc.), data may be 1.1210 + * corrupted. If your data is user-critical (e.g. preferences, 1.1211 + * application data, etc.), you may wish to consider adding options 1.1212 + * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as 1.1213 + * detailed below. Note that no combination of options can be 1.1214 + * guaranteed to totally eliminate the risk of corruption. 1.1215 + * 1.1216 + * @param {string} path The path of the file to modify. 1.1217 + * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write. 1.1218 + * @param {*=} options Optionally, an object determining the behavior 1.1219 + * of this function. This object may contain the following fields: 1.1220 + * - {number} bytes The number of bytes to write. If unspecified, 1.1221 + * |buffer.byteLength|. Required if |buffer| is a C pointer. 1.1222 + * - {string} tmpPath If |null| or unspecified, write all data directly 1.1223 + * to |path|. If specified, write all data to a temporary file called 1.1224 + * |tmpPath| and, once this write is complete, rename the file to 1.1225 + * replace |path|. Performing this additional operation is a little 1.1226 + * slower but also a little safer. 1.1227 + * - {bool} noOverwrite - If set, this function will fail if a file already 1.1228 + * exists at |path|. 1.1229 + * - {bool} flush - If |false| or unspecified, return immediately once the 1.1230 + * write is complete. If |true|, before writing, force the operating system 1.1231 + * to write its internal disk buffers to the disk. This is considerably slower 1.1232 + * (not just for the application but for the whole system) but also safer: 1.1233 + * if the system shuts down improperly (typically due to a kernel freeze 1.1234 + * or a power failure) or if the device is disconnected before the buffer 1.1235 + * is flushed, the file has more chances of not being corrupted. 1.1236 + * - {string} backupTo - If specified, backup the destination file as |backupTo|. 1.1237 + * Note that this function renames the destination file before overwriting it. 1.1238 + * If the process or the operating system freezes or crashes 1.1239 + * during the short window between these operations, 1.1240 + * the destination file will have been moved to its backup. 1.1241 + * 1.1242 + * @return {promise} 1.1243 + * @resolves {number} The number of bytes actually written. 1.1244 + */ 1.1245 +File.writeAtomic = function writeAtomic(path, buffer, options = {}) { 1.1246 + // Copy |options| to avoid modifying the original object but preserve the 1.1247 + // reference to |outExecutionDuration| option if it is passed. 1.1248 + options = clone(options, ["outExecutionDuration"]); 1.1249 + // As options.tmpPath is a path, we need to encode it as |Type.path| message 1.1250 + if ("tmpPath" in options) { 1.1251 + options.tmpPath = Type.path.toMsg(options.tmpPath); 1.1252 + }; 1.1253 + if (isTypedArray(buffer) && (!("bytes" in options))) { 1.1254 + options.bytes = buffer.byteLength; 1.1255 + }; 1.1256 + // Note: Type.void_t.out_ptr.toMsg ensures that 1.1257 + // - the buffer is effectively shared (not neutered) between both 1.1258 + // threads; 1.1259 + // - we take care of any |byteOffset|. 1.1260 + let refObj = {}; 1.1261 + TelemetryStopwatch.start("OSFILE_WRITEATOMIC_JANK_MS", refObj); 1.1262 + let promise = Scheduler.post("writeAtomic", 1.1263 + [Type.path.toMsg(path), 1.1264 + Type.void_t.in_ptr.toMsg(buffer), 1.1265 + options], [options, buffer]); 1.1266 + TelemetryStopwatch.finish("OSFILE_WRITEATOMIC_JANK_MS", refObj); 1.1267 + return promise; 1.1268 +}; 1.1269 + 1.1270 +File.removeDir = function(path, options = {}) { 1.1271 + return Scheduler.post("removeDir", 1.1272 + [Type.path.toMsg(path), options], path); 1.1273 +}; 1.1274 + 1.1275 +/** 1.1276 + * Information on a file, as returned by OS.File.stat or 1.1277 + * OS.File.prototype.stat 1.1278 + * 1.1279 + * @constructor 1.1280 + */ 1.1281 +File.Info = function Info(value) { 1.1282 + // Note that we can't just do this[k] = value[k] because our 1.1283 + // prototype defines getters for all of these fields. 1.1284 + for (let k in value) { 1.1285 + if (k != "creationDate") { 1.1286 + Object.defineProperty(this, k, {value: value[k]}); 1.1287 + } 1.1288 + } 1.1289 + Object.defineProperty(this, "_deprecatedCreationDate", {value: value["creationDate"]}); 1.1290 +}; 1.1291 +File.Info.prototype = SysAll.AbstractInfo.prototype; 1.1292 + 1.1293 +// Deprecated 1.1294 +Object.defineProperty(File.Info.prototype, "creationDate", { 1.1295 + get: function creationDate() { 1.1296 + Deprecated.warning("Field 'creationDate' is deprecated.", "https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info#Cross-platform_Attributes"); 1.1297 + return this._deprecatedCreationDate; 1.1298 + } 1.1299 +}); 1.1300 + 1.1301 +File.Info.fromMsg = function fromMsg(value) { 1.1302 + return new File.Info(value); 1.1303 +}; 1.1304 + 1.1305 +/** 1.1306 + * Get worker's current DEBUG flag. 1.1307 + * Note: This is used for testing purposes. 1.1308 + */ 1.1309 +File.GET_DEBUG = function GET_DEBUG() { 1.1310 + return Scheduler.post("GET_DEBUG"); 1.1311 +}; 1.1312 + 1.1313 +/** 1.1314 + * Iterate asynchronously through a directory 1.1315 + * 1.1316 + * @constructor 1.1317 + */ 1.1318 +let DirectoryIterator = function DirectoryIterator(path, options) { 1.1319 + /** 1.1320 + * Open the iterator on the worker thread 1.1321 + * 1.1322 + * @type {Promise} 1.1323 + * @resolves {*} A message accepted by the methods of DirectoryIterator 1.1324 + * in the worker thread 1.1325 + * @rejects {StopIteration} If all entries have already been visited 1.1326 + * or the iterator has been closed. 1.1327 + */ 1.1328 + this.__itmsg = Scheduler.post( 1.1329 + "new_DirectoryIterator", [Type.path.toMsg(path), options], 1.1330 + path 1.1331 + ); 1.1332 + this._isClosed = false; 1.1333 +}; 1.1334 +DirectoryIterator.prototype = { 1.1335 + iterator: function () this, 1.1336 + __iterator__: function () this, 1.1337 + 1.1338 + // Once close() is called, _itmsg should reject with a 1.1339 + // StopIteration. However, we don't want to create the promise until 1.1340 + // it's needed because it might never be used. In that case, we 1.1341 + // would get a warning on the console. 1.1342 + get _itmsg() { 1.1343 + if (!this.__itmsg) { 1.1344 + this.__itmsg = Promise.reject(StopIteration); 1.1345 + } 1.1346 + return this.__itmsg; 1.1347 + }, 1.1348 + 1.1349 + /** 1.1350 + * Determine whether the directory exists. 1.1351 + * 1.1352 + * @resolves {boolean} 1.1353 + */ 1.1354 + exists: function exists() { 1.1355 + return this._itmsg.then( 1.1356 + function onSuccess(iterator) { 1.1357 + return Scheduler.post("DirectoryIterator_prototype_exists", [iterator]); 1.1358 + } 1.1359 + ); 1.1360 + }, 1.1361 + /** 1.1362 + * Get the next entry in the directory. 1.1363 + * 1.1364 + * @return {Promise} 1.1365 + * @resolves {OS.File.Entry} 1.1366 + * @rejects {StopIteration} If all entries have already been visited. 1.1367 + */ 1.1368 + next: function next() { 1.1369 + let self = this; 1.1370 + let promise = this._itmsg; 1.1371 + 1.1372 + // Get the iterator, call _next 1.1373 + promise = promise.then( 1.1374 + function withIterator(iterator) { 1.1375 + return self._next(iterator); 1.1376 + }); 1.1377 + 1.1378 + return promise; 1.1379 + }, 1.1380 + /** 1.1381 + * Get several entries at once. 1.1382 + * 1.1383 + * @param {number=} length If specified, the number of entries 1.1384 + * to return. If unspecified, return all remaining entries. 1.1385 + * @return {Promise} 1.1386 + * @resolves {Array} An array containing the |length| next entries. 1.1387 + */ 1.1388 + nextBatch: function nextBatch(size) { 1.1389 + if (this._isClosed) { 1.1390 + return Promise.resolve([]); 1.1391 + } 1.1392 + let promise = this._itmsg; 1.1393 + promise = promise.then( 1.1394 + function withIterator(iterator) { 1.1395 + return Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]); 1.1396 + }); 1.1397 + promise = promise.then( 1.1398 + function withEntries(array) { 1.1399 + return array.map(DirectoryIterator.Entry.fromMsg); 1.1400 + }); 1.1401 + return promise; 1.1402 + }, 1.1403 + /** 1.1404 + * Apply a function to all elements of the directory sequentially. 1.1405 + * 1.1406 + * @param {Function} cb This function will be applied to all entries 1.1407 + * of the directory. It receives as arguments 1.1408 + * - the OS.File.Entry corresponding to the entry; 1.1409 + * - the index of the entry in the enumeration; 1.1410 + * - the iterator itself - return |iterator.close()| to stop the loop. 1.1411 + * 1.1412 + * If the callback returns a promise, iteration waits until the 1.1413 + * promise is resolved before proceeding. 1.1414 + * 1.1415 + * @return {Promise} A promise resolved once the loop has reached 1.1416 + * its end. 1.1417 + */ 1.1418 + forEach: function forEach(cb, options) { 1.1419 + if (this._isClosed) { 1.1420 + return Promise.resolve(); 1.1421 + } 1.1422 + 1.1423 + let self = this; 1.1424 + let position = 0; 1.1425 + let iterator; 1.1426 + 1.1427 + // Grab iterator 1.1428 + let promise = this._itmsg.then( 1.1429 + function(aIterator) { 1.1430 + iterator = aIterator; 1.1431 + } 1.1432 + ); 1.1433 + 1.1434 + // Then iterate 1.1435 + let loop = function loop() { 1.1436 + if (self._isClosed) { 1.1437 + return Promise.resolve(); 1.1438 + } 1.1439 + return self._next(iterator).then( 1.1440 + function onSuccess(value) { 1.1441 + return Promise.resolve(cb(value, position++, self)).then(loop); 1.1442 + }, 1.1443 + function onFailure(reason) { 1.1444 + if (reason == StopIteration) { 1.1445 + return; 1.1446 + } 1.1447 + throw reason; 1.1448 + } 1.1449 + ); 1.1450 + }; 1.1451 + 1.1452 + return promise.then(loop); 1.1453 + }, 1.1454 + /** 1.1455 + * Auxiliary method: fetch the next item 1.1456 + * 1.1457 + * @rejects {StopIteration} If all entries have already been visited 1.1458 + * or the iterator has been closed. 1.1459 + */ 1.1460 + _next: function _next(iterator) { 1.1461 + if (this._isClosed) { 1.1462 + return this._itmsg; 1.1463 + } 1.1464 + let self = this; 1.1465 + let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]); 1.1466 + promise = promise.then( 1.1467 + DirectoryIterator.Entry.fromMsg, 1.1468 + function onReject(reason) { 1.1469 + if (reason == StopIteration) { 1.1470 + self.close(); 1.1471 + throw StopIteration; 1.1472 + } 1.1473 + throw reason; 1.1474 + }); 1.1475 + return promise; 1.1476 + }, 1.1477 + /** 1.1478 + * Close the iterator 1.1479 + */ 1.1480 + close: function close() { 1.1481 + if (this._isClosed) { 1.1482 + return Promise.resolve(); 1.1483 + } 1.1484 + this._isClosed = true; 1.1485 + let self = this; 1.1486 + return this._itmsg.then( 1.1487 + function withIterator(iterator) { 1.1488 + // Set __itmsg to null so that the _itmsg getter returns a 1.1489 + // rejected StopIteration promise if it's ever used. 1.1490 + self.__itmsg = null; 1.1491 + return Scheduler.post("DirectoryIterator_prototype_close", [iterator]); 1.1492 + } 1.1493 + ); 1.1494 + } 1.1495 +}; 1.1496 + 1.1497 +DirectoryIterator.Entry = function Entry(value) { 1.1498 + return value; 1.1499 +}; 1.1500 +DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype); 1.1501 + 1.1502 +DirectoryIterator.Entry.fromMsg = function fromMsg(value) { 1.1503 + return new DirectoryIterator.Entry(value); 1.1504 +}; 1.1505 + 1.1506 +File.resetWorker = function() { 1.1507 + return Task.spawn(function*() { 1.1508 + let resources = yield Scheduler.kill({shutdown: false, reset: true}); 1.1509 + if (resources && !resources.killed) { 1.1510 + throw new Error("Could not reset worker, this would leak file descriptors: " + JSON.stringify(resources)); 1.1511 + } 1.1512 + }); 1.1513 +}; 1.1514 + 1.1515 +// Constants 1.1516 +File.POS_START = SysAll.POS_START; 1.1517 +File.POS_CURRENT = SysAll.POS_CURRENT; 1.1518 +File.POS_END = SysAll.POS_END; 1.1519 + 1.1520 +// Exports 1.1521 +File.Error = OSError; 1.1522 +File.DirectoryIterator = DirectoryIterator; 1.1523 + 1.1524 +this.OS = {}; 1.1525 +this.OS.File = File; 1.1526 +this.OS.Constants = SharedAll.Constants; 1.1527 +this.OS.Shared = { 1.1528 + LOG: SharedAll.LOG, 1.1529 + Type: SysAll.Type, 1.1530 + get DEBUG() { 1.1531 + return SharedAll.Config.DEBUG; 1.1532 + }, 1.1533 + set DEBUG(x) { 1.1534 + return SharedAll.Config.DEBUG = x; 1.1535 + } 1.1536 +}; 1.1537 +Object.freeze(this.OS.Shared); 1.1538 +this.OS.Path = Path; 1.1539 + 1.1540 +// Returns a resolved promise when all the queued operation have been completed. 1.1541 +Object.defineProperty(OS.File, "queue", { 1.1542 + get: function() { 1.1543 + return Scheduler.queue; 1.1544 + } 1.1545 +}); 1.1546 + 1.1547 +// Auto-flush OS.File during profile-before-change. This ensures that any I/O 1.1548 +// that has been queued *before* profile-before-change is properly completed. 1.1549 +// To ensure that I/O queued *during* profile-before-change is completed, 1.1550 +// clients should register using AsyncShutdown.addBlocker. 1.1551 +AsyncShutdown.profileBeforeChange.addBlocker( 1.1552 + "OS.File: flush I/O queued before profile-before-change", 1.1553 + // Wait until the latest currently enqueued promise is satisfied/rejected 1.1554 + function() { 1.1555 + let DEBUG = false; 1.1556 + try { 1.1557 + DEBUG = Services.prefs.getBoolPref("toolkit.osfile.debug.failshutdown"); 1.1558 + } catch (ex) { 1.1559 + // Ignore 1.1560 + } 1.1561 + if (DEBUG) { 1.1562 + // Return a promise that will never be satisfied 1.1563 + return Promise.defer().promise; 1.1564 + } else { 1.1565 + return Scheduler.queue; 1.1566 + } 1.1567 + }, 1.1568 + function getDetails() { 1.1569 + let result = { 1.1570 + launched: Scheduler.launched, 1.1571 + shutdown: Scheduler.shutdown, 1.1572 + worker: !!Scheduler._worker, 1.1573 + pendingReset: !!Scheduler.resetTimer, 1.1574 + latestSent: Scheduler.Debugging.latestSent, 1.1575 + latestReceived: Scheduler.Debugging.latestReceived, 1.1576 + messagesSent: Scheduler.Debugging.messagesSent, 1.1577 + messagesReceived: Scheduler.Debugging.messagesReceived, 1.1578 + messagesQueued: Scheduler.Debugging.messagesQueued, 1.1579 + DEBUG: SharedAll.Config.DEBUG 1.1580 + }; 1.1581 + // Convert dates to strings for better readability 1.1582 + for (let key of ["latestSent", "latestReceived"]) { 1.1583 + if (result[key] && typeof result[key][0] == "number") { 1.1584 + result[key][0] = Date(result[key][0]); 1.1585 + } 1.1586 + } 1.1587 + return result; 1.1588 + } 1.1589 +);