toolkit/components/osfile/modules/osfile_async_front.jsm

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

mercurial