Skip to content

Commit

Permalink
inspector: add NodeRuntime.waitingForDebugger event
Browse files Browse the repository at this point in the history
`NodeRuntime.waitingForDebugger` is a new Inspector Protocol event that
will fire when the process being inspected is waiting for the debugger
(for example, when `inspector.waitForDebugger()` is called). This allows
inspecting processes to know when the inspected process is waiting for a
`Runtime.runIfWaitingForDebugger` message to resume execution. It allows
tooling to resume execution of the inspected process as soon as it deems
necessary, without having to guess if the inspected process is waiting
or not, making the workflow more deterministic. With a more
deterministic workflow, it is possible to update Node.js core tests to
avoid race conditions that can cause flakiness. Therefore, tests were
also changed as following:

  * Remove no-op Runtime.runIfWaitingForDebugger from tests that don't
    need it
  * Use NodeRuntime.waitingForDebugger in all tests that need
    Runtime.runIfWaitingForDebugger, to ensure order of operations is
    predictable and correct
  * Simplify test-inspector-multisession-ws

There might be value in adding `NodeWorker.waitingForDebugger` in a
future patch, but as of right now, no Node.js core inspector tests using
worker threads are not failing due to race conditions.

Fixes: #34730
PR-URL: #51560
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
  • Loading branch information
mmarchini authored and richardlau committed Mar 25, 2024
1 parent 06a29f8 commit 3fe59ba
Show file tree
Hide file tree
Showing 25 changed files with 160 additions and 28 deletions.
10 changes: 10 additions & 0 deletions src/inspector/node_protocol.pdl
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ experimental domain NodeWorker

# Support for inspecting node process state.
experimental domain NodeRuntime
# Enable the NodeRuntime events except by `NodeRuntime.waitingForDisconnect`.
command enable

# Disable NodeRuntime events
command disable

# Enable the `NodeRuntime.waitingForDisconnect`.
command notifyWhenWaitingForDisconnect
parameters
Expand All @@ -110,3 +116,7 @@ experimental domain NodeRuntime
# It is fired when the Node process finished all code execution and is
# waiting for all frontends to disconnect.
event waitingForDisconnect

