toolkit/components/osfile/modules/osfile_async_front.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 /**
michael@0 6 * Asynchronous front-end for OS.File.
michael@0 7 *
michael@0 8 * This front-end is meant to be imported from the main thread. In turn,
michael@0 9 * it spawns one worker (perhaps more in the future) and delegates all
michael@0 10 * disk I/O to this worker.
michael@0 11 *
michael@0 12 * Documentation note: most of the functions and methods in this module
michael@0 13 * return promises. For clarity, we denote as follows a promise that may resolve
michael@0 14 * with type |A| and some value |value| or reject with type |B| and some
michael@0 15 * reason |reason|
michael@0 16 * @resolves {A} value
michael@0 17 * @rejects {B} reason
michael@0 18 */
michael@0 19
michael@0 20 "use strict";
michael@0 21
michael@0 22 this.EXPORTED_SYMBOLS = ["OS"];
michael@0 23
michael@0 24 const Cu = Components.utils;
michael@0 25 const Ci = Components.interfaces;
michael@0 26
michael@0 27 let SharedAll = {};
michael@0 28 Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
michael@0 29 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
michael@0 30 Cu.import("resource://gre/modules/Timer.jsm", this);
michael@0 31
michael@0 32 XPCOMUtils.defineLazyModuleGetter(this, 'Deprecated',
michael@0 33 'resource://gre/modules/Deprecated.jsm');
michael@0 34
michael@0 35 // Boilerplate, to simplify the transition to require()
michael@0 36 let LOG = SharedAll.LOG.bind(SharedAll, "Controller");
michael@0 37 let isTypedArray = SharedAll.isTypedArray;
michael@0 38
michael@0 39 // The constructor for file errors.
michael@0 40 let SysAll = {};
michael@0 41 if (SharedAll.Constants.Win) {
michael@0 42 Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll);
michael@0 43 } else if (SharedAll.Constants.libc) {
michael@0 44 Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll);
michael@0 45 } else {
michael@0 46 throw new Error("I am neither under Windows nor under a Posix system");
michael@0 47 }
michael@0 48 let OSError = SysAll.Error;
michael@0 49 let Type = SysAll.Type;
michael@0 50
michael@0 51 let Path = {};
michael@0 52 Cu.import("resource://gre/modules/osfile/ospath.jsm", Path);
michael@0 53
michael@0 54 // The library of promises.
michael@0 55 Cu.import("resource://gre/modules/Promise.jsm", this);
michael@0 56 Cu.import("resource://gre/modules/Task.jsm", this);
michael@0 57
michael@0 58 // The implementation of communications
michael@0 59 Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
michael@0 60
michael@0 61 Cu.import("resource://gre/modules/Services.jsm", this);
michael@0 62 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
michael@0 63 Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
michael@0 64 let Native = Cu.import("resource://gre/modules/osfile/osfile_native.jsm", {});
michael@0 65
michael@0 66 /**
michael@0 67 * Constructors for decoding standard exceptions
michael@0 68 * received from the worker.
michael@0 69 */
michael@0 70 const EXCEPTION_CONSTRUCTORS = {
michael@0 71 EvalError: function(error) {
michael@0 72 return new EvalError(error.message, error.fileName, error.lineNumber);
michael@0 73 },
michael@0 74 InternalError: function(error) {
michael@0 75 return new InternalError(error.message, error.fileName, error.lineNumber);
michael@0 76 },
michael@0 77 RangeError: function(error) {
michael@0 78 return new RangeError(error.message, error.fileName, error.lineNumber);
michael@0 79 },
michael@0 80 ReferenceError: function(error) {
michael@0 81 return new ReferenceError(error.message, error.fileName, error.lineNumber);
michael@0 82 },
michael@0 83 SyntaxError: function(error) {
michael@0 84 return new SyntaxError(error.message, error.fileName, error.lineNumber);
michael@0 85 },
michael@0 86 TypeError: function(error) {
michael@0 87 return new TypeError(error.message, error.fileName, error.lineNumber);
michael@0 88 },
michael@0 89 URIError: function(error) {
michael@0 90 return new URIError(error.message, error.fileName, error.lineNumber);
michael@0 91 },
michael@0 92 OSError: function(error) {
michael@0 93 return OS.File.Error.fromMsg(error);
michael@0 94 }
michael@0 95 };
michael@0 96
michael@0 97 // It's possible for osfile.jsm to get imported before the profile is
michael@0 98 // set up. In this case, some path constants aren't yet available.
michael@0 99 // Here, we make them lazy loaders.
michael@0 100
michael@0 101 function lazyPathGetter(constProp, dirKey) {
michael@0 102 return function () {
michael@0 103 let path;
michael@0 104 try {
michael@0 105 path = Services.dirsvc.get(dirKey, Ci.nsIFile).path;
michael@0 106 delete SharedAll.Constants.Path[constProp];
michael@0 107 SharedAll.Constants.Path[constProp] = path;
michael@0 108 } catch (ex) {
michael@0 109 // Ignore errors if the value still isn't available. Hopefully
michael@0 110 // the next access will return it.
michael@0 111 }
michael@0 112
michael@0 113 return path;
michael@0 114 };
michael@0 115 }
michael@0 116
michael@0 117 for (let [constProp, dirKey] of [
michael@0 118 ["localProfileDir", "ProfLD"],
michael@0 119 ["profileDir", "ProfD"],
michael@0 120 ["userApplicationDataDir", "UAppData"],
michael@0 121 ["winAppDataDir", "AppData"],
michael@0 122 ["winStartMenuProgsDir", "Progs"],
michael@0 123 ]) {
michael@0 124
michael@0 125 if (constProp in SharedAll.Constants.Path) {
michael@0 126 continue;
michael@0 127 }
michael@0 128
michael@0 129 LOG("Installing lazy getter for OS.Constants.Path." + constProp +
michael@0 130 " because it isn't defined and profile may not be loaded.");
michael@0 131 Object.defineProperty(SharedAll.Constants.Path, constProp, {
michael@0 132 get: lazyPathGetter(constProp, dirKey),
michael@0 133 });
michael@0 134 }
michael@0 135
michael@0 136 /**
michael@0 137 * Return a shallow clone of the enumerable properties of an object.
michael@0 138 */
michael@0 139 let clone = SharedAll.clone;
michael@0 140
michael@0 141 /**
michael@0 142 * Extract a shortened version of an object, fit for logging.
michael@0 143 *
michael@0 144 * This function returns a copy of the original object in which all
michael@0 145 * long strings, Arrays, TypedArrays, ArrayBuffers are removed and
michael@0 146 * replaced with placeholders. Use this function to sanitize objects
michael@0 147 * if you wish to log them or to keep them in memory.
michael@0 148 *
michael@0 149 * @param {*} obj The obj to shorten.
michael@0 150 * @return {*} array A shorter object, fit for logging.
michael@0 151 */
michael@0 152 function summarizeObject(obj) {
michael@0 153 if (!obj) {
michael@0 154 return null;
michael@0 155 }
michael@0 156 if (typeof obj == "string") {
michael@0 157 if (obj.length > 1024) {
michael@0 158 return {"Long string": obj.length};
michael@0 159 }
michael@0 160 return obj;
michael@0 161 }
michael@0 162 if (typeof obj == "object") {
michael@0 163 if (Array.isArray(obj)) {
michael@0 164 if (obj.length > 32) {
michael@0 165 return {"Long array": obj.length};
michael@0 166 }
michael@0 167 return [summarizeObject(k) for (k of obj)];
michael@0 168 }
michael@0 169 if ("byteLength" in obj) {
michael@0 170 // Assume TypedArray or ArrayBuffer
michael@0 171 return {"Binary Data": obj.byteLength};
michael@0 172 }
michael@0 173 let result = {};
michael@0 174 for (let k of Object.keys(obj)) {
michael@0 175 result[k] = summarizeObject(obj[k]);
michael@0 176 }
michael@0 177 return result;
michael@0 178 }
michael@0 179 return obj;
michael@0 180 }
michael@0 181
michael@0 182 let Scheduler = {
michael@0 183
michael@0 184 /**
michael@0 185 * |true| once we have sent at least one message to the worker.
michael@0 186 * This field is unaffected by resetting the worker.
michael@0 187 */
michael@0 188 launched: false,
michael@0 189
michael@0 190 /**
michael@0 191 * |true| once shutdown has begun i.e. we should reject any
michael@0 192 * message, including resets.
michael@0 193 */
michael@0 194 shutdown: false,
michael@0 195
michael@0 196 /**
michael@0 197 * A promise resolved once all operations are complete.
michael@0 198 *
michael@0 199 * This promise is never rejected and the result is always undefined.
michael@0 200 */
michael@0 201 queue: Promise.resolve(),
michael@0 202
michael@0 203 /**
michael@0 204 * Miscellaneous debugging information
michael@0 205 */
michael@0 206 Debugging: {
michael@0 207 /**
michael@0 208 * The latest message sent and still waiting for a reply.
michael@0 209 */
michael@0 210 latestSent: undefined,
michael@0 211
michael@0 212 /**
michael@0 213 * The latest reply received, or null if we are waiting for a reply.
michael@0 214 */
michael@0 215 latestReceived: undefined,
michael@0 216
michael@0 217 /**
michael@0 218 * Number of messages sent to the worker. This includes the
michael@0 219 * initial SET_DEBUG, if applicable.
michael@0 220 */
michael@0 221 messagesSent: 0,
michael@0 222
michael@0 223 /**
michael@0 224 * Total number of messages ever queued, including the messages
michael@0 225 * sent.
michael@0 226 */
michael@0 227 messagesQueued: 0,
michael@0 228
michael@0 229 /**
michael@0 230 * Number of messages received from the worker.
michael@0 231 */
michael@0 232 messagesReceived: 0,
michael@0 233 },
michael@0 234
michael@0 235 /**
michael@0 236 * A timer used to automatically shut down the worker after some time.
michael@0 237 */
michael@0 238 resetTimer: null,
michael@0 239
michael@0 240 /**
michael@0 241 * The worker to which to send requests.
michael@0 242 *
michael@0 243 * If the worker has never been created or has been reset, this is a
michael@0 244 * fresh worker, initialized with osfile_async_worker.js.
michael@0 245 *
michael@0 246 * @type {PromiseWorker}
michael@0 247 */
michael@0 248 get worker() {
michael@0 249 if (!this._worker) {
michael@0 250 // Either the worker has never been created or it has been reset
michael@0 251 this._worker = new PromiseWorker(
michael@0 252 "resource://gre/modules/osfile/osfile_async_worker.js", LOG);
michael@0 253 }
michael@0 254 return this._worker;
michael@0 255 },
michael@0 256
michael@0 257 _worker: null,
michael@0 258
michael@0 259 /**
michael@0 260 * Prepare to kill the OS.File worker after a few seconds.
michael@0 261 */
michael@0 262 restartTimer: function(arg) {
michael@0 263 let delay;
michael@0 264 try {
michael@0 265 delay = Services.prefs.getIntPref("osfile.reset_worker_delay");
michael@0 266 } catch(e) {
michael@0 267 // Don't auto-shutdown if we don't have a delay preference set.
michael@0 268 return;
michael@0 269 }
michael@0 270
michael@0 271 if (this.resetTimer) {
michael@0 272 clearTimeout(this.resetTimer);
michael@0 273 }
michael@0 274 this.resetTimer = setTimeout(
michael@0 275 () => Scheduler.kill({reset: true, shutdown: false}),
michael@0 276 delay
michael@0 277 );
michael@0 278 },
michael@0 279
michael@0 280 /**
michael@0 281 * Shutdown OS.File.
michael@0 282 *
michael@0 283 * @param {*} options
michael@0 284 * - {boolean} shutdown If |true|, reject any further request. Otherwise,
michael@0 285 * further requests will resurrect the worker.
michael@0 286 * - {boolean} reset If |true|, instruct the worker to shutdown if this
michael@0 287 * would not cause leaks. Otherwise, assume that the worker will be shutdown
michael@0 288 * through some other mean.
michael@0 289 */
michael@0 290 kill: function({shutdown, reset}) {
michael@0 291 return Task.spawn(function*() {
michael@0 292
michael@0 293 yield this.queue;
michael@0 294
michael@0 295 // Enter critical section: no yield in this block
michael@0 296 // (we want to make sure that we remain the only
michael@0 297 // request in the queue).
michael@0 298
michael@0 299 if (!this.launched || this.shutdown || !this._worker) {
michael@0 300 // Nothing to kill
michael@0 301 this.shutdown = this.shutdown || shutdown;
michael@0 302 this._worker = null;
michael@0 303 return null;
michael@0 304 }
michael@0 305
michael@0 306 // Deactivate the queue, to ensure that no message is sent
michael@0 307 // to an obsolete worker (we reactivate it in the |finally|).
michael@0 308 let deferred = Promise.defer();
michael@0 309 this.queue = deferred.promise;
michael@0 310
michael@0 311
michael@0 312 // Exit critical section
michael@0 313
michael@0 314 let message = ["Meta_shutdown", [reset]];
michael@0 315
michael@0 316 try {
michael@0 317 Scheduler.latestReceived = [];
michael@0 318 Scheduler.latestSent = [Date.now(), ...message];
michael@0 319 let promise = this._worker.post(...message);
michael@0 320
michael@0 321 // Wait for result
michael@0 322 let resources;
michael@0 323 try {
michael@0 324 resources = (yield promise).ok;
michael@0 325
michael@0 326 Scheduler.latestReceived = [Date.now(), message];
michael@0 327 } catch (ex) {
michael@0 328 LOG("Could not dispatch Meta_reset", ex);
michael@0 329 // It's most likely a programmer error, but we'll assume that
michael@0 330 // the worker has been shutdown, as it's less risky than the
michael@0 331 // opposite stance.
michael@0 332 resources = {openedFiles: [], openedDirectoryIterators: [], killed: true};
michael@0 333
michael@0 334 Scheduler.latestReceived = [Date.now(), message, ex];
michael@0 335 }
michael@0 336
michael@0 337 let {openedFiles, openedDirectoryIterators, killed} = resources;
michael@0 338 if (!reset
michael@0 339 && (openedFiles && openedFiles.length
michael@0 340 || ( openedDirectoryIterators && openedDirectoryIterators.length))) {
michael@0 341 // The worker still holds resources. Report them.
michael@0 342
michael@0 343 let msg = "";
michael@0 344 if (openedFiles.length > 0) {
michael@0 345 msg += "The following files are still open:\n" +
michael@0 346 openedFiles.join("\n");
michael@0 347 }
michael@0 348 if (openedDirectoryIterators.length > 0) {
michael@0 349 msg += "The following directory iterators are still open:\n" +
michael@0 350 openedDirectoryIterators.join("\n");
michael@0 351 }
michael@0 352
michael@0 353 LOG("WARNING: File descriptors leaks detected.\n" + msg);
michael@0 354 }
michael@0 355
michael@0 356 // Make sure that we do not leave an invalid |worker| around.
michael@0 357 if (killed || shutdown) {
michael@0 358 this._worker = null;
michael@0 359 }
michael@0 360
michael@0 361 this.shutdown = shutdown;
michael@0 362
michael@0 363 return resources;
michael@0 364
michael@0 365 } finally {
michael@0 366 // Resume accepting messages. If we have set |shutdown| to |true|,
michael@0 367 // any pending/future request will be rejected. Otherwise, any
michael@0 368 // pending/future request will spawn a new worker if necessary.
michael@0 369 deferred.resolve();
michael@0 370 }
michael@0 371
michael@0 372 }.bind(this));
michael@0 373 },
michael@0 374
michael@0 375 /**
michael@0 376 * Push a task at the end of the queue.
michael@0 377 *
michael@0 378 * @param {function} code A function returning a Promise.
michael@0 379 * This function will be executed once all the previously
michael@0 380 * pushed tasks have completed.
michael@0 381 * @return {Promise} A promise with the same behavior as
michael@0 382 * the promise returned by |code|.
michael@0 383 */
michael@0 384 push: function(code) {
michael@0 385 let promise = this.queue.then(code);
michael@0 386 // By definition, |this.queue| can never reject.
michael@0 387 this.queue = promise.then(null, () => undefined);
michael@0 388 // Fork |promise| to ensure that uncaught errors are reported
michael@0 389 return promise.then(null, null);
michael@0 390 },
michael@0 391
michael@0 392 /**
michael@0 393 * Post a message to the worker thread.
michael@0 394 *
michael@0 395 * @param {string} method The name of the method to call.
michael@0 396 * @param {...} args The arguments to pass to the method. These arguments
michael@0 397 * must be clonable.
michael@0 398 * @return {Promise} A promise conveying the result/error caused by
michael@0 399 * calling |method| with arguments |args|.
michael@0 400 */
michael@0 401 post: function post(method, ...args) {
michael@0 402 if (this.shutdown) {
michael@0 403 LOG("OS.File is not available anymore. The following request has been rejected.",
michael@0 404 method, args);
michael@0 405 return Promise.reject(new Error("OS.File has been shut down. Rejecting post to " + method));
michael@0 406 }
michael@0 407 let firstLaunch = !this.launched;
michael@0 408 this.launched = true;
michael@0 409
michael@0 410 if (firstLaunch && SharedAll.Config.DEBUG) {
michael@0 411 // If we have delayed sending SET_DEBUG, do it now.
michael@0 412 this.worker.post("SET_DEBUG", [true]);
michael@0 413 Scheduler.Debugging.messagesSent++;
michael@0 414 }
michael@0 415
michael@0 416 // By convention, the last argument of any message may be an |options| object.
michael@0 417 let options;
michael@0 418 let methodArgs = args[0];
michael@0 419 if (methodArgs) {
michael@0 420 options = methodArgs[methodArgs.length - 1];
michael@0 421 }
michael@0 422 Scheduler.Debugging.messagesQueued++;
michael@0 423 return this.push(Task.async(function*() {
michael@0 424 if (this.shutdown) {
michael@0 425 LOG("OS.File is not available anymore. The following request has been rejected.",
michael@0 426 method, args);
michael@0 427 throw new Error("OS.File has been shut down. Rejecting request to " + method);
michael@0 428 }
michael@0 429
michael@0 430 // Update debugging information. As |args| may be quite
michael@0 431 // expensive, we only keep a shortened version of it.
michael@0 432 Scheduler.Debugging.latestReceived = null;
michael@0 433 Scheduler.Debugging.latestSent = [Date.now(), method, summarizeObject(methodArgs)];
michael@0 434
michael@0 435 // Don't kill the worker just yet
michael@0 436 Scheduler.restartTimer();
michael@0 437
michael@0 438
michael@0 439 let data;
michael@0 440 let reply;
michael@0 441 let isError = false;
michael@0 442 try {
michael@0 443 try {
michael@0 444 data = yield this.worker.post(method, ...args);
michael@0 445 } finally {
michael@0 446 Scheduler.Debugging.messagesReceived++;
michael@0 447 }
michael@0 448 reply = data;
michael@0 449 } catch (error) {
michael@0 450 reply = error;
michael@0 451 isError = true;
michael@0 452 if (error instanceof PromiseWorker.WorkerError) {
michael@0 453 throw EXCEPTION_CONSTRUCTORS[error.data.exn || "OSError"](error.data);
michael@0 454 }
michael@0 455 if (error instanceof ErrorEvent) {
michael@0 456 let message = error.message;
michael@0 457 if (message == "uncaught exception: [object StopIteration]") {
michael@0 458 isError = false;
michael@0 459 throw StopIteration;
michael@0 460 }
michael@0 461 throw new Error(message, error.filename, error.lineno);
michael@0 462 }
michael@0 463 throw error;
michael@0 464 } finally {
michael@0 465 Scheduler.Debugging.latestSent = Scheduler.Debugging.latestSent.slice(0, 2);
michael@0 466 if (isError) {
michael@0 467 Scheduler.Debugging.latestReceived = [Date.now(), reply.message, reply.fileName, reply.lineNumber];
michael@0 468 } else {
michael@0 469 Scheduler.Debugging.latestReceived = [Date.now(), summarizeObject(reply)];
michael@0 470 }
michael@0 471 if (firstLaunch) {
michael@0 472 Scheduler._updateTelemetry();
michael@0 473 }
michael@0 474
michael@0 475 Scheduler.restartTimer();
michael@0 476 }
michael@0 477
michael@0 478 // Check for duration and return result.
michael@0 479 if (!options) {
michael@0 480 return data.ok;
michael@0 481 }
michael@0 482 // Check for options.outExecutionDuration.
michael@0 483 if (typeof options !== "object" ||
michael@0 484 !("outExecutionDuration" in options)) {
michael@0 485 return data.ok;
michael@0 486 }
michael@0 487 // If data.durationMs is not present, return data.ok (there was an
michael@0 488 // exception applying the method).
michael@0 489 if (!("durationMs" in data)) {
michael@0 490 return data.ok;
michael@0 491 }
michael@0 492 // Bug 874425 demonstrates that two successive calls to Date.now()
michael@0 493 // can actually produce an interval with negative duration.
michael@0 494 // We assume that this is due to an operation that is so short
michael@0 495 // that Date.now() is not monotonic, so we round this up to 0.
michael@0 496 let durationMs = Math.max(0, data.durationMs);
michael@0 497 // Accumulate (or initialize) outExecutionDuration
michael@0 498 if (typeof options.outExecutionDuration == "number") {
michael@0 499 options.outExecutionDuration += durationMs;
michael@0 500 } else {
michael@0 501 options.outExecutionDuration = durationMs;
michael@0 502 }
michael@0 503 return data.ok;
michael@0 504 }.bind(this)));
michael@0 505 },
michael@0 506
michael@0 507 /**
michael@0 508 * Post Telemetry statistics.
michael@0 509 *
michael@0 510 * This is only useful on first launch.
michael@0 511 */
michael@0 512 _updateTelemetry: function() {
michael@0 513 let worker = this.worker;
michael@0 514 let workerTimeStamps = worker.workerTimeStamps;
michael@0 515 if (!workerTimeStamps) {
michael@0 516 // If the first call to OS.File results in an uncaught errors,
michael@0 517 // the timestamps are absent. As this case is a developer error,
michael@0 518 // let's not waste time attempting to extract telemetry from it.
michael@0 519 return;
michael@0 520 }
michael@0 521 let HISTOGRAM_LAUNCH = Services.telemetry.getHistogramById("OSFILE_WORKER_LAUNCH_MS");
michael@0 522 HISTOGRAM_LAUNCH.add(worker.workerTimeStamps.entered - worker.launchTimeStamp);
michael@0 523
michael@0 524 let HISTOGRAM_READY = Services.telemetry.getHistogramById("OSFILE_WORKER_READY_MS");
michael@0 525 HISTOGRAM_READY.add(worker.workerTimeStamps.loaded - worker.launchTimeStamp);
michael@0 526 }
michael@0 527 };
michael@0 528
michael@0 529 const PREF_OSFILE_LOG = "toolkit.osfile.log";
michael@0 530 const PREF_OSFILE_LOG_REDIRECT = "toolkit.osfile.log.redirect";
michael@0 531
michael@0 532 /**
michael@0 533 * Safely read a PREF_OSFILE_LOG preference.
michael@0 534 * Returns a value read or, in case of an error, oldPref or false.
michael@0 535 *
michael@0 536 * @param bool oldPref
michael@0 537 * An optional value that the DEBUG flag was set to previously.
michael@0 538 */
michael@0 539 function readDebugPref(prefName, oldPref = false) {
michael@0 540 let pref = oldPref;
michael@0 541 try {
michael@0 542 pref = Services.prefs.getBoolPref(prefName);
michael@0 543 } catch (x) {
michael@0 544 // In case of an error when reading a pref keep it as is.
michael@0 545 }
michael@0 546 // If neither pref nor oldPref were set, default it to false.
michael@0 547 return pref;
michael@0 548 };
michael@0 549
michael@0 550 /**
michael@0 551 * Listen to PREF_OSFILE_LOG changes and update gShouldLog flag
michael@0 552 * appropriately.
michael@0 553 */
michael@0 554 Services.prefs.addObserver(PREF_OSFILE_LOG,
michael@0 555 function prefObserver(aSubject, aTopic, aData) {
michael@0 556 SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, SharedAll.Config.DEBUG);
michael@0 557 if (Scheduler.launched) {
michael@0 558 // Don't start the worker just to set this preference.
michael@0 559 Scheduler.post("SET_DEBUG", [SharedAll.Config.DEBUG]);
michael@0 560 }
michael@0 561 }, false);
michael@0 562 SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, false);
michael@0 563
michael@0 564 Services.prefs.addObserver(PREF_OSFILE_LOG_REDIRECT,
michael@0 565 function prefObserver(aSubject, aTopic, aData) {
michael@0 566 SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, OS.Shared.TEST);
michael@0 567 }, false);
michael@0 568 SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, false);
michael@0 569
michael@0 570
michael@0 571 /**
michael@0 572 * If |true|, use the native implementaiton of OS.File methods
michael@0 573 * whenever possible. Otherwise, force the use of the JS version.
michael@0 574 */
michael@0 575 let nativeWheneverAvailable = true;
michael@0 576 const PREF_OSFILE_NATIVE = "toolkit.osfile.native";
michael@0 577 Services.prefs.addObserver(PREF_OSFILE_NATIVE,
michael@0 578 function prefObserver(aSubject, aTopic, aData) {
michael@0 579 nativeWheneverAvailable = readDebugPref(PREF_OSFILE_NATIVE, nativeWheneverAvailable);
michael@0 580 }, false);
michael@0 581
michael@0 582
michael@0 583 // Update worker's DEBUG flag if it's true.
michael@0 584 // Don't start the worker just for this, though.
michael@0 585 if (SharedAll.Config.DEBUG && Scheduler.launched) {
michael@0 586 Scheduler.post("SET_DEBUG", [true]);
michael@0 587 }
michael@0 588
michael@0 589 // Observer topics used for monitoring shutdown
michael@0 590 const WEB_WORKERS_SHUTDOWN_TOPIC = "web-workers-shutdown";
michael@0 591
michael@0 592 // Preference used to configure test shutdown observer.
michael@0 593 const PREF_OSFILE_TEST_SHUTDOWN_OBSERVER =
michael@0 594 "toolkit.osfile.test.shutdown.observer";
michael@0 595
michael@0 596 AsyncShutdown.webWorkersShutdown.addBlocker(
michael@0 597 "OS.File: flush pending requests, warn about unclosed files, shut down service.",
michael@0 598 () => Scheduler.kill({reset: false, shutdown: true})
michael@0 599 );
michael@0 600
michael@0 601
michael@0 602 // Attaching an observer for PREF_OSFILE_TEST_SHUTDOWN_OBSERVER to enable or
michael@0 603 // disable the test shutdown event observer.
michael@0 604 // Note: By default the PREF_OSFILE_TEST_SHUTDOWN_OBSERVER is unset.
michael@0 605 // Note: This is meant to be used for testing purposes only.
michael@0 606 Services.prefs.addObserver(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER,
michael@0 607 function prefObserver() {
michael@0 608 // The temporary phase topic used to trigger the unclosed
michael@0 609 // phase warning.
michael@0 610 let TOPIC = null;
michael@0 611 try {
michael@0 612 TOPIC = Services.prefs.getCharPref(
michael@0 613 PREF_OSFILE_TEST_SHUTDOWN_OBSERVER);
michael@0 614 } catch (x) {
michael@0 615 }
michael@0 616 if (TOPIC) {
michael@0 617 // Generate a phase, add a blocker.
michael@0 618 // Note that this can work only if AsyncShutdown itself has been
michael@0 619 // configured for testing by the testsuite.
michael@0 620 let phase = AsyncShutdown._getPhase(TOPIC);
michael@0 621 phase.addBlocker(
michael@0 622 "(for testing purposes) OS.File: warn about unclosed files",
michael@0 623 () => Scheduler.kill({shutdown: false, reset: false})
michael@0 624 );
michael@0 625 }
michael@0 626 }, false);
michael@0 627
michael@0 628 /**
michael@0 629 * Representation of a file, with asynchronous methods.
michael@0 630 *
michael@0 631 * @param {*} fdmsg The _message_ representing the platform-specific file
michael@0 632 * handle.
michael@0 633 *
michael@0 634 * @constructor
michael@0 635 */
michael@0 636 let File = function File(fdmsg) {
michael@0 637 // FIXME: At the moment, |File| does not close on finalize
michael@0 638 // (see bug 777715)
michael@0 639 this._fdmsg = fdmsg;
michael@0 640 this._closeResult = null;
michael@0 641 this._closed = null;
michael@0 642 };
michael@0 643
michael@0 644
michael@0 645 File.prototype = {
michael@0 646 /**
michael@0 647 * Close a file asynchronously.
michael@0 648 *
michael@0 649 * This method is idempotent.
michael@0 650 *
michael@0 651 * @return {promise}
michael@0 652 * @resolves {null}
michael@0 653 * @rejects {OS.File.Error}
michael@0 654 */
michael@0 655 close: function close() {
michael@0 656 if (this._fdmsg != null) {
michael@0 657 let msg = this._fdmsg;
michael@0 658 this._fdmsg = null;
michael@0 659 return this._closeResult =
michael@0 660 Scheduler.post("File_prototype_close", [msg], this);
michael@0 661 }
michael@0 662 return this._closeResult;
michael@0 663 },
michael@0 664
michael@0 665 /**
michael@0 666 * Fetch information about the file.
michael@0 667 *
michael@0 668 * @return {promise}
michael@0 669 * @resolves {OS.File.Info} The latest information about the file.
michael@0 670 * @rejects {OS.File.Error}
michael@0 671 */
michael@0 672 stat: function stat() {
michael@0 673 return Scheduler.post("File_prototype_stat", [this._fdmsg], this).then(
michael@0 674 File.Info.fromMsg
michael@0 675 );
michael@0 676 },
michael@0 677
michael@0 678 /**
michael@0 679 * Set the last access and modification date of the file.
michael@0 680 * The time stamp resolution is 1 second at best, but might be worse
michael@0 681 * depending on the platform.
michael@0 682 *
michael@0 683 * @return {promise}
michael@0 684 * @rejects {TypeError}
michael@0 685 * @rejects {OS.File.Error}
michael@0 686 */
michael@0 687 setDates: function setDates(accessDate, modificationDate) {
michael@0 688 return Scheduler.post("File_prototype_setDates",
michael@0 689 [this._fdmsg, accessDate, modificationDate], this);
michael@0 690 },
michael@0 691
michael@0 692 /**
michael@0 693 * Read a number of bytes from the file and into a buffer.
michael@0 694 *
michael@0 695 * @param {Typed array | C pointer} buffer This buffer will be
michael@0 696 * modified by another thread. Using this buffer before the |read|
michael@0 697 * operation has completed is a BAD IDEA.
michael@0 698 * @param {JSON} options
michael@0 699 *
michael@0 700 * @return {promise}
michael@0 701 * @resolves {number} The number of bytes effectively read.
michael@0 702 * @rejects {OS.File.Error}
michael@0 703 */
michael@0 704 readTo: function readTo(buffer, options = {}) {
michael@0 705 // If |buffer| is a typed array and there is no |bytes| options, we
michael@0 706 // need to extract the |byteLength| now, as it will be lost by
michael@0 707 // communication.
michael@0 708 // Options might be a nullish value, so better check for that before using
michael@0 709 // the |in| operator.
michael@0 710 if (isTypedArray(buffer) && !(options && "bytes" in options)) {
michael@0 711 // Preserve reference to option |outExecutionDuration|, if it is passed.
michael@0 712 options = clone(options, ["outExecutionDuration"]);
michael@0 713 options.bytes = buffer.byteLength;
michael@0 714 }
michael@0 715 // Note: Type.void_t.out_ptr.toMsg ensures that
michael@0 716 // - the buffer is effectively shared (not neutered) between both
michael@0 717 // threads;
michael@0 718 // - we take care of any |byteOffset|.
michael@0 719 return Scheduler.post("File_prototype_readTo",
michael@0 720 [this._fdmsg,
michael@0 721 Type.void_t.out_ptr.toMsg(buffer),
michael@0 722 options],
michael@0 723 buffer/*Ensure that |buffer| is not gc-ed*/);
michael@0 724 },
michael@0 725 /**
michael@0 726 * Write bytes from a buffer to this file.
michael@0 727 *
michael@0 728 * Note that, by default, this function may perform several I/O
michael@0 729 * operations to ensure that the buffer is fully written.
michael@0 730 *
michael@0 731 * @param {Typed array | C pointer} buffer The buffer in which the
michael@0 732 * the bytes are stored. The buffer must be large enough to
michael@0 733 * accomodate |bytes| bytes. Using the buffer before the operation
michael@0 734 * is complete is a BAD IDEA.
michael@0 735 * @param {*=} options Optionally, an object that may contain the
michael@0 736 * following fields:
michael@0 737 * - {number} bytes The number of |bytes| to write from the buffer. If
michael@0 738 * unspecified, this is |buffer.byteLength|. Note that |bytes| is required
michael@0 739 * if |buffer| is a C pointer.
michael@0 740 *
michael@0 741 * @return {number} The number of bytes actually written.
michael@0 742 */
michael@0 743 write: function write(buffer, options = {}) {
michael@0 744 // If |buffer| is a typed array and there is no |bytes| options,
michael@0 745 // we need to extract the |byteLength| now, as it will be lost
michael@0 746 // by communication.
michael@0 747 // Options might be a nullish value, so better check for that before using
michael@0 748 // the |in| operator.
michael@0 749 if (isTypedArray(buffer) && !(options && "bytes" in options)) {
michael@0 750 // Preserve reference to option |outExecutionDuration|, if it is passed.
michael@0 751 options = clone(options, ["outExecutionDuration"]);
michael@0 752 options.bytes = buffer.byteLength;
michael@0 753 }
michael@0 754 // Note: Type.void_t.out_ptr.toMsg ensures that
michael@0 755 // - the buffer is effectively shared (not neutered) between both
michael@0 756 // threads;
michael@0 757 // - we take care of any |byteOffset|.
michael@0 758 return Scheduler.post("File_prototype_write",
michael@0 759 [this._fdmsg,
michael@0 760 Type.void_t.in_ptr.toMsg(buffer),
michael@0 761 options],
michael@0 762 buffer/*Ensure that |buffer| is not gc-ed*/);
michael@0 763 },
michael@0 764
michael@0 765 /**
michael@0 766 * Read bytes from this file to a new buffer.
michael@0 767 *
michael@0 768 * @param {number=} bytes If unspecified, read all the remaining bytes from
michael@0 769 * this file. If specified, read |bytes| bytes, or less if the file does not
michael@0 770 * contain that many bytes.
michael@0 771 * @param {JSON} options
michael@0 772 * @return {promise}
michael@0 773 * @resolves {Uint8Array} An array containing the bytes read.
michael@0 774 */
michael@0 775 read: function read(nbytes, options = {}) {
michael@0 776 let promise = Scheduler.post("File_prototype_read",
michael@0 777 [this._fdmsg,
michael@0 778 nbytes, options]);
michael@0 779 return promise.then(
michael@0 780 function onSuccess(data) {
michael@0 781 return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
michael@0 782 });
michael@0 783 },
michael@0 784
michael@0 785 /**
michael@0 786 * Return the current position in the file, as bytes.
michael@0 787 *
michael@0 788 * @return {promise}
michael@0 789 * @resolves {number} The current position in the file,
michael@0 790 * as a number of bytes since the start of the file.
michael@0 791 */
michael@0 792 getPosition: function getPosition() {
michael@0 793 return Scheduler.post("File_prototype_getPosition",
michael@0 794 [this._fdmsg]);
michael@0 795 },
michael@0 796
michael@0 797 /**
michael@0 798 * Set the current position in the file, as bytes.
michael@0 799 *
michael@0 800 * @param {number} pos A number of bytes.
michael@0 801 * @param {number} whence The reference position in the file,
michael@0 802 * which may be either POS_START (from the start of the file),
michael@0 803 * POS_END (from the end of the file) or POS_CUR (from the
michael@0 804 * current position in the file).
michael@0 805 *
michael@0 806 * @return {promise}
michael@0 807 */
michael@0 808 setPosition: function setPosition(pos, whence) {
michael@0 809 return Scheduler.post("File_prototype_setPosition",
michael@0 810 [this._fdmsg, pos, whence]);
michael@0 811 },
michael@0 812
michael@0 813 /**
michael@0 814 * Flushes the file's buffers and causes all buffered data
michael@0 815 * to be written.
michael@0 816 * Disk flushes are very expensive and therefore should be used carefully,
michael@0 817 * sparingly and only in scenarios where it is vital that data survives
michael@0 818 * system crashes. Even though the function will be executed off the
michael@0 819 * main-thread, it might still affect the overall performance of any running
michael@0 820 * application.
michael@0 821 *
michael@0 822 * @return {promise}
michael@0 823 */
michael@0 824 flush: function flush() {
michael@0 825 return Scheduler.post("File_prototype_flush",
michael@0 826 [this._fdmsg]);
michael@0 827 },
michael@0 828
michael@0 829 /**
michael@0 830 * Set the file's access permissions. Without any options, the
michael@0 831 * permissions are set to an approximation of what they would have
michael@0 832 * been if the file had been created in its current directory in the
michael@0 833 * "most typical" fashion for the operating system. In the current
michael@0 834 * implementation, this means that on Unix-like systems (including
michael@0 835 * Android, B2G, etc) we set the POSIX file mode to (0666 & ~umask),
michael@0 836 * and on Windows, we do nothing.
michael@0 837 *
michael@0 838 * This operation is likely to fail if applied to a file that was
michael@0 839 * not created by the currently running program (more precisely,
michael@0 840 * if it was created by a program running under a different OS-level
michael@0 841 * user account). It may also fail, or silently do nothing, if the
michael@0 842 * filesystem containing the file does not support access permissions.
michael@0 843 *
michael@0 844 * @param {*=} options
michael@0 845 * - {number} unixMode If present, the POSIX file mode is set to exactly
michael@0 846 * this value, unless |unixHonorUmask| is also
michael@0 847 * present.
michael@0 848 * - {bool} unixHonorUmask If true, any |unixMode| value is modified by the
michael@0 849 * process umask, as open() would have done.
michael@0 850 */
michael@0 851 setPermissions: function setPermissions(options = {}) {
michael@0 852 return Scheduler.post("File_prototype_setPermissions",
michael@0 853 [this._fdmsg, options]);
michael@0 854 }
michael@0 855 };
michael@0 856
michael@0 857 /**
michael@0 858 * Open a file asynchronously.
michael@0 859 *
michael@0 860 * @return {promise}
michael@0 861 * @resolves {OS.File}
michael@0 862 * @rejects {OS.Error}
michael@0 863 */
michael@0 864 File.open = function open(path, mode, options) {
michael@0 865 return Scheduler.post(
michael@0 866 "open", [Type.path.toMsg(path), mode, options],
michael@0 867 path
michael@0 868 ).then(
michael@0 869 function onSuccess(msg) {
michael@0 870 return new File(msg);
michael@0 871 }
michael@0 872 );
michael@0 873 };
michael@0 874
michael@0 875 /**
michael@0 876 * 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 877 *
michael@0 878 * @param {string} path The path to the file.
michael@0 879 * @param {*=} options Additional options for file opening. This
michael@0 880 * implementation interprets the following fields:
michael@0 881 *
michael@0 882 * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext.
michael@0 883 * If |false| use HEX numbers ie: filename-A65BC0.ext
michael@0 884 * - {number} maxReadableNumber Used to limit the amount of tries after a failed
michael@0 885 * file creation. Default is 20.
michael@0 886 *
michael@0 887 * @return {Object} contains A file object{file} and the path{path}.
michael@0 888 * @throws {OS.File.Error} If the file could not be opened.
michael@0 889 */
michael@0 890 File.openUnique = function openUnique(path, options) {
michael@0 891 return Scheduler.post(
michael@0 892 "openUnique", [Type.path.toMsg(path), options],
michael@0 893 path
michael@0 894 ).then(
michael@0 895 function onSuccess(msg) {
michael@0 896 return {
michael@0 897 path: msg.path,
michael@0 898 file: new File(msg.file)
michael@0 899 };
michael@0 900 }
michael@0 901 );
michael@0 902 };
michael@0 903
michael@0 904 /**
michael@0 905 * Get the information on the file.
michael@0 906 *
michael@0 907 * @return {promise}
michael@0 908 * @resolves {OS.File.Info}
michael@0 909 * @rejects {OS.Error}
michael@0 910 */
michael@0 911 File.stat = function stat(path, options) {
michael@0 912 return Scheduler.post(
michael@0 913 "stat", [Type.path.toMsg(path), options],
michael@0 914 path).then(File.Info.fromMsg);
michael@0 915 };
michael@0 916
michael@0 917
michael@0 918 /**
michael@0 919 * Set the last access and modification date of the file.
michael@0 920 * The time stamp resolution is 1 second at best, but might be worse
michael@0 921 * depending on the platform.
michael@0 922 *
michael@0 923 * @return {promise}
michael@0 924 * @rejects {TypeError}
michael@0 925 * @rejects {OS.File.Error}
michael@0 926 */
michael@0 927 File.setDates = function setDates(path, accessDate, modificationDate) {
michael@0 928 return Scheduler.post("setDates",
michael@0 929 [Type.path.toMsg(path), accessDate, modificationDate],
michael@0 930 this);
michael@0 931 };
michael@0 932
michael@0 933 /**
michael@0 934 * Set the file's access permissions. Without any options, the
michael@0 935 * permissions are set to an approximation of what they would have
michael@0 936 * been if the file had been created in its current directory in the
michael@0 937 * "most typical" fashion for the operating system. In the current
michael@0 938 * implementation, this means that on Unix-like systems (including
michael@0 939 * Android, B2G, etc) we set the POSIX file mode to (0666 & ~umask),
michael@0 940 * and on Windows, we do nothing.
michael@0 941 *
michael@0 942 * This operation is likely to fail if applied to a file that was
michael@0 943 * not created by the currently running program (more precisely,
michael@0 944 * if it was created by a program running under a different OS-level
michael@0 945 * user account). It may also fail, or silently do nothing, if the
michael@0 946 * filesystem containing the file does not support access permissions.
michael@0 947 *
michael@0 948 * @param {string} path The path to the file.
michael@0 949 *
michael@0 950 * @param {*=} options
michael@0 951 * - {number} unixMode If present, the POSIX file mode is set to exactly
michael@0 952 * this value, unless |unixHonorUmask| is also
michael@0 953 * present.
michael@0 954 * - {bool} unixHonorUmask If true, any |unixMode| value is modified by the
michael@0 955 * process umask, as open() would have done.
michael@0 956 */
michael@0 957 File.setPermissions = function setPermissions(path, options = {}) {
michael@0 958 return Scheduler.post("setPermissions",
michael@0 959 [Type.path.toMsg(path), options]);
michael@0 960 };
michael@0 961
michael@0 962 /**
michael@0 963 * Fetch the current directory
michael@0 964 *
michael@0 965 * @return {promise}
michael@0 966 * @resolves {string} The current directory, as a path usable with OS.Path
michael@0 967 * @rejects {OS.Error}
michael@0 968 */
michael@0 969 File.getCurrentDirectory = function getCurrentDirectory() {
michael@0 970 return Scheduler.post(
michael@0 971 "getCurrentDirectory"
michael@0 972 ).then(Type.path.fromMsg);
michael@0 973 };
michael@0 974
michael@0 975 /**
michael@0 976 * Change the current directory
michael@0 977 *
michael@0 978 * @param {string} path The OS-specific path to the current directory.
michael@0 979 * You should use the methods of OS.Path and the constants of OS.Constants.Path
michael@0 980 * to build OS-specific paths in a portable manner.
michael@0 981 *
michael@0 982 * @return {promise}
michael@0 983 * @resolves {null}
michael@0 984 * @rejects {OS.Error}
michael@0 985 */
michael@0 986 File.setCurrentDirectory = function setCurrentDirectory(path) {
michael@0 987 return Scheduler.post(
michael@0 988 "setCurrentDirectory", [Type.path.toMsg(path)], path
michael@0 989 );
michael@0 990 };
michael@0 991
michael@0 992 /**
michael@0 993 * Copy a file to a destination.
michael@0 994 *
michael@0 995 * @param {string} sourcePath The platform-specific path at which
michael@0 996 * the file may currently be found.
michael@0 997 * @param {string} destPath The platform-specific path at which the
michael@0 998 * file should be copied.
michael@0 999 * @param {*=} options An object which may contain the following fields:
michael@0 1000 *
michael@0 1001 * @option {bool} noOverwrite - If true, this function will fail if
michael@0 1002 * a file already exists at |destPath|. Otherwise, if this file exists,
michael@0 1003 * it will be erased silently.
michael@0 1004 *
michael@0 1005 * @rejects {OS.File.Error} In case of any error.
michael@0 1006 *
michael@0 1007 * General note: The behavior of this function is defined only when
michael@0 1008 * it is called on a single file. If it is called on a directory, the
michael@0 1009 * behavior is undefined and may not be the same across all platforms.
michael@0 1010 *
michael@0 1011 * General note: The behavior of this function with respect to metadata
michael@0 1012 * is unspecified. Metadata may or may not be copied with the file. The
michael@0 1013 * behavior may not be the same across all platforms.
michael@0 1014 */
michael@0 1015 File.copy = function copy(sourcePath, destPath, options) {
michael@0 1016 return Scheduler.post("copy", [Type.path.toMsg(sourcePath),
michael@0 1017 Type.path.toMsg(destPath), options], [sourcePath, destPath]);
michael@0 1018 };
michael@0 1019
michael@0 1020 /**
michael@0 1021 * Move a file to a destination.
michael@0 1022 *
michael@0 1023 * @param {string} sourcePath The platform-specific path at which
michael@0 1024 * the file may currently be found.
michael@0 1025 * @param {string} destPath The platform-specific path at which the
michael@0 1026 * file should be moved.
michael@0 1027 * @param {*=} options An object which may contain the following fields:
michael@0 1028 *
michael@0 1029 * @option {bool} noOverwrite - If set, this function will fail if
michael@0 1030 * a file already exists at |destPath|. Otherwise, if this file exists,
michael@0 1031 * it will be erased silently.
michael@0 1032 *
michael@0 1033 * @returns {Promise}
michael@0 1034 * @rejects {OS.File.Error} In case of any error.
michael@0 1035 *
michael@0 1036 * General note: The behavior of this function is defined only when
michael@0 1037 * it is called on a single file. If it is called on a directory, the
michael@0 1038 * behavior is undefined and may not be the same across all platforms.
michael@0 1039 *
michael@0 1040 * General note: The behavior of this function with respect to metadata
michael@0 1041 * is unspecified. Metadata may or may not be moved with the file. The
michael@0 1042 * behavior may not be the same across all platforms.
michael@0 1043 */
michael@0 1044 File.move = function move(sourcePath, destPath, options) {
michael@0 1045 return Scheduler.post("move", [Type.path.toMsg(sourcePath),
michael@0 1046 Type.path.toMsg(destPath), options], [sourcePath, destPath]);
michael@0 1047 };
michael@0 1048
michael@0 1049 /**
michael@0 1050 * Create a symbolic link to a source.
michael@0 1051 *
michael@0 1052 * @param {string} sourcePath The platform-specific path to which
michael@0 1053 * the symbolic link should point.
michael@0 1054 * @param {string} destPath The platform-specific path at which the
michael@0 1055 * symbolic link should be created.
michael@0 1056 *
michael@0 1057 * @returns {Promise}
michael@0 1058 * @rejects {OS.File.Error} In case of any error.
michael@0 1059 */
michael@0 1060 if (!SharedAll.Constants.Win) {
michael@0 1061 File.unixSymLink = function unixSymLink(sourcePath, destPath) {
michael@0 1062 return Scheduler.post("unixSymLink", [Type.path.toMsg(sourcePath),
michael@0 1063 Type.path.toMsg(destPath)], [sourcePath, destPath]);
michael@0 1064 };
michael@0 1065 }
michael@0 1066
michael@0 1067 /**
michael@0 1068 * Gets the number of bytes available on disk to the current user.
michael@0 1069 *
michael@0 1070 * @param {string} Platform-specific path to a directory on the disk to
michael@0 1071 * query for free available bytes.
michael@0 1072 *
michael@0 1073 * @return {number} The number of bytes available for the current user.
michael@0 1074 * @throws {OS.File.Error} In case of any error.
michael@0 1075 */
michael@0 1076 File.getAvailableFreeSpace = function getAvailableFreeSpace(sourcePath) {
michael@0 1077 return Scheduler.post("getAvailableFreeSpace",
michael@0 1078 [Type.path.toMsg(sourcePath)], sourcePath
michael@0 1079 ).then(Type.uint64_t.fromMsg);
michael@0 1080 };
michael@0 1081
michael@0 1082 /**
michael@0 1083 * Remove an empty directory.
michael@0 1084 *
michael@0 1085 * @param {string} path The name of the directory to remove.
michael@0 1086 * @param {*=} options Additional options.
michael@0 1087 * - {bool} ignoreAbsent If |true|, do not fail if the
michael@0 1088 * directory does not exist yet.
michael@0 1089 */
michael@0 1090 File.removeEmptyDir = function removeEmptyDir(path, options) {
michael@0 1091 return Scheduler.post("removeEmptyDir",
michael@0 1092 [Type.path.toMsg(path), options], path);
michael@0 1093 };
michael@0 1094
michael@0 1095 /**
michael@0 1096 * Remove an existing file.
michael@0 1097 *
michael@0 1098 * @param {string} path The name of the file.
michael@0 1099 */
michael@0 1100 File.remove = function remove(path) {
michael@0 1101 return Scheduler.post("remove",
michael@0 1102 [Type.path.toMsg(path)]);
michael@0 1103 };
michael@0 1104
michael@0 1105
michael@0 1106
michael@0 1107 /**
michael@0 1108 * Create a directory and, optionally, its parent directories.
michael@0 1109 *
michael@0 1110 * @param {string} path The name of the directory.
michael@0 1111 * @param {*=} options Additional options.
michael@0 1112 *
michael@0 1113 * - {string} from If specified, the call to |makeDir| creates all the
michael@0 1114 * ancestors of |path| that are descendants of |from|. Note that |path|
michael@0 1115 * must be a descendant of |from|, and that |from| and its existing
michael@0 1116 * subdirectories present in |path| must be user-writeable.
michael@0 1117 * Example:
michael@0 1118 * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
michael@0 1119 * creates directories profileDir/foo, profileDir/foo/bar
michael@0 1120 * - {bool} ignoreExisting If |false|, throw an error if the directory
michael@0 1121 * already exists. |true| by default. Ignored if |from| is specified.
michael@0 1122 * - {number} unixMode Under Unix, if specified, a file creation mode,
michael@0 1123 * as per libc function |mkdir|. If unspecified, dirs are
michael@0 1124 * created with a default mode of 0700 (dir is private to
michael@0 1125 * the user, the user can read, write and execute). Ignored under Windows
michael@0 1126 * or if the file system does not support file creation modes.
michael@0 1127 * - {C pointer} winSecurity Under Windows, if specified, security
michael@0 1128 * attributes as per winapi function |CreateDirectory|. If
michael@0 1129 * unspecified, use the default security descriptor, inherited from
michael@0 1130 * the parent directory. Ignored under Unix or if the file system
michael@0 1131 * does not support security descriptors.
michael@0 1132 */
michael@0 1133 File.makeDir = function makeDir(path, options) {
michael@0 1134 return Scheduler.post("makeDir",
michael@0 1135 [Type.path.toMsg(path), options], path);
michael@0 1136 };
michael@0 1137
michael@0 1138 /**
michael@0 1139 * Return the contents of a file
michael@0 1140 *
michael@0 1141 * @param {string} path The path to the file.
michael@0 1142 * @param {number=} bytes Optionally, an upper bound to the number of bytes
michael@0 1143 * to read. DEPRECATED - please use options.bytes instead.
michael@0 1144 * @param {JSON} options Additional options.
michael@0 1145 * - {boolean} sequential A flag that triggers a population of the page cache
michael@0 1146 * with data from a file so that subsequent reads from that file would not
michael@0 1147 * block on disk I/O. If |true| or unspecified, inform the system that the
michael@0 1148 * contents of the file will be read in order. Otherwise, make no such
michael@0 1149 * assumption. |true| by default.
michael@0 1150 * - {number} bytes An upper bound to the number of bytes to read.
michael@0 1151 * - {string} compression If "lz4" and if the file is compressed using the lz4
michael@0 1152 * compression algorithm, decompress the file contents on the fly.
michael@0 1153 *
michael@0 1154 * @resolves {Uint8Array} A buffer holding the bytes
michael@0 1155 * read from the file.
michael@0 1156 */
michael@0 1157 File.read = function read(path, bytes, options = {}) {
michael@0 1158 if (typeof bytes == "object") {
michael@0 1159 // Passing |bytes| as an argument is deprecated.
michael@0 1160 // We should now be passing it as a field of |options|.
michael@0 1161 options = bytes || {};
michael@0 1162 } else {
michael@0 1163 options = clone(options, ["outExecutionDuration"]);
michael@0 1164 if (typeof bytes != "undefined") {
michael@0 1165 options.bytes = bytes;
michael@0 1166 }
michael@0 1167 }
michael@0 1168
michael@0 1169 if (options.compression || !nativeWheneverAvailable) {
michael@0 1170 // We need to use the JS implementation.
michael@0 1171 let promise = Scheduler.post("read",
michael@0 1172 [Type.path.toMsg(path), bytes, options], path);
michael@0 1173 return promise.then(
michael@0 1174 function onSuccess(data) {
michael@0 1175 if (typeof data == "string") {
michael@0 1176 return data;
michael@0 1177 }
michael@0 1178 return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
michael@0 1179 });
michael@0 1180 }
michael@0 1181
michael@0 1182 // Otherwise, use the native implementation.
michael@0 1183 return Scheduler.push(() => Native.read(path, options));
michael@0 1184 };
michael@0 1185
michael@0 1186 /**
michael@0 1187 * Find outs if a file exists.
michael@0 1188 *
michael@0 1189 * @param {string} path The path to the file.
michael@0 1190 *
michael@0 1191 * @return {bool} true if the file exists, false otherwise.
michael@0 1192 */
michael@0 1193 File.exists = function exists(path) {
michael@0 1194 return Scheduler.post("exists",
michael@0 1195 [Type.path.toMsg(path)], path);
michael@0 1196 };
michael@0 1197
michael@0 1198 /**
michael@0 1199 * Write a file, atomically.
michael@0 1200 *
michael@0 1201 * By opposition to a regular |write|, this operation ensures that,
michael@0 1202 * until the contents are fully written, the destination file is
michael@0 1203 * not modified.
michael@0 1204 *
michael@0 1205 * Limitation: In a few extreme cases (hardware failure during the
michael@0 1206 * write, user unplugging disk during the write, etc.), data may be
michael@0 1207 * corrupted. If your data is user-critical (e.g. preferences,
michael@0 1208 * application data, etc.), you may wish to consider adding options
michael@0 1209 * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as
michael@0 1210 * detailed below. Note that no combination of options can be
michael@0 1211 * guaranteed to totally eliminate the risk of corruption.
michael@0 1212 *
michael@0 1213 * @param {string} path The path of the file to modify.
michael@0 1214 * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write.
michael@0 1215 * @param {*=} options Optionally, an object determining the behavior
michael@0 1216 * of this function. This object may contain the following fields:
michael@0 1217 * - {number} bytes The number of bytes to write. If unspecified,
michael@0 1218 * |buffer.byteLength|. Required if |buffer| is a C pointer.
michael@0 1219 * - {string} tmpPath If |null| or unspecified, write all data directly
michael@0 1220 * to |path|. If specified, write all data to a temporary file called
michael@0 1221 * |tmpPath| and, once this write is complete, rename the file to
michael@0 1222 * replace |path|. Performing this additional operation is a little
michael@0 1223 * slower but also a little safer.
michael@0 1224 * - {bool} noOverwrite - If set, this function will fail if a file already
michael@0 1225 * exists at |path|.
michael@0 1226 * - {bool} flush - If |false| or unspecified, return immediately once the
michael@0 1227 * write is complete. If |true|, before writing, force the operating system
michael@0 1228 * to write its internal disk buffers to the disk. This is considerably slower
michael@0 1229 * (not just for the application but for the whole system) but also safer:
michael@0 1230 * if the system shuts down improperly (typically due to a kernel freeze
michael@0 1231 * or a power failure) or if the device is disconnected before the buffer
michael@0 1232 * is flushed, the file has more chances of not being corrupted.
michael@0 1233 * - {string} backupTo - If specified, backup the destination file as |backupTo|.
michael@0 1234 * Note that this function renames the destination file before overwriting it.
michael@0 1235 * If the process or the operating system freezes or crashes
michael@0 1236 * during the short window between these operations,
michael@0 1237 * the destination file will have been moved to its backup.
michael@0 1238 *
michael@0 1239 * @return {promise}
michael@0 1240 * @resolves {number} The number of bytes actually written.
michael@0 1241 */
michael@0 1242 File.writeAtomic = function writeAtomic(path, buffer, options = {}) {
michael@0 1243 // Copy |options| to avoid modifying the original object but preserve the
michael@0 1244 // reference to |outExecutionDuration| option if it is passed.
michael@0 1245 options = clone(options, ["outExecutionDuration"]);
michael@0 1246 // As options.tmpPath is a path, we need to encode it as |Type.path| message
michael@0 1247 if ("tmpPath" in options) {
michael@0 1248 options.tmpPath = Type.path.toMsg(options.tmpPath);
michael@0 1249 };
michael@0 1250 if (isTypedArray(buffer) && (!("bytes" in options))) {
michael@0 1251 options.bytes = buffer.byteLength;
michael@0 1252 };
michael@0 1253 // Note: Type.void_t.out_ptr.toMsg ensures that
michael@0 1254 // - the buffer is effectively shared (not neutered) between both
michael@0 1255 // threads;
michael@0 1256 // - we take care of any |byteOffset|.
michael@0 1257 let refObj = {};
michael@0 1258 TelemetryStopwatch.start("OSFILE_WRITEATOMIC_JANK_MS", refObj);
michael@0 1259 let promise = Scheduler.post("writeAtomic",
michael@0 1260 [Type.path.toMsg(path),
michael@0 1261 Type.void_t.in_ptr.toMsg(buffer),
michael@0 1262 options], [options, buffer]);
michael@0 1263 TelemetryStopwatch.finish("OSFILE_WRITEATOMIC_JANK_MS", refObj);
michael@0 1264 return promise;
michael@0 1265 };
michael@0 1266
michael@0 1267 File.removeDir = function(path, options = {}) {
michael@0 1268 return Scheduler.post("removeDir",
michael@0 1269 [Type.path.toMsg(path), options], path);
michael@0 1270 };
michael@0 1271
michael@0 1272 /**
michael@0 1273 * Information on a file, as returned by OS.File.stat or
michael@0 1274 * OS.File.prototype.stat
michael@0 1275 *
michael@0 1276 * @constructor
michael@0 1277 */
michael@0 1278 File.Info = function Info(value) {
michael@0 1279 // Note that we can't just do this[k] = value[k] because our
michael@0 1280 // prototype defines getters for all of these fields.
michael@0 1281 for (let k in value) {
michael@0 1282 if (k != "creationDate") {
michael@0 1283 Object.defineProperty(this, k, {value: value[k]});
michael@0 1284 }
michael@0 1285 }
michael@0 1286 Object.defineProperty(this, "_deprecatedCreationDate", {value: value["creationDate"]});
michael@0 1287 };
michael@0 1288 File.Info.prototype = SysAll.AbstractInfo.prototype;
michael@0 1289
michael@0 1290 // Deprecated
michael@0 1291 Object.defineProperty(File.Info.prototype, "creationDate", {
michael@0 1292 get: function creationDate() {
michael@0 1293 Deprecated.warning("Field 'creationDate' is deprecated.", "https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info#Cross-platform_Attributes");
michael@0 1294 return this._deprecatedCreationDate;
michael@0 1295 }
michael@0 1296 });
michael@0 1297
michael@0 1298 File.Info.fromMsg = function fromMsg(value) {
michael@0 1299 return new File.Info(value);
michael@0 1300 };
michael@0 1301
michael@0 1302 /**
michael@0 1303 * Get worker's current DEBUG flag.
michael@0 1304 * Note: This is used for testing purposes.
michael@0 1305 */
michael@0 1306 File.GET_DEBUG = function GET_DEBUG() {
michael@0 1307 return Scheduler.post("GET_DEBUG");
michael@0 1308 };
michael@0 1309
michael@0 1310 /**
michael@0 1311 * Iterate asynchronously through a directory
michael@0 1312 *
michael@0 1313 * @constructor
michael@0 1314 */
michael@0 1315 let DirectoryIterator = function DirectoryIterator(path, options) {
michael@0 1316 /**
michael@0 1317 * Open the iterator on the worker thread
michael@0 1318 *
michael@0 1319 * @type {Promise}
michael@0 1320 * @resolves {*} A message accepted by the methods of DirectoryIterator
michael@0 1321 * in the worker thread
michael@0 1322 * @rejects {StopIteration} If all entries have already been visited
michael@0 1323 * or the iterator has been closed.
michael@0 1324 */
michael@0 1325 this.__itmsg = Scheduler.post(
michael@0 1326 "new_DirectoryIterator", [Type.path.toMsg(path), options],
michael@0 1327 path
michael@0 1328 );
michael@0 1329 this._isClosed = false;
michael@0 1330 };
michael@0 1331 DirectoryIterator.prototype = {
michael@0 1332 iterator: function () this,
michael@0 1333 __iterator__: function () this,
michael@0 1334
michael@0 1335 // Once close() is called, _itmsg should reject with a
michael@0 1336 // StopIteration. However, we don't want to create the promise until
michael@0 1337 // it's needed because it might never be used. In that case, we
michael@0 1338 // would get a warning on the console.
michael@0 1339 get _itmsg() {
michael@0 1340 if (!this.__itmsg) {
michael@0 1341 this.__itmsg = Promise.reject(StopIteration);
michael@0 1342 }
michael@0 1343 return this.__itmsg;
michael@0 1344 },
michael@0 1345
michael@0 1346 /**
michael@0 1347 * Determine whether the directory exists.
michael@0 1348 *
michael@0 1349 * @resolves {boolean}
michael@0 1350 */
michael@0 1351 exists: function exists() {
michael@0 1352 return this._itmsg.then(
michael@0 1353 function onSuccess(iterator) {
michael@0 1354 return Scheduler.post("DirectoryIterator_prototype_exists", [iterator]);
michael@0 1355 }
michael@0 1356 );
michael@0 1357 },
michael@0 1358 /**
michael@0 1359 * Get the next entry in the directory.
michael@0 1360 *
michael@0 1361 * @return {Promise}
michael@0 1362 * @resolves {OS.File.Entry}
michael@0 1363 * @rejects {StopIteration} If all entries have already been visited.
michael@0 1364 */
michael@0 1365 next: function next() {
michael@0 1366 let self = this;
michael@0 1367 let promise = this._itmsg;
michael@0 1368
michael@0 1369 // Get the iterator, call _next
michael@0 1370 promise = promise.then(
michael@0 1371 function withIterator(iterator) {
michael@0 1372 return self._next(iterator);
michael@0 1373 });
michael@0 1374
michael@0 1375 return promise;
michael@0 1376 },
michael@0 1377 /**
michael@0 1378 * Get several entries at once.
michael@0 1379 *
michael@0 1380 * @param {number=} length If specified, the number of entries
michael@0 1381 * to return. If unspecified, return all remaining entries.
michael@0 1382 * @return {Promise}
michael@0 1383 * @resolves {Array} An array containing the |length| next entries.
michael@0 1384 */
michael@0 1385 nextBatch: function nextBatch(size) {
michael@0 1386 if (this._isClosed) {
michael@0 1387 return Promise.resolve([]);
michael@0 1388 }
michael@0 1389 let promise = this._itmsg;
michael@0 1390 promise = promise.then(
michael@0 1391 function withIterator(iterator) {
michael@0 1392 return Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]);
michael@0 1393 });
michael@0 1394 promise = promise.then(
michael@0 1395 function withEntries(array) {
michael@0 1396 return array.map(DirectoryIterator.Entry.fromMsg);
michael@0 1397 });
michael@0 1398 return promise;
michael@0 1399 },
michael@0 1400 /**
michael@0 1401 * Apply a function to all elements of the directory sequentially.
michael@0 1402 *
michael@0 1403 * @param {Function} cb This function will be applied to all entries
michael@0 1404 * of the directory. It receives as arguments
michael@0 1405 * - the OS.File.Entry corresponding to the entry;
michael@0 1406 * - the index of the entry in the enumeration;
michael@0 1407 * - the iterator itself - return |iterator.close()| to stop the loop.
michael@0 1408 *
michael@0 1409 * If the callback returns a promise, iteration waits until the
michael@0 1410 * promise is resolved before proceeding.
michael@0 1411 *
michael@0 1412 * @return {Promise} A promise resolved once the loop has reached
michael@0 1413 * its end.
michael@0 1414 */
michael@0 1415 forEach: function forEach(cb, options) {
michael@0 1416 if (this._isClosed) {
michael@0 1417 return Promise.resolve();
michael@0 1418 }
michael@0 1419
michael@0 1420 let self = this;
michael@0 1421 let position = 0;
michael@0 1422 let iterator;
michael@0 1423
michael@0 1424 // Grab iterator
michael@0 1425 let promise = this._itmsg.then(
michael@0 1426 function(aIterator) {
michael@0 1427 iterator = aIterator;
michael@0 1428 }
michael@0 1429 );
michael@0 1430
michael@0 1431 // Then iterate
michael@0 1432 let loop = function loop() {
michael@0 1433 if (self._isClosed) {
michael@0 1434 return Promise.resolve();
michael@0 1435 }
michael@0 1436 return self._next(iterator).then(
michael@0 1437 function onSuccess(value) {
michael@0 1438 return Promise.resolve(cb(value, position++, self)).then(loop);
michael@0 1439 },
michael@0 1440 function onFailure(reason) {
michael@0 1441 if (reason == StopIteration) {
michael@0 1442 return;
michael@0 1443 }
michael@0 1444 throw reason;
michael@0 1445 }
michael@0 1446 );
michael@0 1447 };
michael@0 1448
michael@0 1449 return promise.then(loop);
michael@0 1450 },
michael@0 1451 /**
michael@0 1452 * Auxiliary method: fetch the next item
michael@0 1453 *
michael@0 1454 * @rejects {StopIteration} If all entries have already been visited
michael@0 1455 * or the iterator has been closed.
michael@0 1456 */
michael@0 1457 _next: function _next(iterator) {
michael@0 1458 if (this._isClosed) {
michael@0 1459 return this._itmsg;
michael@0 1460 }
michael@0 1461 let self = this;
michael@0 1462 let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]);
michael@0 1463 promise = promise.then(
michael@0 1464 DirectoryIterator.Entry.fromMsg,
michael@0 1465 function onReject(reason) {
michael@0 1466 if (reason == StopIteration) {
michael@0 1467 self.close();
michael@0 1468 throw StopIteration;
michael@0 1469 }
michael@0 1470 throw reason;
michael@0 1471 });
michael@0 1472 return promise;
michael@0 1473 },
michael@0 1474 /**
michael@0 1475 * Close the iterator
michael@0 1476 */
michael@0 1477 close: function close() {
michael@0 1478 if (this._isClosed) {
michael@0 1479 return Promise.resolve();
michael@0 1480 }
michael@0 1481 this._isClosed = true;
michael@0 1482 let self = this;
michael@0 1483 return this._itmsg.then(
michael@0 1484 function withIterator(iterator) {
michael@0 1485 // Set __itmsg to null so that the _itmsg getter returns a
michael@0 1486 // rejected StopIteration promise if it's ever used.
michael@0 1487 self.__itmsg = null;
michael@0 1488 return Scheduler.post("DirectoryIterator_prototype_close", [iterator]);
michael@0 1489 }
michael@0 1490 );
michael@0 1491 }
michael@0 1492 };
michael@0 1493
michael@0 1494 DirectoryIterator.Entry = function Entry(value) {
michael@0 1495 return value;
michael@0 1496 };
michael@0 1497 DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype);
michael@0 1498
michael@0 1499 DirectoryIterator.Entry.fromMsg = function fromMsg(value) {
michael@0 1500 return new DirectoryIterator.Entry(value);
michael@0 1501 };
michael@0 1502
michael@0 1503 File.resetWorker = function() {
michael@0 1504 return Task.spawn(function*() {
michael@0 1505 let resources = yield Scheduler.kill({shutdown: false, reset: true});
michael@0 1506 if (resources && !resources.killed) {
michael@0 1507 throw new Error("Could not reset worker, this would leak file descriptors: " + JSON.stringify(resources));
michael@0 1508 }
michael@0 1509 });
michael@0 1510 };
michael@0 1511
michael@0 1512 // Constants
michael@0 1513 File.POS_START = SysAll.POS_START;
michael@0 1514 File.POS_CURRENT = SysAll.POS_CURRENT;
michael@0 1515 File.POS_END = SysAll.POS_END;
michael@0 1516
michael@0 1517 // Exports
michael@0 1518 File.Error = OSError;
michael@0 1519 File.DirectoryIterator = DirectoryIterator;
michael@0 1520
michael@0 1521 this.OS = {};
michael@0 1522 this.OS.File = File;
michael@0 1523 this.OS.Constants = SharedAll.Constants;
michael@0 1524 this.OS.Shared = {
michael@0 1525 LOG: SharedAll.LOG,
michael@0 1526 Type: SysAll.Type,
michael@0 1527 get DEBUG() {
michael@0 1528 return SharedAll.Config.DEBUG;
michael@0 1529 },
michael@0 1530 set DEBUG(x) {
michael@0 1531 return SharedAll.Config.DEBUG = x;
michael@0 1532 }
michael@0 1533 };
michael@0 1534 Object.freeze(this.OS.Shared);
michael@0 1535 this.OS.Path = Path;
michael@0 1536
michael@0 1537 // Returns a resolved promise when all the queued operation have been completed.
michael@0 1538 Object.defineProperty(OS.File, "queue", {
michael@0 1539 get: function() {
michael@0 1540 return Scheduler.queue;
michael@0 1541 }
michael@0 1542 });
michael@0 1543
michael@0 1544 // Auto-flush OS.File during profile-before-change. This ensures that any I/O
michael@0 1545 // that has been queued *before* profile-before-change is properly completed.
michael@0 1546 // To ensure that I/O queued *during* profile-before-change is completed,
michael@0 1547 // clients should register using AsyncShutdown.addBlocker.
michael@0 1548 AsyncShutdown.profileBeforeChange.addBlocker(
michael@0 1549 "OS.File: flush I/O queued before profile-before-change",
michael@0 1550 // Wait until the latest currently enqueued promise is satisfied/rejected
michael@0 1551 function() {
michael@0 1552 let DEBUG = false;
michael@0 1553 try {
michael@0 1554 DEBUG = Services.prefs.getBoolPref("toolkit.osfile.debug.failshutdown");
michael@0 1555 } catch (ex) {
michael@0 1556 // Ignore
michael@0 1557 }
michael@0 1558 if (DEBUG) {
michael@0 1559 // Return a promise that will never be satisfied
michael@0 1560 return Promise.defer().promise;
michael@0 1561 } else {
michael@0 1562 return Scheduler.queue;
michael@0 1563 }
michael@0 1564 },
michael@0 1565 function getDetails() {
michael@0 1566 let result = {
michael@0 1567 launched: Scheduler.launched,
michael@0 1568 shutdown: Scheduler.shutdown,
michael@0 1569 worker: !!Scheduler._worker,
michael@0 1570 pendingReset: !!Scheduler.resetTimer,
michael@0 1571 latestSent: Scheduler.Debugging.latestSent,
michael@0 1572 latestReceived: Scheduler.Debugging.latestReceived,
michael@0 1573 messagesSent: Scheduler.Debugging.messagesSent,
michael@0 1574 messagesReceived: Scheduler.Debugging.messagesReceived,
michael@0 1575 messagesQueued: Scheduler.Debugging.messagesQueued,
michael@0 1576 DEBUG: SharedAll.Config.DEBUG
michael@0 1577 };
michael@0 1578 // Convert dates to strings for better readability
michael@0 1579 for (let key of ["latestSent", "latestReceived"]) {
michael@0 1580 if (result[key] && typeof result[key][0] == "number") {
michael@0 1581 result[key][0] = Date(result[key][0]);
michael@0 1582 }
michael@0 1583 }
michael@0 1584 return result;
michael@0 1585 }
michael@0 1586 );

mercurial