240 lines
8.3 KiB
JavaScript
240 lines
8.3 KiB
JavaScript
/* 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) {
|
|
if (xpcInspector.lastNestRequestor.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() {
|
|
const rawGlobals = 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();
|
|
});
|
|
|
|
// When pausing from a content script, also ensure pausing the related document
|
|
const { innerWindowId } = this._thread.targetActor;
|
|
if (innerWindowId) {
|
|
const windowGlobal = WindowGlobalChild.getByInnerWindowId(innerWindowId);
|
|
if (windowGlobal?.browsingContext?.window) {
|
|
rawGlobals.push(windowGlobal.browsingContext.window);
|
|
}
|
|
}
|
|
|
|
return rawGlobals.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 preEnterData = [];
|
|
// Disable events in all open windows.
|
|
for (const window of this.getAllWindowDebuggees()) {
|
|
const { windowUtils, document } = window;
|
|
const wasPaused = !!document?.pausedByDevTools;
|
|
if (document) {
|
|
document.pausedByDevTools = true;
|
|
}
|
|
windowUtils.suppressEventHandling(true);
|
|
windowUtils.suspendTimeouts();
|
|
preEnterData.push({
|
|
docShell: window.docShell,
|
|
wasPaused,
|
|
});
|
|
}
|
|
return preEnterData;
|
|
}
|
|
|
|
/**
|
|
* Prepare to exit a nested event loop by enabling debuggee events.
|
|
*/
|
|
postExit(preEnterData) {
|
|
// Enable events in all window paused in preEnter
|
|
for (const { docShell, wasPaused } of preEnterData) {
|
|
// Do not try to resume documents which are in destruction
|
|
// as resume methods would throw
|
|
if (docShell.isBeingDestroyed()) {
|
|
continue;
|
|
}
|
|
const window = docShell.domWindow;
|
|
const { windowUtils, document } = window;
|
|
if (document) {
|
|
document.pausedByDevTools = wasPaused;
|
|
}
|
|
windowUtils.resumeTimeouts();
|
|
windowUtils.suppressEventHandling(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
exports.EventLoop = EventLoop;
|