inspector: support for worker inspection in chrome devtools

Fixes: https://github.com/nodejs/node/issues/56343
PR-URL: https://github.com/nodejs/node/pull/56759
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Shima Ryuhei 2025-05-12 22:25:00 +09:00 committed by GitHub
parent cfd2021c35
commit 2281a04e5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 443 additions and 23 deletions

View file

@ -1205,6 +1205,17 @@ added: v22.4.0
Enable experimental [`Web Storage`][] support.
### `--experimental-worker-inspection`
<!-- YAML
added:
- REPLACEME
-->
> Stability: 1.1 - Active Development
Enable experimental support for the worker inspection with Chrome DevTools.
### `--expose-gc`
<!-- YAML

View file

@ -57,6 +57,10 @@ class MainThreadHandle : public std::enable_shared_from_this<MainThreadHandle> {
std::unique_ptr<InspectorSessionDelegate> MakeDelegateThreadSafe(
std::unique_ptr<InspectorSessionDelegate> delegate);
bool Expired();
void SetTargetSessionId(int target_session_id) {
target_session_id_ = target_session_id;
}
std::optional<int> GetTargetSessionId() { return target_session_id_; }
private:
void Reset();
@ -65,6 +69,7 @@ class MainThreadHandle : public std::enable_shared_from_this<MainThreadHandle> {
Mutex block_lock_;
int next_session_id_ = 0;
std::atomic_int next_object_id_ = {1};
std::optional<int> target_session_id_ = std::nullopt;
friend class MainThreadInterface;
};

View file

@ -32,6 +32,8 @@
'src/inspector/network_inspector.h',
'src/inspector/network_agent.cc',
'src/inspector/network_agent.h',
'src/inspector/target_agent.cc',
'src/inspector/target_agent.h',
'src/inspector/worker_inspector.cc',
'src/inspector/worker_inspector.h',
],
@ -47,6 +49,8 @@
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeRuntime.h',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Network.cpp',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Network.h',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Target.cpp',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Target.h',
],
'node_protocol_files': [
'<(protocol_tool_path)/lib/Forward_h.template',

View file

@ -249,3 +249,28 @@ experimental domain NodeRuntime
# This event is fired when the runtime is waiting for the debugger. For
# example, when inspector.waitingForDebugger is called
event waitingForDebugger
# https://chromedevtools.github.io/devtools-protocol/1-3/Target/
experimental domain Target
type SessionID extends string
type TargetID extends string
type TargetInfo extends object
properties
TargetID targetId
string type
string title
string url
boolean attached
boolean canAccessOpener
event targetCreated
parameters
TargetInfo targetInfo
event attachedToTarget
parameters
SessionID sessionId
TargetInfo targetInfo
boolean waitingForDebugger
command setAutoAttach
parameters
boolean autoAttach
boolean waitForDebuggerOnStart

View file

@ -0,0 +1,136 @@
#include "target_agent.h"
#include <string_view>
#include "crdtp/dispatch.h"
#include "inspector/worker_inspector.h"
#include "main_thread_interface.h"
namespace node {
namespace inspector {
namespace protocol {
std::unordered_map<int, std::shared_ptr<MainThreadHandle>>
TargetAgent::target_session_id_worker_map_ =
std::unordered_map<int, std::shared_ptr<MainThreadHandle>>();
int TargetAgent::next_session_id_ = 1;
class WorkerTargetDelegate : public WorkerDelegate {
public:
explicit WorkerTargetDelegate(std::shared_ptr<TargetAgent> target_agent)
: target_agent_(target_agent) {}
void WorkerCreated(const std::string& title,
const std::string& url,
bool waiting,
std::shared_ptr<MainThreadHandle> worker) override {
target_agent_->createAndAttachIfNecessary(worker, title, url);
}
private:
const std::shared_ptr<TargetAgent> target_agent_;
};
std::unique_ptr<Target::TargetInfo> createTargetInfo(
const std::string_view target_id,
const std::string_view type,
const std::string_view title,
const std::string_view url) {
return Target::TargetInfo::create()
.setTargetId(std::string(target_id))
.setType(std::string(type))
.setTitle(std::string(title))
.setUrl(std::string(url))
.setAttached(false)
.setCanAccessOpener(true)
.build();
}
void TargetAgent::Wire(UberDispatcher* dispatcher) {
frontend_ = std::make_unique<Target::Frontend>(dispatcher->channel());
Target::Dispatcher::wire(dispatcher, this);
}
void TargetAgent::createAndAttachIfNecessary(
std::shared_ptr<MainThreadHandle> worker,
const std::string& title,
const std::string& url) {
std::string target_id = std::to_string(getNextTargetId());
std::string type = "node_worker";
targetCreated(target_id, type, title, url);
bool attached = false;
if (auto_attach_) {
attached = true;
attachedToTarget(worker, target_id, type, title, url);
}
targets_.push_back({target_id, type, title, url, worker, attached});
}
void TargetAgent::listenWorker(std::weak_ptr<WorkerManager> worker_manager) {
auto manager = worker_manager.lock();
if (!manager) {
return;
}
std::unique_ptr<WorkerDelegate> delegate(
new WorkerTargetDelegate(shared_from_this()));
worker_event_handle_ = manager->SetAutoAttach(std::move(delegate));
}
void TargetAgent::reset() {
if (worker_event_handle_) {
worker_event_handle_.reset();
}
}
void TargetAgent::targetCreated(const std::string_view target_id,
const std::string_view type,
const std::string_view title,
const std::string_view url) {
frontend_->targetCreated(createTargetInfo(target_id, type, title, url));
}
int TargetAgent::getNextSessionId() {
return next_session_id_++;
}
int TargetAgent::getNextTargetId() {
return next_target_id_++;
}
void TargetAgent::attachedToTarget(std::shared_ptr<MainThreadHandle> worker,
const std::string& target_id,
const std::string& type,
const std::string& title,
const std::string& url) {
int session_id = getNextSessionId();
target_session_id_worker_map_[session_id] = worker;
worker->SetTargetSessionId(session_id);
frontend_->attachedToTarget(std::to_string(session_id),
createTargetInfo(target_id, type, title, url),
true);
}
// TODO(islandryu): Currently, setAutoAttach applies the main thread's value to
// all threads. Modify it to be managed per worker thread.
crdtp::DispatchResponse TargetAgent::setAutoAttach(
bool auto_attach, bool wait_for_debugger_on_start) {
auto_attach_ = auto_attach;
wait_for_debugger_on_start_ = wait_for_debugger_on_start;
if (auto_attach) {
for (auto& target : targets_) {
if (!target.attached) {
target.attached = true;
attachedToTarget(target.worker,
target.target_id,
target.type,
target.title,
target.url);
}
}
}
return DispatchResponse::Success();
}
} // namespace protocol
} // namespace inspector
} // namespace node

View file

@ -0,0 +1,75 @@
#ifndef SRC_INSPECTOR_TARGET_AGENT_H_
#define SRC_INSPECTOR_TARGET_AGENT_H_
#include <string_view>
#include <unordered_map>
#include <vector>
#include "inspector/worker_inspector.h"
#include "node/inspector/protocol/Target.h"
namespace node {
namespace inspector {
class TargetInspector;
namespace protocol {
struct TargetInfo {
std::string target_id;
std::string type;
std::string title;
std::string url;
std::shared_ptr<MainThreadHandle> worker;
bool attached;
};
class TargetAgent : public Target::Backend,
public std::enable_shared_from_this<TargetAgent> {
public:
void Wire(UberDispatcher* dispatcher);
void createAndAttachIfNecessary(std::shared_ptr<MainThreadHandle> worker,
const std::string& title,
const std::string& url);
DispatchResponse setAutoAttach(bool auto_attach,
bool wait_for_debugger_on_start) override;
void listenWorker(std::weak_ptr<WorkerManager> worker_manager);
void reset();
static std::unordered_map<int, std::shared_ptr<MainThreadHandle>>
target_session_id_worker_map_;
bool isThisThread(MainThreadHandle* worker) { return worker == main_thread_; }
private:
int getNextTargetId();
int getNextSessionId();
void targetCreated(const std::string_view target_id,
const std::string_view type,
const std::string_view title,
const std::string_view url);
void attachedToTarget(std::shared_ptr<MainThreadHandle> worker,
const std::string& target_id,
const std::string& type,
const std::string& title,
const std::string& url);
std::shared_ptr<Target::Frontend> frontend_;
std::weak_ptr<WorkerManager> worker_manager_;
static int next_session_id_;
int next_target_id_ = 1;
std::unique_ptr<WorkerManagerEventHandle> worker_event_handle_ = nullptr;
bool auto_attach_ = false;
// TODO(islandryu): If false, implement it so that each thread does not wait
// for the worker to execute.
bool wait_for_debugger_on_start_ = true;
std::vector<TargetInfo> targets_;
MainThreadHandle* main_thread_;
};
} // namespace protocol
} // namespace inspector
} // namespace node
#endif // SRC_INSPECTOR_TARGET_AGENT_H_

View file

@ -8,6 +8,7 @@
#include "inspector/node_string.h"
#include "inspector/protocol_helper.h"
#include "inspector/runtime_agent.h"
#include "inspector/target_agent.h"
#include "inspector/tracing_agent.h"
#include "inspector/worker_agent.h"
#include "inspector/worker_inspector.h"
@ -218,9 +219,11 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
const std::unique_ptr<V8Inspector>& inspector,
std::shared_ptr<WorkerManager> worker_manager,
std::unique_ptr<InspectorSessionDelegate> delegate,
std::shared_ptr<MainThreadHandle> main_thread_,
std::shared_ptr<MainThreadHandle> main_thread,
bool prevent_shutdown)
: delegate_(std::move(delegate)), prevent_shutdown_(prevent_shutdown),
: delegate_(std::move(delegate)),
main_thread_(main_thread),
prevent_shutdown_(prevent_shutdown),
retaining_context_(false) {
session_ = inspector->connect(CONTEXT_GROUP_ID,
this,
@ -239,6 +242,11 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
network_inspector_ =
std::make_unique<NetworkInspector>(env, inspector.get());
network_inspector_->Wire(node_dispatcher_.get());
if (env->options()->experimental_worker_inspection) {
target_agent_ = std::make_shared<protocol::TargetAgent>();
target_agent_->Wire(node_dispatcher_.get());
target_agent_->listenWorker(worker_manager);
}
}
~ChannelImpl() override {
@ -252,6 +260,9 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
runtime_agent_.reset(); // Dispose before the dispatchers
network_inspector_->Disable();
network_inspector_.reset(); // Dispose before the dispatchers
if (target_agent_) {
target_agent_->reset();
}
}
void emitNotificationFromBackend(v8::Local<v8::Context> context,
@ -334,6 +345,15 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
// crdtp::FrontendChannel
void FlushProtocolNotifications() override {}
std::string serializeToJSON(std::unique_ptr<Serializable> message) {
std::vector<uint8_t> cbor = message->Serialize();
std::string json;
crdtp::Status status = ConvertCBORToJSON(crdtp::SpanFrom(cbor), &json);
CHECK(status.ok());
USE(status);
return json;
}
void sendMessageToFrontend(const StringView& message) {
if (per_process::enabled_debug_list.enabled(
DebugCategory::INSPECTOR_SERVER)) {
@ -342,7 +362,18 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
"[inspector send] %s\n",
raw_message);
}
delegate_->SendMessageToFrontend(message);
std::optional<int> target_session_id = main_thread_->GetTargetSessionId();
if (target_session_id.has_value()) {
std::string raw_message = protocol::StringUtil::StringViewToUtf8(message);
std::unique_ptr<protocol::DictionaryValue> value =
protocol::DictionaryValue::cast(JsonUtil::parseJSON(raw_message));
std::string target_session_id_str = std::to_string(*target_session_id);
value->setString("sessionId", target_session_id_str);
std::string json = serializeToJSON(std::move(value));
delegate_->SendMessageToFrontend(Utf8ToStringView(json)->string());
} else {
delegate_->SendMessageToFrontend(message);
}
}
void sendMessageToFrontend(const std::string& message) {
@ -352,24 +383,14 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
// crdtp::FrontendChannel
void SendProtocolResponse(int callId,
std::unique_ptr<Serializable> message) override {
std::vector<uint8_t> cbor = message->Serialize();
std::string json;
crdtp::Status status = ConvertCBORToJSON(crdtp::SpanFrom(cbor), &json);
DCHECK(status.ok());
USE(status);
std::string json = serializeToJSON(std::move(message));
sendMessageToFrontend(json);
}
// crdtp::FrontendChannel
void SendProtocolNotification(
std::unique_ptr<Serializable> message) override {
std::vector<uint8_t> cbor = message->Serialize();
std::string json;
crdtp::Status status = ConvertCBORToJSON(crdtp::SpanFrom(cbor), &json);
DCHECK(status.ok());
USE(status);
std::string json = serializeToJSON(std::move(message));
sendMessageToFrontend(json);
}
@ -383,10 +404,12 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
std::unique_ptr<protocol::RuntimeAgent> runtime_agent_;
std::unique_ptr<protocol::TracingAgent> tracing_agent_;
std::unique_ptr<protocol::WorkerAgent> worker_agent_;
std::shared_ptr<protocol::TargetAgent> target_agent_;
std::unique_ptr<NetworkInspector> network_inspector_;
std::unique_ptr<InspectorSessionDelegate> delegate_;
std::unique_ptr<v8_inspector::V8InspectorSession> session_;
std::unique_ptr<UberDispatcher> node_dispatcher_;
std::shared_ptr<MainThreadHandle> main_thread_;
bool prevent_shutdown_;
bool retaining_context_;
};

View file

@ -4,7 +4,9 @@
#include "crypto/crypto_util.h"
#include "debug_utils-inl.h"
#include "inspector/main_thread_interface.h"
#include "inspector/node_json.h"
#include "inspector/node_string.h"
#include "inspector/target_agent.h"
#include "inspector_socket_server.h"
#include "ncrypto.h"
#include "node.h"
@ -218,6 +220,7 @@ class InspectorIoDelegate: public node::inspector::SocketServerDelegate {
void StartSession(int session_id, const std::string& target_id) override;
void MessageReceived(int session_id, const std::string& message) override;
void EndSession(int session_id) override;
std::optional<std::string> GetTargetSessionId(const std::string& message);
std::vector<std::string> GetTargetIds() override;
std::string GetTargetTitle(const std::string& id) override;
@ -342,20 +345,72 @@ InspectorIoDelegate::InspectorIoDelegate(
void InspectorIoDelegate::StartSession(int session_id,
const std::string& target_id) {
auto session = main_thread_->Connect(
std::unique_ptr<InspectorSessionDelegate>(
new IoSessionDelegate(request_queue_->handle(), session_id)), true);
if (session) {
sessions_[session_id] = std::move(session);
fprintf(stderr, "Debugger attached.\n");
fprintf(stderr, "Debugger attached.\n");
}
std::optional<std::string> InspectorIoDelegate::GetTargetSessionId(
const std::string& message) {
std::string_view view(message.data(), message.size());
std::unique_ptr<protocol::DictionaryValue> value =
protocol::DictionaryValue::cast(JsonUtil::parseJSON(view));
protocol::String target_session_id;
protocol::Value* target_session_id_value = value->get("sessionId");
if (target_session_id_value) {
target_session_id_value->asString(&target_session_id);
}
if (!target_session_id.empty()) {
return target_session_id;
}
return std::nullopt;
}
void InspectorIoDelegate::MessageReceived(int session_id,
const std::string& message) {
auto session = sessions_.find(session_id);
if (session != sessions_.end())
std::optional<std::string> target_session_id_str =
GetTargetSessionId(message);
std::shared_ptr<MainThreadHandle> worker = nullptr;
int merged_session_id = session_id;
if (target_session_id_str) {
bool is_number = std::all_of(target_session_id_str->begin(),
target_session_id_str->end(),
::isdigit);
if (is_number) {
int target_session_id = std::stoi(*target_session_id_str);
worker = protocol::TargetAgent::target_session_id_worker_map_
[target_session_id];
if (worker) {
merged_session_id += target_session_id << 16;
}
}
}
auto session = sessions_.find(merged_session_id);
if (session == sessions_.end()) {
std::unique_ptr<InspectorSession> session;
if (worker) {
session = worker->Connect(
std::unique_ptr<InspectorSessionDelegate>(
new IoSessionDelegate(request_queue_->handle(), session_id)),
true);
} else {
session = main_thread_->Connect(
std::unique_ptr<InspectorSessionDelegate>(
new IoSessionDelegate(request_queue_->handle(), session_id)),
true);
}
if (session) {
sessions_[merged_session_id] = std::move(session);
sessions_[merged_session_id]->Dispatch(
Utf8ToStringView(message)->string());
} else {
fprintf(stderr, "Failed to connect to inspector session.\n");
}
} else {
session->second->Dispatch(Utf8ToStringView(message)->string());
}
}
void InspectorIoDelegate::EndSession(int session_id) {

View file

@ -642,6 +642,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
AddOption("--experimental-network-inspection",
"experimental network inspection support",
&EnvironmentOptions::experimental_network_inspection);
AddOption("--experimental-worker-inspection",
"experimental worker inspection support",
&EnvironmentOptions::experimental_worker_inspection);
AddOption(
"--heap-prof",
"Start the V8 heap profiler on start up, and write the heap profile "

View file

@ -173,6 +173,7 @@ class EnvironmentOptions : public Options {
std::string cpu_prof_name;
bool cpu_prof = false;
bool experimental_network_inspection = false;
bool experimental_worker_inspection = false;
std::string heap_prof_dir;
std::string heap_prof_name;
static const uint64_t kDefaultHeapProfInterval = 512 * 1024;

3
test/fixtures/inspect-worker/index.js vendored Normal file
View file

@ -0,0 +1,3 @@
const { Worker } = require('worker_threads');
new Worker(__dirname + '/worker.js', { type: 'module' });

View file

@ -0,0 +1,4 @@
console.log("worker thread");
process.on('exit', () => {
console.log('Worker1: Exiting...');
});

View file

@ -0,0 +1,75 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
common.skipIfInspectorDisabled();
const { NodeInstance } = require('../common/inspector-helper.js');
async function setupInspector(session, sessionId = undefined) {
await session.send({ method: 'NodeRuntime.enable', sessionId });
await session.waitForNotification('NodeRuntime.waitingForDebugger');
await session.send({ method: 'Runtime.enable', sessionId });
await session.send({ method: 'Debugger.enable', sessionId });
await session.send({ method: 'Runtime.runIfWaitingForDebugger', sessionId });
await session.send({ method: 'NodeRuntime.disable', sessionId });
await session.waitForNotification((notification) => {
return notification.method === 'Debugger.scriptParsed' &&
notification.params.url === 'node:internal/bootstrap/realm' &&
notification.sessionId === sessionId;
});
}
async function test(isSetAutoAttachBeforeExecution) {
const child = new NodeInstance(['--inspect-brk=0', '--experimental-worker-inspection'],
'',
fixtures.path('inspect-worker/index.js')
);
const session = await child.connectInspectorSession();
await setupInspector(session);
if (isSetAutoAttachBeforeExecution) {
await session.send({ method: 'Target.setAutoAttach', params: { autoAttach: true, waitForDebuggerOnStart: true } });
}
await session.waitForNotification('Debugger.paused');
await session.send({ method: 'Debugger.resume' });
const sessionId = '1';
await session.waitForNotification('Target.targetCreated');
if (!isSetAutoAttachBeforeExecution) {
await session.send({ method: 'Target.setAutoAttach', params: { autoAttach: true, waitForDebuggerOnStart: true } });
}
await session.waitForNotification((notification) => {
return notification.method === 'Target.attachedToTarget' &&
notification.params.sessionId === sessionId;
});
await setupInspector(session, sessionId);
await session.waitForNotification('Debugger.paused');
await session.send({ method: 'Debugger.resume', sessionId });
await session.waitForDisconnect();
}
test(true).then(common.mustCall());
test(false).then(common.mustCall());
function withPermissionOptionTest() {
const permissionErrorThrow = common.mustCall();
const child = new NodeInstance(['--inspect-brk=0', '--experimental-worker-inspection', '--permission'],
'',
fixtures.path('inspect-worker/index.js'),
{
log: (_, msg) => {
if (msg.includes('Access to this API has been restricted')) {
permissionErrorThrow();
}
},
error: () => {},
}
);
child.connectInspectorSession();
}
withPermissionOptionTest();