From 525a2d22440744184220b2a5f43a0bffb9a03fc4 Mon Sep 17 00:00:00 2001 From: islandryu Date: Sat, 25 Jan 2025 17:26:31 +0900 Subject: [PATCH 1/7] inspector: support for worker inspection in chrome devtools Fixes: https://github.com/nodejs/node/issues/56343 --- doc/api/cli.md | 12 +++ src/inspector/main_thread_interface.h | 8 ++ src/inspector/node_inspector.gypi | 5 + src/inspector/node_protocol.pdl | 22 +++++ src/inspector/target_agent.cc | 96 +++++++++++++++++++ src/inspector/target_agent.h | 49 ++++++++++ src/inspector_agent.cc | 56 ++++++++--- src/inspector_io.cc | 61 ++++++++++-- src/node_options.cc | 3 + src/node_options.h | 1 + test/parallel/test-inspector-worker-target.js | 32 +++++++ 11 files changed, 322 insertions(+), 23 deletions(-) create mode 100644 src/inspector/target_agent.cc create mode 100644 src/inspector/target_agent.h create mode 100644 test/parallel/test-inspector-worker-target.js diff --git a/doc/api/cli.md b/doc/api/cli.md index ececfcefb687c7..b24381e6d48bc7 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1136,6 +1136,18 @@ added: v22.4.0 Enable experimental [`Web Storage`][] support. +### `--experimental-worker-inspection` + + + +> Stability: 1 - Experimental + +Enable experimental support for the worker inspection with Chrome DevTools. + ### `--expose-gc` > Stability: 1 - Experimental diff --git a/src/inspector/main_thread_interface.h b/src/inspector/main_thread_interface.h index 975821773d197f..3311f9df014a39 100644 --- a/src/inspector/main_thread_interface.h +++ b/src/inspector/main_thread_interface.h @@ -60,10 +60,7 @@ class MainThreadHandle : public std::enable_shared_from_this { void SetTargetSessionId(int target_session_id) { target_session_id_ = target_session_id; } - std::optional GetTargetSessionId() { - return target_session_id_; - } - + std::optional GetTargetSessionId() { return target_session_id_; } private: void Reset(); diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index b90d5208cdd7b9..89c7365d17b801 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -224,8 +224,10 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, std::unique_ptr delegate, std::shared_ptr main_thread, bool prevent_shutdown) - : delegate_(std::move(delegate)), main_thread_(main_thread), - prevent_shutdown_(prevent_shutdown), retaining_context_(false) { + : delegate_(std::move(delegate)), + main_thread_(main_thread), + prevent_shutdown_(prevent_shutdown), + retaining_context_(false) { session_ = inspector->connect(CONTEXT_GROUP_ID, this, StringView(), @@ -346,8 +348,7 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, // crdtp::FrontendChannel void FlushProtocolNotifications() override {} - std::string serializeToJSON( - std::unique_ptr message) { + std::string serializeToJSON(std::unique_ptr message) { std::vector cbor = message->Serialize(); std::string json; crdtp::Status status = ConvertCBORToJSON(crdtp::SpanFrom(cbor), &json); @@ -368,13 +369,11 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, if (target_session_id.has_value()) { std::string raw_message = protocol::StringUtil::StringViewToUtf8(message); std::unique_ptr value = - protocol::DictionaryValue::cast( - JsonUtil::parseJSON(raw_message)); + 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()); + delegate_->SendMessageToFrontend(Utf8ToStringView(json)->string()); } else { delegate_->SendMessageToFrontend(message); } From 01011f6aac5e22716a08ba77272d5f3f76bb719e Mon Sep 17 00:00:00 2001 From: islandryu Date: Mon, 3 Feb 2025 22:09:53 +0900 Subject: [PATCH 5/7] fix test --- test/fixtures/inspect-worker/index.js | 3 + test/fixtures/inspect-worker/worker.js | 4 ++ test/parallel/test-inspector-worker-target.js | 55 +++++++++++++------ 3 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 test/fixtures/inspect-worker/index.js create mode 100644 test/fixtures/inspect-worker/worker.js diff --git a/test/fixtures/inspect-worker/index.js b/test/fixtures/inspect-worker/index.js new file mode 100644 index 00000000000000..864176a7ba2126 --- /dev/null +++ b/test/fixtures/inspect-worker/index.js @@ -0,0 +1,3 @@ +const { Worker } = require('worker_threads'); + +new Worker(__dirname + '/worker.js', { type: 'module' }); \ No newline at end of file diff --git a/test/fixtures/inspect-worker/worker.js b/test/fixtures/inspect-worker/worker.js new file mode 100644 index 00000000000000..ebae421509e794 --- /dev/null +++ b/test/fixtures/inspect-worker/worker.js @@ -0,0 +1,4 @@ +console.log("worker thread"); +process.on('exit', () => { + console.log('Worker1: Exiting...'); +}); \ No newline at end of file diff --git a/test/parallel/test-inspector-worker-target.js b/test/parallel/test-inspector-worker-target.js index 0470a5da33a90d..521d0933308508 100644 --- a/test/parallel/test-inspector-worker-target.js +++ b/test/parallel/test-inspector-worker-target.js @@ -1,27 +1,50 @@ -// Flags: --inspect=0 --experimental-worker-inspection 'use strict'; const common = require('../common'); const fixtures = require('../common/fixtures'); -const { Worker } = require('worker_threads'); common.skipIfInspectorDisabled(); -const assert = require('assert'); -const { Session } = require('inspector'); +const { NodeInstance } = require('../common/inspector-helper.js'); -const session = new Session(); +(async () => { + const child = new NodeInstance(['--inspect-brk', '--experimental-worker-inspection'], + '', + fixtures.path('inspect-worker/index.js') + ); -session.connect(); + 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' && + notification.params.url === 'node:internal/bootstrap/realm'; + }); -new Worker(fixtures.path('worker-script.mjs'), { type: 'module' }); -session.on('Target.targetCreated', common.mustCall(({ params }) => { - const targetInfo = params.targetInfo; - assert.strictEqual(targetInfo.type, 'worker'); - assert.ok(targetInfo.url.includes('worker-script.mjs')); - assert.strictEqual(targetInfo.targetId, '1'); -})); + await session.waitForNotification('Debugger.paused'); + await session.send({ method: 'Debugger.resume' }); -session.on('Target.attachedToTarget', common.mustCall(({ params }) => { - assert.strictEqual(params.sessionId, '1'); -})); + const sessionId = '1'; + await session.waitForNotification('Target.targetCreated'); + await session.waitForNotification((notification) => { + return notification.method === 'Target.attachedToTarget' && + notification.params.sessionId === sessionId; + }); + + 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.enable', sessionId }); + await session.waitForNotification((notification) => { + return notification.method === 'Debugger.scriptParsed' && + notification.params.url === 'node:internal/bootstrap/realm'; + }); + await session.waitForNotification('Debugger.paused'); + await session.send({ method: 'Debugger.resume', sessionId }); + await session.waitForDisconnect(); +})().then(common.mustCall()); From cde7b0977db4e197eb8a2b0df6657603a4a0fe70 Mon Sep 17 00:00:00 2001 From: islandryu Date: Sat, 8 Feb 2025 20:13:16 +0900 Subject: [PATCH 6/7] support setAutoAttach --- src/inspector/node_protocol.pdl | 4 ++ src/inspector/target_agent.cc | 46 ++++++++++++++-- src/inspector/target_agent.h | 44 +++++++++++---- test/fixtures/inspect-worker/index.js | 2 +- test/fixtures/inspect-worker/worker.js | 2 +- test/parallel/test-inspector-worker-target.js | 53 +++++++++++-------- 6 files changed, 111 insertions(+), 40 deletions(-) diff --git a/src/inspector/node_protocol.pdl b/src/inspector/node_protocol.pdl index c72459e2a242f7..25fb0a6e2215b3 100644 --- a/src/inspector/node_protocol.pdl +++ b/src/inspector/node_protocol.pdl @@ -241,3 +241,7 @@ experimental domain Target SessionID sessionId TargetInfo targetInfo boolean waitingForDebugger + command setAutoAttach + parameters + boolean autoAttach + boolean waitForDebuggerOnStart diff --git a/src/inspector/target_agent.cc b/src/inspector/target_agent.cc index a556995d056033..d059705767371e 100644 --- a/src/inspector/target_agent.cc +++ b/src/inspector/target_agent.cc @@ -1,4 +1,5 @@ #include "target_agent.h" +#include "crdtp/dispatch.h" #include "inspector/worker_inspector.h" #include "main_thread_interface.h" @@ -19,11 +20,7 @@ class WorkerTargetDelegate : public WorkerDelegate { const std::string& url, bool waiting, std::shared_ptr worker) override { - std::string target_id = std::to_string(target_agent_->getNextTargetId()); - std::string type = "worker"; - - target_agent_->targetCreated(target_id, type, title, url); - target_agent_->attachedToTarget(worker, target_id, type, title, url); + target_agent_->createAndAttachIfNecessary(worker, title, url); } private: @@ -49,6 +46,22 @@ void TargetAgent::Wire(UberDispatcher* dispatcher) { Target::Dispatcher::wire(dispatcher, this); } +void TargetAgent::createAndAttachIfNecessary( + std::shared_ptr worker, + const std::string& title, + const std::string& url) { + std::string target_id = std::to_string(getNextTargetId()); + std::string type = "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 worker_manager) { auto manager = worker_manager.lock(); if (!manager) { @@ -93,6 +106,29 @@ void TargetAgent::attachedToTarget(std::shared_ptr worker, 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 diff --git a/src/inspector/target_agent.h b/src/inspector/target_agent.h index cefae4a6ac8b26..19bb6b309f0932 100644 --- a/src/inspector/target_agent.h +++ b/src/inspector/target_agent.h @@ -1,6 +1,8 @@ #ifndef SRC_INSPECTOR_TARGET_AGENT_H_ #define SRC_INSPECTOR_TARGET_AGENT_H_ +#include +#include #include "inspector/worker_inspector.h" #include "node/inspector/protocol/Target.h" @@ -11,35 +13,57 @@ class TargetInspector; namespace protocol { +struct TargetInfo { + std::string target_id; + std::string type; + std::string title; + std::string url; + std::shared_ptr worker; + bool attached; +}; + class TargetAgent : public Target::Backend, public std::enable_shared_from_this { public: void Wire(UberDispatcher* dispatcher); - void targetCreated(const std::string& target_id, - const std::string& type, - const std::string& title, - const std::string& url); - void attachedToTarget(std::shared_ptr worker, - const std::string& target_id, - const std::string& type, - const std::string& title, - const std::string& url); + void createAndAttachIfNecessary(std::shared_ptr worker, + const std::string& title, + const std::string& url); - int getNextTargetId(); + DispatchResponse setAutoAttach(bool auto_attach, + bool wait_for_debugger_on_start) override; void listenWorker(std::weak_ptr worker_manager); void reset(); static std::unordered_map> target_session_id_worker_map_; + bool isThisThread(MainThreadHandle* worker) { return worker == main_thread_; } + private: + int getNextTargetId(); int getNextSessionId(); + void targetCreated(const std::string& target_id, + const std::string& type, + const std::string& title, + const std::string& url); + void attachedToTarget(std::shared_ptr worker, + const std::string& target_id, + const std::string& type, + const std::string& title, + const std::string& url); std::shared_ptr frontend_; std::weak_ptr worker_manager_; static int next_session_id_; int next_target_id_ = 1; std::unique_ptr 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 targets_; + MainThreadHandle* main_thread_; }; } // namespace protocol diff --git a/test/fixtures/inspect-worker/index.js b/test/fixtures/inspect-worker/index.js index 864176a7ba2126..b0f883ef4b0453 100644 --- a/test/fixtures/inspect-worker/index.js +++ b/test/fixtures/inspect-worker/index.js @@ -1,3 +1,3 @@ const { Worker } = require('worker_threads'); -new Worker(__dirname + '/worker.js', { type: 'module' }); \ No newline at end of file +new Worker(__dirname + '/worker.js', { type: 'module' }); diff --git a/test/fixtures/inspect-worker/worker.js b/test/fixtures/inspect-worker/worker.js index ebae421509e794..9729bd7b41c6e1 100644 --- a/test/fixtures/inspect-worker/worker.js +++ b/test/fixtures/inspect-worker/worker.js @@ -1,4 +1,4 @@ console.log("worker thread"); process.on('exit', () => { console.log('Worker1: Exiting...'); -}); \ No newline at end of file +}); diff --git a/test/parallel/test-inspector-worker-target.js b/test/parallel/test-inspector-worker-target.js index 521d0933308508..f8655aea59f076 100644 --- a/test/parallel/test-inspector-worker-target.js +++ b/test/parallel/test-inspector-worker-target.js @@ -7,44 +7,51 @@ common.skipIfInspectorDisabled(); const { NodeInstance } = require('../common/inspector-helper.js'); -(async () => { - const child = new NodeInstance(['--inspect-brk', '--experimental-worker-inspection'], +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 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' && - notification.params.url === 'node:internal/bootstrap/realm'; - }); + 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 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.enable', sessionId }); - await session.waitForNotification((notification) => { - return notification.method === 'Debugger.scriptParsed' && - notification.params.url === 'node:internal/bootstrap/realm'; - }); + await setupInspector(session, sessionId); await session.waitForNotification('Debugger.paused'); await session.send({ method: 'Debugger.resume', sessionId }); await session.waitForDisconnect(); -})().then(common.mustCall()); +} + +test(true); +test(false); From b669db4c47db39b034b02dedac9c8c0fdaf35d73 Mon Sep 17 00:00:00 2001 From: islandryu Date: Sun, 9 Feb 2025 18:32:09 +0900 Subject: [PATCH 7/7] fix stoi error --- src/inspector_io.cc | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/inspector_io.cc b/src/inspector_io.cc index 4a777f11856448..c197d15f135fd2 100644 --- a/src/inspector_io.cc +++ b/src/inspector_io.cc @@ -366,12 +366,17 @@ void InspectorIoDelegate::MessageReceived(int session_id, GetTargetSessionId(message); std::shared_ptr worker = nullptr; int merged_session_id = session_id; - if (!target_session_id_str->empty()) { - 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; + 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; + } } }