michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Asynchronous front-end for OS.File. michael@0: * michael@0: * This front-end is meant to be imported from the main thread. In turn, michael@0: * it spawns one worker (perhaps more in the future) and delegates all michael@0: * disk I/O to this worker. michael@0: * michael@0: * Documentation note: most of the functions and methods in this module michael@0: * return promises. For clarity, we denote as follows a promise that may resolve michael@0: * with type |A| and some value |value| or reject with type |B| and some michael@0: * reason |reason| michael@0: * @resolves {A} value michael@0: * @rejects {B} reason michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["OS"]; michael@0: michael@0: const Cu = Components.utils; michael@0: const Ci = Components.interfaces; michael@0: michael@0: let SharedAll = {}; michael@0: Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); michael@0: Cu.import("resource://gre/modules/Timer.jsm", this); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Deprecated', michael@0: 'resource://gre/modules/Deprecated.jsm'); michael@0: michael@0: // Boilerplate, to simplify the transition to require() michael@0: let LOG = SharedAll.LOG.bind(SharedAll, "Controller"); michael@0: let isTypedArray = SharedAll.isTypedArray; michael@0: michael@0: // The constructor for file errors. michael@0: let SysAll = {}; michael@0: if (SharedAll.Constants.Win) { michael@0: Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll); michael@0: } else if (SharedAll.Constants.libc) { michael@0: Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll); michael@0: } else { michael@0: throw new Error("I am neither under Windows nor under a Posix system"); michael@0: } michael@0: let OSError = SysAll.Error; michael@0: let Type = SysAll.Type; michael@0: michael@0: let Path = {}; michael@0: Cu.import("resource://gre/modules/osfile/ospath.jsm", Path); michael@0: michael@0: // The library of promises. michael@0: Cu.import("resource://gre/modules/Promise.jsm", this); michael@0: Cu.import("resource://gre/modules/Task.jsm", this); michael@0: michael@0: // The implementation of communications michael@0: Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this); michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm", this); michael@0: Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); michael@0: Cu.import("resource://gre/modules/AsyncShutdown.jsm", this); michael@0: let Native = Cu.import("resource://gre/modules/osfile/osfile_native.jsm", {}); michael@0: michael@0: /** michael@0: * Constructors for decoding standard exceptions michael@0: * received from the worker. michael@0: */ michael@0: const EXCEPTION_CONSTRUCTORS = { michael@0: EvalError: function(error) { michael@0: return new EvalError(error.message, error.fileName, error.lineNumber); michael@0: }, michael@0: InternalError: function(error) { michael@0: return new InternalError(error.message, error.fileName, error.lineNumber); michael@0: }, michael@0: RangeError: function(error) { michael@0: return new RangeError(error.message, error.fileName, error.lineNumber); michael@0: }, michael@0: ReferenceError: function(error) { michael@0: return new ReferenceError(error.message, error.fileName, error.lineNumber); michael@0: }, michael@0: SyntaxError: function(error) { michael@0: return new SyntaxError(error.message, error.fileName, error.lineNumber); michael@0: }, michael@0: TypeError: function(error) { michael@0: return new TypeError(error.message, error.fileName, error.lineNumber); michael@0: }, michael@0: URIError: function(error) { michael@0: return new URIError(error.message, error.fileName, error.lineNumber); michael@0: }, michael@0: OSError: function(error) { michael@0: return OS.File.Error.fromMsg(error); michael@0: } michael@0: }; michael@0: michael@0: // It's possible for osfile.jsm to get imported before the profile is michael@0: // set up. In this case, some path constants aren't yet available. michael@0: // Here, we make them lazy loaders. michael@0: michael@0: function lazyPathGetter(constProp, dirKey) { michael@0: return function () { michael@0: let path; michael@0: try { michael@0: path = Services.dirsvc.get(dirKey, Ci.nsIFile).path; michael@0: delete SharedAll.Constants.Path[constProp]; michael@0: SharedAll.Constants.Path[constProp] = path; michael@0: } catch (ex) { michael@0: // Ignore errors if the value still isn't available. Hopefully michael@0: // the next access will return it. michael@0: } michael@0: michael@0: return path; michael@0: }; michael@0: } michael@0: michael@0: for (let [constProp, dirKey] of [ michael@0: ["localProfileDir", "ProfLD"], michael@0: ["profileDir", "ProfD"], michael@0: ["userApplicationDataDir", "UAppData"], michael@0: ["winAppDataDir", "AppData"], michael@0: ["winStartMenuProgsDir", "Progs"], michael@0: ]) { michael@0: michael@0: if (constProp in SharedAll.Constants.Path) { michael@0: continue; michael@0: } michael@0: michael@0: LOG("Installing lazy getter for OS.Constants.Path." + constProp + michael@0: " because it isn't defined and profile may not be loaded."); michael@0: Object.defineProperty(SharedAll.Constants.Path, constProp, { michael@0: get: lazyPathGetter(constProp, dirKey), michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Return a shallow clone of the enumerable properties of an object. michael@0: */ michael@0: let clone = SharedAll.clone; michael@0: michael@0: /** michael@0: * Extract a shortened version of an object, fit for logging. michael@0: * michael@0: * This function returns a copy of the original object in which all michael@0: * long strings, Arrays, TypedArrays, ArrayBuffers are removed and michael@0: * replaced with placeholders. Use this function to sanitize objects michael@0: * if you wish to log them or to keep them in memory. michael@0: * michael@0: * @param {*} obj The obj to shorten. michael@0: * @return {*} array A shorter object, fit for logging. michael@0: */ michael@0: function summarizeObject(obj) { michael@0: if (!obj) { michael@0: return null; michael@0: } michael@0: if (typeof obj == "string") { michael@0: if (obj.length > 1024) { michael@0: return {"Long string": obj.length}; michael@0: } michael@0: return obj; michael@0: } michael@0: if (typeof obj == "object") { michael@0: if (Array.isArray(obj)) { michael@0: if (obj.length > 32) { michael@0: return {"Long array": obj.length}; michael@0: } michael@0: return [summarizeObject(k) for (k of obj)]; michael@0: } michael@0: if ("byteLength" in obj) { michael@0: // Assume TypedArray or ArrayBuffer michael@0: return {"Binary Data": obj.byteLength}; michael@0: } michael@0: let result = {}; michael@0: for (let k of Object.keys(obj)) { michael@0: result[k] = summarizeObject(obj[k]); michael@0: } michael@0: return result; michael@0: } michael@0: return obj; michael@0: } michael@0: michael@0: let Scheduler = { michael@0: michael@0: /** michael@0: * |true| once we have sent at least one message to the worker. michael@0: * This field is unaffected by resetting the worker. michael@0: */ michael@0: launched: false, michael@0: michael@0: /** michael@0: * |true| once shutdown has begun i.e. we should reject any michael@0: * message, including resets. michael@0: */ michael@0: shutdown: false, michael@0: michael@0: /** michael@0: * A promise resolved once all operations are complete. michael@0: * michael@0: * This promise is never rejected and the result is always undefined. michael@0: */ michael@0: queue: Promise.resolve(), michael@0: michael@0: /** michael@0: * Miscellaneous debugging information michael@0: */ michael@0: Debugging: { michael@0: /** michael@0: * The latest message sent and still waiting for a reply. michael@0: */ michael@0: latestSent: undefined, michael@0: michael@0: /** michael@0: * The latest reply received, or null if we are waiting for a reply. michael@0: */ michael@0: latestReceived: undefined, michael@0: michael@0: /** michael@0: * Number of messages sent to the worker. This includes the michael@0: * initial SET_DEBUG, if applicable. michael@0: */ michael@0: messagesSent: 0, michael@0: michael@0: /** michael@0: * Total number of messages ever queued, including the messages michael@0: * sent. michael@0: */ michael@0: messagesQueued: 0, michael@0: michael@0: /** michael@0: * Number of messages received from the worker. michael@0: */ michael@0: messagesReceived: 0, michael@0: }, michael@0: michael@0: /** michael@0: * A timer used to automatically shut down the worker after some time. michael@0: */ michael@0: resetTimer: null, michael@0: michael@0: /** michael@0: * The worker to which to send requests. michael@0: * michael@0: * If the worker has never been created or has been reset, this is a michael@0: * fresh worker, initialized with osfile_async_worker.js. michael@0: * michael@0: * @type {PromiseWorker} michael@0: */ michael@0: get worker() { michael@0: if (!this._worker) { michael@0: // Either the worker has never been created or it has been reset michael@0: this._worker = new PromiseWorker( michael@0: "resource://gre/modules/osfile/osfile_async_worker.js", LOG); michael@0: } michael@0: return this._worker; michael@0: }, michael@0: michael@0: _worker: null, michael@0: michael@0: /** michael@0: * Prepare to kill the OS.File worker after a few seconds. michael@0: */ michael@0: restartTimer: function(arg) { michael@0: let delay; michael@0: try { michael@0: delay = Services.prefs.getIntPref("osfile.reset_worker_delay"); michael@0: } catch(e) { michael@0: // Don't auto-shutdown if we don't have a delay preference set. michael@0: return; michael@0: } michael@0: michael@0: if (this.resetTimer) { michael@0: clearTimeout(this.resetTimer); michael@0: } michael@0: this.resetTimer = setTimeout( michael@0: () => Scheduler.kill({reset: true, shutdown: false}), michael@0: delay michael@0: ); michael@0: }, michael@0: michael@0: /** michael@0: * Shutdown OS.File. michael@0: * michael@0: * @param {*} options michael@0: * - {boolean} shutdown If |true|, reject any further request. Otherwise, michael@0: * further requests will resurrect the worker. michael@0: * - {boolean} reset If |true|, instruct the worker to shutdown if this michael@0: * would not cause leaks. Otherwise, assume that the worker will be shutdown michael@0: * through some other mean. michael@0: */ michael@0: kill: function({shutdown, reset}) { michael@0: return Task.spawn(function*() { michael@0: michael@0: yield this.queue; michael@0: michael@0: // Enter critical section: no yield in this block michael@0: // (we want to make sure that we remain the only michael@0: // request in the queue). michael@0: michael@0: if (!this.launched || this.shutdown || !this._worker) { michael@0: // Nothing to kill michael@0: this.shutdown = this.shutdown || shutdown; michael@0: this._worker = null; michael@0: return null; michael@0: } michael@0: michael@0: // Deactivate the queue, to ensure that no message is sent michael@0: // to an obsolete worker (we reactivate it in the |finally|). michael@0: let deferred = Promise.defer(); michael@0: this.queue = deferred.promise; michael@0: michael@0: michael@0: // Exit critical section michael@0: michael@0: let message = ["Meta_shutdown", [reset]]; michael@0: michael@0: try { michael@0: Scheduler.latestReceived = []; michael@0: Scheduler.latestSent = [Date.now(), ...message]; michael@0: let promise = this._worker.post(...message); michael@0: michael@0: // Wait for result michael@0: let resources; michael@0: try { michael@0: resources = (yield promise).ok; michael@0: michael@0: Scheduler.latestReceived = [Date.now(), message]; michael@0: } catch (ex) { michael@0: LOG("Could not dispatch Meta_reset", ex); michael@0: // It's most likely a programmer error, but we'll assume that michael@0: // the worker has been shutdown, as it's less risky than the michael@0: // opposite stance. michael@0: resources = {openedFiles: [], openedDirectoryIterators: [], killed: true}; michael@0: michael@0: Scheduler.latestReceived = [Date.now(), message, ex]; michael@0: } michael@0: michael@0: let {openedFiles, openedDirectoryIterators, killed} = resources; michael@0: if (!reset michael@0: && (openedFiles && openedFiles.length michael@0: || ( openedDirectoryIterators && openedDirectoryIterators.length))) { michael@0: // The worker still holds resources. Report them. michael@0: michael@0: let msg = ""; michael@0: if (openedFiles.length > 0) { michael@0: msg += "The following files are still open:\n" + michael@0: openedFiles.join("\n"); michael@0: } michael@0: if (openedDirectoryIterators.length > 0) { michael@0: msg += "The following directory iterators are still open:\n" + michael@0: openedDirectoryIterators.join("\n"); michael@0: } michael@0: michael@0: LOG("WARNING: File descriptors leaks detected.\n" + msg); michael@0: } michael@0: michael@0: // Make sure that we do not leave an invalid |worker| around. michael@0: if (killed || shutdown) { michael@0: this._worker = null; michael@0: } michael@0: michael@0: this.shutdown = shutdown; michael@0: michael@0: return resources; michael@0: michael@0: } finally { michael@0: // Resume accepting messages. If we have set |shutdown| to |true|, michael@0: // any pending/future request will be rejected. Otherwise, any michael@0: // pending/future request will spawn a new worker if necessary. michael@0: deferred.resolve(); michael@0: } michael@0: michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Push a task at the end of the queue. michael@0: * michael@0: * @param {function} code A function returning a Promise. michael@0: * This function will be executed once all the previously michael@0: * pushed tasks have completed. michael@0: * @return {Promise} A promise with the same behavior as michael@0: * the promise returned by |code|. michael@0: */ michael@0: push: function(code) { michael@0: let promise = this.queue.then(code); michael@0: // By definition, |this.queue| can never reject. michael@0: this.queue = promise.then(null, () => undefined); michael@0: // Fork |promise| to ensure that uncaught errors are reported michael@0: return promise.then(null, null); michael@0: }, michael@0: michael@0: /** michael@0: * Post a message to the worker thread. michael@0: * michael@0: * @param {string} method The name of the method to call. michael@0: * @param {...} args The arguments to pass to the method. These arguments michael@0: * must be clonable. michael@0: * @return {Promise} A promise conveying the result/error caused by michael@0: * calling |method| with arguments |args|. michael@0: */ michael@0: post: function post(method, ...args) { michael@0: if (this.shutdown) { michael@0: LOG("OS.File is not available anymore. The following request has been rejected.", michael@0: method, args); michael@0: return Promise.reject(new Error("OS.File has been shut down. Rejecting post to " + method)); michael@0: } michael@0: let firstLaunch = !this.launched; michael@0: this.launched = true; michael@0: michael@0: if (firstLaunch && SharedAll.Config.DEBUG) { michael@0: // If we have delayed sending SET_DEBUG, do it now. michael@0: this.worker.post("SET_DEBUG", [true]); michael@0: Scheduler.Debugging.messagesSent++; michael@0: } michael@0: michael@0: // By convention, the last argument of any message may be an |options| object. michael@0: let options; michael@0: let methodArgs = args[0]; michael@0: if (methodArgs) { michael@0: options = methodArgs[methodArgs.length - 1]; michael@0: } michael@0: Scheduler.Debugging.messagesQueued++; michael@0: return this.push(Task.async(function*() { michael@0: if (this.shutdown) { michael@0: LOG("OS.File is not available anymore. The following request has been rejected.", michael@0: method, args); michael@0: throw new Error("OS.File has been shut down. Rejecting request to " + method); michael@0: } michael@0: michael@0: // Update debugging information. As |args| may be quite michael@0: // expensive, we only keep a shortened version of it. michael@0: Scheduler.Debugging.latestReceived = null; michael@0: Scheduler.Debugging.latestSent = [Date.now(), method, summarizeObject(methodArgs)]; michael@0: michael@0: // Don't kill the worker just yet michael@0: Scheduler.restartTimer(); michael@0: michael@0: michael@0: let data; michael@0: let reply; michael@0: let isError = false; michael@0: try { michael@0: try { michael@0: data = yield this.worker.post(method, ...args); michael@0: } finally { michael@0: Scheduler.Debugging.messagesReceived++; michael@0: } michael@0: reply = data; michael@0: } catch (error) { michael@0: reply = error; michael@0: isError = true; michael@0: if (error instanceof PromiseWorker.WorkerError) { michael@0: throw EXCEPTION_CONSTRUCTORS[error.data.exn || "OSError"](error.data); michael@0: } michael@0: if (error instanceof ErrorEvent) { michael@0: let message = error.message; michael@0: if (message == "uncaught exception: [object StopIteration]") { michael@0: isError = false; michael@0: throw StopIteration; michael@0: } michael@0: throw new Error(message, error.filename, error.lineno); michael@0: } michael@0: throw error; michael@0: } finally { michael@0: Scheduler.Debugging.latestSent = Scheduler.Debugging.latestSent.slice(0, 2); michael@0: if (isError) { michael@0: Scheduler.Debugging.latestReceived = [Date.now(), reply.message, reply.fileName, reply.lineNumber]; michael@0: } else { michael@0: Scheduler.Debugging.latestReceived = [Date.now(), summarizeObject(reply)]; michael@0: } michael@0: if (firstLaunch) { michael@0: Scheduler._updateTelemetry(); michael@0: } michael@0: michael@0: Scheduler.restartTimer(); michael@0: } michael@0: michael@0: // Check for duration and return result. michael@0: if (!options) { michael@0: return data.ok; michael@0: } michael@0: // Check for options.outExecutionDuration. michael@0: if (typeof options !== "object" || michael@0: !("outExecutionDuration" in options)) { michael@0: return data.ok; michael@0: } michael@0: // If data.durationMs is not present, return data.ok (there was an michael@0: // exception applying the method). michael@0: if (!("durationMs" in data)) { michael@0: return data.ok; michael@0: } michael@0: // Bug 874425 demonstrates that two successive calls to Date.now() michael@0: // can actually produce an interval with negative duration. michael@0: // We assume that this is due to an operation that is so short michael@0: // that Date.now() is not monotonic, so we round this up to 0. michael@0: let durationMs = Math.max(0, data.durationMs); michael@0: // Accumulate (or initialize) outExecutionDuration michael@0: if (typeof options.outExecutionDuration == "number") { michael@0: options.outExecutionDuration += durationMs; michael@0: } else { michael@0: options.outExecutionDuration = durationMs; michael@0: } michael@0: return data.ok; michael@0: }.bind(this))); michael@0: }, michael@0: michael@0: /** michael@0: * Post Telemetry statistics. michael@0: * michael@0: * This is only useful on first launch. michael@0: */ michael@0: _updateTelemetry: function() { michael@0: let worker = this.worker; michael@0: let workerTimeStamps = worker.workerTimeStamps; michael@0: if (!workerTimeStamps) { michael@0: // If the first call to OS.File results in an uncaught errors, michael@0: // the timestamps are absent. As this case is a developer error, michael@0: // let's not waste time attempting to extract telemetry from it. michael@0: return; michael@0: } michael@0: let HISTOGRAM_LAUNCH = Services.telemetry.getHistogramById("OSFILE_WORKER_LAUNCH_MS"); michael@0: HISTOGRAM_LAUNCH.add(worker.workerTimeStamps.entered - worker.launchTimeStamp); michael@0: michael@0: let HISTOGRAM_READY = Services.telemetry.getHistogramById("OSFILE_WORKER_READY_MS"); michael@0: HISTOGRAM_READY.add(worker.workerTimeStamps.loaded - worker.launchTimeStamp); michael@0: } michael@0: }; michael@0: michael@0: const PREF_OSFILE_LOG = "toolkit.osfile.log"; michael@0: const PREF_OSFILE_LOG_REDIRECT = "toolkit.osfile.log.redirect"; michael@0: michael@0: /** michael@0: * Safely read a PREF_OSFILE_LOG preference. michael@0: * Returns a value read or, in case of an error, oldPref or false. michael@0: * michael@0: * @param bool oldPref michael@0: * An optional value that the DEBUG flag was set to previously. michael@0: */ michael@0: function readDebugPref(prefName, oldPref = false) { michael@0: let pref = oldPref; michael@0: try { michael@0: pref = Services.prefs.getBoolPref(prefName); michael@0: } catch (x) { michael@0: // In case of an error when reading a pref keep it as is. michael@0: } michael@0: // If neither pref nor oldPref were set, default it to false. michael@0: return pref; michael@0: }; michael@0: michael@0: /** michael@0: * Listen to PREF_OSFILE_LOG changes and update gShouldLog flag michael@0: * appropriately. michael@0: */ michael@0: Services.prefs.addObserver(PREF_OSFILE_LOG, michael@0: function prefObserver(aSubject, aTopic, aData) { michael@0: SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, SharedAll.Config.DEBUG); michael@0: if (Scheduler.launched) { michael@0: // Don't start the worker just to set this preference. michael@0: Scheduler.post("SET_DEBUG", [SharedAll.Config.DEBUG]); michael@0: } michael@0: }, false); michael@0: SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, false); michael@0: michael@0: Services.prefs.addObserver(PREF_OSFILE_LOG_REDIRECT, michael@0: function prefObserver(aSubject, aTopic, aData) { michael@0: SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, OS.Shared.TEST); michael@0: }, false); michael@0: SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, false); michael@0: michael@0: michael@0: /** michael@0: * If |true|, use the native implementaiton of OS.File methods michael@0: * whenever possible. Otherwise, force the use of the JS version. michael@0: */ michael@0: let nativeWheneverAvailable = true; michael@0: const PREF_OSFILE_NATIVE = "toolkit.osfile.native"; michael@0: Services.prefs.addObserver(PREF_OSFILE_NATIVE, michael@0: function prefObserver(aSubject, aTopic, aData) { michael@0: nativeWheneverAvailable = readDebugPref(PREF_OSFILE_NATIVE, nativeWheneverAvailable); michael@0: }, false); michael@0: michael@0: michael@0: // Update worker's DEBUG flag if it's true. michael@0: // Don't start the worker just for this, though. michael@0: if (SharedAll.Config.DEBUG && Scheduler.launched) { michael@0: Scheduler.post("SET_DEBUG", [true]); michael@0: } michael@0: michael@0: // Observer topics used for monitoring shutdown michael@0: const WEB_WORKERS_SHUTDOWN_TOPIC = "web-workers-shutdown"; michael@0: michael@0: // Preference used to configure test shutdown observer. michael@0: const PREF_OSFILE_TEST_SHUTDOWN_OBSERVER = michael@0: "toolkit.osfile.test.shutdown.observer"; michael@0: michael@0: AsyncShutdown.webWorkersShutdown.addBlocker( michael@0: "OS.File: flush pending requests, warn about unclosed files, shut down service.", michael@0: () => Scheduler.kill({reset: false, shutdown: true}) michael@0: ); michael@0: michael@0: michael@0: // Attaching an observer for PREF_OSFILE_TEST_SHUTDOWN_OBSERVER to enable or michael@0: // disable the test shutdown event observer. michael@0: // Note: By default the PREF_OSFILE_TEST_SHUTDOWN_OBSERVER is unset. michael@0: // Note: This is meant to be used for testing purposes only. michael@0: Services.prefs.addObserver(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER, michael@0: function prefObserver() { michael@0: // The temporary phase topic used to trigger the unclosed michael@0: // phase warning. michael@0: let TOPIC = null; michael@0: try { michael@0: TOPIC = Services.prefs.getCharPref( michael@0: PREF_OSFILE_TEST_SHUTDOWN_OBSERVER); michael@0: } catch (x) { michael@0: } michael@0: if (TOPIC) { michael@0: // Generate a phase, add a blocker. michael@0: // Note that this can work only if AsyncShutdown itself has been michael@0: // configured for testing by the testsuite. michael@0: let phase = AsyncShutdown._getPhase(TOPIC); michael@0: phase.addBlocker( michael@0: "(for testing purposes) OS.File: warn about unclosed files", michael@0: () => Scheduler.kill({shutdown: false, reset: false}) michael@0: ); michael@0: } michael@0: }, false); michael@0: michael@0: /** michael@0: * Representation of a file, with asynchronous methods. michael@0: * michael@0: * @param {*} fdmsg The _message_ representing the platform-specific file michael@0: * handle. michael@0: * michael@0: * @constructor michael@0: */ michael@0: let File = function File(fdmsg) { michael@0: // FIXME: At the moment, |File| does not close on finalize michael@0: // (see bug 777715) michael@0: this._fdmsg = fdmsg; michael@0: this._closeResult = null; michael@0: this._closed = null; michael@0: }; michael@0: michael@0: michael@0: File.prototype = { michael@0: /** michael@0: * Close a file asynchronously. michael@0: * michael@0: * This method is idempotent. michael@0: * michael@0: * @return {promise} michael@0: * @resolves {null} michael@0: * @rejects {OS.File.Error} michael@0: */ michael@0: close: function close() { michael@0: if (this._fdmsg != null) { michael@0: let msg = this._fdmsg; michael@0: this._fdmsg = null; michael@0: return this._closeResult = michael@0: Scheduler.post("File_prototype_close", [msg], this); michael@0: } michael@0: return this._closeResult; michael@0: }, michael@0: michael@0: /** michael@0: * Fetch information about the file. michael@0: * michael@0: * @return {promise} michael@0: * @resolves {OS.File.Info} The latest information about the file. michael@0: * @rejects {OS.File.Error} michael@0: */ michael@0: stat: function stat() { michael@0: return Scheduler.post("File_prototype_stat", [this._fdmsg], this).then( michael@0: File.Info.fromMsg michael@0: ); michael@0: }, michael@0: michael@0: /** michael@0: * Set the last access and modification date of the file. michael@0: * The time stamp resolution is 1 second at best, but might be worse michael@0: * depending on the platform. michael@0: * michael@0: * @return {promise} michael@0: * @rejects {TypeError} michael@0: * @rejects {OS.File.Error} michael@0: */ michael@0: setDates: function setDates(accessDate, modificationDate) { michael@0: return Scheduler.post("File_prototype_setDates", michael@0: [this._fdmsg, accessDate, modificationDate], this); michael@0: }, michael@0: michael@0: /** michael@0: * Read a number of bytes from the file and into a buffer. michael@0: * michael@0: * @param {Typed array | C pointer} buffer This buffer will be michael@0: * modified by another thread. Using this buffer before the |read| michael@0: * operation has completed is a BAD IDEA. michael@0: * @param {JSON} options michael@0: * michael@0: * @return {promise} michael@0: * @resolves {number} The number of bytes effectively read. michael@0: * @rejects {OS.File.Error} michael@0: */ michael@0: readTo: function readTo(buffer, options = {}) { michael@0: // If |buffer| is a typed array and there is no |bytes| options, we michael@0: // need to extract the |byteLength| now, as it will be lost by michael@0: // communication. michael@0: // Options might be a nullish value, so better check for that before using michael@0: // the |in| operator. michael@0: if (isTypedArray(buffer) && !(options && "bytes" in options)) { michael@0: // Preserve reference to option |outExecutionDuration|, if it is passed. michael@0: options = clone(options, ["outExecutionDuration"]); michael@0: options.bytes = buffer.byteLength; michael@0: } michael@0: // Note: Type.void_t.out_ptr.toMsg ensures that michael@0: // - the buffer is effectively shared (not neutered) between both michael@0: // threads; michael@0: // - we take care of any |byteOffset|. michael@0: return Scheduler.post("File_prototype_readTo", michael@0: [this._fdmsg, michael@0: Type.void_t.out_ptr.toMsg(buffer), michael@0: options], michael@0: buffer/*Ensure that |buffer| is not gc-ed*/); michael@0: }, michael@0: /** michael@0: * Write bytes from a buffer to this file. michael@0: * michael@0: * Note that, by default, this function may perform several I/O michael@0: * operations to ensure that the buffer is fully written. michael@0: * michael@0: * @param {Typed array | C pointer} buffer The buffer in which the michael@0: * the bytes are stored. The buffer must be large enough to michael@0: * accomodate |bytes| bytes. Using the buffer before the operation michael@0: * is complete is a BAD IDEA. michael@0: * @param {*=} options Optionally, an object that may contain the michael@0: * following fields: michael@0: * - {number} bytes The number of |bytes| to write from the buffer. If michael@0: * unspecified, this is |buffer.byteLength|. Note that |bytes| is required michael@0: * if |buffer| is a C pointer. michael@0: * michael@0: * @return {number} The number of bytes actually written. michael@0: */ michael@0: write: function write(buffer, options = {}) { michael@0: // If |buffer| is a typed array and there is no |bytes| options, michael@0: // we need to extract the |byteLength| now, as it will be lost michael@0: // by communication. michael@0: // Options might be a nullish value, so better check for that before using michael@0: // the |in| operator. michael@0: if (isTypedArray(buffer) && !(options && "bytes" in options)) { michael@0: // Preserve reference to option |outExecutionDuration|, if it is passed. michael@0: options = clone(options, ["outExecutionDuration"]); michael@0: options.bytes = buffer.byteLength; michael@0: } michael@0: // Note: Type.void_t.out_ptr.toMsg ensures that michael@0: // - the buffer is effectively shared (not neutered) between both michael@0: // threads; michael@0: // - we take care of any |byteOffset|. michael@0: return Scheduler.post("File_prototype_write", michael@0: [this._fdmsg, michael@0: Type.void_t.in_ptr.toMsg(buffer), michael@0: options], michael@0: buffer/*Ensure that |buffer| is not gc-ed*/); michael@0: }, michael@0: michael@0: /** michael@0: * Read bytes from this file to a new buffer. michael@0: * michael@0: * @param {number=} bytes If unspecified, read all the remaining bytes from michael@0: * this file. If specified, read |bytes| bytes, or less if the file does not michael@0: * contain that many bytes. michael@0: * @param {JSON} options michael@0: * @return {promise} michael@0: * @resolves {Uint8Array} An array containing the bytes read. michael@0: */ michael@0: read: function read(nbytes, options = {}) { michael@0: let promise = Scheduler.post("File_prototype_read", michael@0: [this._fdmsg, michael@0: nbytes, options]); michael@0: return promise.then( michael@0: function onSuccess(data) { michael@0: return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Return the current position in the file, as bytes. michael@0: * michael@0: * @return {promise} michael@0: * @resolves {number} The current position in the file, michael@0: * as a number of bytes since the start of the file. michael@0: */ michael@0: getPosition: function getPosition() { michael@0: return Scheduler.post("File_prototype_getPosition", michael@0: [this._fdmsg]); michael@0: }, michael@0: michael@0: /** michael@0: * Set the current position in the file, as bytes. michael@0: * michael@0: * @param {number} pos A number of bytes. michael@0: * @param {number} whence The reference position in the file, michael@0: * which may be either POS_START (from the start of the file), michael@0: * POS_END (from the end of the file) or POS_CUR (from the michael@0: * current position in the file). michael@0: * michael@0: * @return {promise} michael@0: */ michael@0: setPosition: function setPosition(pos, whence) { michael@0: return Scheduler.post("File_prototype_setPosition", michael@0: [this._fdmsg, pos, whence]); michael@0: }, michael@0: michael@0: /** michael@0: * Flushes the file's buffers and causes all buffered data michael@0: * to be written. michael@0: * Disk flushes are very expensive and therefore should be used carefully, michael@0: * sparingly and only in scenarios where it is vital that data survives michael@0: * system crashes. Even though the function will be executed off the michael@0: * main-thread, it might still affect the overall performance of any running michael@0: * application. michael@0: * michael@0: * @return {promise} michael@0: */ michael@0: flush: function flush() { michael@0: return Scheduler.post("File_prototype_flush", michael@0: [this._fdmsg]); michael@0: }, michael@0: michael@0: /** michael@0: * Set the file's access permissions. Without any options, the michael@0: * permissions are set to an approximation of what they would have michael@0: * been if the file had been created in its current directory in the michael@0: * "most typical" fashion for the operating system. In the current michael@0: * implementation, this means that on Unix-like systems (including michael@0: * Android, B2G, etc) we set the POSIX file mode to (0666 & ~umask), michael@0: * and on Windows, we do nothing. michael@0: * michael@0: * This operation is likely to fail if applied to a file that was michael@0: * not created by the currently running program (more precisely, michael@0: * if it was created by a program running under a different OS-level michael@0: * user account). It may also fail, or silently do nothing, if the michael@0: * filesystem containing the file does not support access permissions. michael@0: * michael@0: * @param {*=} options michael@0: * - {number} unixMode If present, the POSIX file mode is set to exactly michael@0: * this value, unless |unixHonorUmask| is also michael@0: * present. michael@0: * - {bool} unixHonorUmask If true, any |unixMode| value is modified by the michael@0: * process umask, as open() would have done. michael@0: */ michael@0: setPermissions: function setPermissions(options = {}) { michael@0: return Scheduler.post("File_prototype_setPermissions", michael@0: [this._fdmsg, options]); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Open a file asynchronously. michael@0: * michael@0: * @return {promise} michael@0: * @resolves {OS.File} michael@0: * @rejects {OS.Error} michael@0: */ michael@0: File.open = function open(path, mode, options) { michael@0: return Scheduler.post( michael@0: "open", [Type.path.toMsg(path), mode, options], michael@0: path michael@0: ).then( michael@0: function onSuccess(msg) { michael@0: return new File(msg); michael@0: } michael@0: ); michael@0: }; michael@0: michael@0: /** michael@0: * 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. michael@0: * michael@0: * @param {string} path The path to the file. michael@0: * @param {*=} options Additional options for file opening. This michael@0: * implementation interprets the following fields: michael@0: * michael@0: * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext. michael@0: * If |false| use HEX numbers ie: filename-A65BC0.ext michael@0: * - {number} maxReadableNumber Used to limit the amount of tries after a failed michael@0: * file creation. Default is 20. michael@0: * michael@0: * @return {Object} contains A file object{file} and the path{path}. michael@0: * @throws {OS.File.Error} If the file could not be opened. michael@0: */ michael@0: File.openUnique = function openUnique(path, options) { michael@0: return Scheduler.post( michael@0: "openUnique", [Type.path.toMsg(path), options], michael@0: path michael@0: ).then( michael@0: function onSuccess(msg) { michael@0: return { michael@0: path: msg.path, michael@0: file: new File(msg.file) michael@0: }; michael@0: } michael@0: ); michael@0: }; michael@0: michael@0: /** michael@0: * Get the information on the file. michael@0: * michael@0: * @return {promise} michael@0: * @resolves {OS.File.Info} michael@0: * @rejects {OS.Error} michael@0: */ michael@0: File.stat = function stat(path, options) { michael@0: return Scheduler.post( michael@0: "stat", [Type.path.toMsg(path), options], michael@0: path).then(File.Info.fromMsg); michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Set the last access and modification date of the file. michael@0: * The time stamp resolution is 1 second at best, but might be worse michael@0: * depending on the platform. michael@0: * michael@0: * @return {promise} michael@0: * @rejects {TypeError} michael@0: * @rejects {OS.File.Error} michael@0: */ michael@0: File.setDates = function setDates(path, accessDate, modificationDate) { michael@0: return Scheduler.post("setDates", michael@0: [Type.path.toMsg(path), accessDate, modificationDate], michael@0: this); michael@0: }; michael@0: michael@0: /** michael@0: * Set the file's access permissions. Without any options, the michael@0: * permissions are set to an approximation of what they would have michael@0: * been if the file had been created in its current directory in the michael@0: * "most typical" fashion for the operating system. In the current michael@0: * implementation, this means that on Unix-like systems (including michael@0: * Android, B2G, etc) we set the POSIX file mode to (0666 & ~umask), michael@0: * and on Windows, we do nothing. michael@0: * michael@0: * This operation is likely to fail if applied to a file that was michael@0: * not created by the currently running program (more precisely, michael@0: * if it was created by a program running under a different OS-level michael@0: * user account). It may also fail, or silently do nothing, if the michael@0: * filesystem containing the file does not support access permissions. michael@0: * michael@0: * @param {string} path The path to the file. michael@0: * michael@0: * @param {*=} options michael@0: * - {number} unixMode If present, the POSIX file mode is set to exactly michael@0: * this value, unless |unixHonorUmask| is also michael@0: * present. michael@0: * - {bool} unixHonorUmask If true, any |unixMode| value is modified by the michael@0: * process umask, as open() would have done. michael@0: */ michael@0: File.setPermissions = function setPermissions(path, options = {}) { michael@0: return Scheduler.post("setPermissions", michael@0: [Type.path.toMsg(path), options]); michael@0: }; michael@0: michael@0: /** michael@0: * Fetch the current directory michael@0: * michael@0: * @return {promise} michael@0: * @resolves {string} The current directory, as a path usable with OS.Path michael@0: * @rejects {OS.Error} michael@0: */ michael@0: File.getCurrentDirectory = function getCurrentDirectory() { michael@0: return Scheduler.post( michael@0: "getCurrentDirectory" michael@0: ).then(Type.path.fromMsg); michael@0: }; michael@0: michael@0: /** michael@0: * Change the current directory michael@0: * michael@0: * @param {string} path The OS-specific path to the current directory. michael@0: * You should use the methods of OS.Path and the constants of OS.Constants.Path michael@0: * to build OS-specific paths in a portable manner. michael@0: * michael@0: * @return {promise} michael@0: * @resolves {null} michael@0: * @rejects {OS.Error} michael@0: */ michael@0: File.setCurrentDirectory = function setCurrentDirectory(path) { michael@0: return Scheduler.post( michael@0: "setCurrentDirectory", [Type.path.toMsg(path)], path michael@0: ); michael@0: }; michael@0: michael@0: /** michael@0: * Copy a file to a destination. michael@0: * michael@0: * @param {string} sourcePath The platform-specific path at which michael@0: * the file may currently be found. michael@0: * @param {string} destPath The platform-specific path at which the michael@0: * file should be copied. michael@0: * @param {*=} options An object which may contain the following fields: michael@0: * michael@0: * @option {bool} noOverwrite - If true, this function will fail if michael@0: * a file already exists at |destPath|. Otherwise, if this file exists, michael@0: * it will be erased silently. michael@0: * michael@0: * @rejects {OS.File.Error} In case of any error. michael@0: * michael@0: * General note: The behavior of this function is defined only when michael@0: * it is called on a single file. If it is called on a directory, the michael@0: * behavior is undefined and may not be the same across all platforms. michael@0: * michael@0: * General note: The behavior of this function with respect to metadata michael@0: * is unspecified. Metadata may or may not be copied with the file. The michael@0: * behavior may not be the same across all platforms. michael@0: */ michael@0: File.copy = function copy(sourcePath, destPath, options) { michael@0: return Scheduler.post("copy", [Type.path.toMsg(sourcePath), michael@0: Type.path.toMsg(destPath), options], [sourcePath, destPath]); michael@0: }; michael@0: michael@0: /** michael@0: * Move a file to a destination. michael@0: * michael@0: * @param {string} sourcePath The platform-specific path at which michael@0: * the file may currently be found. michael@0: * @param {string} destPath The platform-specific path at which the michael@0: * file should be moved. michael@0: * @param {*=} options An object which may contain the following fields: michael@0: * michael@0: * @option {bool} noOverwrite - If set, this function will fail if michael@0: * a file already exists at |destPath|. Otherwise, if this file exists, michael@0: * it will be erased silently. michael@0: * michael@0: * @returns {Promise} michael@0: * @rejects {OS.File.Error} In case of any error. michael@0: * michael@0: * General note: The behavior of this function is defined only when michael@0: * it is called on a single file. If it is called on a directory, the michael@0: * behavior is undefined and may not be the same across all platforms. michael@0: * michael@0: * General note: The behavior of this function with respect to metadata michael@0: * is unspecified. Metadata may or may not be moved with the file. The michael@0: * behavior may not be the same across all platforms. michael@0: */ michael@0: File.move = function move(sourcePath, destPath, options) { michael@0: return Scheduler.post("move", [Type.path.toMsg(sourcePath), michael@0: Type.path.toMsg(destPath), options], [sourcePath, destPath]); michael@0: }; michael@0: michael@0: /** michael@0: * Create a symbolic link to a source. michael@0: * michael@0: * @param {string} sourcePath The platform-specific path to which michael@0: * the symbolic link should point. michael@0: * @param {string} destPath The platform-specific path at which the michael@0: * symbolic link should be created. michael@0: * michael@0: * @returns {Promise} michael@0: * @rejects {OS.File.Error} In case of any error. michael@0: */ michael@0: if (!SharedAll.Constants.Win) { michael@0: File.unixSymLink = function unixSymLink(sourcePath, destPath) { michael@0: return Scheduler.post("unixSymLink", [Type.path.toMsg(sourcePath), michael@0: Type.path.toMsg(destPath)], [sourcePath, destPath]); michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Gets the number of bytes available on disk to the current user. michael@0: * michael@0: * @param {string} Platform-specific path to a directory on the disk to michael@0: * query for free available bytes. michael@0: * michael@0: * @return {number} The number of bytes available for the current user. michael@0: * @throws {OS.File.Error} In case of any error. michael@0: */ michael@0: File.getAvailableFreeSpace = function getAvailableFreeSpace(sourcePath) { michael@0: return Scheduler.post("getAvailableFreeSpace", michael@0: [Type.path.toMsg(sourcePath)], sourcePath michael@0: ).then(Type.uint64_t.fromMsg); michael@0: }; michael@0: michael@0: /** michael@0: * Remove an empty directory. michael@0: * michael@0: * @param {string} path The name of the directory to remove. michael@0: * @param {*=} options Additional options. michael@0: * - {bool} ignoreAbsent If |true|, do not fail if the michael@0: * directory does not exist yet. michael@0: */ michael@0: File.removeEmptyDir = function removeEmptyDir(path, options) { michael@0: return Scheduler.post("removeEmptyDir", michael@0: [Type.path.toMsg(path), options], path); michael@0: }; michael@0: michael@0: /** michael@0: * Remove an existing file. michael@0: * michael@0: * @param {string} path The name of the file. michael@0: */ michael@0: File.remove = function remove(path) { michael@0: return Scheduler.post("remove", michael@0: [Type.path.toMsg(path)]); michael@0: }; michael@0: michael@0: michael@0: michael@0: /** michael@0: * Create a directory and, optionally, its parent directories. michael@0: * michael@0: * @param {string} path The name of the directory. michael@0: * @param {*=} options Additional options. michael@0: * michael@0: * - {string} from If specified, the call to |makeDir| creates all the michael@0: * ancestors of |path| that are descendants of |from|. Note that |path| michael@0: * must be a descendant of |from|, and that |from| and its existing michael@0: * subdirectories present in |path| must be user-writeable. michael@0: * Example: michael@0: * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir }); michael@0: * creates directories profileDir/foo, profileDir/foo/bar michael@0: * - {bool} ignoreExisting If |false|, throw an error if the directory michael@0: * already exists. |true| by default. Ignored if |from| is specified. michael@0: * - {number} unixMode Under Unix, if specified, a file creation mode, michael@0: * as per libc function |mkdir|. If unspecified, dirs are michael@0: * created with a default mode of 0700 (dir is private to michael@0: * the user, the user can read, write and execute). Ignored under Windows michael@0: * or if the file system does not support file creation modes. michael@0: * - {C pointer} winSecurity Under Windows, if specified, security michael@0: * attributes as per winapi function |CreateDirectory|. If michael@0: * unspecified, use the default security descriptor, inherited from michael@0: * the parent directory. Ignored under Unix or if the file system michael@0: * does not support security descriptors. michael@0: */ michael@0: File.makeDir = function makeDir(path, options) { michael@0: return Scheduler.post("makeDir", michael@0: [Type.path.toMsg(path), options], path); michael@0: }; michael@0: michael@0: /** michael@0: * Return the contents of a file michael@0: * michael@0: * @param {string} path The path to the file. michael@0: * @param {number=} bytes Optionally, an upper bound to the number of bytes michael@0: * to read. DEPRECATED - please use options.bytes instead. michael@0: * @param {JSON} options Additional options. michael@0: * - {boolean} sequential A flag that triggers a population of the page cache michael@0: * with data from a file so that subsequent reads from that file would not michael@0: * block on disk I/O. If |true| or unspecified, inform the system that the michael@0: * contents of the file will be read in order. Otherwise, make no such michael@0: * assumption. |true| by default. michael@0: * - {number} bytes An upper bound to the number of bytes to read. michael@0: * - {string} compression If "lz4" and if the file is compressed using the lz4 michael@0: * compression algorithm, decompress the file contents on the fly. michael@0: * michael@0: * @resolves {Uint8Array} A buffer holding the bytes michael@0: * read from the file. michael@0: */ michael@0: File.read = function read(path, bytes, options = {}) { michael@0: if (typeof bytes == "object") { michael@0: // Passing |bytes| as an argument is deprecated. michael@0: // We should now be passing it as a field of |options|. michael@0: options = bytes || {}; michael@0: } else { michael@0: options = clone(options, ["outExecutionDuration"]); michael@0: if (typeof bytes != "undefined") { michael@0: options.bytes = bytes; michael@0: } michael@0: } michael@0: michael@0: if (options.compression || !nativeWheneverAvailable) { michael@0: // We need to use the JS implementation. michael@0: let promise = Scheduler.post("read", michael@0: [Type.path.toMsg(path), bytes, options], path); michael@0: return promise.then( michael@0: function onSuccess(data) { michael@0: if (typeof data == "string") { michael@0: return data; michael@0: } michael@0: return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); michael@0: }); michael@0: } michael@0: michael@0: // Otherwise, use the native implementation. michael@0: return Scheduler.push(() => Native.read(path, options)); michael@0: }; michael@0: michael@0: /** michael@0: * Find outs if a file exists. michael@0: * michael@0: * @param {string} path The path to the file. michael@0: * michael@0: * @return {bool} true if the file exists, false otherwise. michael@0: */ michael@0: File.exists = function exists(path) { michael@0: return Scheduler.post("exists", michael@0: [Type.path.toMsg(path)], path); michael@0: }; michael@0: michael@0: /** michael@0: * Write a file, atomically. michael@0: * michael@0: * By opposition to a regular |write|, this operation ensures that, michael@0: * until the contents are fully written, the destination file is michael@0: * not modified. michael@0: * michael@0: * Limitation: In a few extreme cases (hardware failure during the michael@0: * write, user unplugging disk during the write, etc.), data may be michael@0: * corrupted. If your data is user-critical (e.g. preferences, michael@0: * application data, etc.), you may wish to consider adding options michael@0: * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as michael@0: * detailed below. Note that no combination of options can be michael@0: * guaranteed to totally eliminate the risk of corruption. michael@0: * michael@0: * @param {string} path The path of the file to modify. michael@0: * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write. michael@0: * @param {*=} options Optionally, an object determining the behavior michael@0: * of this function. This object may contain the following fields: michael@0: * - {number} bytes The number of bytes to write. If unspecified, michael@0: * |buffer.byteLength|. Required if |buffer| is a C pointer. michael@0: * - {string} tmpPath If |null| or unspecified, write all data directly michael@0: * to |path|. If specified, write all data to a temporary file called michael@0: * |tmpPath| and, once this write is complete, rename the file to michael@0: * replace |path|. Performing this additional operation is a little michael@0: * slower but also a little safer. michael@0: * - {bool} noOverwrite - If set, this function will fail if a file already michael@0: * exists at |path|. michael@0: * - {bool} flush - If |false| or unspecified, return immediately once the michael@0: * write is complete. If |true|, before writing, force the operating system michael@0: * to write its internal disk buffers to the disk. This is considerably slower michael@0: * (not just for the application but for the whole system) but also safer: michael@0: * if the system shuts down improperly (typically due to a kernel freeze michael@0: * or a power failure) or if the device is disconnected before the buffer michael@0: * is flushed, the file has more chances of not being corrupted. michael@0: * - {string} backupTo - If specified, backup the destination file as |backupTo|. michael@0: * Note that this function renames the destination file before overwriting it. michael@0: * If the process or the operating system freezes or crashes michael@0: * during the short window between these operations, michael@0: * the destination file will have been moved to its backup. michael@0: * michael@0: * @return {promise} michael@0: * @resolves {number} The number of bytes actually written. michael@0: */ michael@0: File.writeAtomic = function writeAtomic(path, buffer, options = {}) { michael@0: // Copy |options| to avoid modifying the original object but preserve the michael@0: // reference to |outExecutionDuration| option if it is passed. michael@0: options = clone(options, ["outExecutionDuration"]); michael@0: // As options.tmpPath is a path, we need to encode it as |Type.path| message michael@0: if ("tmpPath" in options) { michael@0: options.tmpPath = Type.path.toMsg(options.tmpPath); michael@0: }; michael@0: if (isTypedArray(buffer) && (!("bytes" in options))) { michael@0: options.bytes = buffer.byteLength; michael@0: }; michael@0: // Note: Type.void_t.out_ptr.toMsg ensures that michael@0: // - the buffer is effectively shared (not neutered) between both michael@0: // threads; michael@0: // - we take care of any |byteOffset|. michael@0: let refObj = {}; michael@0: TelemetryStopwatch.start("OSFILE_WRITEATOMIC_JANK_MS", refObj); michael@0: let promise = Scheduler.post("writeAtomic", michael@0: [Type.path.toMsg(path), michael@0: Type.void_t.in_ptr.toMsg(buffer), michael@0: options], [options, buffer]); michael@0: TelemetryStopwatch.finish("OSFILE_WRITEATOMIC_JANK_MS", refObj); michael@0: return promise; michael@0: }; michael@0: michael@0: File.removeDir = function(path, options = {}) { michael@0: return Scheduler.post("removeDir", michael@0: [Type.path.toMsg(path), options], path); michael@0: }; michael@0: michael@0: /** michael@0: * Information on a file, as returned by OS.File.stat or michael@0: * OS.File.prototype.stat michael@0: * michael@0: * @constructor michael@0: */ michael@0: File.Info = function Info(value) { michael@0: // Note that we can't just do this[k] = value[k] because our michael@0: // prototype defines getters for all of these fields. michael@0: for (let k in value) { michael@0: if (k != "creationDate") { michael@0: Object.defineProperty(this, k, {value: value[k]}); michael@0: } michael@0: } michael@0: Object.defineProperty(this, "_deprecatedCreationDate", {value: value["creationDate"]}); michael@0: }; michael@0: File.Info.prototype = SysAll.AbstractInfo.prototype; michael@0: michael@0: // Deprecated michael@0: Object.defineProperty(File.Info.prototype, "creationDate", { michael@0: get: function creationDate() { michael@0: Deprecated.warning("Field 'creationDate' is deprecated.", "https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info#Cross-platform_Attributes"); michael@0: return this._deprecatedCreationDate; michael@0: } michael@0: }); michael@0: michael@0: File.Info.fromMsg = function fromMsg(value) { michael@0: return new File.Info(value); michael@0: }; michael@0: michael@0: /** michael@0: * Get worker's current DEBUG flag. michael@0: * Note: This is used for testing purposes. michael@0: */ michael@0: File.GET_DEBUG = function GET_DEBUG() { michael@0: return Scheduler.post("GET_DEBUG"); michael@0: }; michael@0: michael@0: /** michael@0: * Iterate asynchronously through a directory michael@0: * michael@0: * @constructor michael@0: */ michael@0: let DirectoryIterator = function DirectoryIterator(path, options) { michael@0: /** michael@0: * Open the iterator on the worker thread michael@0: * michael@0: * @type {Promise} michael@0: * @resolves {*} A message accepted by the methods of DirectoryIterator michael@0: * in the worker thread michael@0: * @rejects {StopIteration} If all entries have already been visited michael@0: * or the iterator has been closed. michael@0: */ michael@0: this.__itmsg = Scheduler.post( michael@0: "new_DirectoryIterator", [Type.path.toMsg(path), options], michael@0: path michael@0: ); michael@0: this._isClosed = false; michael@0: }; michael@0: DirectoryIterator.prototype = { michael@0: iterator: function () this, michael@0: __iterator__: function () this, michael@0: michael@0: // Once close() is called, _itmsg should reject with a michael@0: // StopIteration. However, we don't want to create the promise until michael@0: // it's needed because it might never be used. In that case, we michael@0: // would get a warning on the console. michael@0: get _itmsg() { michael@0: if (!this.__itmsg) { michael@0: this.__itmsg = Promise.reject(StopIteration); michael@0: } michael@0: return this.__itmsg; michael@0: }, michael@0: michael@0: /** michael@0: * Determine whether the directory exists. michael@0: * michael@0: * @resolves {boolean} michael@0: */ michael@0: exists: function exists() { michael@0: return this._itmsg.then( michael@0: function onSuccess(iterator) { michael@0: return Scheduler.post("DirectoryIterator_prototype_exists", [iterator]); michael@0: } michael@0: ); michael@0: }, michael@0: /** michael@0: * Get the next entry in the directory. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves {OS.File.Entry} michael@0: * @rejects {StopIteration} If all entries have already been visited. michael@0: */ michael@0: next: function next() { michael@0: let self = this; michael@0: let promise = this._itmsg; michael@0: michael@0: // Get the iterator, call _next michael@0: promise = promise.then( michael@0: function withIterator(iterator) { michael@0: return self._next(iterator); michael@0: }); michael@0: michael@0: return promise; michael@0: }, michael@0: /** michael@0: * Get several entries at once. michael@0: * michael@0: * @param {number=} length If specified, the number of entries michael@0: * to return. If unspecified, return all remaining entries. michael@0: * @return {Promise} michael@0: * @resolves {Array} An array containing the |length| next entries. michael@0: */ michael@0: nextBatch: function nextBatch(size) { michael@0: if (this._isClosed) { michael@0: return Promise.resolve([]); michael@0: } michael@0: let promise = this._itmsg; michael@0: promise = promise.then( michael@0: function withIterator(iterator) { michael@0: return Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]); michael@0: }); michael@0: promise = promise.then( michael@0: function withEntries(array) { michael@0: return array.map(DirectoryIterator.Entry.fromMsg); michael@0: }); michael@0: return promise; michael@0: }, michael@0: /** michael@0: * Apply a function to all elements of the directory sequentially. michael@0: * michael@0: * @param {Function} cb This function will be applied to all entries michael@0: * of the directory. It receives as arguments michael@0: * - the OS.File.Entry corresponding to the entry; michael@0: * - the index of the entry in the enumeration; michael@0: * - the iterator itself - return |iterator.close()| to stop the loop. michael@0: * michael@0: * If the callback returns a promise, iteration waits until the michael@0: * promise is resolved before proceeding. michael@0: * michael@0: * @return {Promise} A promise resolved once the loop has reached michael@0: * its end. michael@0: */ michael@0: forEach: function forEach(cb, options) { michael@0: if (this._isClosed) { michael@0: return Promise.resolve(); michael@0: } michael@0: michael@0: let self = this; michael@0: let position = 0; michael@0: let iterator; michael@0: michael@0: // Grab iterator michael@0: let promise = this._itmsg.then( michael@0: function(aIterator) { michael@0: iterator = aIterator; michael@0: } michael@0: ); michael@0: michael@0: // Then iterate michael@0: let loop = function loop() { michael@0: if (self._isClosed) { michael@0: return Promise.resolve(); michael@0: } michael@0: return self._next(iterator).then( michael@0: function onSuccess(value) { michael@0: return Promise.resolve(cb(value, position++, self)).then(loop); michael@0: }, michael@0: function onFailure(reason) { michael@0: if (reason == StopIteration) { michael@0: return; michael@0: } michael@0: throw reason; michael@0: } michael@0: ); michael@0: }; michael@0: michael@0: return promise.then(loop); michael@0: }, michael@0: /** michael@0: * Auxiliary method: fetch the next item michael@0: * michael@0: * @rejects {StopIteration} If all entries have already been visited michael@0: * or the iterator has been closed. michael@0: */ michael@0: _next: function _next(iterator) { michael@0: if (this._isClosed) { michael@0: return this._itmsg; michael@0: } michael@0: let self = this; michael@0: let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]); michael@0: promise = promise.then( michael@0: DirectoryIterator.Entry.fromMsg, michael@0: function onReject(reason) { michael@0: if (reason == StopIteration) { michael@0: self.close(); michael@0: throw StopIteration; michael@0: } michael@0: throw reason; michael@0: }); michael@0: return promise; michael@0: }, michael@0: /** michael@0: * Close the iterator michael@0: */ michael@0: close: function close() { michael@0: if (this._isClosed) { michael@0: return Promise.resolve(); michael@0: } michael@0: this._isClosed = true; michael@0: let self = this; michael@0: return this._itmsg.then( michael@0: function withIterator(iterator) { michael@0: // Set __itmsg to null so that the _itmsg getter returns a michael@0: // rejected StopIteration promise if it's ever used. michael@0: self.__itmsg = null; michael@0: return Scheduler.post("DirectoryIterator_prototype_close", [iterator]); michael@0: } michael@0: ); michael@0: } michael@0: }; michael@0: michael@0: DirectoryIterator.Entry = function Entry(value) { michael@0: return value; michael@0: }; michael@0: DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype); michael@0: michael@0: DirectoryIterator.Entry.fromMsg = function fromMsg(value) { michael@0: return new DirectoryIterator.Entry(value); michael@0: }; michael@0: michael@0: File.resetWorker = function() { michael@0: return Task.spawn(function*() { michael@0: let resources = yield Scheduler.kill({shutdown: false, reset: true}); michael@0: if (resources && !resources.killed) { michael@0: throw new Error("Could not reset worker, this would leak file descriptors: " + JSON.stringify(resources)); michael@0: } michael@0: }); michael@0: }; michael@0: michael@0: // Constants michael@0: File.POS_START = SysAll.POS_START; michael@0: File.POS_CURRENT = SysAll.POS_CURRENT; michael@0: File.POS_END = SysAll.POS_END; michael@0: michael@0: // Exports michael@0: File.Error = OSError; michael@0: File.DirectoryIterator = DirectoryIterator; michael@0: michael@0: this.OS = {}; michael@0: this.OS.File = File; michael@0: this.OS.Constants = SharedAll.Constants; michael@0: this.OS.Shared = { michael@0: LOG: SharedAll.LOG, michael@0: Type: SysAll.Type, michael@0: get DEBUG() { michael@0: return SharedAll.Config.DEBUG; michael@0: }, michael@0: set DEBUG(x) { michael@0: return SharedAll.Config.DEBUG = x; michael@0: } michael@0: }; michael@0: Object.freeze(this.OS.Shared); michael@0: this.OS.Path = Path; michael@0: michael@0: // Returns a resolved promise when all the queued operation have been completed. michael@0: Object.defineProperty(OS.File, "queue", { michael@0: get: function() { michael@0: return Scheduler.queue; michael@0: } michael@0: }); michael@0: michael@0: // Auto-flush OS.File during profile-before-change. This ensures that any I/O michael@0: // that has been queued *before* profile-before-change is properly completed. michael@0: // To ensure that I/O queued *during* profile-before-change is completed, michael@0: // clients should register using AsyncShutdown.addBlocker. michael@0: AsyncShutdown.profileBeforeChange.addBlocker( michael@0: "OS.File: flush I/O queued before profile-before-change", michael@0: // Wait until the latest currently enqueued promise is satisfied/rejected michael@0: function() { michael@0: let DEBUG = false; michael@0: try { michael@0: DEBUG = Services.prefs.getBoolPref("toolkit.osfile.debug.failshutdown"); michael@0: } catch (ex) { michael@0: // Ignore michael@0: } michael@0: if (DEBUG) { michael@0: // Return a promise that will never be satisfied michael@0: return Promise.defer().promise; michael@0: } else { michael@0: return Scheduler.queue; michael@0: } michael@0: }, michael@0: function getDetails() { michael@0: let result = { michael@0: launched: Scheduler.launched, michael@0: shutdown: Scheduler.shutdown, michael@0: worker: !!Scheduler._worker, michael@0: pendingReset: !!Scheduler.resetTimer, michael@0: latestSent: Scheduler.Debugging.latestSent, michael@0: latestReceived: Scheduler.Debugging.latestReceived, michael@0: messagesSent: Scheduler.Debugging.messagesSent, michael@0: messagesReceived: Scheduler.Debugging.messagesReceived, michael@0: messagesQueued: Scheduler.Debugging.messagesQueued, michael@0: DEBUG: SharedAll.Config.DEBUG michael@0: }; michael@0: // Convert dates to strings for better readability michael@0: for (let key of ["latestSent", "latestReceived"]) { michael@0: if (result[key] && typeof result[key][0] == "number") { michael@0: result[key][0] = Date(result[key][0]); michael@0: } michael@0: } michael@0: return result; michael@0: } michael@0: );