# This event is fired when the runtime is waiting for the debugger. For
# example, when inspector.waitingForDebugger is called
event waitingForDebugger
28 changes: 27 additions & 1 deletion src/inspector/runtime_agent.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ namespace inspector {
namespace protocol {

RuntimeAgent::RuntimeAgent()
: notify_when_waiting_for_disconnect_(false) {}
: notify_when_waiting_for_disconnect_(false),
enabled_(false),
is_waiting_for_debugger_(false) {}

void RuntimeAgent::Wire(UberDispatcher* dispatcher) {
frontend_ = std::make_unique<NodeRuntime::Frontend>(dispatcher->channel());
Expand All @@ -20,6 +22,30 @@ DispatchResponse RuntimeAgent::notifyWhenWaitingForDisconnect(bool enabled) {
return DispatchResponse::OK();
}

DispatchResponse RuntimeAgent::enable() {
enabled_ = true;
if (is_waiting_for_debugger_) {
frontend_->waitingForDebugger();
}
return DispatchResponse::OK();
}

DispatchResponse RuntimeAgent::disable() {
enabled_ = false;
return DispatchResponse::OK();
}

void RuntimeAgent::setWaitingForDebugger() {
is_waiting_for_debugger_ = true;
if (enabled_) {
frontend_->waitingForDebugger();
}
}

void RuntimeAgent::unsetWaitingForDebugger() {
is_waiting_for_debugger_ = false;
}

bool RuntimeAgent::notifyWaitingForDisconnect() {
if (notify_when_waiting_for_disconnect_) {
frontend_->waitingForDisconnect();
Expand Down
10 changes: 10 additions & 0 deletions src/inspector/runtime_agent.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ class RuntimeAgent : public NodeRuntime::Backend {

DispatchResponse notifyWhenWaitingForDisconnect(bool enabled) override;

DispatchResponse enable() override;

DispatchResponse disable() override;

bool notifyWaitingForDisconnect();

void setWaitingForDebugger();

void unsetWaitingForDebugger();

private:
std::shared_ptr<NodeRuntime::Frontend> frontend_;
bool notify_when_waiting_for_disconnect_;
bool enabled_;
bool is_waiting_for_debugger_;
};
} // namespace protocol
} // namespace inspector
Expand Down
13 changes: 13 additions & 0 deletions src/inspector_agent.cc
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
return retaining_context_;
}

void setWaitingForDebugger() { runtime_agent_->setWaitingForDebugger(); }

void unsetWaitingForDebugger() { runtime_agent_->unsetWaitingForDebugger(); }

bool retainingContext() {
return retaining_context_;
}
Expand Down Expand Up @@ -418,6 +422,9 @@ class NodeInspectorClient : public V8InspectorClient {

void waitForFrontend() {
waiting_for_frontend_ = true;
for (const auto& id_channel : channels_) {
id_channel.second->setWaitingForDebugger();
}
runMessageLoop();
}

Expand Down Expand Up @@ -465,6 +472,9 @@ class NodeInspectorClient : public V8InspectorClient {

void runIfWaitingForDebugger(int context_group_id) override {
waiting_for_frontend_ = false;
for (const auto& id_channel : channels_) {
id_channel.second->unsetWaitingForDebugger();
}
}

int connectFrontend(std::unique_ptr<InspectorSessionDelegate> delegate,
Expand All @@ -476,6 +486,9 @@ class NodeInspectorClient : public V8InspectorClient {
std::move(delegate),
getThreadHandle(),
prevent_shutdown);
if (waiting_for_frontend_) {
channels_[session_id]->setWaitingForDebugger();
}
return session_id;
}

Expand Down
3 changes: 3 additions & 0 deletions test/common/inspector-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ class InspectorSession {
console.log('[test]', 'Verify node waits for the frontend to disconnect');
await this.send({ 'method': 'Debugger.resume' });
await this.waitForNotification((notification) => {
if (notification.method === 'Debugger.paused') {
this.send({ 'method': 'Debugger.resume' });
}
return notification.method === 'Runtime.executionContextDestroyed' &&
notification.params.executionContextId === 1;
});
Expand Down
1 change: 0 additions & 1 deletion test/parallel/test-inspect-async-hook-setup-at-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ async function runTests() {
'params': { 'maxDepth': 10 } },
{ 'method': 'Debugger.setBlackboxPatterns',
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' },
]);

await waitForInitialSetup(session);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ async function checkAsyncStackTrace(session) {
async function runTests() {
const instance = new NodeInstance(undefined, script);
const session = await instance.connectInspectorSession();
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send([
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
Expand All @@ -39,6 +41,7 @@ async function runTests() {
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' },
]);
await session.send({ method: 'NodeRuntime.disable' });
await skipBreakpointAtStart(session);
await checkAsyncStackTrace(session);

Expand Down
1 change: 0 additions & 1 deletion test/parallel/test-inspector-async-hook-setup-at-signal.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ async function runTests() {
'params': { 'maxDepth': 10 } },
{ 'method': 'Debugger.setBlackboxPatterns',
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' },
]);

await waitForInitialSetup(session);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ function runTest() {
async function runTests() {
const instance = new NodeInstance(undefined, script);
const session = await instance.connectInspectorSession();
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send([
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
Expand All @@ -29,6 +31,7 @@ async function runTests() {
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' },
]);
session.send({ method: 'NodeRuntime.disable' });

await session.waitForBreakOnLine(0, '[eval]');
await session.send({ 'method': 'Debugger.resume' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ async function checkAsyncStackTrace(session) {
async function runTests() {
const instance = new NodeInstance(undefined, script);
const session = await instance.connectInspectorSession();
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send([
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
Expand All @@ -35,6 +37,7 @@ async function runTests() {
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' },
]);
await session.send({ method: 'NodeRuntime.disable' });

await skipFirstBreakpoint(session);
await checkAsyncStackTrace(session);
Expand Down
3 changes: 3 additions & 0 deletions test/parallel/test-inspector-break-e.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ const { NodeInstance } = require('../common/inspector-helper.js');
async function runTests() {
const instance = new NodeInstance(undefined, 'console.log(10)');
const session = await instance.connectInspectorSession();
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send([
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
{ 'method': 'Runtime.runIfWaitingForDebugger' },
]);
await session.send({ method: 'NodeRuntime.disable' });
await session.waitForBreakOnLine(0, '[eval]');
await session.runToCompletion();
assert.strictEqual((await instance.expectShutdown()).exitCode, 0);
Expand Down
5 changes: 4 additions & 1 deletion test/parallel/test-inspector-break-when-eval.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ async function setupDebugger(session) {
'params': { 'maxDepth': 0 } },
{ 'method': 'Runtime.runIfWaitingForDebugger' },
];
session.send(commands);
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send(commands);
await session.send({ method: 'NodeRuntime.disable' });

await session.waitForNotification('Debugger.paused', 'Initial pause');

Expand Down
5 changes: 4 additions & 1 deletion test/parallel/test-inspector-console.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ async function runTest() {
{ 'method': 'Runtime.runIfWaitingForDebugger' },
];

session.send(commands);
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send(commands);
await session.send({ method: 'NodeRuntime.disable' });

const msg = await session.waitForNotification('Runtime.consoleAPICalled');

Expand Down
5 changes: 4 additions & 1 deletion test/parallel/test-inspector-debug-brk-flag.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ async function testBreakpointOnStart(session) {
{ 'method': 'Runtime.runIfWaitingForDebugger' },
];

session.send(commands);
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send(commands);
await session.send({ method: 'NodeRuntime.disable' });
await session.waitForBreakOnLine(0, session.scriptURL());
}

Expand Down
2 changes: 2 additions & 0 deletions test/parallel/test-inspector-debug-end.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ async function testSessionNoCrash() {

const instance = new NodeInstance('--inspect-brk=0', script);
const session = await instance.connectInspectorSession();
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send({ 'method': 'Runtime.runIfWaitingForDebugger' });
await session.waitForServerDisconnect();
strictEqual((await instance.expectShutdown()).exitCode, 42);
Expand Down
3 changes: 3 additions & 0 deletions test/parallel/test-inspector-esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ async function testBreakpointOnStart(session) {
{ 'method': 'Runtime.runIfWaitingForDebugger' },
];

await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send(commands);
await session.send({ method: 'NodeRuntime.disable' });
await session.waitForBreakOnLine(
0, UrlResolve(session.scriptURL().toString(), 'message.mjs'));
}
Expand Down
3 changes: 3 additions & 0 deletions test/parallel/test-inspector-exception.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ async function testBreakpointOnStart(session) {
{ 'method': 'Runtime.runIfWaitingForDebugger' },
];

await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send(commands);
await session.send({ method: 'NodeRuntime.disable' });
await session.waitForBreakOnLine(21, pathToFileURL(script).toString());
}

Expand Down
3 changes: 3 additions & 0 deletions test/parallel/test-inspector-inspect-brk-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ const { NodeInstance } = require('../common/inspector-helper.js');
async function runTest() {
const child = new NodeInstance(['--inspect-brk-node=0', '-p', '42']);
const session = await child.connectInspectorSession();
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send({ method: 'Runtime.enable' });
await session.send({ method: 'Debugger.enable' });
await session.send({ method: 'Runtime.runIfWaitingForDebugger' });
await session.send({ method: 'NodeRuntime.disable' });
await session.waitForNotification((notification) => {
// The main assertion here is that we do hit the loader script first.
return notification.method === 'Debugger.scriptParsed' &&
Expand Down
22 changes: 15 additions & 7 deletions test/parallel/test-inspector-multisession-ws.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ session.on('Debugger.paused', () => {
session.connect();
session.post('Debugger.enable');
console.log('Ready');
console.log('Ready');
`;

async function setupSession(node) {
const session = await node.connectInspectorSession();
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send([
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
Expand All @@ -37,20 +38,19 @@ async function setupSession(node) {
'params': { 'interval': 100 } },
{ 'method': 'Debugger.setBlackboxPatterns',
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' },
]);

return session;
}

async function testSuspend(sessionA, sessionB) {
console.log('[test]', 'Breaking in code and verifying events are fired');
await sessionA.waitForNotification('Debugger.paused', 'Initial pause');
await Promise.all([
sessionA.waitForNotification('Debugger.paused', 'Initial sessionA paused'),
sessionB.waitForNotification('Debugger.paused', 'Initial sessionB paused'),
]);
sessionA.send({ 'method': 'Debugger.resume' });

await sessionA.waitForNotification('Runtime.consoleAPICalled',
'Console output');
// NOTE(mmarchini): Remove second console.log when
// https://bugs.chromium.org/p/v8/issues/detail?id=10287 is fixed.
await sessionA.waitForNotification('Runtime.consoleAPICalled',
'Console output');
sessionA.send({ 'method': 'Debugger.pause' });
Expand All @@ -65,6 +65,14 @@ async function runTest() {

const [session1, session2] =
await Promise.all([setupSession(child), setupSession(child)]);
await Promise.all([
session1.send({ method: 'Runtime.runIfWaitingForDebugger' }),
session2.send({ method: 'Runtime.runIfWaitingForDebugger' }),
]);
await Promise.all([
session1.send({ method: 'NodeRuntime.disable' }),
session2.send({ method: 'NodeRuntime.disable' }),
]);
await testSuspend(session2, session1);
console.log('[test]', 'Should shut down after both sessions disconnect');

Expand Down
11 changes: 7 additions & 4 deletions test/parallel/test-inspector-scriptparsed-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ async function runTests() {
const instance = new NodeInstance(['--inspect-brk=0', '--expose-internals'],
script);
const session = await instance.connectInspectorSession();
await session.send([
{ 'method': 'Debugger.enable' },
{ 'method': 'Runtime.runIfWaitingForDebugger' },
]);

await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send({ 'method': 'Debugger.enable' });
await session.send({ method: 'Runtime.runIfWaitingForDebugger' });
await session.send({ method: 'NodeRuntime.disable' });

await session.waitForBreakOnLine(2, '[eval]');

await session.send({ 'method': 'Runtime.enable' });
Expand Down
11 changes: 7 additions & 4 deletions test/parallel/test-inspector-stop-profile-after-done.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ async function runTests() {
}, ${common.platformTimeout(30)});`);
const session = await child.connectInspectorSession();

session.send([
await session.send({ method: 'NodeRuntime.enable' });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send([
{ method: 'Profiler.setSamplingInterval',
params: { interval: common.platformTimeout(300) } },
{ method: 'Profiler.enable' },
{ method: 'Runtime.runIfWaitingForDebugger' },
{ method: 'Profiler.start' }]);
{ method: 'Profiler.enable' }]);
await session.send({ method: 'Runtime.runIfWaitingForDebugger' });
await session.send({ method: 'NodeRuntime.disable' });
await session.send({ method: 'Profiler.start' });
while (await child.nextStderrString() !==
'Waiting for the debugger to disconnect...');
await session.send({ method: 'Profiler.stop' });
Expand Down

0 comments on commit 3fe59ba

Please sign in to comment.