summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/utils/event-loop.js
blob: 519d97ba7e3ca7035f69d16e53ebeb5cf93f0686 (plain)
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/* 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";

const xpcInspector = require("xpcInspector");

/**
 * An object that represents a nested event loop. It is used as the nest
 * requestor with nsIJSInspector instances.
 *
 * @param ThreadActor thread
 *        The thread actor that is creating this nested event loop.
 */
class EventLoop {
  constructor({ thread }) {
    this._thread = thread;

    // A flag which is true in between the two calls to enter() and exit().
    this._entered = false;
    // Another flag which is true only after having called exit().
    // Note that this EventLoop may still be paused and its enter() method
    // still be on hold, if another EventLoop paused about this one.
    this._resolved = false;
  }

  /**
   * This is meant for other thread actors, and is used by other thread actor's
   * EventLoop's isTheLastPausedThreadActor()
   */
  get thread() {
    return this._thread;
  }
  /**
   * Similarly, it will be used by another thread actor's EventLoop's enter() method
   */
  get resolved() {
    return this._resolved;
  }

  /**
   * Tells if the last thread actor to have paused (i.e. last EventLoop on the stack)
   * is the current one.
   *
   * We avoid trying to exit this event loop,
   * if another thread actor pile up a more recent one.
   * All the event loops will be effectively exited when
   * the thread actor which piled up the most recent nested event loop resumes.
   *
   * For convenience for the callsite, this will return true if nothing paused.
   */
  isTheLastPausedThreadActor() {
    if (xpcInspector.eventLoopNestLevel > 0) {
      return xpcInspector.lastNestRequestor.thread === this._thread;
    }
    return true;
  }

  /**
   * Enter a new nested event loop.
   */
  enter() {
    if (this._entered) {
      throw new Error(
        "Can't enter an event loop that has already been entered!"
      );
    }

    const preEnterData = this.preEnter();

    this._entered = true;
    // Note: next line will synchronously block the execution until exit() is being called.
    //
    // This enterNestedEventLoop is a bit magical and will break run-to-completion rule of JS.
    // JS will become multi-threaded. Some other task may start running on change state
    // while we are blocked on this enterNestedEventLoop function call.
    // You may find valuable information about Tasks and Event Loops on:
    // https://docs.google.com/document/d/1jTMd-H_BwH9_QNUDxPse80vq884_hMvd234lvE5gqY8/edit?usp=sharing
    //
    // Note #2: this will update xpcInspector.lastNestRequestor to this
    xpcInspector.enterNestedEventLoop(this);

    // If this code runs, it means that we just exited this event loop and lastNestRequestor is no longer equal to this.
    //
    // We will now "recursively" exit all the resolved EventLoops which are blocked on `enterNestedEventLoop`:
    // - if the new lastNestRequestor is resolved, request to exit it as well
    // - this lastNestRequestor is another EventLoop instance
    // - exiting this EventLoop unblocks its "enter" method and moves lastNestRequestor to the next requestor (if any)
    // - we go back to the first step, and attempt to exit the new lastNestRequestor if it is resolved, etc...
    if (xpcInspector.eventLoopNestLevel > 0) {
      const { resolved } = xpcInspector.lastNestRequestor;
      if (resolved) {
        xpcInspector.exitNestedEventLoop();
      }
    }

    this.postExit(preEnterData);
  }

  /**
   * Exit this nested event loop.
   *
   * @returns boolean
   *          True if we exited this nested event loop because it was on top of
   *          the stack, false if there is another nested event loop above this
   *          one that hasn't exited yet.
   */
  exit() {
    if (!this._entered) {
      throw new Error("Can't exit an event loop before it has been entered!");
    }
    this._entered = false;
    this._resolved = true;

    // If another ThreadActor paused and spawn a new nested event loop after this one,
    // let it resume the thread and ignore this call.
    // The code calling exitNestedEventLoop from EventLoop.enter will resume execution,
    // by seeing that resolved attribute that we just toggled is true.
    //
    // Note that ThreadActor.resume method avoids calling exit thanks to `isTheLastPausedThreadActor`
    // So for all use requests to resume, the ThreadActor won't call exit until it is the last
    // thread actor to have entered a nested EventLoop.
    if (this === xpcInspector.lastNestRequestor) {
      xpcInspector.exitNestedEventLoop();
      return true;
    }
    return false;
  }

  /**
   * Retrieve the list of all DOM Windows debugged by the current thread actor.
   */
  getAllWindowDebuggees() {
    return this._thread.dbg
      .getDebuggees()
      .filter(debuggee => {
        // Select only debuggee that relates to windows
        // e.g. ignore sandboxes, jsm and such
        return debuggee.class == "Window";
      })
      .map(debuggee => {
        // Retrieve the JS reference for these windows
        return debuggee.unsafeDereference();
      })

      .filter(window => {
        // Ignore document which have already been nuked,
        // so navigated to another location and removed from memory completely.
        if (Cu.isDeadWrapper(window)) {
          return false;
        }
        // Also ignore document which are closed, as trying to access window.parent or top would throw NS_ERROR_NOT_INITIALIZED
        if (window.closed) {
          return false;
        }
        // Ignore remote iframes, which will be debugged by another thread actor,
        // running in the remote process
        if (Cu.isRemoteProxy(window)) {
          return false;
        }
        // Accept "top remote iframe document":
        // document of iframe whose immediate parent is in another process.
        if (Cu.isRemoteProxy(window.parent) && !Cu.isRemoteProxy(window)) {
          return true;
        }

        // If EFT is enabled, accept any same process document (top-level or iframe).
        if (this.thread.getParent().ignoreSubFrames) {
          return true;
        }

        try {
          // Ignore iframes running in the same process as their parent document,
          // as they will be paused automatically when pausing their owner top level document
          return window.top === window;
        } catch (e) {
          // Warn if this is throwing for an unknown reason, but suppress the
          // exception regardless so that we can enter the nested event loop.
          if (!/not initialized/.test(e)) {
            console.warn(`Exception in getAllWindowDebuggees: ${e}`);
          }
          return false;
        }
      });
  }

  /**
   * Prepare to enter a nested event loop by disabling debuggee events.
   */
  preEnter() {
    const docShells = [];
    // Disable events in all open windows.
    for (const window of this.getAllWindowDebuggees()) {
      const { windowUtils } = window;
      windowUtils.suppressEventHandling(true);
      windowUtils.suspendTimeouts();
      docShells.push(window.docShell);
    }
    return docShells;
  }

  /**
   * Prepare to exit a nested event loop by enabling debuggee events.
   */
  postExit(pausedDocShells) {
    // Enable events in all window paused in preEnter
    for (const docShell of pausedDocShells) {
      // Do not try to resume documents which are in destruction
      // as resume methods would throw
      if (docShell.isBeingDestroyed()) {
        continue;
      }
      const { windowUtils } = docShell.domWindow;
      windowUtils.resumeTimeouts();
      windowUtils.suppressEventHandling(false);
    }
  }
}

exports.EventLoop = EventLoop;