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