Bug 1700909 - [devtools] Migrate gDevTools.showToolbox from descriptor to commands. r=jdescottes

This migrate gDevTools.showToolbox, but also the Toolbox now receives a commands right away,
and no longer need to create commands out of the descriptor front.

I'm removing browser_two_tabs as it is focusing on testing Tab Descriptors (RootFront.listTabs+getTab)
and Tab targets (TabDescriptor.getTarget).
Using getTarget on descriptor is legacy codepath for a while now.
We should now rather cover commands instead of these low level RDP methods.

Differential Revision: https://phabricator.services.mozilla.com/D157796
This commit is contained in:
Alexandre Poirot 2022-09-29 14:43:26 +00:00
Родитель 28c70e4209
Коммит bf8918c2e9
16 изменённых файлов: 103 добавлений и 211 удалений

Просмотреть файл

@ -202,8 +202,7 @@ function onReloadBrowser() {
}
async function openToolbox(commands) {
const { descriptorFront } = commands;
const form = descriptorFront._form;
const form = commands.descriptorFront._form;
appendStatusMessage(
`Create toolbox for target descriptor: ${JSON.stringify({ form }, null, 2)}`
);
@ -218,7 +217,7 @@ async function openToolbox(commands) {
const toolboxOptions = { doc: document };
appendStatusMessage(`Show toolbox with ${selectedTool} selected`);
gToolbox = await gDevTools.showToolbox(descriptorFront, {
gToolbox = await gDevTools.showToolbox(commands, {
toolId: selectedTool,
hostType: Toolbox.HostType.BROWSERTOOLBOX,
hostOptions: toolboxOptions,

Просмотреть файл

@ -368,9 +368,8 @@ var gDevToolsBrowser = (exports.gDevToolsBrowser = {
if (processId) {
try {
const commands = await CommandsFactory.forProcess(processId);
const descriptor = commands.descriptorFront;
// Display a new toolbox in a new window
const toolbox = await gDevTools.showToolbox(descriptor, {
const toolbox = await gDevTools.showToolbox(commands, {
hostType: Toolbox.HostType.WINDOW,
hostOptions: {
// Will be used by the WINDOW host to decide whether to create a

Просмотреть файл

@ -72,9 +72,9 @@ const DEVTOOLS_ALWAYS_ON_TOP = "devtools.toolbox.alwaysOnTop";
function DevTools() {
this._tools = new Map(); // Map<toolId, tool>
this._themes = new Map(); // Map<themeId, theme>
this._toolboxes = new Map(); // Map<descriptor, toolbox>
this._toolboxes = new Map(); // Map<commands, toolbox>
// List of toolboxes that are still in process of creation
this._creatingToolboxes = new Map(); // Map<descriptor, toolbox Promise>
this._creatingToolboxes = new Map(); // Map<commands, toolbox Promise>
EventEmitter.decorate(this);
this._telemetry = new Telemetry();
@ -475,8 +475,8 @@ DevTools.prototype = {
_firstShowToolbox: true,
/**
* Show a Toolbox for a descriptor (either by creating a new one, or if a
* toolbox already exists for the descriptor, by bringing to the front the
* Show a Toolbox for a given "commands" (either by creating a new one, or if a
* toolbox already exists for the commands, by bringing to the front the
* existing one).
*
* If a Toolbox already exists, we will still update it based on some of the
@ -486,8 +486,8 @@ DevTools.prototype = {
* - if |hostType| is provided then the toolbox will be switched to the
* specified HostType.
*
* @param {TargetDescriptor} descriptor
* The target descriptor the toolbox will debug
* @param {Commands Object} commands
* The commands object which designates which context the toolbox will debug
* @param {Object}
* - {String} toolId
* The id of the tool to show
@ -507,7 +507,7 @@ DevTools.prototype = {
* The toolbox that was opened
*/
async showToolbox(
descriptor,
commands,
{
toolId,
hostType,
@ -517,7 +517,7 @@ DevTools.prototype = {
hostOptions,
} = {}
) {
let toolbox = this._toolboxes.get(descriptor);
let toolbox = this._toolboxes.get(commands);
if (toolbox) {
if (hostType != null && toolbox.hostType != hostType) {
@ -536,20 +536,20 @@ DevTools.prototype = {
} else {
// Toolbox creation is async, we have to be careful about races.
// Check if we are already waiting for a Toolbox for the provided
// descriptor before creating a new one.
const promise = this._creatingToolboxes.get(descriptor);
// commands before creating a new one.
const promise = this._creatingToolboxes.get(commands);
if (promise) {
return promise;
}
const toolboxPromise = this._createToolbox(
descriptor,
commands,
toolId,
hostType,
hostOptions
);
this._creatingToolboxes.set(descriptor, toolboxPromise);
this._creatingToolboxes.set(commands, toolboxPromise);
toolbox = await toolboxPromise;
this._creatingToolboxes.delete(descriptor);
this._creatingToolboxes.delete(commands);
if (startTime) {
this.logToolboxOpenTime(toolbox, startTime);
@ -607,10 +607,7 @@ DevTools.prototype = {
const openerCommands = await LocalTabCommandsFactory.getCommandsForTab(
openerTab
);
if (
openerCommands &&
this.getToolboxForDescriptor(openerCommands.descriptorFront)
) {
if (this.getToolboxForCommands(openerCommands)) {
console.log(
"Can't open a toolbox for this document as this is debugged from its opener tab"
);
@ -618,7 +615,7 @@ DevTools.prototype = {
}
}
const commands = await LocalTabCommandsFactory.createCommandsForTab(tab);
return this.showToolbox(commands.descriptorFront, {
return this.showToolbox(commands, {
toolId,
hostType,
startTime,
@ -640,7 +637,7 @@ DevTools.prototype = {
async showToolboxForWebExtension(extensionId) {
// Ensure spawning only one commands instance per extension at a time by caching its commands.
// showToolbox will later reopen the previously opened toolbox if called with the same
// descriptor.
// commands.
let commandsPromise = this._commandsPromiseByWebExtId.get(extensionId);
if (!commandsPromise) {
commandsPromise = CommandsFactory.forAddon(extensionId);
@ -651,7 +648,7 @@ DevTools.prototype = {
this._commandsPromiseByWebExtId.delete(extensionId);
});
return this.showToolbox(commands.descriptorFront, {
return this.showToolbox(commands, {
hostType: Toolbox.HostType.WINDOW,
hostOptions: {
// The toolbox is always displayed on top so that we can keep
@ -716,15 +713,15 @@ DevTools.prototype = {
},
/**
* Unconditionally create a new Toolbox instance for the provided descriptor.
* Unconditionally create a new Toolbox instance for the provided commands.
* See `showToolbox` for the arguments' jsdoc.
*/
async _createToolbox(descriptor, toolId, hostType, hostOptions) {
const manager = new ToolboxHostManager(descriptor, hostType, hostOptions);
async _createToolbox(commands, toolId, hostType, hostOptions) {
const manager = new ToolboxHostManager(commands, hostType, hostOptions);
const toolbox = await manager.create(toolId);
this._toolboxes.set(descriptor, toolbox);
this._toolboxes.set(commands, toolbox);
this.emit("toolbox-created", toolbox);
@ -733,7 +730,7 @@ DevTools.prototype = {
});
toolbox.once("destroyed", () => {
this._toolboxes.delete(descriptor);
this._toolboxes.delete(commands);
this.emit("toolbox-destroyed", toolbox);
});
@ -744,16 +741,29 @@ DevTools.prototype = {
},
/**
* Return the toolbox for a given descriptor.
* Return the toolbox for a given commands object.
*
* @param {Descriptor} descriptor
* Target descriptor that owns this toolbox
* @param {Commands Object} commands
* Debugging context commands that owns this toolbox
*
* @return {Toolbox} toolbox
* The toolbox that is debugging the given target descriptor
* The toolbox that is debugging the given context designated by the commands
*/
getToolboxForDescriptor(descriptor) {
return this._toolboxes.get(descriptor);
getToolboxForCommands(commands) {
return this._toolboxes.get(commands);
},
/**
* TabDescriptorFront requires a synchronous method and don't have a reference to its
* related commands object. So expose something handcrafted just for this.
*/
getToolboxForDescriptorFront(descriptorFront) {
for (const [commands, toolbox] of this._toolboxes) {
if (commands.descriptorFront == descriptorFront) {
return toolbox;
}
}
return null;
},
/**
@ -762,7 +772,7 @@ DevTools.prototype = {
*/
async getToolboxForTab(tab) {
const commands = await LocalTabCommandsFactory.getCommandsForTab(tab);
return commands && this.getToolboxForDescriptor(commands.descriptorFront);
return this.getToolboxForCommands(commands);
},
/**
@ -774,10 +784,10 @@ DevTools.prototype = {
*/
async closeToolboxForTab(tab) {
const commands = await LocalTabCommandsFactory.getCommandsForTab(tab);
const descriptor = commands.descriptorFront;
let toolbox = await this._creatingToolboxes.get(descriptor);
let toolbox = await this._creatingToolboxes.get(commands);
if (!toolbox) {
toolbox = this._toolboxes.get(descriptor);
toolbox = this._toolboxes.get(commands);
}
if (!toolbox) {
return;

Просмотреть файл

@ -173,7 +173,6 @@ skip-if =
os == "win" && !debug # bug 1683265
[browser_toolbox_zoom_popup.js]
fail-if = a11y_checks # bug 1687737 tools-chevron-menu-button is not accessible
[browser_two_tabs.js]
[browser_webextension_descriptor.js]
[browser_webextension_dropdown.js]
skip-if =

Просмотреть файл

@ -1,110 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Check regression when opening two tabs
*/
var { DevToolsServer } = require("devtools/server/devtools-server");
var { DevToolsClient } = require("devtools/client/devtools-client");
const { createCommandsDictionary } = require("devtools/shared/commands/index");
const TAB_URL_1 = "data:text/html;charset=utf-8,foo";
const TAB_URL_2 = "data:text/html;charset=utf-8,bar";
add_task(async () => {
DevToolsServer.init();
DevToolsServer.registerAllActors();
const tab1 = await addTab(TAB_URL_1);
const tab2 = await addTab(TAB_URL_2);
// Connect to devtools server to fetch the two target actors for each tab
const client = new DevToolsClient(DevToolsServer.connectPipe());
await client.connect();
const tabDescriptors = await client.mainRoot.listTabs();
await Promise.all(
tabDescriptors.map(async descriptor => {
const commands = await createCommandsDictionary(descriptor);
// Descriptor's getTarget will only work if the TargetCommand watches for the first top target
await commands.targetCommand.startListening();
})
);
const tabs = await Promise.all(tabDescriptors.map(d => d.getTarget()));
const targetFront1 = tabs.find(a => a.url === TAB_URL_1);
const targetFront2 = tabs.find(a => a.url === TAB_URL_2);
await checkGetTab(client, tab1, tab2, targetFront1, targetFront2);
await checkGetTabFailures(client);
await checkSelectedTargetActor(targetFront2);
await removeTab(tab2);
await checkFirstTargetActor(targetFront1);
await removeTab(tab1);
await client.close();
});
async function checkGetTab(client, tab1, tab2, targetFront1, targetFront2) {
let front = await getTabTarget(client, { tab: tab1 });
is(targetFront1, front, "getTab returns the same target form for first tab");
front = await getTabTarget(client, {
browserId: tab1.linkedBrowser.browserId,
});
is(
targetFront1,
front,
"getTab returns the same target form when filtering by browserId"
);
front = await getTabTarget(client, { tab: tab2 });
is(targetFront2, front, "getTab returns the same target form for second tab");
}
async function checkGetTabFailures(client) {
try {
await getTabTarget(client, { browserId: -999 });
ok(false, "getTab unexpectedly succeed with a wrong browserId");
} catch (error) {
is(
error.message,
"Protocol error (noTab): Unable to find tab with browserId '-999' (no browsing-context) from: " +
client.mainRoot.actorID
);
}
}
async function checkSelectedTargetActor(targetFront2) {
// Send a naive request to the second target actor to check if it works
const consoleFront = await targetFront2.getFront("console");
const response = await consoleFront.startListeners([]);
ok(
"startedListeners" in response,
"Actor from the selected tab should respond to the request."
);
}
async function checkFirstTargetActor(targetFront1) {
// then send a request to the first target actor to check if it still works
const consoleFront = await targetFront1.getFront("console");
const response = await consoleFront.startListeners([]);
ok(
"startedListeners" in response,
"Actor from the first tab should still respond."
);
}
async function getTabTarget(client, filter) {
let commands;
if (filter.tab) {
commands = await CommandsFactory.forTab(filter.tab, { client });
} else if (filter.browserId) {
commands = await CommandsFactory.forRemoteTab(filter.browserId, { client });
}
await commands.targetCommand.startListening();
// By default, commands will close the client when the tab is closed.
// Disable this default behavior for this test.
// Bug 1698890: The test should probably stop assuming this.
commands.shouldCloseClient = false;
return commands.descriptorFront.getTarget();
}

Просмотреть файл

@ -55,12 +55,12 @@ const LAST_HOST = "devtools.toolbox.host";
const PREVIOUS_HOST = "devtools.toolbox.previousHost";
let ID_COUNTER = 1;
function ToolboxHostManager(descriptor, hostType, hostOptions) {
this.descriptor = descriptor;
function ToolboxHostManager(commands, hostType, hostOptions) {
this.commands = commands;
// When debugging a local tab, we keep a reference of the current tab into which the toolbox is displayed.
// This will only change from the descriptor's localTab when we start debugging popups (i.e. window.open).
this.currentTab = this.descriptor.localTab;
this.currentTab = this.commands.descriptorFront.localTab;
// Keep the previously instantiated Host for all tabs where we displayed the Toolbox.
// This will only be useful when we start debugging popups (i.e. window.open).
@ -107,7 +107,7 @@ ToolboxHostManager.prototype = {
10
);
const toolbox = new Toolbox(
this.descriptor,
this.commands,
toolId,
this.host.type,
this.host.frame.contentWindow,
@ -206,7 +206,7 @@ ToolboxHostManager.prototype = {
this.hostPerTab.clear();
this.host = null;
this.hostType = null;
this.descriptor = null;
this.commands = null;
},
/**

Просмотреть файл

@ -94,16 +94,15 @@ async function initToolbox(url, host) {
try {
const commands = await commandsFromURL(url);
const descriptor = commands.descriptorFront;
const toolbox = gDevTools.getToolboxForDescriptor(descriptor);
const toolbox = gDevTools.getToolboxForCommands(commands);
if (toolbox && toolbox.isDestroying()) {
// If a toolbox already exists for the descriptor, wait for current
// If a toolbox already exists for the commands, wait for current
// toolbox destroy to be finished.
await toolbox.destroy();
}
// Display an error page if we are connected to a remote target and we lose it
descriptor.once("descriptor-destroyed", function() {
commands.descriptorFront.once("descriptor-destroyed", function() {
// Prevent trying to display the error page if the toolbox tab is being destroyed
if (host.contentDocument) {
const error = new Error("Debug target was disconnected");
@ -112,7 +111,7 @@ async function initToolbox(url, host) {
});
const options = { customIframe: host };
await gDevTools.showToolbox(descriptor, {
await gDevTools.showToolbox(commands, {
toolId: tool,
hostType: Toolbox.HostType.PAGE,
hostOptions: options,

Просмотреть файл

@ -42,8 +42,6 @@ var Startup = Cc["@mozilla.org/devtools/startup-clh;1"].getService(
Ci.nsISupports
).wrappedJSObject;
const { createCommandsDictionary } = require("devtools/shared/commands/index");
const { BrowserLoader } = ChromeUtils.import(
"resource://devtools/shared/loader/browser-loader.js"
);
@ -205,8 +203,8 @@ const DEVTOOLS_F12_DISABLED_PREF = "devtools.experiment.f12.shortcut_disabled";
* target. Visually, it's a document that includes the tools tabs and all
* the iframes where the tool panels will be living in.
*
* @param {object} descriptorFront
* The context to inspect identified by this descriptor.
* @param {object} commands
* The context to inspect identified by this commands.
* @param {string} selectedTool
* Tool to select initially
* @param {Toolbox.HostType} hostType
@ -221,7 +219,7 @@ const DEVTOOLS_F12_DISABLED_PREF = "devtools.experiment.f12.shortcut_disabled";
* timestamps (unaffected by system clock changes).
*/
function Toolbox(
descriptorFront,
commands,
selectedTool,
hostType,
contentWindow,
@ -233,7 +231,12 @@ function Toolbox(
this.selection = new Selection();
this.telemetry = new Telemetry();
this._descriptorFront = descriptorFront;
// This attribute is meant to be a public attribute on the Toolbox object
// It exposes commands modules listed in devtools/shared/commands/index.js
// which are an abstraction on top of RDP methods.
// See devtools/shared/commands/README.md
this.commands = commands;
this._descriptorFront = commands.descriptorFront;
// The session ID is used to determine which telemetry events belong to which
// toolbox session. Because we use Amplitude to analyse the telemetry data we
@ -845,13 +848,6 @@ Toolbox.prototype = {
);
});
// This attribute is meant to be a public attribute on the Toolbox object
// It exposes commands modules listed in devtools/shared/commands/index.js
// which are an abstraction on top of RDP methods.
// See devtools/shared/commands/README.md
// Bug 1700909 will make the commands be instantiated by gDevTools instead of the Toolbox.
this.commands = await createCommandsDictionary(this._descriptorFront);
this.commands.targetCommand.on(
"target-thread-wrong-order-on-resume",
this._onTargetThreadFrontResumeWrongOrder.bind(this)
@ -4235,17 +4231,17 @@ Toolbox.prototype = {
// Notify toolbox-host-manager that the host can be destroyed.
this.emit("toolbox-unload");
// targetCommand need to be notified that the toolbox is being torn down.
// All Commands need to be destroyed.
// This is done after other destruction tasks since it may tear down
// fronts and the debugger transport which earlier destroy methods may
// require to complete.
// (i.e. avoid exceptions about closing connection with pending requests)
//
// For similar reasons, only destroy the target-list after every
// For similar reasons, only destroy the TargetCommand after every
// other outstanding cleanup is done. Destroying the target list
// will lead to destroy frame targets which can temporarily make
// some fronts unresponsive and block the cleanup.
this.commands.targetCommand.destroy();
return this._descriptorFront.destroy();
return this.commands.destroy();
}, console.error)
.then(() => {
this.emit("destroyed");

Просмотреть файл

@ -308,7 +308,7 @@ class TabDescriptorFront extends DescriptorMixin(
// Always destroy the toolbox opened for this local tab descriptor.
// When the toolbox is in a Window Host, it won't be removed from the
// DOM when the tab is closed.
const toolbox = gDevTools.getToolboxForDescriptor(this);
const toolbox = gDevTools.getToolboxForDescriptorFront(this);
if (toolbox) {
// Toolbox.destroy will call target.destroy eventually.
await toolbox.destroy();

Просмотреть файл

@ -43,7 +43,10 @@ add_task(async function() {
const menuList = toolbox.doc.getElementById("toolbox-frame-menu");
const frames = Array.from(menuList.querySelectorAll(".command"));
const onNewRoot = inspector.once("new-root");
// Wait for the inspector to be reloaded
// (instead of only new-root) in order to wait for full
// async update of the inspector.
const onNewRoot = inspector.once("reloaded");
frames[1].click();
await onNewRoot;

Просмотреть файл

@ -21,7 +21,7 @@ add_task(async function() {
const commands = CommandsFactory.forLocalTabWorker(tab, WORKER_URL);
const workerDescriptorFront = commands.descriptorFront;
const toolbox = await gDevTools.showToolbox(workerDescriptorFront, {
const toolbox = await gDevTools.showToolbox(commands, {
toolId: "jsdebugger",
hostType: Toolbox.HostType.WINDOW,
});

Просмотреть файл

@ -169,7 +169,7 @@ async function initWorkerDebugger(TAB_URL, WORKER_URL) {
const target = workerDescriptorFront.parentFront;
const client = commands.client;
const toolbox = await gDevTools.showToolbox(workerDescriptorFront, {
const toolbox = await gDevTools.showToolbox(commands, {
toolId: "jsdebugger",
hostType: Toolbox.HostType.WINDOW,
});

Просмотреть файл

@ -18,10 +18,9 @@ const { Toolbox } = require("devtools/client/framework/toolbox");
*/
async function setupExtensionDebuggingToolbox(id) {
const commands = await CommandsFactory.forAddon(id);
const descriptor = commands.descriptorFront;
const { toolbox, storage } = await openStoragePanel({
descriptor,
commands,
hostType: Toolbox.HostType.WINDOW,
});

Просмотреть файл

@ -152,18 +152,17 @@ async function openTabAndSetupStorage(url, options = {}) {
* Open the toolbox, with the storage tool visible.
*
* @param tab {XULTab} Optional, the tab for the toolbox; defaults to selected tab
* @param descriptor {Object} Optional, the descriptor for the toolbox; defaults to a tab descriptor
* @param commands {Object} Optional, the commands for the toolbox; defaults to a tab commands
* @param hostType {Toolbox.HostType} Optional, type of host that will host the toolbox
*
* @return {Promise} a promise that resolves when the storage inspector is ready
*/
var openStoragePanel = async function({ tab, descriptor, hostType } = {}) {
var openStoragePanel = async function({ tab, commands, hostType } = {}) {
info("Opening the storage inspector");
if (!descriptor) {
const commands = await LocalTabCommandsFactory.createCommandsForTab(
if (!commands) {
commands = await LocalTabCommandsFactory.createCommandsForTab(
tab || gBrowser.selectedTab
);
descriptor = commands.descriptorFront;
}
let storage, toolbox;
@ -171,7 +170,7 @@ var openStoragePanel = async function({ tab, descriptor, hostType } = {}) {
// Checking if the toolbox and the storage are already loaded
// The storage-updated event should only be waited for if the storage
// isn't loaded yet
toolbox = gDevTools.getToolboxForDescriptor(descriptor);
toolbox = gDevTools.getToolboxForCommands(commands);
if (toolbox) {
storage = toolbox.getPanel("storage");
if (storage) {
@ -188,7 +187,7 @@ var openStoragePanel = async function({ tab, descriptor, hostType } = {}) {
}
info("Opening the toolbox");
toolbox = await gDevTools.showToolbox(descriptor, {
toolbox = await gDevTools.showToolbox(commands, {
toolId: "storage",
hostType,
});

Просмотреть файл

@ -66,12 +66,12 @@ Methods
- ``theme {ThemeDefinition|String}`` - The theme definition object or the theme identifier.
``showToolbox(target [, toolId [, hostType [, hostOptions]]])``
``showToolbox(commands[, toolId [, hostType [, hostOptions]]])``
Opens a toolbox for given target either by creating a new one or activating an existing one.
**Parameters:**
- ``target {Target}`` - The target the toolbox will debug.
- ``commands {Object}`` - The commands object designating which debugging context the toolbox will debug.
- ``toolId {String}`` - The tool that should be activated. If unspecified the previously active tool is shown.
- ``hostType {String}`` - The position the toolbox will be placed. One of ``bottom``, ``side``, ``window``, ``custom``. See :ref:`HostType <devtoolsapi-host-type>` for details.
- ``hostOptions {Object}`` - An options object passed to the selected host. See :ref:`HostType <devtoolsapi-host-type>` for details.

Просмотреть файл

@ -513,15 +513,16 @@ exports.WatcherActor = protocol.ActorClassWithSpec(watcherSpec, {
* We will eventually get rid of this code once all targets are properly supported by
* the Watcher Actor and we have target helpers for all of them.
*/
const frameResourceTypes = Resources.getResourceTypesForTargetType(
resourceTypes,
Targets.TYPES.FRAME
);
if (frameResourceTypes.length) {
const targetActor = this._getTargetActorInParentProcess();
if (targetActor) {
await targetActor.addSessionDataEntry("resources", frameResourceTypes);
}
const targetActorResourceTypes = Resources.getResourceTypesForTargetType(
resourceTypes,
targetActor.targetType
);
await targetActor.addSessionDataEntry(
"resources",
targetActorResourceTypes
);
}
},
@ -583,15 +584,13 @@ exports.WatcherActor = protocol.ActorClassWithSpec(watcherSpec, {
}
// See comment in watchResources.
const frameResourceTypes = Resources.getResourceTypesForTargetType(
resourceTypes,
Targets.TYPES.FRAME
);
if (frameResourceTypes.length) {
const targetActor = this._getTargetActorInParentProcess();
if (targetActor) {
targetActor.removeSessionDataEntry("resources", frameResourceTypes);
}
const targetActorResourceTypes = Resources.getResourceTypesForTargetType(
resourceTypes,
targetActor.targetType
);
targetActor.removeSessionDataEntry("resources", targetActorResourceTypes);
}
// Unregister the JS Window Actor if there is no more DevTools code observing any target/resource