DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (c68fe15a81fc)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
/* vim: set ts=2 sw=2 sts=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

var EXPORTED_SYMBOLS = ["ContentEventListenerChild"];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");

class ContentEventListenerChild extends JSWindowActorChild {
  actorCreated() {
    this._contentEvents = new Map();
    this._shutdown = false;
    this._chromeEventHandler = null;
    Services.cpmm.sharedData.addEventListener("change", this);
  }

  willDestroy() {
    this._shutdown = true;
    Services.cpmm.sharedData.removeEventListener("change", this);
    this._updateContentEventListeners(/* clearListeners = */ true);
    if (this._contentEvents.size != 0) {
      throw new Error(`Didn't expect content events after willDestroy`);
    }
  }

  handleEvent(event) {
    switch (event.type) {
      case "DOMWindowCreated": {
        this._updateContentEventListeners();
        break;
      }

      case "change": {
        if (
          !event.changedKeys.includes("BrowserTestUtils:ContentEventListener")
        ) {
          return;
        }
        this._updateContentEventListeners();
        break;
      }
    }
  }

  /**
   * This method first determines the desired set of content event listeners
   * for the window. This is either the empty set, if clearListeners is true,
   * or is retrieved from the message manager's shared data. It then compares
   * this event listener data to the existing set of listeners that we have
   * registered, as recorded in this._contentEvents. Each content event listener
   * has been assigned a unique id by the parent process. If a listener was
   * added, but is not in the new event data, it is removed. If a listener was
   * not present, but is in the new event data, it is added. If it is in both,
   * then a basic check is done to see if they are the same.
   *
   * @param {bool} clearListeners [optional]
   *        If this is true, then instead of checking shared data to decide
   *        what the desired set of listeners is, just use the empty set. This
   *        will result in any existing listeners being cleared, and is used
   *        when the window is going away.
   */
  _updateContentEventListeners(clearListeners = false) {
    // If we've already begun the destruction process, any new event
    // listeners for our bc id can't possibly really be for us, so ignore them.
    if (this._shutdown && !clearListeners) {
      throw new Error(
        "Tried to update after we shut down content event listening"
      );
    }

    let newEventData;
    if (!clearListeners) {
      newEventData = Services.cpmm.sharedData.get(
        "BrowserTestUtils:ContentEventListener"
      );
    }
    if (!newEventData) {
      newEventData = new Map();
    }

    // Check that entries that continue to exist are the same and remove entries
    // that no longer exist.
    for (let [
      listenerId,
      { eventName, listener, listenerOptions },
    ] of this._contentEvents.entries()) {
      let newData = newEventData.get(listenerId);
      if (newData) {
        if (newData.eventName !== eventName) {
          // Could potentially check if listenerOptions are the same, but
          // checkFnSource can't be checked unless we store it, and this is
          // just a smoke test anyways, so don't bother.
          throw new Error(
            "Got new content event listener that disagreed with existing data"
          );
        }
        continue;
      }
      if (!this._chromeEventHandler) {
        throw new Error(
          "Trying to remove an event listener for waitForContentEvent without a cached event handler"
        );
      }
      this._chromeEventHandler.removeEventListener(
        eventName,
        listener,
        listenerOptions
      );
      this._contentEvents.delete(listenerId);
    }

    let actorChild = this;

    // Add in new entries.
    for (let [
      listenerId,
      { eventName, listenerOptions, checkFnSource },
    ] of newEventData.entries()) {
      let oldData = this._contentEvents.get(listenerId);
      if (oldData) {
        // We checked that the data is the same in the previous loop.
        continue;
      }

      /* eslint-disable no-eval */
      let checkFn;
      if (checkFnSource) {
        checkFn = eval(`(() => (${unescape(checkFnSource)}))()`);
      }
      /* eslint-enable no-eval */

      function listener(event) {
        if (checkFn && !checkFn(event)) {
          return;
        }
        actorChild.sendAsyncMessage("ContentEventListener:Run", {
          listenerId,
        });
      }

      // Cache the chrome event handler because this.docShell won't be
      // available during shut down.
      if (!this._chromeEventHandler) {
        try {
          this._chromeEventHandler = this.docShell.chromeEventHandler;
        } catch (error) {
          if (error.name === "InvalidStateError") {
            // We'll arrive here if we no longer have our manager, so we can
            // just swallow this error.
            continue;
          }
          throw error;
        }
      }

      // Some windows, like top-level browser windows, maybe not have a chrome
      // event handler set up as this point, but we don't actually care about
      // events on those windows, so ignore them.
      if (!this._chromeEventHandler) {
        continue;
      }

      this._chromeEventHandler.addEventListener(
        eventName,
        listener,
        listenerOptions
      );
      this._contentEvents.set(listenerId, {
        eventName,
        listener,
        listenerOptions,
      });
    }

    // If there are no active content events, clear our reference to the chrome
    // event handler to prevent leaks.
    if (this._contentEvents.size == 0) {
      this._chromeEventHandler = null;
    }
  }
}