tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

event-loop.js (8483B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const xpcInspector = require("xpcInspector");
      8 
      9 /**
     10 * An object that represents a nested event loop. It is used as the nest
     11 * requestor with nsIJSInspector instances.
     12 *
     13 * @param ThreadActor thread
     14 *        The thread actor that is creating this nested event loop.
     15 */
     16 class EventLoop {
     17  constructor({ thread }) {
     18    this._thread = thread;
     19 
     20    // A flag which is true in between the two calls to enter() and exit().
     21    this._entered = false;
     22    // Another flag which is true only after having called exit().
     23    // Note that this EventLoop may still be paused and its enter() method
     24    // still be on hold, if another EventLoop paused about this one.
     25    this._resolved = false;
     26  }
     27 
     28  /**
     29   * This is meant for other thread actors, and is used by other thread actor's
     30   * EventLoop's isTheLastPausedThreadActor()
     31   */
     32  get thread() {
     33    return this._thread;
     34  }
     35  /**
     36   * Similarly, it will be used by another thread actor's EventLoop's enter() method
     37   */
     38  get resolved() {
     39    return this._resolved;
     40  }
     41 
     42  /**
     43   * Tells if the last thread actor to have paused (i.e. last EventLoop on the stack)
     44   * is the current one.
     45   *
     46   * We avoid trying to exit this event loop,
     47   * if another thread actor pile up a more recent one.
     48   * All the event loops will be effectively exited when
     49   * the thread actor which piled up the most recent nested event loop resumes.
     50   *
     51   * For convenience for the callsite, this will return true if nothing paused.
     52   */
     53  isTheLastPausedThreadActor() {
     54    if (xpcInspector.eventLoopNestLevel > 0) {
     55      return xpcInspector.lastNestRequestor.thread === this._thread;
     56    }
     57    return true;
     58  }
     59 
     60  /**
     61   * Enter a new nested event loop.
     62   */
     63  enter() {
     64    if (this._entered) {
     65      throw new Error(
     66        "Can't enter an event loop that has already been entered!"
     67      );
     68    }
     69 
     70    const preEnterData = this.preEnter();
     71 
     72    this._entered = true;
     73    // Note: next line will synchronously block the execution until exit() is being called.
     74    //
     75    // This enterNestedEventLoop is a bit magical and will break run-to-completion rule of JS.
     76    // JS will become multi-threaded. Some other task may start running on change state
     77    // while we are blocked on this enterNestedEventLoop function call.
     78    // You may find valuable information about Tasks and Event Loops on:
     79    // https://docs.google.com/document/d/1jTMd-H_BwH9_QNUDxPse80vq884_hMvd234lvE5gqY8/edit?usp=sharing
     80    //
     81    // Note #2: this will update xpcInspector.lastNestRequestor to this
     82    xpcInspector.enterNestedEventLoop(this);
     83 
     84    // If this code runs, it means that we just exited this event loop and lastNestRequestor is no longer equal to this.
     85    //
     86    // We will now "recursively" exit all the resolved EventLoops which are blocked on `enterNestedEventLoop`:
     87    // - if the new lastNestRequestor is resolved, request to exit it as well
     88    // - this lastNestRequestor is another EventLoop instance
     89    // - exiting this EventLoop unblocks its "enter" method and moves lastNestRequestor to the next requestor (if any)
     90    // - we go back to the first step, and attempt to exit the new lastNestRequestor if it is resolved, etc...
     91    if (xpcInspector.eventLoopNestLevel > 0) {
     92      if (xpcInspector.lastNestRequestor.resolved) {
     93        xpcInspector.exitNestedEventLoop();
     94      }
     95    }
     96 
     97    this.postExit(preEnterData);
     98  }
     99 
    100  /**
    101   * Exit this nested event loop.
    102   *
    103   * @returns boolean
    104   *          True if we exited this nested event loop because it was on top of
    105   *          the stack, false if there is another nested event loop above this
    106   *          one that hasn't exited yet.
    107   */
    108  exit() {
    109    if (!this._entered) {
    110      throw new Error("Can't exit an event loop before it has been entered!");
    111    }
    112    this._entered = false;
    113    this._resolved = true;
    114 
    115    // If another ThreadActor paused and spawn a new nested event loop after this one,
    116    // let it resume the thread and ignore this call.
    117    // The code calling exitNestedEventLoop from EventLoop.enter will resume execution,
    118    // by seeing that resolved attribute that we just toggled is true.
    119    //
    120    // Note that ThreadActor.resume method avoids calling exit thanks to `isTheLastPausedThreadActor`
    121    // So for all use requests to resume, the ThreadActor won't call exit until it is the last
    122    // thread actor to have entered a nested EventLoop.
    123    if (this === xpcInspector.lastNestRequestor) {
    124      xpcInspector.exitNestedEventLoop();
    125      return true;
    126    }
    127    return false;
    128  }
    129 
    130  /**
    131   * Retrieve the list of all DOM Windows debugged by the current thread actor.
    132   */
    133  getAllWindowDebuggees() {
    134    const rawGlobals = this._thread.dbg
    135      .getDebuggees()
    136      .filter(debuggee => {
    137        // Select only debuggee that relates to windows
    138        // e.g. ignore sandboxes, jsm and such
    139        return debuggee.class == "Window";
    140      })
    141      .map(debuggee => {
    142        // Retrieve the JS reference for these windows
    143        return debuggee.unsafeDereference();
    144      });
    145 
    146    // When pausing from a content script, also ensure pausing the related document
    147    const { innerWindowId } = this._thread.targetActor;
    148    if (innerWindowId) {
    149      const windowGlobal = WindowGlobalChild.getByInnerWindowId(innerWindowId);
    150      if (windowGlobal?.browsingContext?.window) {
    151        rawGlobals.push(windowGlobal.browsingContext.window);
    152      }
    153    }
    154 
    155    return rawGlobals.filter(window => {
    156      // Ignore document which have already been nuked,
    157      // so navigated to another location and removed from memory completely.
    158      if (Cu.isDeadWrapper(window)) {
    159        return false;
    160      }
    161      // Also ignore document which are closed, as trying to access window.parent or top would throw NS_ERROR_NOT_INITIALIZED
    162      if (window.closed) {
    163        return false;
    164      }
    165      // Ignore remote iframes, which will be debugged by another thread actor,
    166      // running in the remote process
    167      if (Cu.isRemoteProxy(window)) {
    168        return false;
    169      }
    170      // Accept "top remote iframe document":
    171      // document of iframe whose immediate parent is in another process.
    172      if (Cu.isRemoteProxy(window.parent) && !Cu.isRemoteProxy(window)) {
    173        return true;
    174      }
    175 
    176      // If EFT is enabled, accept any same process document (top-level or iframe).
    177      if (this.thread.getParent().ignoreSubFrames) {
    178        return true;
    179      }
    180 
    181      try {
    182        // Ignore iframes running in the same process as their parent document,
    183        // as they will be paused automatically when pausing their owner top level document
    184        return window.top === window;
    185      } catch (e) {
    186        // Warn if this is throwing for an unknown reason, but suppress the
    187        // exception regardless so that we can enter the nested event loop.
    188        if (!/not initialized/.test(e)) {
    189          console.warn(`Exception in getAllWindowDebuggees: ${e}`);
    190        }
    191        return false;
    192      }
    193    });
    194  }
    195 
    196  /**
    197   * Prepare to enter a nested event loop by disabling debuggee events.
    198   */
    199  preEnter() {
    200    const preEnterData = [];
    201    // Disable events in all open windows.
    202    for (const window of this.getAllWindowDebuggees()) {
    203      const { windowUtils, document } = window;
    204      const wasPaused = !!document?.pausedByDevTools;
    205      if (document) {
    206        document.pausedByDevTools = true;
    207      }
    208      windowUtils.suppressEventHandling(true);
    209      windowUtils.suspendTimeouts();
    210      preEnterData.push({
    211        docShell: window.docShell,
    212        wasPaused,
    213      });
    214    }
    215    return preEnterData;
    216  }
    217 
    218  /**
    219   * Prepare to exit a nested event loop by enabling debuggee events.
    220   */
    221  postExit(preEnterData) {
    222    // Enable events in all window paused in preEnter
    223    for (const { docShell, wasPaused } of preEnterData) {
    224      // Do not try to resume documents which are in destruction
    225      // as resume methods would throw
    226      if (docShell.isBeingDestroyed()) {
    227        continue;
    228      }
    229      const window = docShell.domWindow;
    230      const { windowUtils, document } = window;
    231      if (document) {
    232        document.pausedByDevTools = wasPaused;
    233      }
    234      windowUtils.resumeTimeouts();
    235      windowUtils.suppressEventHandling(false);
    236    }
    237  }
    238 }
    239 
    240 exports.EventLoop = EventLoop;