1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5/**
6 * Managing safe shutdown of asynchronous services.
7 *
8 * Firefox shutdown is composed of phases that take place
9 * sequentially. Typically, each shutdown phase removes some
10 * capabilities from the application. For instance, at the end of
11 * phase profileBeforeChange, no service is permitted to write to the
12 * profile directory (with the exception of Telemetry). Consequently,
13 * if any service has requested I/O to the profile directory before or
14 * during phase profileBeforeChange, the system must be informed that
15 * these requests need to be completed before the end of phase
16 * profileBeforeChange. Failing to inform the system of this
17 * requirement can (and has been known to) cause data loss.
18 *
19 * Example: At some point during shutdown, the Add-On Manager needs to
20 * ensure that all add-ons have safely written their data to disk,
21 * before writing its own data. Since the data is saved to the
22 * profile, this must be completed during phase profileBeforeChange.
23 *
24 * AsyncShutdown.profileBeforeChange.addBlocker(
25 *   "Add-on manager: shutting down",
26 *   function condition() {
27 *     // Do things.
28 *     // Perform I/O that must take place during phase profile-before-change
29 *     return promise;
30 *   }
31 * });
32 *
33 * In this example, function |condition| will be called at some point
34 * during phase profileBeforeChange and phase profileBeforeChange
35 * itself is guaranteed to not terminate until |promise| is either
36 * resolved or rejected.
37 */
38
39"use strict";
40
41const Cu = Components.utils;
42const Cc = Components.classes;
43const Ci = Components.interfaces;
44Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
45Cu.import("resource://gre/modules/Services.jsm", this);
46
47XPCOMUtils.defineLazyModuleGetter(this, "Promise",
48  "resource://gre/modules/Promise.jsm");
49XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
50  "resource://gre/modules/PromiseUtils.jsm");
51XPCOMUtils.defineLazyModuleGetter(this, "Task",
52  "resource://gre/modules/Task.jsm");
53XPCOMUtils.defineLazyServiceGetter(this, "gDebug",
54  "@mozilla.org/xpcom/debug;1", "nsIDebug2");
55Object.defineProperty(this, "gCrashReporter", {
56  get: function() {
57    delete this.gCrashReporter;
58    try {
59      let reporter = Cc["@mozilla.org/xre/app-info;1"].
60            getService(Ci.nsICrashReporter);
61      return this.gCrashReporter = reporter;
62    } catch (ex) {
63      return this.gCrashReporter = null;
64    }
65  },
66  configurable: true
67});
68
69// `true` if this is a content process, `false` otherwise.
70// It would be nicer to go through `Services.appInfo`, but some tests need to be
71// able to replace that field with a custom implementation before it is first
72// called.
73const isContent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
74
75// Display timeout warnings after 10 seconds
76const DELAY_WARNING_MS = 10 * 1000;
77
78
79// Crash the process if shutdown is really too long
80// (allowing for sleep).
81const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout";
82var DELAY_CRASH_MS = 60 * 1000; // One minute
83try {
84  DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
85} catch (ex) {
86  // Ignore errors
87}
88Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() {
89  DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
90}, false);
91
92/**
93 * A set of Promise that supports waiting.
94 *
95 * Promise items may be added or removed during the wait. The wait will
96 * resolve once all Promise items have been resolved or removed.
97 */
98function PromiseSet() {
99  /**
100   * key: the Promise passed pass the client of the `PromiseSet`.
101   * value: an indirection on top of `key`, as an object with
102   *   the following fields:
103   *   - indirection: a Promise resolved if `key` is resolved or
104   *     if `resolve` is called
105   *   - resolve: a function used to resolve the indirection.
106   */
107  this._indirections = new Map();
108}
109PromiseSet.prototype = {
110  /**
111   * Wait until all Promise have been resolved or removed.
112   *
113   * Note that calling `wait()` causes Promise to be removed from the
114   * Set once they are resolved.
115   *
116   * @return {Promise} Resolved once all Promise have been resolved or removed,
117   * or rejected after at least one Promise has rejected.
118   */
119  wait: function() {
120    // Pick an arbitrary element in the map, if any exists.
121    let entry = this._indirections.entries().next();
122    if (entry.done) {
123      // No indirections left, we are done.
124      return Promise.resolve();
125    }
126
127    let [, indirection] = entry.value;
128    let promise = indirection.promise;
129    promise = promise.then(() =>
130      // At this stage, the entry has been cleaned up.
131      this.wait()
132    );
133    return promise;
134  },
135
136  /**
137   * Add a new Promise to the set.
138   *
139   * Calls to wait (including ongoing calls) will only return once
140   * `key` has either resolved or been removed.
141   */
142  add: function(key) {
143    this._ensurePromise(key);
144    let indirection = PromiseUtils.defer();
145    key.then(
146      x => {
147        // Clean up immediately.
148        // This needs to be done before the call to `resolve`, otherwise
149        // `wait()` may loop forever.
150        this._indirections.delete(key);
151        indirection.resolve(x);
152      },
153      err => {
154        this._indirections.delete(key);
155        indirection.reject(err);
156      });
157    this._indirections.set(key, indirection);
158  },
159
160  /**
161   * Remove a Promise from the set.
162   *
163   * Calls to wait (including ongoing calls) will ignore this promise,
164   * unless it is added again.
165   */
166  delete: function(key) {
167    this._ensurePromise(key);
168    let value = this._indirections.get(key);
169    if (!value) {
170      return false;
171    }
172    this._indirections.delete(key);
173    value.resolve();
174    return true;
175  },
176
177  _ensurePromise: function(key) {
178    if (!key || typeof key != "object") {
179      throw new Error("Expected an object");
180    }
181    if ((!("then" in key)) || typeof key.then != "function") {
182      throw new Error("Expected a Promise");
183    }
184  },
185
186};
187
188
189/**
190 * Display a warning.
191 *
192 * As this code is generally used during shutdown, there are chances
193 * that the UX will not be available to display warnings on the
194 * console. We therefore use dump() rather than Cu.reportError().
195 */
196function log(msg, prefix = "", error = null) {
197  try {
198    dump(prefix + msg + "\n");
199    if (error) {
200      dump(prefix + error + "\n");
201      if (typeof error == "object" && "stack" in error) {
202        dump(prefix + error.stack + "\n");
203      }
204    }
205  } catch (ex) {
206    dump("INTERNAL ERROR in AsyncShutdown: cannot log message.\n");
207  }
208}
209const PREF_DEBUG_LOG = "toolkit.asyncshutdown.log";
210var DEBUG_LOG = false;
211try {
212  DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG);
213} catch (ex) {
214  // Ignore errors
215}
216Services.prefs.addObserver(PREF_DEBUG_LOG, function() {
217  DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG);
218}, false);
219
220function debug(msg, error=null) {
221  if (DEBUG_LOG) {
222    log(msg, "DEBUG: ", error);
223  }
224}
225function warn(msg, error = null) {
226  log(msg, "WARNING: ", error);
227}
228function fatalerr(msg, error = null) {
229  log(msg, "FATAL ERROR: ", error);
230}
231
232// Utility function designed to get the current state of execution
233// of a blocker.
234// We are a little paranoid here to ensure that in case of evaluation
235// error we do not block the AsyncShutdown.
236function safeGetState(fetchState) {
237  if (!fetchState) {
238    return "(none)";
239  }
240  let data, string;
241  try {
242    // Evaluate fetchState(), normalize the result into something that we can
243    // safely stringify or upload.
244    let state = fetchState();
245    if (!state) {
246      return "(none)";
247    }
248    string = JSON.stringify(state);
249    data = JSON.parse(string);
250    // Simplify the rest of the code by ensuring that we can simply
251    // concatenate the result to a message.
252    if (data && typeof data == "object") {
253      data.toString = function() {
254        return string;
255      };
256    }
257    return data;
258  } catch (ex) {
259
260    // Make sure that this causes test failures
261    Promise.reject(ex);
262
263    if (string) {
264      return string;
265    }
266    try {
267      return "Error getting state: " + ex + " at " + ex.stack;
268    } catch (ex2) {
269      return "Error getting state but could not display error";
270    }
271  }
272}
273
274/**
275 * Countdown for a given duration, skipping beats if the computer is too busy,
276 * sleeping or otherwise unavailable.
277 *
278 * @param {number} delay An approximate delay to wait in milliseconds (rounded
279 * up to the closest second).
280 *
281 * @return Deferred
282 */
283function looseTimer(delay) {
284  let DELAY_BEAT = 1000;
285  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
286  let beats = Math.ceil(delay / DELAY_BEAT);
287  let deferred = Promise.defer();
288  timer.initWithCallback(function() {
289    if (beats <= 0) {
290      deferred.resolve();
291    }
292    --beats;
293  }, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
294  // Ensure that the timer is both canceled once we are done with it
295  // and not garbage-collected until then.
296  deferred.promise.then(() => timer.cancel(), () => timer.cancel());
297  return deferred;
298}
299
300/**
301 * Given an nsIStackFrame object, find the caller filename, line number,
302 * and stack if necessary, and return them as an object.
303 *
304 * @param {nsIStackFrame} topFrame Top frame of the call stack.
305 * @param {string} filename Pre-supplied filename or null if unknown.
306 * @param {number} lineNumber Pre-supplied line number or null if unknown.
307 * @param {string} stack Pre-supplied stack or null if unknown.
308 *
309 * @return object
310 */
311function getOrigin(topFrame, filename = null, lineNumber = null, stack = null) {
312  try {
313    // Determine the filename and line number of the caller.
314    let frame = topFrame;
315
316    for (; frame && frame.filename == topFrame.filename; frame = frame.caller) {
317      // Climb up the stack
318    }
319
320    if (filename == null) {
321      filename = frame ? frame.filename : "?";
322    }
323    if (lineNumber == null) {
324      lineNumber = frame ? frame.lineNumber : 0;
325    }
326    if (stack == null) {
327      // Now build the rest of the stack as a string, using Task.jsm's rewriting
328      // to ensure that we do not lose information at each call to `Task.spawn`.
329      let frames = [];
330      while (frame != null) {
331        frames.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber);
332        frame = frame.caller;
333      }
334      stack = Task.Debugging.generateReadableStack(frames.join("\n")).split("\n");
335    }
336
337    return {
338      filename: filename,
339      lineNumber: lineNumber,
340      stack: stack,
341    };
342  } catch (ex) {
343    return {
344      filename: "<internal error: could not get origin>",
345      lineNumber: -1,
346      stack: "<internal error: could not get origin>",
347    }
348  }
349}
350
351this.EXPORTED_SYMBOLS = ["AsyncShutdown"];
352
353/**
354 * {string} topic -> phase
355 */
356var gPhases = new Map();
357
358this.AsyncShutdown = {
359  /**
360   * Access function getPhase. For testing purposes only.
361   */
362  get _getPhase() {
363    let accepted = false;
364    try {
365      accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing");
366    } catch (ex) {
367      // Ignore errors
368    }
369    if (accepted) {
370      return getPhase;
371    }
372    return undefined;
373  }
374};
375
376/**
377 * Register a new phase.
378 *
379 * @param {string} topic The notification topic for this Phase.
380 * @see {https://developer.mozilla.org/en-US/docs/Observer_Notifications}
381 */
382function getPhase(topic) {
383  let phase = gPhases.get(topic);
384  if (phase) {
385    return phase;
386  }
387  let spinner = new Spinner(topic);
388  phase = Object.freeze({
389    /**
390     * Register a blocker for the completion of a phase.
391     *
392     * @param {string} name The human-readable name of the blocker. Used
393     * for debugging/error reporting. Please make sure that the name
394     * respects the following model: "Some Service: some action in progress" -
395     * for instance "OS.File: flushing all pending I/O";
396     * @param {function|promise|*} condition A condition blocking the
397     * completion of the phase. Generally, this is a function
398     * returning a promise. This function is evaluated during the
399     * phase and the phase is guaranteed to not terminate until the
400     * resulting promise is either resolved or rejected. If
401     * |condition| is not a function but another value |v|, it behaves
402     * as if it were a function returning |v|.
403     * @param {object*} details Optionally, an object with details
404     * that may be useful for error reporting, as a subset of of the following
405     * fields:
406     * - fetchState (strongly recommended) A function returning
407     *    information about the current state of the blocker as an
408     *    object. Used for providing more details when logging errors or
409     *    crashing.
410     * - stack. A string containing stack information. This module can
411     *    generally infer stack information if it is not provided.
412     * - lineNumber A number containing the line number for the caller.
413     *    This module can generally infer this information if it is not
414     *    provided.
415     * - filename A string containing the filename for the caller. This
416     *    module can generally infer  the information if it is not provided.
417     *
418     * Examples:
419     * AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise",
420     *      promise); // profileBeforeChange will not complete until
421     *                // promise is resolved or rejected
422     *
423     * AsyncShutdown.profileBeforeChange.addBlocker("Module: a callback",
424     *     function callback() {
425     *       // ...
426     *       // Execute this code during profileBeforeChange
427     *       return promise;
428     *       // profileBeforeChange will not complete until promise
429     *       // is resolved or rejected
430     * });
431     *
432     * AsyncShutdown.profileBeforeChange.addBlocker("Module: trivial callback",
433     *     function callback() {
434     *       // ...
435     *       // Execute this code during profileBeforeChange
436     *       // No specific guarantee about completion of profileBeforeChange
437     * });
438     */
439    addBlocker: function(name, condition, details = null) {
440      spinner.addBlocker(name, condition, details);
441    },
442    /**
443     * Remove the blocker for a condition.
444     *
445     * If several blockers have been registered for the same
446     * condition, remove all these blockers. If no blocker has been
447     * registered for this condition, this is a noop.
448     *
449     * @return {boolean} true if a blocker has been removed, false
450     * otherwise. Note that a result of false may mean either that
451     * the blocker has never been installed or that the phase has
452     * completed and the blocker has already been resolved.
453     */
454    removeBlocker: function(condition) {
455      return spinner.removeBlocker(condition);
456    },
457
458    get name() {
459      return spinner.name;
460    },
461
462    /**
463     * Trigger the phase without having to broadcast a
464     * notification. For testing purposes only.
465     */
466    get _trigger() {
467      let accepted = false;
468      try {
469        accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing");
470      } catch (ex) {
471        // Ignore errors
472      }
473      if (accepted) {
474        return () => spinner.observe();
475      }
476      return undefined;
477    }
478  });
479  gPhases.set(topic, phase);
480  return phase;
481}
482
483/**
484 * Utility class used to spin the event loop until all blockers for a
485 * Phase are satisfied.
486 *
487 * @param {string} topic The xpcom notification for that phase.
488 */
489function Spinner(topic) {
490  this._barrier = new Barrier(topic);
491  this._topic = topic;
492  Services.obs.addObserver(this, topic, false);
493}
494
495Spinner.prototype = {
496  /**
497   * Register a new condition for this phase.
498   *
499   * See the documentation of `addBlocker` in property `client`
500   * of instances of `Barrier`.
501   */
502  addBlocker: function(name, condition, details) {
503    this._barrier.client.addBlocker(name, condition, details);
504  },
505  /**
506   * Remove the blocker for a condition.
507   *
508   * See the documentation of `removeBlocker` in rpoperty `client`
509   * of instances of `Barrier`
510   *
511   * @return {boolean} true if a blocker has been removed, false
512   * otherwise. Note that a result of false may mean either that
513   * the blocker has never been installed or that the phase has
514   * completed and the blocker has already been resolved.
515   */
516  removeBlocker: function(condition) {
517    return this._barrier.client.removeBlocker(condition);
518  },
519
520  get name() {
521    return this._barrier.client.name;
522  },
523
524  // nsIObserver.observe
525  observe: function() {
526    let topic = this._topic;
527    debug(`Starting phase ${ topic }`);
528    Services.obs.removeObserver(this, topic);
529
530    let satisfied = false; // |true| once we have satisfied all conditions
531    let promise;
532    try {
533      promise = this._barrier.wait({
534        warnAfterMS: DELAY_WARNING_MS,
535        crashAfterMS: DELAY_CRASH_MS
536      }).catch(
537        // Additional precaution to be entirely sure that we cannot reject.
538      );
539    } catch (ex) {
540      debug("Error waiting for notification");
541      throw ex;
542    }
543
544    // Now, spin the event loop
545    debug("Spinning the event loop");
546    promise.then(() => satisfied = true); // This promise cannot reject
547    let thread = Services.tm.mainThread;
548    while (!satisfied) {
549      try {
550        thread.processNextEvent(true);
551      } catch (ex) {
552        // An uncaught error should not stop us, but it should still
553        // be reported and cause tests to fail.
554        Promise.reject(ex);
555      }
556    }
557    debug(`Finished phase ${ topic }`);
558  }
559};
560
561/**
562 * A mechanism used to register blockers that prevent some action from
563 * happening.
564 *
565 * An instance of |Barrier| provides a capability |client| that
566 * clients can use to register blockers. The barrier is resolved once
567 * all registered blockers have been resolved. The owner of the
568 * |Barrier| may wait for the resolution of the barrier and obtain
569 * information on which blockers have not been resolved yet.
570 *
571 * @param {string} name The name of the blocker. Used mainly for error-
572 *     reporting.
573 */
574function Barrier(name) {
575  if (!name) {
576    throw new TypeError("Instances of Barrier need a (non-empty) name");
577  }
578
579
580  /**
581   * The set of all Promise for which we need to wait before the barrier
582   * is lifted. Note that this set may be changed while we are waiting.
583   *
584   * Set to `null` once the wait is complete.
585   */
586  this._waitForMe = new PromiseSet();
587
588  /**
589   * A map from conditions, as passed by users during the call to `addBlocker`,
590   * to `promise`, as present in `this._waitForMe`.
591   *
592   * Used to let users perform cleanup through `removeBlocker`.
593   * Set to `null` once the wait is complete.
594   *
595   * Key: condition (any, as passed by user)
596   * Value: promise used as a key in `this._waitForMe`. Note that there is
597   * no guarantee that the key is still present in `this._waitForMe`.
598   */
599  this._conditionToPromise = new Map();
600
601  /**
602   * A map from Promise, as present in `this._waitForMe` or
603   * `this._conditionToPromise`, to information on blockers.
604   *
605   * Key: Promise (as present in this._waitForMe or this._conditionToPromise).
606   * Value:  {
607   *  trigger: function,
608   *  promise,
609   *  name,
610   *  fetchState: function,
611   *  stack,
612   *  filename,
613   *  lineNumber
614   * };
615   */
616  this._promiseToBlocker = new Map();
617
618  /**
619   * The name of the barrier.
620   */
621  if (typeof name != "string") {
622    throw new TypeError("The name of the barrier must be a string");
623  }
624  this._name = name;
625
626  /**
627   * A cache for the promise returned by wait().
628   */
629  this._promise = null;
630
631  /**
632   * `true` once we have started waiting.
633   */
634  this._isStarted = false;
635
636  /**
637   * The capability of adding blockers. This object may safely be returned
638   * or passed to clients.
639   */
640  this.client = {
641    /**
642     * The name of the barrier owning this client.
643     */
644    get name() {
645      return name;
646    },
647
648    /**
649     * Register a blocker for the completion of this barrier.
650     *
651     * @param {string} name The human-readable name of the blocker. Used
652     * for debugging/error reporting. Please make sure that the name
653     * respects the following model: "Some Service: some action in progress" -
654     * for instance "OS.File: flushing all pending I/O";
655     * @param {function|promise|*} condition A condition blocking the
656     * completion of the phase. Generally, this is a function
657     * returning a promise. This function is evaluated during the
658     * phase and the phase is guaranteed to not terminate until the
659     * resulting promise is either resolved or rejected. If
660     * |condition| is not a function but another value |v|, it behaves
661     * as if it were a function returning |v|.
662     * @param {object*} details Optionally, an object with details
663     * that may be useful for error reporting, as a subset of of the following
664     * fields:
665     * - fetchState (strongly recommended) A function returning
666     *    information about the current state of the blocker as an
667     *    object. Used for providing more details when logging errors or
668     *    crashing.
669     * - stack. A string containing stack information. This module can
670     *    generally infer stack information if it is not provided.
671     * - lineNumber A number containing the line number for the caller.
672     *    This module can generally infer this information if it is not
673     *    provided.
674     * - filename A string containing the filename for the caller. This
675     *    module can generally infer  the information if it is not provided.
676     */
677    addBlocker: (name, condition, details) => {
678      if (typeof name != "string") {
679        throw new TypeError("Expected a human-readable name as first argument");
680      }
681      if (details && typeof details == "function") {
682        details = {
683          fetchState: details
684        };
685      } else if (!details) {
686        details = {};
687      }
688      if (typeof details != "object") {
689        throw new TypeError("Expected an object as third argument to `addBlocker`, got " + details);
690      }
691      if (!this._waitForMe) {
692        throw new Error(`Phase "${ this._name }" is finished, it is too late to register completion condition "${ name }"`);
693      }
694      debug(`Adding blocker ${ name } for phase ${ this._name }`);
695
696      // Normalize the details
697
698      let fetchState = details.fetchState || null;
699      if (fetchState != null && typeof fetchState != "function") {
700        throw new TypeError("Expected a function for option `fetchState`");
701      }
702      let filename = details.filename || null;
703      let lineNumber = details.lineNumber || null;
704      let stack = details.stack || null;
705
706      // Split the condition between a trigger function and a promise.
707
708      // The function to call to notify the blocker that we have started waiting.
709      // This function returns a promise resolved/rejected once the
710      // condition is complete, and never throws.
711      let trigger;
712
713      // A promise resolved once the condition is complete.
714      let promise;
715      if (typeof condition == "function") {
716        promise = new Promise((resolve, reject) => {
717          trigger = () => {
718            try {
719              resolve(condition());
720            } catch (ex) {
721              reject(ex);
722            }
723          }
724        });
725      } else {
726        // If `condition` is not a function, `trigger` is not particularly
727        // interesting, and `condition` needs to be normalized to a promise.
728        trigger = () => {};
729        promise = Promise.resolve(condition);
730      }
731
732      // Make sure that `promise` never rejects.
733      promise = promise.then(null, error => {
734        let msg = `A blocker encountered an error while we were waiting.
735          Blocker:  ${ name }
736          Phase: ${ this._name }
737          State: ${ safeGetState(fetchState) }`;
738        warn(msg, error);
739
740        // The error should remain uncaught, to ensure that it
741        // still causes tests to fail.
742        Promise.reject(error);
743      }).catch(
744        // Added as a last line of defense, in case `warn`, `this._name` or
745        // `safeGetState` somehow throws an error.
746      );
747
748      let topFrame = null;
749      if (filename == null || lineNumber == null || stack == null) {
750        topFrame = Components.stack;
751      }
752
753      let blocker = {
754        trigger: trigger,
755        promise: promise,
756        name: name,
757        fetchState: fetchState,
758        getOrigin: () => getOrigin(topFrame, filename, lineNumber, stack),
759      };
760
761      this._waitForMe.add(promise);
762      this._promiseToBlocker.set(promise, blocker);
763      this._conditionToPromise.set(condition, promise);
764
765      // As conditions may hold lots of memory, we attempt to cleanup
766      // as soon as we are done (which might be in the next tick, if
767      // we have been passed a resolved promise).
768      promise = promise.then(() => {
769        debug(`Completed blocker ${ name } for phase ${ this._name }`);
770        this._removeBlocker(condition);
771      });
772
773      if (this._isStarted) {
774        // The wait has already started. The blocker should be
775        // notified asap. We do it out of band as clients probably
776        // expect `addBlocker` to return immediately.
777        Promise.resolve().then(trigger);
778      }
779    },
780
781    /**
782     * Remove the blocker for a condition.
783     *
784     * If several blockers have been registered for the same
785     * condition, remove all these blockers. If no blocker has been
786     * registered for this condition, this is a noop.
787     *
788     * @return {boolean} true if at least one blocker has been
789     * removed, false otherwise.
790     */
791    removeBlocker: (condition) => {
792      return this._removeBlocker(condition);
793    }
794  };
795}
796Barrier.prototype = Object.freeze({
797  /**
798   * The current state of the barrier, as a JSON-serializable object
799   * designed for error-reporting.
800   */
801  get state() {
802    if (!this._isStarted) {
803      return "Not started";
804    }
805    if (!this._waitForMe) {
806      return "Complete";
807    }
808    let frozen = [];
809    for (let blocker of this._promiseToBlocker.values()) {
810      let {name, fetchState} = blocker;
811      let {stack, filename, lineNumber} = blocker.getOrigin();
812      frozen.push({
813        name: name,
814        state: safeGetState(fetchState),
815        filename: filename,
816        lineNumber: lineNumber,
817        stack: stack
818      });
819    }
820    return frozen;
821  },
822
823  /**
824   * Wait until all currently registered blockers are complete.
825   *
826   * Once this method has been called, any attempt to register a new blocker
827   * for this barrier will cause an error.
828   *
829   * Successive calls to this method always return the same value.
830   *
831   * @param {object=}  options Optionally, an object  that may contain
832   * the following fields:
833   *  {number} warnAfterMS If provided and > 0, print a warning if the barrier
834   *   has not been resolved after the given number of milliseconds.
835   *  {number} crashAfterMS If provided and > 0, crash the process if the barrier
836   *   has not been resolved after the give number of milliseconds (rounded up
837   *   to the next second). To avoid crashing simply because the computer is busy
838   *   or going to sleep, we actually wait for ceil(crashAfterMS/1000) successive
839   *   periods of at least one second. Upon crashing, if a crash reporter is present,
840   *   prepare a crash report with the state of this barrier.
841   *
842   *
843   * @return {Promise} A promise satisfied once all blockers are complete.
844   */
845  wait: function(options = {}) {
846    // This method only implements caching on top of _wait()
847    if (this._promise) {
848      return this._promise;
849    }
850    return this._promise = this._wait(options);
851  },
852  _wait: function(options) {
853
854    // Sanity checks
855    if (this._isStarted) {
856      throw new TypeError("Internal error: already started " + this._name);
857    }
858    if (!this._waitForMe || !this._conditionToPromise || !this._promiseToBlocker) {
859      throw new TypeError("Internal error: already finished " + this._name);
860    }
861
862    let topic = this._name;
863
864    // Notify blockers
865    for (let blocker of this._promiseToBlocker.values()) {
866      blocker.trigger(); // We have guarantees that this method will never throw
867    }
868
869    this._isStarted = true;
870
871    // Now, wait
872    let promise = this._waitForMe.wait();
873
874    promise = promise.then(null, function onError(error) {
875      // I don't think that this can happen.
876      // However, let's be overcautious with async/shutdown error reporting.
877      let msg = "An uncaught error appeared while completing the phase." +
878        " Phase: " + topic;
879      warn(msg, error);
880    });
881
882    promise = promise.then(() => {
883      // Cleanup memory
884      this._waitForMe = null;
885      this._promiseToBlocker = null;
886      this._conditionToPromise = null;
887    });
888
889    // Now handle warnings and crashes
890    let warnAfterMS = DELAY_WARNING_MS;
891    if (options && "warnAfterMS" in options) {
892      if (typeof options.warnAfterMS == "number"
893         || options.warnAfterMS == null) {
894        // Change the delay or deactivate warnAfterMS
895        warnAfterMS = options.warnAfterMS;
896      } else {
897        throw new TypeError("Wrong option value for warnAfterMS");
898      }
899    }
900
901    if (warnAfterMS && warnAfterMS > 0) {
902      // If the promise takes too long to be resolved/rejected,
903      // we need to notify the user.
904      let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
905      timer.initWithCallback(() => {
906        let msg = "At least one completion condition is taking too long to complete." +
907        " Conditions: " + JSON.stringify(this.state) +
908        " Barrier: " + topic;
909        warn(msg);
910      }, warnAfterMS, Ci.nsITimer.TYPE_ONE_SHOT);
911
912      promise = promise.then(function onSuccess() {
913        timer.cancel();
914        // As a side-effect, this prevents |timer| from
915        // being garbage-collected too early.
916      });
917    }
918
919    let crashAfterMS = DELAY_CRASH_MS;
920    if (options && "crashAfterMS" in options) {
921      if (typeof options.crashAfterMS == "number"
922         || options.crashAfterMS == null) {
923        // Change the delay or deactivate crashAfterMS
924        crashAfterMS = options.crashAfterMS;
925      } else {
926        throw new TypeError("Wrong option value for crashAfterMS");
927      }
928    }
929
930    if (crashAfterMS  > 0) {
931      let timeToCrash = null;
932
933      // If after |crashAfterMS| milliseconds (adjusted to take into
934      // account sleep and otherwise busy computer) we have not finished
935      // this shutdown phase, we assume that the shutdown is somehow
936      // frozen, presumably deadlocked. At this stage, the only thing we
937      // can do to avoid leaving the user's computer in an unstable (and
938      // battery-sucking) situation is report the issue and crash.
939      timeToCrash = looseTimer(crashAfterMS);
940      timeToCrash.promise.then(
941        function onTimeout() {
942          // Report the problem as best as we can, then crash.
943          let state = this.state;
944
945          // If you change the following message, please make sure
946          // that any information on the topic and state appears
947          // within the first 200 characters of the message. This
948          // helps automatically sort oranges.
949          let msg = "AsyncShutdown timeout in " + topic +
950            " Conditions: " + JSON.stringify(state) +
951            " At least one completion condition failed to complete" +
952            " within a reasonable amount of time. Causing a crash to" +
953            " ensure that we do not leave the user with an unresponsive" +
954            " process draining resources.";
955          fatalerr(msg);
956          if (gCrashReporter && gCrashReporter.enabled) {
957            let data = {
958              phase: topic,
959              conditions: state
960            };
961            gCrashReporter.annotateCrashReport("AsyncShutdownTimeout",
962              JSON.stringify(data));
963          } else {
964            warn("No crash reporter available");
965          }
966
967          // To help sorting out bugs, we want to make sure that the
968          // call to nsIDebug2.abort points to a guilty client, rather
969          // than to AsyncShutdown itself. We pick a client that is
970          // still blocking and use its filename/lineNumber,
971          // which have been determined during the call to `addBlocker`.
972          let filename = "?";
973          let lineNumber = -1;
974          for (let blocker of this._promiseToBlocker.values()) {
975            ({filename, lineNumber} = blocker.getOrigin());
976            break;
977          }
978          gDebug.abort(filename, lineNumber);
979        }.bind(this),
980        function onSatisfied() {
981          // The promise has been rejected, which means that we have satisfied
982          // all completion conditions.
983        });
984
985      promise = promise.then(function() {
986        timeToCrash.reject();
987      }/* No error is possible here*/);
988    }
989
990    return promise;
991  },
992
993  _removeBlocker: function(condition) {
994    if (!this._waitForMe || !this._promiseToBlocker || !this._conditionToPromise) {
995      // We have already cleaned up everything.
996      return false;
997    }
998
999    let promise = this._conditionToPromise.get(condition);
1000    if (!promise) {
1001      // The blocker has already been removed
1002      return false;
1003    }
1004    this._conditionToPromise.delete(condition);
1005    this._promiseToBlocker.delete(promise);
1006    return this._waitForMe.delete(promise);
1007  },
1008
1009});
1010
1011
1012
1013// List of well-known phases
1014// Ideally, phases should be registered from the component that decides
1015// when they start/stop. For compatibility with existing startup/shutdown
1016// mechanisms, we register a few phases here.
1017
1018// Parent process
1019if (!isContent) {
1020  this.AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
1021  this.AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
1022  this.AsyncShutdown.placesClosingInternalConnection = getPhase("places-will-close-connection");
1023  this.AsyncShutdown.sendTelemetry = getPhase("profile-before-change-telemetry");
1024}
1025
1026// Notifications that fire in the parent and content process, but should
1027// only have phases in the parent process.
1028if (!isContent) {
1029  this.AsyncShutdown.quitApplicationGranted = getPhase("quit-application-granted");
1030}
1031
1032// Don't add a barrier for content-child-shutdown because this
1033// makes it easier to cause shutdown hangs.
1034
1035// All processes
1036this.AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown");
1037this.AsyncShutdown.xpcomWillShutdown = getPhase("xpcom-will-shutdown");
1038
1039this.AsyncShutdown.Barrier = Barrier;
1040
1041Object.freeze(this.AsyncShutdown);
1042