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;