зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1681698 - [devtools] Bootstrap the thread actor early, when the target actor instantiates r=jdescottes,nchevobbe,devtools-backward-compat-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D99356
This commit is contained in:
Родитель
29f31d806f
Коммит
6b61530950
|
@ -522,6 +522,24 @@ function TargetMixin(parentClass) {
|
|||
);
|
||||
}
|
||||
this.threadFront = await this.getFront("thread");
|
||||
|
||||
// Avoid attaching if the thread actor was already attached on target creation from the server side.
|
||||
// This doesn't include:
|
||||
// * targets that aren't yet supported by the Watcher (like web extensions),
|
||||
// * workers, which still use a unique codepath for thread actor attach
|
||||
// * all targets when connecting to an older server
|
||||
// @backward-compat { version 87 } If all targets are supported by watcher actor, and workers no longer use
|
||||
// its unique attach sequence, we can assume the thread front is always attached.
|
||||
const isAttached =
|
||||
this.getTrait("supportsThreadActorIsAttached") &&
|
||||
(await this.threadFront.isAttached());
|
||||
if (isAttached) {
|
||||
// If the Thread actor has already been attached from the server side
|
||||
// by the Watcher Actor, we still have to pass options that aren't yet managed via
|
||||
// the Watcher actor's addWatcherDataEntry codepath (bug 1687261).
|
||||
await this.threadFront.reconfigure(options);
|
||||
return this.threadFront;
|
||||
}
|
||||
if (
|
||||
this.isDestroyedOrBeingDestroyed() ||
|
||||
this.threadFront.isDestroyed()
|
||||
|
|
|
@ -25,7 +25,6 @@ support-files =
|
|||
doc_html_tooltip_doorhanger-02.xhtml
|
||||
doc_html_tooltip_hover.xhtml
|
||||
doc_html_tooltip_rtl.xhtml
|
||||
doc_inline-debugger-statement.html
|
||||
doc_inplace-editor_autocomplete_offset.xhtml
|
||||
doc_layoutHelpers_getBoxQuads1.html
|
||||
doc_layoutHelpers_getBoxQuads2-a.html
|
||||
|
@ -78,8 +77,6 @@ support-files =
|
|||
[browser_cubic-bezier-06.js]
|
||||
[browser_cubic-bezier-07.js]
|
||||
tags = addons
|
||||
[browser_dbg_debugger-statement.js]
|
||||
skip-if = e10s && debug
|
||||
[browser_dbg_listworkers.js]
|
||||
[browser_filter-editor-01.js]
|
||||
[browser_filter-editor-02.js]
|
||||
|
|
|
@ -1,164 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Tests the behavior of the debugger statement.
|
||||
*/
|
||||
|
||||
// Use distinct origins in order to use distinct processes when fission is enabled
|
||||
const TAB_URL = URL_ROOT_COM + "doc_inline-debugger-statement.html";
|
||||
const IFRAME_URL = URL_ROOT_ORG + "doc_inline-debugger-statement.html";
|
||||
|
||||
add_task(async () => {
|
||||
const tab = await addTab(TAB_URL);
|
||||
const tabBrowsingContext = tab.linkedBrowser.browsingContext;
|
||||
|
||||
const iframeBrowsingContext = await SpecialPowers.spawn(
|
||||
tabBrowsingContext,
|
||||
[IFRAME_URL],
|
||||
async function(url) {
|
||||
const iframe = content.document.createElement("iframe");
|
||||
const onLoad = new Promise(r =>
|
||||
iframe.addEventListener("load", r, { once: true })
|
||||
);
|
||||
iframe.src = url;
|
||||
content.document.body.appendChild(iframe);
|
||||
await onLoad;
|
||||
return iframe.browsingContext;
|
||||
}
|
||||
);
|
||||
|
||||
const target = await TargetFactory.forTab(tab);
|
||||
await target.attach();
|
||||
const { client } = target;
|
||||
|
||||
info("## Test debugger statement against the top level tab document");
|
||||
// This function, by calling the debugger statement function, will bump the increment
|
||||
const threadFront = await testEarlyDebuggerStatement(
|
||||
client,
|
||||
tabBrowsingContext,
|
||||
target
|
||||
);
|
||||
await testDebuggerStatement(client, tabBrowsingContext, threadFront, 1);
|
||||
|
||||
info("## Test debugger statement againt a distinct origin iframe");
|
||||
if (isFissionEnabled()) {
|
||||
// We have to use the watcher in order to create the frame target
|
||||
// and also have to attach to it in order to later be able to
|
||||
// create the thread front
|
||||
const watcherFront = await target.getWatcherFront();
|
||||
await watcherFront.watchTargets("frame");
|
||||
const iframeTarget = await target.getBrowsingContextTarget(
|
||||
iframeBrowsingContext.id
|
||||
);
|
||||
await iframeTarget.attach();
|
||||
|
||||
// This function, by calling the debugger statement function, will bump the increment
|
||||
const iframeThreadFront = await testEarlyDebuggerStatement(
|
||||
client,
|
||||
iframeBrowsingContext,
|
||||
iframeTarget
|
||||
);
|
||||
await testDebuggerStatement(
|
||||
client,
|
||||
iframeBrowsingContext,
|
||||
iframeThreadFront,
|
||||
1
|
||||
);
|
||||
} else {
|
||||
// But in this case, the increment will be 0 as the previous call to `testEarlyDebuggerStatement`
|
||||
// bumped the tab's document increment and not the iframe's one.
|
||||
await testDebuggerStatement(client, iframeBrowsingContext, threadFront, 0);
|
||||
}
|
||||
|
||||
await target.destroy();
|
||||
});
|
||||
|
||||
async function testEarlyDebuggerStatement(
|
||||
client,
|
||||
browsingContext,
|
||||
targetFront
|
||||
) {
|
||||
const onPaused = function(packet) {
|
||||
ok(false, "Pause shouldn't be called before we've attached!");
|
||||
};
|
||||
|
||||
// using the DevToolsClient to listen to the pause packet, as the
|
||||
// threadFront is not yet attached.
|
||||
client.on("paused", onPaused);
|
||||
|
||||
// This should continue without nesting an event loop and calling
|
||||
// the onPaused hook, because we haven't attached yet.
|
||||
const increment = await SpecialPowers.spawn(
|
||||
browsingContext,
|
||||
[],
|
||||
async function() {
|
||||
content.wrappedJSObject.runDebuggerStatement();
|
||||
// Pile up another setTimeout in order to guarantee that the other one ran
|
||||
await new Promise(r => content.setTimeout(r));
|
||||
return content.wrappedJSObject.increment;
|
||||
}
|
||||
);
|
||||
is(increment, 1, "As the thread wasn't paused, setTimeout worked");
|
||||
|
||||
client.off("paused", onPaused);
|
||||
|
||||
// Now attach
|
||||
const threadFront = await targetFront.attachThread();
|
||||
ok(true, "Pause wasn't called before we've attached.");
|
||||
|
||||
return threadFront;
|
||||
}
|
||||
|
||||
async function testDebuggerStatement(
|
||||
client,
|
||||
browsingContext,
|
||||
threadFront,
|
||||
incrementOriginalValue
|
||||
) {
|
||||
const onPaused = threadFront.once("paused");
|
||||
|
||||
// Reach around the debugging protocol and execute the debugger statement.
|
||||
// Not that this will be paused and spawn will only resolve once
|
||||
// the thread will be resumed
|
||||
const onResumed = SpecialPowers.spawn(browsingContext, [], function() {
|
||||
content.wrappedJSObject.runDebuggerStatement();
|
||||
});
|
||||
|
||||
info("Waiting for paused event");
|
||||
await onPaused;
|
||||
ok(true, "The pause handler was triggered on a debugger statement.");
|
||||
|
||||
// Pile up another setTimeout in order to guarantee that the other did not run
|
||||
/* eslint-disable-next-line mozilla/no-arbitrary-setTimeout */
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
let increment = await SpecialPowers.spawn(
|
||||
browsingContext,
|
||||
[],
|
||||
async function() {
|
||||
return content.wrappedJSObject.increment;
|
||||
}
|
||||
);
|
||||
is(
|
||||
increment,
|
||||
incrementOriginalValue,
|
||||
"setTimeout are frozen while the thread is paused"
|
||||
);
|
||||
|
||||
await threadFront.resume();
|
||||
await onResumed;
|
||||
|
||||
increment = await SpecialPowers.spawn(browsingContext, [], async function() {
|
||||
// Pile up another setTimeout in order to guarantee that the other did run
|
||||
await new Promise(r => content.setTimeout(r));
|
||||
return content.wrappedJSObject.increment;
|
||||
});
|
||||
is(
|
||||
increment,
|
||||
incrementOriginalValue + 1,
|
||||
"setTimeout are resumed after the thread is resumed"
|
||||
);
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
<!doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Debugger test page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button>Click me!</button>
|
||||
|
||||
<script type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
var increment = 0;
|
||||
/* exported runDebuggerStatement */
|
||||
function runDebuggerStatement() {
|
||||
setTimeout(() => increment++, 0);
|
||||
// eslint-disable-next-line no-debugger
|
||||
debugger;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -133,9 +133,11 @@ exports.RootActor = protocol.ActorClassWithSpec(rootSpec, {
|
|||
"dom.worker.console.dispatch_events_to_main_thread"
|
||||
)
|
||||
: true,
|
||||
// @backward-compat { version 86 } ThreadActor.attach no longer pause the thread,
|
||||
// @backward-compat { version 86 } ThreadActor.attach no longer pauses the thread,
|
||||
// so that we no longer have to resume.
|
||||
noPauseOnThreadActorAttach: true,
|
||||
// @backward-compat { version 87 } ThreadActor supports isAttached request
|
||||
supportsThreadActorIsAttached: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
const { ActorClassWithSpec } = require("devtools/shared/protocol");
|
||||
|
||||
const Resources = require("devtools/server/actors/resources/index");
|
||||
const {
|
||||
WatchedDataHelpers,
|
||||
} = require("devtools/server/actors/watcher/WatchedDataHelpers.jsm");
|
||||
const { RESOURCES, BREAKPOINTS } = WatchedDataHelpers.SUPPORTED_DATA;
|
||||
const { STATES: THREAD_STATES } = require("devtools/server/actors/thread");
|
||||
|
||||
module.exports = function(targetType, targetActorSpec, implementation) {
|
||||
const proto = {
|
||||
|
@ -25,9 +30,9 @@ module.exports = function(targetType, targetActorSpec, implementation) {
|
|||
* The values to be added to this type of data
|
||||
*/
|
||||
async addWatcherDataEntry(type, entries) {
|
||||
if (type == "resources") {
|
||||
if (type == RESOURCES) {
|
||||
await this._watchTargetResources(entries);
|
||||
} else if (type == "breakpoints") {
|
||||
} else if (type == BREAKPOINTS) {
|
||||
// Breakpoints require the target to be attached,
|
||||
// mostly to have the thread actor instantiated
|
||||
// (content process targets don't have attach method,
|
||||
|
@ -36,18 +41,30 @@ module.exports = function(targetType, targetActorSpec, implementation) {
|
|||
this.attach();
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
entries.map(({ location, options }) =>
|
||||
this.threadActor.setBreakpoint(location, options)
|
||||
)
|
||||
);
|
||||
const isTargetCreation =
|
||||
this.threadActor.state == THREAD_STATES.DETACHED;
|
||||
if (isTargetCreation && !this.targetType.endsWith("worker")) {
|
||||
// If addWatcherDataEntry is called during target creation, attach the
|
||||
// thread actor automatically and pass the initial breakpoints.
|
||||
// However, do not attach the thread actor for Workers. They use a codepath
|
||||
// which releases the worker on `attach`. For them, the client will call `attach`. (bug 1691986)
|
||||
await this.threadActor.attach({ breakpoints: entries });
|
||||
} else {
|
||||
// If addWatcherDataEntry is called for an existing target, set the new
|
||||
// breakpoints on the already running thread actor.
|
||||
await Promise.all(
|
||||
entries.map(({ location, options }) =>
|
||||
this.threadActor.setBreakpoint(location, options)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeWatcherDataEntry(type, entries) {
|
||||
if (type == "resources") {
|
||||
if (type == RESOURCES) {
|
||||
return this._unwatchTargetResources(entries);
|
||||
} else if (type == "breakpoints") {
|
||||
} else if (type == BREAKPOINTS) {
|
||||
for (const { location } of entries) {
|
||||
this.threadActor.removeBreakpoint(location);
|
||||
}
|
||||
|
|
|
@ -149,6 +149,7 @@ const STATES = {
|
|||
// When paused on any type of breakpoint, or, when the client requested an interrupt.
|
||||
PAUSED: "paused",
|
||||
};
|
||||
exports.STATES = STATES;
|
||||
|
||||
// Possible values for the `why.type` attribute in "paused" event
|
||||
const PAUSE_REASONS = {
|
||||
|
@ -398,8 +399,22 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
|
|||
Actor.prototype.destroy.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Tells if the thread actor has been initialized/attached on target creation
|
||||
* by the server codebase. (And not late, from the frontend, by the TargetMixinFront class)
|
||||
*/
|
||||
isAttached() {
|
||||
return !!this.alreadyAttached;
|
||||
},
|
||||
|
||||
// Request handlers
|
||||
attach(options) {
|
||||
// Note that the client avoids trying to call attach if already attached.
|
||||
// But just in case, avoid any possible duplicate call to attach.
|
||||
if (this.alreadyAttached) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state === STATES.EXITED) {
|
||||
throw {
|
||||
error: "exited",
|
||||
|
@ -426,12 +441,14 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
|
|||
thread: this,
|
||||
});
|
||||
|
||||
this.dbg.enable();
|
||||
this.reconfigure(options);
|
||||
|
||||
// Set everything up so that breakpoint can work
|
||||
// Switch state from DETACHED to RUNNING
|
||||
this._state = STATES.RUNNING;
|
||||
|
||||
this.alreadyAttached = true;
|
||||
this.dbg.enable();
|
||||
|
||||
// Notify the parent that we've finished attaching. If this is a worker
|
||||
// thread which was paused until attaching, this will allow content to
|
||||
// begin executing.
|
||||
|
|
|
@ -122,3 +122,10 @@ const WatchedDataHelpers = {
|
|||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
// Allow this JSM to also be loaded as a CommonJS module
|
||||
// Because this module is used from the worker thread,
|
||||
// (via target-actor-mixin), and workers can't load JSMs.
|
||||
if (typeof module == "object") {
|
||||
module.exports.WatchedDataHelpers = WatchedDataHelpers;
|
||||
}
|
||||
|
|
|
@ -10,17 +10,28 @@ const {
|
|||
} = require("devtools/shared/resources/resource-watcher");
|
||||
|
||||
const BREAKPOINT_TEST_URL = URL_ROOT_SSL + "breakpoint_document.html";
|
||||
const REMOTE_IFRAME_URL =
|
||||
"https://example.org/document-builder.sjs?html=" +
|
||||
encodeURIComponent("<script>debugger;</script>");
|
||||
|
||||
add_task(async function() {
|
||||
// Check hitting the "debugger;" statement before and after calling
|
||||
// watchResource(THREAD_TYPES). Both should break. First will
|
||||
// be a cached resource and second will be a live one.
|
||||
await checkBreakpointBeforeWatchResources();
|
||||
|
||||
await checkBreakpointAfterWatchResources();
|
||||
|
||||
// Check setting a real breakpoint on a given line
|
||||
await checkRealBreakpoint();
|
||||
|
||||
// Check the "pause on exception" setting
|
||||
await checkPauseOnException();
|
||||
|
||||
// Check an edge case where spamming setBreakpoints calls causes issues
|
||||
await checkSetBeforeWatch();
|
||||
|
||||
// Check debugger statement for (remote) iframes
|
||||
await checkDebuggerStatementInIframes();
|
||||
});
|
||||
|
||||
async function checkBreakpointBeforeWatchResources() {
|
||||
|
@ -415,6 +426,95 @@ async function checkSetBeforeWatch() {
|
|||
await client.close();
|
||||
}
|
||||
|
||||
async function checkDebuggerStatementInIframes() {
|
||||
info("Check whether ResourceWatcher gets breakpoint for (remote) iframes");
|
||||
|
||||
const tab = await addTab(BREAKPOINT_TEST_URL);
|
||||
|
||||
const { client, resourceWatcher, targetList } = await initResourceWatcher(
|
||||
tab
|
||||
);
|
||||
|
||||
info("Call watchResources");
|
||||
const availableResources = [];
|
||||
await resourceWatcher.watchResources([ResourceWatcher.TYPES.THREAD_STATE], {
|
||||
onAvailable: resources => availableResources.push(...resources),
|
||||
});
|
||||
|
||||
is(
|
||||
availableResources.length,
|
||||
0,
|
||||
"Got no THREAD_STATE when calling watchResources"
|
||||
);
|
||||
|
||||
info("Inject the iframe with an inline 'debugger' statement");
|
||||
// Note that we do not wait for the resolution of spawn as it will be paused
|
||||
SpecialPowers.spawn(
|
||||
gBrowser.selectedBrowser,
|
||||
[REMOTE_IFRAME_URL],
|
||||
async function(url) {
|
||||
const iframe = content.document.createElement("iframe");
|
||||
iframe.src = url;
|
||||
content.document.body.appendChild(iframe);
|
||||
}
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => availableResources.length == 1,
|
||||
"Got the THREAD_STATE related to the iframe's debugger statement"
|
||||
);
|
||||
const threadState = availableResources.pop();
|
||||
|
||||
assertPausedResource(threadState, {
|
||||
state: "paused",
|
||||
why: {
|
||||
type: "debuggerStatement",
|
||||
},
|
||||
frame: {
|
||||
type: "global",
|
||||
asyncCause: null,
|
||||
state: "on-stack",
|
||||
// this: object actor's form referring to `this` variable
|
||||
displayName: "(global)",
|
||||
// arguments: []
|
||||
where: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const iframeTarget = threadState.targetFront;
|
||||
if (isFissionEnabled()) {
|
||||
is(
|
||||
iframeTarget.url,
|
||||
REMOTE_IFRAME_URL,
|
||||
"With fission, the pause is from the iframe's target"
|
||||
);
|
||||
} else {
|
||||
is(
|
||||
iframeTarget,
|
||||
targetList.targetFront,
|
||||
"Without fission, the pause is from the top level target"
|
||||
);
|
||||
}
|
||||
const { threadFront } = iframeTarget;
|
||||
|
||||
await threadFront.resume();
|
||||
|
||||
await waitFor(
|
||||
() => availableResources.length == 1,
|
||||
"Wait until we receive the resumed event"
|
||||
);
|
||||
|
||||
const resumed = availableResources.pop();
|
||||
|
||||
assertResumedResource(resumed);
|
||||
|
||||
targetList.destroy();
|
||||
await client.close();
|
||||
}
|
||||
|
||||
async function assertPausedResource(resource, expected) {
|
||||
is(
|
||||
resource.resourceType,
|
||||
|
|
|
@ -9,7 +9,23 @@ const { Front, types } = require("devtools/shared/protocol.js");
|
|||
module.exports = function({ resource, watcherFront, targetFront }) {
|
||||
// only "paused" have a frame attribute, and legacy listeners are already passing a FrameFront
|
||||
if (resource.frame && !(resource.frame instanceof Front)) {
|
||||
resource.frame = types.getType("frame").read(resource.frame, targetFront);
|
||||
// Use ThreadFront as parent as debugger's commands.js expects FrameFront to be children
|
||||
// of the ThreadFront.
|
||||
resource.frame = types
|
||||
.getType("frame")
|
||||
.read(resource.frame, targetFront.threadFront);
|
||||
}
|
||||
|
||||
// If we are using server side request (i.e. watcherFront is defined)
|
||||
// Fake paused and resumed events as the thread front depends on them.
|
||||
// We can't emit "EventEmitter" events, as ThreadFront uses `Front.before`
|
||||
// to listen for paused and resumed. ("before" is part of protocol.js Front and not part of EventEmitter)
|
||||
if (watcherFront) {
|
||||
if (resource.state == "paused") {
|
||||
targetFront.threadFront._beforePaused(resource);
|
||||
} else if (resource.state == "resumed") {
|
||||
targetFront.threadFront._beforeResumed(resource);
|
||||
}
|
||||
}
|
||||
|
||||
return resource;
|
||||
|
|
|
@ -179,6 +179,13 @@ const threadSpec = generateActorSpec({
|
|||
logEventBreakpoints: Arg(0, "string"),
|
||||
},
|
||||
},
|
||||
|
||||
isAttached: {
|
||||
request: {},
|
||||
response: {
|
||||
value: RetVal("boolean"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -173,7 +173,9 @@ function WorkerDebuggerLoader(options) {
|
|||
}
|
||||
|
||||
// If the url has no extension, use ".js" by default.
|
||||
return url.endsWith(".js") ? url : url + ".js";
|
||||
// Also allow loading JSMs, but they would need a shim in order to
|
||||
// be loaded as a CommonJS module. (See WatchedDataHelpers.jsm)
|
||||
return url.endsWith(".js") || url.endsWith(".jsm") ? url : url + ".js";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,7 +21,6 @@ file-whitespace:
|
|||
- devtools/client/inspector/markup/test
|
||||
- devtools/client/inspector/rules/test
|
||||
- devtools/client/inspector/test
|
||||
- devtools/client/shared/test/doc_inline-debugger-statement.html
|
||||
# Excluded because tests were failing unexpectedly
|
||||
- devtools/client/styleeditor/test/sync_with_csp.css
|
||||
- devtools/client/webconsole/test/browser/test-message-categories-css-parser.css
|
||||
|
|
Загрузка…
Ссылка в новой задаче