From d99bb251b56575b1bdfe7162c396cc71a6fec2cb Mon Sep 17 00:00:00 2001 From: islandryu Date: Tue, 29 Apr 2025 20:03:51 +0900 Subject: [PATCH 1/5] inspector: initial support for Network.loadNetworkResource Fixes: https://github.com/nodejs/node/issues/57873 --- doc/api/cli.md | 11 ++ doc/api/inspector.md | 37 +++++ lib/inspector.js | 8 ++ lib/internal/inspector/network_resources.js | 27 ++++ src/inspector/io_agent.cc | 80 +++++++++++ src/inspector/io_agent.h | 24 ++++ src/inspector/network_agent.cc | 41 +++++- src/inspector/network_agent.h | 11 +- src/inspector/network_inspector.cc | 2 +- src/inspector/network_resource_manager.cc | 58 ++++++++ src/inspector/network_resource_manager.h | 34 +++++ src/inspector/node_inspector.gypi | 6 + src/inspector/node_protocol.pdl | 33 +++++ src/inspector_agent.cc | 5 + src/inspector_js_api.cc | 15 +++ src/node_options.cc | 3 + src/node_options.h | 1 + .../inspector-network-resource/app.js.map | 10 ++ .../test-inspector-network-resource.js | 126 ++++++++++++++++++ typings/internalBinding/inspector.d.ts | 1 + 20 files changed, 528 insertions(+), 5 deletions(-) create mode 100644 lib/internal/inspector/network_resources.js create mode 100644 src/inspector/io_agent.cc create mode 100644 src/inspector/io_agent.h create mode 100644 src/inspector/network_resource_manager.cc create mode 100644 src/inspector/network_resource_manager.h create mode 100644 test/fixtures/inspector-network-resource/app.js.map create mode 100644 test/parallel/test-inspector-network-resource.js diff --git a/doc/api/cli.md b/doc/api/cli.md index 2a43a489458950..d0fffe4e9b524d 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1069,6 +1069,17 @@ passing a second `parentURL` argument for contextual resolution. Previously gated the entire `import.meta.resolve` feature. +### `--experimental-inspector-network-resource` + + + +> Stability: 1.1 - Active Development + +Enable experimental support for inspector network resources. + ### `--experimental-loader=module` + +> Stability: 1.1 - Active Development + +This feature is only available with the `--experimental-inspector-network-resource` flag enabled. + +The inspector.NetworkResources.put method is used to provide a response for a loadNetworkResource +request issued via the Chrome DevTools Protocol (CDP). +This is typically triggered when a source map is specified by URL, and a DevTools frontend—such as +Chrome—requests the resource to retrieve the source map. + +This method allows developers to predefine the resource content to be served in response to such CDP requests. + +```js +const inspector = require('node:inspector'); +// By preemptively calling put to register the resource, a source map can be resolved when +// a loadNetworkResource request is made from the frontend. +async function setNetworkResources() { + const mapUrl = 'http://localhost:3000/dist/app.js.map'; + const tsUrl = 'http://localhost:3000/src/app.ts'; + const distAppJsMap = await fetch(mapUrl).then((res) => res.text()); + const srcAppTs = await fetch(tsUrl).then((res) => res.text()); + inspector.NetworkResources.put(mapUrl, distAppJsMap); + inspector.NetworkResources.put(tsUrl, srcAppTs); +}; +setNetworkResources().then(() => { + require('./dist/app'); +}); +``` + +For more details, see the official CDP documentation: [Network.loadNetworkResource](https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-loadNetworkResource) + ## Support of breakpoints The Chrome DevTools Protocol [`Debugger` domain][] allows an diff --git a/lib/inspector.js b/lib/inspector.js index 7dc5746f422f74..4bf3ef7b61e99f 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -36,6 +36,9 @@ const { } = require('internal/validators'); const { isMainThread } = require('worker_threads'); const { _debugEnd } = internalBinding('process_methods'); +const { + put, +} = require('internal/inspector/network_resources'); const { Connection, @@ -218,6 +221,10 @@ const Network = { dataReceived: (params) => broadcastToFrontend('Network.dataReceived', params), }; +const NetworkResources = { + put, +}; + module.exports = { open: inspectorOpen, close: _debugEnd, @@ -226,4 +233,5 @@ module.exports = { console, Session, Network, + NetworkResources, }; diff --git a/lib/internal/inspector/network_resources.js b/lib/internal/inspector/network_resources.js new file mode 100644 index 00000000000000..166b4222cc297f --- /dev/null +++ b/lib/internal/inspector/network_resources.js @@ -0,0 +1,27 @@ +'use strict'; + +const { getOptionValue } = require('internal/options'); +const { validateString } = require('internal/validators'); +const { putNetworkResource } = internalBinding('inspector'); + +/** + * Registers a resource for the inspector using the internal 'putNetworkResource' binding. + * @param {string} url - The URL of the resource. + * @param {string} data - The content of the resource to provide. + */ +function put(url, data) { + if (!getOptionValue('--experimental-inspector-network-resource')) { + process.emitWarning( + 'The --experimental-inspector-network-resource option is not enabled. ' + + 'Please enable it to use the putNetworkResource function'); + return; + } + validateString(url, 'url'); + validateString(data, 'data'); + + putNetworkResource(url, data); +} + +module.exports = { + put, +}; diff --git a/src/inspector/io_agent.cc b/src/inspector/io_agent.cc new file mode 100644 index 00000000000000..015e822c8e248f --- /dev/null +++ b/src/inspector/io_agent.cc @@ -0,0 +1,80 @@ +#include "io_agent.h" +#include +#include +#include +#include +#include "crdtp/dispatch.h" +#include "inspector/network_resource_manager.h" + +namespace node::inspector::protocol { + +void IoAgent::Wire(UberDispatcher* dispatcher) { + frontend_ = std::make_shared(dispatcher->channel()); + IO::Dispatcher::wire(dispatcher, this); +} + +DispatchResponse IoAgent::read(const String& in_handle, + Maybe in_offset, + Maybe in_size, + String* out_data, + bool* out_eof) { + std::string in_handle_str = in_handle; + uint64_t stream_id = 0; + bool is_number = + std::all_of(in_handle_str.begin(), in_handle_str.end(), ::isdigit); + if (!is_number) { + *out_data = ""; + *out_eof = true; + return DispatchResponse::Success(); + } + stream_id = std::stoull(in_handle_str); + + std::string url = NetworkResourceManager::GetUrlForStreamId(stream_id); + if (url.empty()) { + *out_data = ""; + *out_eof = true; + return DispatchResponse::Success(); + } + std::string txt = NetworkResourceManager::Get(url); + std::string_view txt_view(txt); + + int offset = 0; + bool offset_was_specified = false; + if (in_offset.isJust()) { + offset = in_offset.fromJust(); + offset_was_specified = true; + } else if (offset_map_.find(stream_id) != offset_map_.end()) { + offset = offset_map_[stream_id]; + } + int size = 1 << 20; + if (in_size.isJust()) { + size = in_size.fromJust(); + } + if (static_cast(offset) < txt_view.length()) { + std::string_view out_view = txt_view.substr(offset, size); + out_data->assign(out_view.data(), out_view.size()); + *out_eof = false; + if (!offset_was_specified) { + offset_map_[stream_id] = offset + size; + } + } else { + *out_data = ""; + *out_eof = true; + } + + return DispatchResponse::Success(); +} + +DispatchResponse IoAgent::close(const String& in_handle) { + std::string in_handle_str = in_handle; + uint64_t stream_id = 0; + bool is_number = + std::all_of(in_handle_str.begin(), in_handle_str.end(), ::isdigit); + if (is_number) { + stream_id = std::stoull(in_handle_str); + // Use accessor to erase resource and mapping by stream id + NetworkResourceManager::EraseByStreamId(stream_id); + } + return DispatchResponse::Success(); +} +} // namespace node::inspector::protocol diff --git a/src/inspector/io_agent.h b/src/inspector/io_agent.h new file mode 100644 index 00000000000000..b1fde6506d5c36 --- /dev/null +++ b/src/inspector/io_agent.h @@ -0,0 +1,24 @@ +#ifndef SRC_INSPECTOR_IO_AGENT_H_ +#define SRC_INSPECTOR_IO_AGENT_H_ + +#include "node/inspector/protocol/IO.h" + +namespace node::inspector::protocol { + +class IoAgent : public IO::Backend { + public: + IoAgent() {} + void Wire(UberDispatcher* dispatcher); + DispatchResponse read(const String& in_handle, + Maybe in_offset, + Maybe in_size, + String* out_data, + bool* out_eof) override; + DispatchResponse close(const String& in_handle) override; + + private: + std::shared_ptr frontend_; + std::unordered_map offset_map_ = {}; // Maps stream_id to offset +}; +} // namespace node::inspector::protocol +#endif // SRC_INSPECTOR_IO_AGENT_H_ diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index 496b5c41a0bfc2..129fcf44ec8e35 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -1,8 +1,14 @@ #include "network_agent.h" +#include #include "debug_utils-inl.h" +#include "env-inl.h" +#include "inspector/network_resource_manager.h" #include "inspector/protocol_helper.h" #include "network_inspector.h" +#include "node_metadata.h" #include "util-inl.h" +#include "uv.h" +#include "v8-context.h" #include "v8.h" namespace node { @@ -203,8 +209,9 @@ std::unique_ptr createResponseFromObject( } NetworkAgent::NetworkAgent(NetworkInspector* inspector, - v8_inspector::V8Inspector* v8_inspector) - : inspector_(inspector), v8_inspector_(v8_inspector) { + v8_inspector::V8Inspector* v8_inspector, + Environment* env) + : inspector_(inspector), v8_inspector_(v8_inspector), env_(env) { event_notifier_map_["requestWillBeSent"] = &NetworkAgent::requestWillBeSent; event_notifier_map_["responseReceived"] = &NetworkAgent::responseReceived; event_notifier_map_["loadingFailed"] = &NetworkAgent::loadingFailed; @@ -329,10 +336,38 @@ protocol::DispatchResponse NetworkAgent::streamResourceContent( // If the request is finished, remove the entry. requests_.erase(in_requestId); } - return protocol::DispatchResponse::Success(); } +protocol::DispatchResponse NetworkAgent::loadNetworkResource( + const protocol::String& in_url, + std::unique_ptr* + out_resource) { + if (!env_->options()->experimental_inspector_network_resource) { + return protocol::DispatchResponse::ServerError( + "Network resource loading is not enabled. This feature is " + "experimental and requires --experimental-inspector-network-resource " + "flag to be set."); + } + std::string data = NetworkResourceManager::Get(in_url); + bool found = !data.empty(); + if (found) { + uint64_t stream_id = NetworkResourceManager::GetStreamId(in_url); + auto result = protocol::Network::LoadNetworkResourcePageResult::create() + .setSuccess(true) + .setStream(std::to_string(stream_id)) + .build(); + *out_resource = std::move(result); + return protocol::DispatchResponse::Success(); + } else { + auto result = protocol::Network::LoadNetworkResourcePageResult::create() + .setSuccess(false) + .build(); + *out_resource = std::move(result); + return protocol::DispatchResponse::Success(); + } +} + void NetworkAgent::requestWillBeSent(v8::Local context, v8::Local params) { protocol::String request_id; diff --git a/src/inspector/network_agent.h b/src/inspector/network_agent.h index c5303885d61e18..19f0c007a6754d 100644 --- a/src/inspector/network_agent.h +++ b/src/inspector/network_agent.h @@ -1,6 +1,8 @@ #ifndef SRC_INSPECTOR_NETWORK_AGENT_H_ #define SRC_INSPECTOR_NETWORK_AGENT_H_ +#include "env.h" +#include "io_agent.h" #include "node/inspector/protocol/Network.h" #include @@ -39,7 +41,8 @@ struct RequestEntry { class NetworkAgent : public protocol::Network::Backend { public: explicit NetworkAgent(NetworkInspector* inspector, - v8_inspector::V8Inspector* v8_inspector); + v8_inspector::V8Inspector* v8_inspector, + Environment* env); void Wire(protocol::UberDispatcher* dispatcher); @@ -60,6 +63,11 @@ class NetworkAgent : public protocol::Network::Backend { const protocol::String& in_requestId, protocol::Binary* out_bufferedData) override; + protocol::DispatchResponse loadNetworkResource( + const protocol::String& in_url, + std::unique_ptr* + out_resource) override; + void emitNotification(v8::Local context, const protocol::String& event, v8::Local params); @@ -89,6 +97,7 @@ class NetworkAgent : public protocol::Network::Backend { v8::Local); std::unordered_map event_notifier_map_; std::map requests_; + Environment* env_; }; } // namespace inspector diff --git a/src/inspector/network_inspector.cc b/src/inspector/network_inspector.cc index e93db7bbe922f6..b125c79d28cd80 100644 --- a/src/inspector/network_inspector.cc +++ b/src/inspector/network_inspector.cc @@ -6,7 +6,7 @@ namespace inspector { NetworkInspector::NetworkInspector(Environment* env, v8_inspector::V8Inspector* v8_inspector) : enabled_(false), env_(env) { - network_agent_ = std::make_unique(this, v8_inspector); + network_agent_ = std::make_unique(this, v8_inspector, env); } NetworkInspector::~NetworkInspector() { network_agent_.reset(); diff --git a/src/inspector/network_resource_manager.cc b/src/inspector/network_resource_manager.cc new file mode 100644 index 00000000000000..cc55e2b9d94f22 --- /dev/null +++ b/src/inspector/network_resource_manager.cc @@ -0,0 +1,58 @@ +#include "inspector/network_resource_manager.h" +#include +#include +#include +#include + +namespace node { +namespace inspector { + +std::unordered_map NetworkResourceManager::resources_; +std::unordered_map + NetworkResourceManager::url_to_stream_id_; +std::atomic NetworkResourceManager::stream_id_counter_{1}; + +void NetworkResourceManager::Put(const std::string& url, + const std::string& data) { + resources_[url] = data; + url_to_stream_id_[url] = ++stream_id_counter_; +} + +std::string NetworkResourceManager::Get(const std::string& url) { + auto it = resources_.find(url); + if (it != resources_.end()) return it->second; + return {}; +} + +uint64_t NetworkResourceManager::NextStreamId() { + return ++stream_id_counter_; +} + +std::string NetworkResourceManager::GetUrlForStreamId(uint64_t stream_id) { + for (const auto& pair : url_to_stream_id_) { + if (pair.second == stream_id) { + return pair.first; + } + } + return std::string(); +} + +void NetworkResourceManager::EraseByStreamId(uint64_t stream_id) { + for (auto it = url_to_stream_id_.begin(); it != url_to_stream_id_.end(); + ++it) { + if (it->second == stream_id) { + resources_.erase(it->first); + url_to_stream_id_.erase(it); + break; + } + } +} + +uint64_t NetworkResourceManager::GetStreamId(const std::string& url) { + auto it = url_to_stream_id_.find(url); + if (it != url_to_stream_id_.end()) return it->second; + return 0; +} + +} // namespace inspector +} // namespace node diff --git a/src/inspector/network_resource_manager.h b/src/inspector/network_resource_manager.h new file mode 100644 index 00000000000000..6c566792287574 --- /dev/null +++ b/src/inspector/network_resource_manager.h @@ -0,0 +1,34 @@ +// network_resource_manager.h +#ifndef SRC_INSPECTOR_NETWORK_RESOURCE_MANAGER_H_ +#define SRC_INSPECTOR_NETWORK_RESOURCE_MANAGER_H_ + +#include +#include +#include + +namespace node { +namespace inspector { + +class NetworkResourceManager { + public: + static void Put(const std::string& url, const std::string& data); + static std::string Get(const std::string& url); + + // Accessor to get URL for a given stream id + static std::string GetUrlForStreamId(uint64_t stream_id); + // Erase resource and mapping by stream id + static void EraseByStreamId(uint64_t stream_id); + // Returns the stream id for a given url, or 0 if not found + static uint64_t GetStreamId(const std::string& url); + + private: + static uint64_t NextStreamId(); + static std::unordered_map resources_; + static std::unordered_map url_to_stream_id_; + static std::atomic stream_id_counter_; +}; + +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_NETWORK_RESOURCE_MANAGER_H_ diff --git a/src/inspector/node_inspector.gypi b/src/inspector/node_inspector.gypi index 176663780afc95..ad81f837e84d76 100644 --- a/src/inspector/node_inspector.gypi +++ b/src/inspector/node_inspector.gypi @@ -36,6 +36,10 @@ 'src/inspector/target_agent.h', 'src/inspector/worker_inspector.cc', 'src/inspector/worker_inspector.h', + 'src/inspector/io_agent.cc', + 'src/inspector/io_agent.h', + 'src/inspector/network_resource_manager.cc', + 'src/inspector/network_resource_manager.h', ], 'node_inspector_generated_sources': [ '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Forward.h', @@ -51,6 +55,8 @@ '<(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', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/IO.h', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/IO.cpp', ], 'node_protocol_files': [ '<(protocol_tool_path)/lib/Forward_h.template', diff --git a/src/inspector/node_protocol.pdl b/src/inspector/node_protocol.pdl index 3608bfd317022c..46631bc20ad081 100644 --- a/src/inspector/node_protocol.pdl +++ b/src/inspector/node_protocol.pdl @@ -180,6 +180,11 @@ experimental domain Network # Request / response headers as keys / values of JSON object. type Headers extends object + type LoadNetworkResourcePageResult extends object + properties + boolean success + optional IO.StreamHandle stream + # Disables network tracking, prevents network events from being sent to the client. command disable @@ -215,6 +220,13 @@ experimental domain Network returns # Data that has been buffered until streaming is enabled. binary bufferedData + # Fetches the resource and returns the content. + command loadNetworkResource + parameters + # URL of the resource to get content for. + string url + returns + LoadNetworkResourcePageResult resource # Fired when page is about to send HTTP request. event requestWillBeSent @@ -321,3 +333,24 @@ experimental domain Target parameters boolean autoAttach boolean waitForDebuggerOnStart +domain IO + type StreamHandle extends string + # Read a chunk of the stream + command read + parameters + # Handle of the stream to read. + StreamHandle handle + # Seek to the specified offset before reading (if not specified, proceed with offset + # following the last read). Some types of streams may only support sequential reads. + optional integer offset + # Maximum number of bytes to read (left upon the agent discretion if not specified). + optional integer size + returns + # Data that were read. + string data + # Set if the end-of-file condition occurred while reading. + boolean eof + command close + parameters + # Handle of the stream to close. + StreamHandle handle diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index b4f8cab5b6084a..3d43c51ed0bfcc 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -239,6 +239,10 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, } runtime_agent_ = std::make_unique(); runtime_agent_->Wire(node_dispatcher_.get()); + if (env->options()->experimental_inspector_network_resource) { + io_agent_ = std::make_unique(); + io_agent_->Wire(node_dispatcher_.get()); + } network_inspector_ = std::make_unique(env, inspector.get()); network_inspector_->Wire(node_dispatcher_.get()); @@ -406,6 +410,7 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, std::unique_ptr worker_agent_; std::shared_ptr target_agent_; std::unique_ptr network_inspector_; + std::shared_ptr io_agent_; std::unique_ptr delegate_; std::unique_ptr session_; std::unique_ptr node_dispatcher_; diff --git a/src/inspector_js_api.cc b/src/inspector_js_api.cc index 69029247accf5b..58a5e06c5f44e4 100644 --- a/src/inspector_js_api.cc +++ b/src/inspector_js_api.cc @@ -1,4 +1,5 @@ #include "base_object-inl.h" +#include "inspector/network_resource_manager.h" #include "inspector/protocol_helper.h" #include "inspector_agent.h" #include "inspector_io.h" @@ -334,6 +335,18 @@ void Url(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(OneByteString(env->isolate(), url)); } +void PutNetworkResource(const v8::FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_GE(args.Length(), 2); + CHECK(args[0]->IsString()); + CHECK(args[1]->IsString()); + + Utf8Value url(env->isolate(), args[0].As()); + Utf8Value data(env->isolate(), args[1].As()); + + NetworkResourceManager::Put(*url, *data); +} + void Initialize(Local target, Local unused, Local context, void* priv) { Environment* env = Environment::GetCurrent(context); @@ -378,6 +391,7 @@ void Initialize(Local target, Local unused, SetMethodNoSideEffect(context, target, "isEnabled", IsEnabled); SetMethod(context, target, "emitProtocolEvent", EmitProtocolEvent); SetMethod(context, target, "setupNetworkTracking", SetupNetworkTracking); + SetMethod(context, target, "putNetworkResource", PutNetworkResource); Local console_string = FIXED_ONE_BYTE_STRING(isolate, "console"); @@ -420,6 +434,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(JSBindingsConnection::New); registry->Register(JSBindingsConnection::Dispatch); registry->Register(JSBindingsConnection::Disconnect); + registry->Register(PutNetworkResource); } } // namespace inspector diff --git a/src/node_options.cc b/src/node_options.cc index 46d865f7377e9f..f54ea2e6cceff7 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -711,6 +711,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--experimental-worker-inspection", "experimental worker inspection support", &EnvironmentOptions::experimental_worker_inspection); + AddOption("--experimental-inspector-network-resource", + "experimental load network resources via the inspector", + &EnvironmentOptions::experimental_inspector_network_resource); AddOption( "--heap-prof", "Start the V8 heap profiler on start up, and write the heap profile " diff --git a/src/node_options.h b/src/node_options.h index 6702888e20148c..84a9d1de8606ff 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -175,6 +175,7 @@ class EnvironmentOptions : public Options { bool cpu_prof = false; bool experimental_network_inspection = false; bool experimental_worker_inspection = false; + bool experimental_inspector_network_resource = false; std::string heap_prof_dir; std::string heap_prof_name; static const uint64_t kDefaultHeapProfInterval = 512 * 1024; diff --git a/test/fixtures/inspector-network-resource/app.js.map b/test/fixtures/inspector-network-resource/app.js.map new file mode 100644 index 00000000000000..ca4bb9159baeaa --- /dev/null +++ b/test/fixtures/inspector-network-resource/app.js.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "file": "app.js", + "sourceRoot": "", + "sources": [ + "http://localhost:3000/app.ts" + ], + "names": [], + "mappings": ";AAAA,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC" +} diff --git a/test/parallel/test-inspector-network-resource.js b/test/parallel/test-inspector-network-resource.js new file mode 100644 index 00000000000000..2e2fc404562312 --- /dev/null +++ b/test/parallel/test-inspector-network-resource.js @@ -0,0 +1,126 @@ +// Flags: --inspect=0 --experimental-network-inspection +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const { NodeInstance } = require('../common/inspector-helper'); +const test = require('node:test'); +const assert = require('node:assert'); +const path = require('path'); +const fs = require('fs'); + +const resourceUrl = 'http://localhost:3000/app.js'; +const resourcePath = path.join(__dirname, '../fixtures/inspector-network-resource/app.js.map'); + +const script = ` + const { NetworkResources } = require('node:inspector'); + const fs = require('fs'); + NetworkResources.put('${resourceUrl}', fs.readFileSync('${resourcePath.replace(/\\/g, '\\').replace(/'/g, "\\'")}', 'utf8')); + console.log('Network resource loaded:', '${resourceUrl}'); +`; + +async function setupSessionAndPauseAtEvalLine4(script) { + const instance = new NodeInstance([ + '--inspect-brk=0', + '--experimental-inspector-network-resource', + ], script); + const session = await instance.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.waitForNotification((notification) => { + return ( + notification.method === 'Debugger.scriptParsed' && + notification.params.url.includes('[eval]') + ); + }); + // Set breakpoint at line 4 of [eval] script + await session.send({ + method: 'Debugger.setBreakpointByUrl', + params: { + lineNumber: 4, + url: '[eval]' + } + }); + await session.waitForNotification('Debugger.paused'); + await session.send({ method: 'Debugger.resume' }); + await session.waitForNotification('Debugger.paused'); + return { instance, session }; +} + +test('should load and stream a static network resource using loadNetworkResource and IO.read', async () => { + const { session } = await setupSessionAndPauseAtEvalLine4(script); + const { resource } = await session.send({ + method: 'Network.loadNetworkResource', + params: { url: resourceUrl }, + }); + assert(resource.success, 'Resource should be loaded successfully'); + assert(resource.stream, 'Resource should have a stream handle'); + let result = await session.send({ method: 'IO.read', params: { handle: resource.stream } }); + let data = result.data; + let eof = result.eof; + let content = ''; + while (!eof) { + content += data; + result = await session.send({ method: 'IO.read', params: { handle: resource.stream } }); + data = result.data; + eof = result.eof; + } + content += data; + const expected = fs.readFileSync(resourcePath, 'utf8'); + assert.strictEqual(content, expected); + await session.send({ method: 'IO.close', params: { handle: resource.stream } }); + await session.send({ method: 'Debugger.resume' }); + await session.waitForDisconnect(); +}); + +test('should return success: false for missing resource', async () => { + const { session } = await setupSessionAndPauseAtEvalLine4(script); + const { resource } = await session.send({ + method: 'Network.loadNetworkResource', + params: { url: 'http://localhost:3000/does-not-exist.js' }, + }); + assert.strictEqual(resource.success, false); + assert(!resource.stream, 'No stream should be returned for missing resource'); + await session.send({ method: 'Debugger.resume' }); + await session.waitForDisconnect(); +}); + +test('should error or return empty for wrong stream id', async () => { + const { session } = await setupSessionAndPauseAtEvalLine4(script); + const { resource } = await session.send({ + method: 'Network.loadNetworkResource', + params: { url: resourceUrl }, + }); + assert(resource.success); + const bogus = '999999'; + const result = await session.send({ method: 'IO.read', params: { handle: bogus } }); + assert(result.eof, 'Should be eof for bogus stream id'); + assert.strictEqual(result.data, ''); + await session.send({ method: 'IO.close', params: { handle: resource.stream } }); + await session.send({ method: 'Debugger.resume' }); + await session.waitForDisconnect(); +}); + +test('should support IO.read with size and offset', async () => { + const { session } = await setupSessionAndPauseAtEvalLine4(script); + const { resource } = await session.send({ + method: 'Network.loadNetworkResource', + params: { url: resourceUrl }, + }); + assert(resource.success); + assert(resource.stream); + const expected = fs.readFileSync(resourcePath, 'utf8'); + let result = await session.send({ method: 'IO.read', params: { handle: resource.stream, size: 5 } }); + assert.strictEqual(result.data, expected.slice(0, 5)); + result = await session.send({ method: 'IO.read', params: { handle: resource.stream, offset: 5, size: 5 } }); + assert.strictEqual(result.data, expected.slice(5, 10)); + result = await session.send({ method: 'IO.read', params: { handle: resource.stream, offset: 10 } }); + assert.strictEqual(result.data, expected.slice(10)); + await session.send({ method: 'IO.close', params: { handle: resource.stream } }); + await session.send({ method: 'Debugger.resume' }); + await session.waitForDisconnect(); +}); diff --git a/typings/internalBinding/inspector.d.ts b/typings/internalBinding/inspector.d.ts index ab32838b2366ca..c0ee57774d87dd 100644 --- a/typings/internalBinding/inspector.d.ts +++ b/typings/internalBinding/inspector.d.ts @@ -33,4 +33,5 @@ export interface InspectorBinding { console: Console; Connection: InspectorConnectionConstructor; MainThreadConnection: InspectorConnectionConstructor; + putNetworkResource: (url: string, resource: string) => void; } From fbdcdccd8e89b77f4114537c12fdfe1b1d69cbf8 Mon Sep 17 00:00:00 2001 From: islandryu Date: Mon, 30 Jun 2025 00:16:15 +0900 Subject: [PATCH 2/5] avoid making networkResourceManager a static class. Co-authored-by: Chengzhong Wu --- src/inspector/io_agent.cc | 6 +- src/inspector/io_agent.h | 7 +- src/inspector/network_agent.cc | 18 ++-- src/inspector/network_agent.h | 11 ++- src/inspector/network_inspector.cc | 13 ++- src/inspector/network_inspector.h | 9 +- src/inspector/network_resource_manager.cc | 10 +- src/inspector/network_resource_manager.h | 20 ++-- src/inspector/worker_inspector.cc | 13 ++- src/inspector/worker_inspector.h | 24 +++-- src/inspector_agent.cc | 27 +++++- src/inspector_agent.h | 3 + src/inspector_js_api.cc | 2 +- .../test-inspector-network-resource.js | 96 ++++++++++++++----- 14 files changed, 188 insertions(+), 71 deletions(-) diff --git a/src/inspector/io_agent.cc b/src/inspector/io_agent.cc index 015e822c8e248f..e99eae8c36b93c 100644 --- a/src/inspector/io_agent.cc +++ b/src/inspector/io_agent.cc @@ -29,13 +29,13 @@ DispatchResponse IoAgent::read(const String& in_handle, } stream_id = std::stoull(in_handle_str); - std::string url = NetworkResourceManager::GetUrlForStreamId(stream_id); + std::string url = network_resource_manager_->GetUrlForStreamId(stream_id); if (url.empty()) { *out_data = ""; *out_eof = true; return DispatchResponse::Success(); } - std::string txt = NetworkResourceManager::Get(url); + std::string txt = network_resource_manager_->Get(url); std::string_view txt_view(txt); int offset = 0; @@ -73,7 +73,7 @@ DispatchResponse IoAgent::close(const String& in_handle) { if (is_number) { stream_id = std::stoull(in_handle_str); // Use accessor to erase resource and mapping by stream id - NetworkResourceManager::EraseByStreamId(stream_id); + network_resource_manager_->EraseByStreamId(stream_id); } return DispatchResponse::Success(); } diff --git a/src/inspector/io_agent.h b/src/inspector/io_agent.h index b1fde6506d5c36..78d9829efad9a2 100644 --- a/src/inspector/io_agent.h +++ b/src/inspector/io_agent.h @@ -1,13 +1,17 @@ #ifndef SRC_INSPECTOR_IO_AGENT_H_ #define SRC_INSPECTOR_IO_AGENT_H_ +#include +#include "inspector/network_resource_manager.h" #include "node/inspector/protocol/IO.h" namespace node::inspector::protocol { class IoAgent : public IO::Backend { public: - IoAgent() {} + explicit IoAgent( + std::shared_ptr network_resource_manager) + : network_resource_manager_(std::move(network_resource_manager)) {} void Wire(UberDispatcher* dispatcher); DispatchResponse read(const String& in_handle, Maybe in_offset, @@ -19,6 +23,7 @@ class IoAgent : public IO::Backend { private: std::shared_ptr frontend_; std::unordered_map offset_map_ = {}; // Maps stream_id to offset + std::shared_ptr network_resource_manager_; }; } // namespace node::inspector::protocol #endif // SRC_INSPECTOR_IO_AGENT_H_ diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index 129fcf44ec8e35..45bce967da1ab9 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -208,10 +208,15 @@ std::unique_ptr createResponseFromObject( .build(); } -NetworkAgent::NetworkAgent(NetworkInspector* inspector, - v8_inspector::V8Inspector* v8_inspector, - Environment* env) - : inspector_(inspector), v8_inspector_(v8_inspector), env_(env) { +NetworkAgent::NetworkAgent( + NetworkInspector* inspector, + v8_inspector::V8Inspector* v8_inspector, + Environment* env, + std::shared_ptr network_resource_manager) + : inspector_(inspector), + v8_inspector_(v8_inspector), + env_(env), + network_resource_manager_(std::move(network_resource_manager)) { event_notifier_map_["requestWillBeSent"] = &NetworkAgent::requestWillBeSent; event_notifier_map_["responseReceived"] = &NetworkAgent::responseReceived; event_notifier_map_["loadingFailed"] = &NetworkAgent::loadingFailed; @@ -349,10 +354,11 @@ protocol::DispatchResponse NetworkAgent::loadNetworkResource( "experimental and requires --experimental-inspector-network-resource " "flag to be set."); } - std::string data = NetworkResourceManager::Get(in_url); + CHECK_NOT_NULL(network_resource_manager_); + std::string data = network_resource_manager_->Get(in_url); bool found = !data.empty(); if (found) { - uint64_t stream_id = NetworkResourceManager::GetStreamId(in_url); + uint64_t stream_id = network_resource_manager_->GetStreamId(in_url); auto result = protocol::Network::LoadNetworkResourcePageResult::create() .setSuccess(true) .setStream(std::to_string(stream_id)) diff --git a/src/inspector/network_agent.h b/src/inspector/network_agent.h index 19f0c007a6754d..f814c7f5cf6662 100644 --- a/src/inspector/network_agent.h +++ b/src/inspector/network_agent.h @@ -3,9 +3,11 @@ #include "env.h" #include "io_agent.h" +#include "network_resource_manager.h" #include "node/inspector/protocol/Network.h" #include +#include #include namespace node { @@ -40,9 +42,11 @@ struct RequestEntry { class NetworkAgent : public protocol::Network::Backend { public: - explicit NetworkAgent(NetworkInspector* inspector, - v8_inspector::V8Inspector* v8_inspector, - Environment* env); + explicit NetworkAgent( + NetworkInspector* inspector, + v8_inspector::V8Inspector* v8_inspector, + Environment* env, + std::shared_ptr network_resource_manager); void Wire(protocol::UberDispatcher* dispatcher); @@ -98,6 +102,7 @@ class NetworkAgent : public protocol::Network::Backend { std::unordered_map event_notifier_map_; std::map requests_; Environment* env_; + std::shared_ptr network_resource_manager_; }; } // namespace inspector diff --git a/src/inspector/network_inspector.cc b/src/inspector/network_inspector.cc index b125c79d28cd80..3a79ba988571ca 100644 --- a/src/inspector/network_inspector.cc +++ b/src/inspector/network_inspector.cc @@ -3,10 +3,15 @@ namespace node { namespace inspector { -NetworkInspector::NetworkInspector(Environment* env, - v8_inspector::V8Inspector* v8_inspector) - : enabled_(false), env_(env) { - network_agent_ = std::make_unique(this, v8_inspector, env); +NetworkInspector::NetworkInspector( + Environment* env, + v8_inspector::V8Inspector* v8_inspector, + std::shared_ptr network_resource_manager) + : enabled_(false), + env_(env), + network_resource_manager_(std::move(network_resource_manager)) { + network_agent_ = std::make_unique( + this, v8_inspector, env, network_resource_manager_); } NetworkInspector::~NetworkInspector() { network_agent_.reset(); diff --git a/src/inspector/network_inspector.h b/src/inspector/network_inspector.h index 4095a05394cd8a..bcdca254230ea9 100644 --- a/src/inspector/network_inspector.h +++ b/src/inspector/network_inspector.h @@ -1,8 +1,10 @@ #ifndef SRC_INSPECTOR_NETWORK_INSPECTOR_H_ #define SRC_INSPECTOR_NETWORK_INSPECTOR_H_ +#include #include "env.h" #include "network_agent.h" +#include "network_resource_manager.h" namespace node { class Environment; @@ -11,8 +13,10 @@ namespace inspector { class NetworkInspector { public: - explicit NetworkInspector(Environment* env, - v8_inspector::V8Inspector* v8_inspector); + explicit NetworkInspector( + Environment* env, + v8_inspector::V8Inspector* v8_inspector, + std::shared_ptr network_resource_manager); ~NetworkInspector(); void Wire(protocol::UberDispatcher* dispatcher); @@ -32,6 +36,7 @@ class NetworkInspector { bool enabled_; Environment* env_; std::unique_ptr network_agent_; + std::shared_ptr network_resource_manager_; }; } // namespace inspector diff --git a/src/inspector/network_resource_manager.cc b/src/inspector/network_resource_manager.cc index cc55e2b9d94f22..9e076c196d1b32 100644 --- a/src/inspector/network_resource_manager.cc +++ b/src/inspector/network_resource_manager.cc @@ -7,18 +7,15 @@ namespace node { namespace inspector { -std::unordered_map NetworkResourceManager::resources_; -std::unordered_map - NetworkResourceManager::url_to_stream_id_; -std::atomic NetworkResourceManager::stream_id_counter_{1}; - void NetworkResourceManager::Put(const std::string& url, const std::string& data) { + Mutex::ScopedLock lock(mutex_); resources_[url] = data; url_to_stream_id_[url] = ++stream_id_counter_; } std::string NetworkResourceManager::Get(const std::string& url) { + Mutex::ScopedLock lock(mutex_); auto it = resources_.find(url); if (it != resources_.end()) return it->second; return {}; @@ -29,6 +26,7 @@ uint64_t NetworkResourceManager::NextStreamId() { } std::string NetworkResourceManager::GetUrlForStreamId(uint64_t stream_id) { + Mutex::ScopedLock lock(mutex_); for (const auto& pair : url_to_stream_id_) { if (pair.second == stream_id) { return pair.first; @@ -38,6 +36,7 @@ std::string NetworkResourceManager::GetUrlForStreamId(uint64_t stream_id) { } void NetworkResourceManager::EraseByStreamId(uint64_t stream_id) { + Mutex::ScopedLock lock(mutex_); for (auto it = url_to_stream_id_.begin(); it != url_to_stream_id_.end(); ++it) { if (it->second == stream_id) { @@ -49,6 +48,7 @@ void NetworkResourceManager::EraseByStreamId(uint64_t stream_id) { } uint64_t NetworkResourceManager::GetStreamId(const std::string& url) { + Mutex::ScopedLock lock(mutex_); auto it = url_to_stream_id_.find(url); if (it != url_to_stream_id_.end()) return it->second; return 0; diff --git a/src/inspector/network_resource_manager.h b/src/inspector/network_resource_manager.h index 6c566792287574..ffc2b59ca5b609 100644 --- a/src/inspector/network_resource_manager.h +++ b/src/inspector/network_resource_manager.h @@ -5,27 +5,29 @@ #include #include #include +#include "node_mutex.h" namespace node { namespace inspector { class NetworkResourceManager { public: - static void Put(const std::string& url, const std::string& data); - static std::string Get(const std::string& url); + void Put(const std::string& url, const std::string& data); + std::string Get(const std::string& url); // Accessor to get URL for a given stream id - static std::string GetUrlForStreamId(uint64_t stream_id); + std::string GetUrlForStreamId(uint64_t stream_id); // Erase resource and mapping by stream id - static void EraseByStreamId(uint64_t stream_id); + void EraseByStreamId(uint64_t stream_id); // Returns the stream id for a given url, or 0 if not found - static uint64_t GetStreamId(const std::string& url); + uint64_t GetStreamId(const std::string& url); private: - static uint64_t NextStreamId(); - static std::unordered_map resources_; - static std::unordered_map url_to_stream_id_; - static std::atomic stream_id_counter_; + uint64_t NextStreamId(); + std::unordered_map resources_; + std::unordered_map url_to_stream_id_; + std::atomic stream_id_counter_{1}; + Mutex mutex_; // Protects access to resources_ and url_to_stream_id_ }; } // namespace inspector diff --git a/src/inspector/worker_inspector.cc b/src/inspector/worker_inspector.cc index fd479181e9566c..58c7d2602b327e 100644 --- a/src/inspector/worker_inspector.cc +++ b/src/inspector/worker_inspector.cc @@ -60,12 +60,14 @@ ParentInspectorHandle::ParentInspectorHandle( const std::string& url, std::shared_ptr parent_thread, bool wait_for_connect, - const std::string& name) + const std::string& name, + std::shared_ptr network_resource_manager) : id_(id), url_(url), parent_thread_(parent_thread), wait_(wait_for_connect), - name_(name) {} + name_(name), + network_resource_manager_(network_resource_manager) {} ParentInspectorHandle::~ParentInspectorHandle() { parent_thread_->Post( @@ -101,10 +103,13 @@ void WorkerManager::WorkerStarted(uint64_t session_id, } std::unique_ptr WorkerManager::NewParentHandle( - uint64_t thread_id, const std::string& url, const std::string& name) { + uint64_t thread_id, + const std::string& url, + const std::string& name, + std::shared_ptr network_resource_manager) { bool wait = !delegates_waiting_on_start_.empty(); return std::make_unique( - thread_id, url, thread_, wait, name); + thread_id, url, thread_, wait, name, network_resource_manager); } void WorkerManager::RemoveAttachDelegate(int id) { diff --git a/src/inspector/worker_inspector.h b/src/inspector/worker_inspector.h index 24403bb1704c40..28a249aea4d91c 100644 --- a/src/inspector/worker_inspector.h +++ b/src/inspector/worker_inspector.h @@ -1,6 +1,7 @@ #ifndef SRC_INSPECTOR_WORKER_INSPECTOR_H_ #define SRC_INSPECTOR_WORKER_INSPECTOR_H_ +#include "inspector/network_resource_manager.h" #if !HAVE_INSPECTOR #error("This header can only be used when inspector is enabled") #endif @@ -54,16 +55,18 @@ struct WorkerInfo { class ParentInspectorHandle { public: - ParentInspectorHandle(uint64_t id, - const std::string& url, - std::shared_ptr parent_thread, - bool wait_for_connect, - const std::string& name); + ParentInspectorHandle( + uint64_t id, + const std::string& url, + std::shared_ptr parent_thread, + bool wait_for_connect, + const std::string& name, + std::shared_ptr network_resource_manager); ~ParentInspectorHandle(); std::unique_ptr NewParentInspectorHandle( uint64_t thread_id, const std::string& url, const std::string& name) { return std::make_unique( - thread_id, url, parent_thread_, wait_, name); + thread_id, url, parent_thread_, wait_, name, network_resource_manager_); } void WorkerStarted(std::shared_ptr worker_thread, bool waiting); @@ -74,6 +77,9 @@ class ParentInspectorHandle { std::unique_ptr Connect( std::unique_ptr delegate, bool prevent_shutdown); + std::shared_ptr GetNetworkResourceManager() { + return network_resource_manager_; + } private: uint64_t id_; @@ -81,6 +87,7 @@ class ParentInspectorHandle { std::shared_ptr parent_thread_; bool wait_; std::string name_; + std::shared_ptr network_resource_manager_; }; class WorkerManager : public std::enable_shared_from_this { @@ -89,7 +96,10 @@ class WorkerManager : public std::enable_shared_from_this { : thread_(thread) {} std::unique_ptr NewParentHandle( - uint64_t thread_id, const std::string& url, const std::string& name); + uint64_t thread_id, + const std::string& url, + const std::string& name, + std::shared_ptr network_resource_manager); void WorkerStarted(uint64_t session_id, const WorkerInfo& info, bool waiting); void WorkerFinished(uint64_t session_id); std::unique_ptr SetAutoAttach( diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 3d43c51ed0bfcc..202ed756c98d8a 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -13,6 +13,7 @@ #include "inspector/worker_agent.h" #include "inspector/worker_inspector.h" #include "inspector_io.h" +#include "node.h" #include "node/inspector/protocol/Protocol.h" #include "node_errors.h" #include "node_internals.h" @@ -240,11 +241,17 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, runtime_agent_ = std::make_unique(); runtime_agent_->Wire(node_dispatcher_.get()); if (env->options()->experimental_inspector_network_resource) { - io_agent_ = std::make_unique(); + io_agent_ = std::make_unique( + env->inspector_agent()->GetNetworkResourceManager()); io_agent_->Wire(node_dispatcher_.get()); + network_inspector_ = std::make_unique( + env, + inspector.get(), + env->inspector_agent()->GetNetworkResourceManager()); + } else { + network_inspector_ = + std::make_unique(env, inspector.get(), nullptr); } - network_inspector_ = - std::make_unique(env, inspector.get()); network_inspector_->Wire(node_dispatcher_.get()); if (env->options()->experimental_worker_inspection) { target_agent_ = std::make_shared(); @@ -1159,7 +1166,8 @@ std::unique_ptr Agent::GetParentHandle( CHECK_NOT_NULL(client_); if (!parent_handle_) { - return client_->getWorkerManager()->NewParentHandle(thread_id, url, name); + return client_->getWorkerManager()->NewParentHandle( + thread_id, url, name, GetNetworkResourceManager()); } else { return parent_handle_->NewParentInspectorHandle(thread_id, url, name); } @@ -1225,6 +1233,17 @@ std::shared_ptr Agent::GetWorkerManager() { return client_->getWorkerManager(); } +std::shared_ptr Agent::GetNetworkResourceManager() { + if (parent_handle_) { + return parent_handle_->GetNetworkResourceManager(); + } else if (network_resource_manager_) { + return network_resource_manager_; + } else { + network_resource_manager_ = std::make_shared(); + return network_resource_manager_; + } +} + std::string Agent::GetWsUrl() const { if (io_ == nullptr) return ""; diff --git a/src/inspector_agent.h b/src/inspector_agent.h index ad7a8e6c069968..e43dced8f410f3 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -1,5 +1,6 @@ #pragma once +#include "inspector/network_resource_manager.h" #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if !HAVE_INSPECTOR @@ -127,6 +128,7 @@ class Agent { std::shared_ptr GetWorkerManager(); inline Environment* env() const { return parent_env_; } + std::shared_ptr GetNetworkResourceManager(); private: void ToggleAsyncHook(v8::Isolate* isolate, v8::Local fn); @@ -153,6 +155,7 @@ class Agent { bool network_tracking_enabled_ = false; bool pending_enable_network_tracking = false; bool pending_disable_network_tracking = false; + std::shared_ptr network_resource_manager_; }; } // namespace inspector diff --git a/src/inspector_js_api.cc b/src/inspector_js_api.cc index 58a5e06c5f44e4..64823c68b11e94 100644 --- a/src/inspector_js_api.cc +++ b/src/inspector_js_api.cc @@ -344,7 +344,7 @@ void PutNetworkResource(const v8::FunctionCallbackInfo& args) { Utf8Value url(env->isolate(), args[0].As()); Utf8Value data(env->isolate(), args[1].As()); - NetworkResourceManager::Put(*url, *data); + env->inspector_agent()->GetNetworkResourceManager()->Put(*url, *data); } void Initialize(Local target, Local unused, diff --git a/test/parallel/test-inspector-network-resource.js b/test/parallel/test-inspector-network-resource.js index 2e2fc404562312..32c0ba60360b5c 100644 --- a/test/parallel/test-inspector-network-resource.js +++ b/test/parallel/test-inspector-network-resource.js @@ -18,11 +18,12 @@ const script = ` const fs = require('fs'); NetworkResources.put('${resourceUrl}', fs.readFileSync('${resourcePath.replace(/\\/g, '\\').replace(/'/g, "\\'")}', 'utf8')); console.log('Network resource loaded:', '${resourceUrl}'); + debugger; `; -async function setupSessionAndPauseAtEvalLine4(script) { +async function setupSessionAndPauseAtEvalLastLine(script) { const instance = new NodeInstance([ - '--inspect-brk=0', + '--inspect-wait=0', '--experimental-inspector-network-resource', ], script); const session = await instance.connectInspectorSession(); @@ -31,28 +32,12 @@ async function setupSessionAndPauseAtEvalLine4(script) { await session.send({ method: 'Runtime.enable' }); await session.send({ method: 'Debugger.enable' }); await session.send({ method: 'Runtime.runIfWaitingForDebugger' }); - await session.waitForNotification((notification) => { - return ( - notification.method === 'Debugger.scriptParsed' && - notification.params.url.includes('[eval]') - ); - }); - // Set breakpoint at line 4 of [eval] script - await session.send({ - method: 'Debugger.setBreakpointByUrl', - params: { - lineNumber: 4, - url: '[eval]' - } - }); - await session.waitForNotification('Debugger.paused'); - await session.send({ method: 'Debugger.resume' }); await session.waitForNotification('Debugger.paused'); return { instance, session }; } test('should load and stream a static network resource using loadNetworkResource and IO.read', async () => { - const { session } = await setupSessionAndPauseAtEvalLine4(script); + const { session } = await setupSessionAndPauseAtEvalLastLine(script); const { resource } = await session.send({ method: 'Network.loadNetworkResource', params: { url: resourceUrl }, @@ -78,7 +63,7 @@ test('should load and stream a static network resource using loadNetworkResource }); test('should return success: false for missing resource', async () => { - const { session } = await setupSessionAndPauseAtEvalLine4(script); + const { session } = await setupSessionAndPauseAtEvalLastLine(script); const { resource } = await session.send({ method: 'Network.loadNetworkResource', params: { url: 'http://localhost:3000/does-not-exist.js' }, @@ -90,7 +75,7 @@ test('should return success: false for missing resource', async () => { }); test('should error or return empty for wrong stream id', async () => { - const { session } = await setupSessionAndPauseAtEvalLine4(script); + const { session } = await setupSessionAndPauseAtEvalLastLine(script); const { resource } = await session.send({ method: 'Network.loadNetworkResource', params: { url: resourceUrl }, @@ -106,7 +91,7 @@ test('should error or return empty for wrong stream id', async () => { }); test('should support IO.read with size and offset', async () => { - const { session } = await setupSessionAndPauseAtEvalLine4(script); + const { session } = await setupSessionAndPauseAtEvalLastLine(script); const { resource } = await session.send({ method: 'Network.loadNetworkResource', params: { url: resourceUrl }, @@ -124,3 +109,70 @@ test('should support IO.read with size and offset', async () => { await session.send({ method: 'Debugger.resume' }); await session.waitForDisconnect(); }); + +test('should load resource put from another thread', async () => { + const workerScript = ` + console.log('this is worker thread'); + debugger; + `; + const script = ` + const { NetworkResources } = require('node:inspector'); + const fs = require('fs'); + NetworkResources.put('${resourceUrl}', fs.readFileSync('${resourcePath.replace(/\\/g, '\\').replace(/'/g, "\\'")}', 'utf8')); + const { Worker } = require('worker_threads'); + const worker = new Worker(\`${workerScript}\`, {eval: true}); + `; + const instance = new NodeInstance([ + '--experimental-inspector-network-resource', + '--experimental-worker-inspection', + '--inspect-brk=0', + ], script); + const session = await instance.connectInspectorSession(); + await setupInspector(session); + await session.waitForNotification('Debugger.paused'); + await session.send({ method: 'Debugger.resume' }); + const sessionId = '1'; + await session.waitForNotification('Target.targetCreated'); + 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'); + + const { resource } = await session.send({ + method: 'Network.loadNetworkResource', + params: { url: resourceUrl, sessionId }, + }); + + assert(resource.success, 'Resource should be loaded successfully'); + assert(resource.stream, 'Resource should have a stream handle'); + let result = await session.send({ method: 'IO.read', params: { handle: resource.stream, sessionId } }); + let data = result.data; + let eof = result.eof; + let content = ''; + while (!eof) { + content += data; + result = await session.send({ method: 'IO.read', params: { handle: resource.stream, sessionId } }); + data = result.data; + eof = result.eof; + } + content += data; + const expected = fs.readFileSync(resourcePath, 'utf8'); + assert.strictEqual(content, expected); + await session.send({ method: 'IO.close', params: { handle: resource.stream, sessionId } }); + + await session.send({ method: 'Debugger.resume', sessionId }); + + await session.waitForDisconnect(); + + async function setupInspector(session, sessionId) { + 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 }); + } +}); From e3b871cf0ca939156303c4f6ab55e04c0a89e2c6 Mon Sep 17 00:00:00 2001 From: islandryu Date: Mon, 30 Jun 2025 22:58:05 +0900 Subject: [PATCH 3/5] use url as stream id --- src/inspector/io_agent.cc | 35 ++++--------------- src/inspector/io_agent.h | 3 +- src/inspector/network_agent.cc | 3 +- src/inspector/network_resource_manager.cc | 33 ++--------------- src/inspector/network_resource_manager.h | 11 ++---- .../test-inspector-network-resource.js | 24 +++++++------ 6 files changed, 26 insertions(+), 83 deletions(-) diff --git a/src/inspector/io_agent.cc b/src/inspector/io_agent.cc index e99eae8c36b93c..d74ed80cdce5c7 100644 --- a/src/inspector/io_agent.cc +++ b/src/inspector/io_agent.cc @@ -18,23 +18,7 @@ DispatchResponse IoAgent::read(const String& in_handle, Maybe in_size, String* out_data, bool* out_eof) { - std::string in_handle_str = in_handle; - uint64_t stream_id = 0; - bool is_number = - std::all_of(in_handle_str.begin(), in_handle_str.end(), ::isdigit); - if (!is_number) { - *out_data = ""; - *out_eof = true; - return DispatchResponse::Success(); - } - stream_id = std::stoull(in_handle_str); - - std::string url = network_resource_manager_->GetUrlForStreamId(stream_id); - if (url.empty()) { - *out_data = ""; - *out_eof = true; - return DispatchResponse::Success(); - } + std::string url = in_handle; std::string txt = network_resource_manager_->Get(url); std::string_view txt_view(txt); @@ -43,8 +27,8 @@ DispatchResponse IoAgent::read(const String& in_handle, if (in_offset.isJust()) { offset = in_offset.fromJust(); offset_was_specified = true; - } else if (offset_map_.find(stream_id) != offset_map_.end()) { - offset = offset_map_[stream_id]; + } else if (offset_map_.find(url) != offset_map_.end()) { + offset = offset_map_[url]; } int size = 1 << 20; if (in_size.isJust()) { @@ -55,7 +39,7 @@ DispatchResponse IoAgent::read(const String& in_handle, out_data->assign(out_view.data(), out_view.size()); *out_eof = false; if (!offset_was_specified) { - offset_map_[stream_id] = offset + size; + offset_map_[url] = offset + size; } } else { *out_data = ""; @@ -66,15 +50,8 @@ DispatchResponse IoAgent::read(const String& in_handle, } DispatchResponse IoAgent::close(const String& in_handle) { - std::string in_handle_str = in_handle; - uint64_t stream_id = 0; - bool is_number = - std::all_of(in_handle_str.begin(), in_handle_str.end(), ::isdigit); - if (is_number) { - stream_id = std::stoull(in_handle_str); - // Use accessor to erase resource and mapping by stream id - network_resource_manager_->EraseByStreamId(stream_id); - } + std::string url = in_handle; + network_resource_manager_->Erase(url); return DispatchResponse::Success(); } } // namespace node::inspector::protocol diff --git a/src/inspector/io_agent.h b/src/inspector/io_agent.h index 78d9829efad9a2..df76f0f5bb1fff 100644 --- a/src/inspector/io_agent.h +++ b/src/inspector/io_agent.h @@ -22,7 +22,8 @@ class IoAgent : public IO::Backend { private: std::shared_ptr frontend_; - std::unordered_map offset_map_ = {}; // Maps stream_id to offset + std::unordered_map offset_map_ = + {}; // Maps stream_id to offset std::shared_ptr network_resource_manager_; }; } // namespace node::inspector::protocol diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index 45bce967da1ab9..3b5d9615021101 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -358,10 +358,9 @@ protocol::DispatchResponse NetworkAgent::loadNetworkResource( std::string data = network_resource_manager_->Get(in_url); bool found = !data.empty(); if (found) { - uint64_t stream_id = network_resource_manager_->GetStreamId(in_url); auto result = protocol::Network::LoadNetworkResourcePageResult::create() .setSuccess(true) - .setStream(std::to_string(stream_id)) + .setStream(in_url) .build(); *out_resource = std::move(result); return protocol::DispatchResponse::Success(); diff --git a/src/inspector/network_resource_manager.cc b/src/inspector/network_resource_manager.cc index 9e076c196d1b32..47c625164694ee 100644 --- a/src/inspector/network_resource_manager.cc +++ b/src/inspector/network_resource_manager.cc @@ -11,7 +11,6 @@ void NetworkResourceManager::Put(const std::string& url, const std::string& data) { Mutex::ScopedLock lock(mutex_); resources_[url] = data; - url_to_stream_id_[url] = ++stream_id_counter_; } std::string NetworkResourceManager::Get(const std::string& url) { @@ -21,37 +20,9 @@ std::string NetworkResourceManager::Get(const std::string& url) { return {}; } -uint64_t NetworkResourceManager::NextStreamId() { - return ++stream_id_counter_; -} - -std::string NetworkResourceManager::GetUrlForStreamId(uint64_t stream_id) { - Mutex::ScopedLock lock(mutex_); - for (const auto& pair : url_to_stream_id_) { - if (pair.second == stream_id) { - return pair.first; - } - } - return std::string(); -} - -void NetworkResourceManager::EraseByStreamId(uint64_t stream_id) { - Mutex::ScopedLock lock(mutex_); - for (auto it = url_to_stream_id_.begin(); it != url_to_stream_id_.end(); - ++it) { - if (it->second == stream_id) { - resources_.erase(it->first); - url_to_stream_id_.erase(it); - break; - } - } -} - -uint64_t NetworkResourceManager::GetStreamId(const std::string& url) { +void NetworkResourceManager::Erase(const std::string& stream_id) { Mutex::ScopedLock lock(mutex_); - auto it = url_to_stream_id_.find(url); - if (it != url_to_stream_id_.end()) return it->second; - return 0; + resources_.erase(stream_id); } } // namespace inspector diff --git a/src/inspector/network_resource_manager.h b/src/inspector/network_resource_manager.h index ffc2b59ca5b609..10a058ddc48bd4 100644 --- a/src/inspector/network_resource_manager.h +++ b/src/inspector/network_resource_manager.h @@ -15,19 +15,12 @@ class NetworkResourceManager { void Put(const std::string& url, const std::string& data); std::string Get(const std::string& url); - // Accessor to get URL for a given stream id - std::string GetUrlForStreamId(uint64_t stream_id); // Erase resource and mapping by stream id - void EraseByStreamId(uint64_t stream_id); - // Returns the stream id for a given url, or 0 if not found - uint64_t GetStreamId(const std::string& url); + void Erase(const std::string& stream_id); private: - uint64_t NextStreamId(); std::unordered_map resources_; - std::unordered_map url_to_stream_id_; - std::atomic stream_id_counter_{1}; - Mutex mutex_; // Protects access to resources_ and url_to_stream_id_ + Mutex mutex_; // Protects access to resources_ }; } // namespace inspector diff --git a/test/parallel/test-inspector-network-resource.js b/test/parallel/test-inspector-network-resource.js index 32c0ba60360b5c..b6b54f6b7bbabd 100644 --- a/test/parallel/test-inspector-network-resource.js +++ b/test/parallel/test-inspector-network-resource.js @@ -13,6 +13,7 @@ const fs = require('fs'); const resourceUrl = 'http://localhost:3000/app.js'; const resourcePath = path.join(__dirname, '../fixtures/inspector-network-resource/app.js.map'); +const resourceText = fs.readFileSync(resourcePath, 'utf8'); const script = ` const { NetworkResources } = require('node:inspector'); const fs = require('fs'); @@ -55,8 +56,7 @@ test('should load and stream a static network resource using loadNetworkResource eof = result.eof; } content += data; - const expected = fs.readFileSync(resourcePath, 'utf8'); - assert.strictEqual(content, expected); + assert.strictEqual(content, resourceText); await session.send({ method: 'IO.close', params: { handle: resource.stream } }); await session.send({ method: 'Debugger.resume' }); await session.waitForDisconnect(); @@ -98,13 +98,12 @@ test('should support IO.read with size and offset', async () => { }); assert(resource.success); assert(resource.stream); - const expected = fs.readFileSync(resourcePath, 'utf8'); let result = await session.send({ method: 'IO.read', params: { handle: resource.stream, size: 5 } }); - assert.strictEqual(result.data, expected.slice(0, 5)); + assert.strictEqual(result.data, resourceText.slice(0, 5)); result = await session.send({ method: 'IO.read', params: { handle: resource.stream, offset: 5, size: 5 } }); - assert.strictEqual(result.data, expected.slice(5, 10)); + assert.strictEqual(result.data, resourceText.slice(5, 10)); result = await session.send({ method: 'IO.read', params: { handle: resource.stream, offset: 10 } }); - assert.strictEqual(result.data, expected.slice(10)); + assert.strictEqual(result.data, resourceText.slice(10)); await session.send({ method: 'IO.close', params: { handle: resource.stream } }); await session.send({ method: 'Debugger.resume' }); await session.waitForDisconnect(); @@ -131,12 +130,16 @@ test('should load resource put from another thread', async () => { await setupInspector(session); await session.waitForNotification('Debugger.paused'); await session.send({ method: 'Debugger.resume' }); - const sessionId = '1'; await session.waitForNotification('Target.targetCreated'); await session.send({ method: 'Target.setAutoAttach', params: { autoAttach: true, waitForDebuggerOnStart: true } }); + let sessionId; await session.waitForNotification((notification) => { - return notification.method === 'Target.attachedToTarget' && - notification.params.sessionId === sessionId; + if (notification.method === 'Target.attachedToTarget') { + sessionId = notification.params.sessionId; + return true; + } + return false; + }); await setupInspector(session, sessionId); @@ -160,8 +163,7 @@ test('should load resource put from another thread', async () => { eof = result.eof; } content += data; - const expected = fs.readFileSync(resourcePath, 'utf8'); - assert.strictEqual(content, expected); + assert.strictEqual(content, resourceText); await session.send({ method: 'IO.close', params: { handle: resource.stream, sessionId } }); await session.send({ method: 'Debugger.resume', sessionId }); From 34e9882d5cdd37c95f937298561f1592f7d5231f Mon Sep 17 00:00:00 2001 From: islandryu Date: Thu, 3 Jul 2025 21:56:28 +0900 Subject: [PATCH 4/5] use std::optional --- src/inspector/io_agent.cc | 12 ++++++------ src/inspector/io_agent.h | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/inspector/io_agent.cc b/src/inspector/io_agent.cc index d74ed80cdce5c7..749774f5c3144f 100644 --- a/src/inspector/io_agent.cc +++ b/src/inspector/io_agent.cc @@ -14,8 +14,8 @@ void IoAgent::Wire(UberDispatcher* dispatcher) { } DispatchResponse IoAgent::read(const String& in_handle, - Maybe in_offset, - Maybe in_size, + std::optional in_offset, + std::optional in_size, String* out_data, bool* out_eof) { std::string url = in_handle; @@ -24,15 +24,15 @@ DispatchResponse IoAgent::read(const String& in_handle, int offset = 0; bool offset_was_specified = false; - if (in_offset.isJust()) { - offset = in_offset.fromJust(); + if (in_offset.has_value()) { + offset = *in_offset; offset_was_specified = true; } else if (offset_map_.find(url) != offset_map_.end()) { offset = offset_map_[url]; } int size = 1 << 20; - if (in_size.isJust()) { - size = in_size.fromJust(); + if (in_size.has_value()) { + size = *in_size; } if (static_cast(offset) < txt_view.length()) { std::string_view out_view = txt_view.substr(offset, size); diff --git a/src/inspector/io_agent.h b/src/inspector/io_agent.h index df76f0f5bb1fff..4a12311bae32de 100644 --- a/src/inspector/io_agent.h +++ b/src/inspector/io_agent.h @@ -14,8 +14,8 @@ class IoAgent : public IO::Backend { : network_resource_manager_(std::move(network_resource_manager)) {} void Wire(UberDispatcher* dispatcher); DispatchResponse read(const String& in_handle, - Maybe in_offset, - Maybe in_size, + std::optional in_offset, + std::optional in_size, String* out_data, bool* out_eof) override; DispatchResponse close(const String& in_handle) override; From 04d8ecac9633c41ec406e936d2c26c0c0a4daa62 Mon Sep 17 00:00:00 2001 From: islandryu Date: Thu, 3 Jul 2025 23:49:09 +0900 Subject: [PATCH 5/5] add flag to manpage --- doc/node.1 | 3 +++ test/parallel/test-inspector-network-resource.js | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/node.1 b/doc/node.1 index 8a7c81e6973c66..d489220b2386fd 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -229,6 +229,9 @@ Enable experimental WebAssembly module support. .It Fl -experimental-quic Enable the experimental QUIC support. . +.It Fl -experimental-inspector-network-resource +Enable experimental support for inspector network resources. +. .It Fl -force-context-aware Disable loading native addons that are not context-aware. . diff --git a/test/parallel/test-inspector-network-resource.js b/test/parallel/test-inspector-network-resource.js index b6b54f6b7bbabd..73620866931c24 100644 --- a/test/parallel/test-inspector-network-resource.js +++ b/test/parallel/test-inspector-network-resource.js @@ -14,10 +14,11 @@ const resourceUrl = 'http://localhost:3000/app.js'; const resourcePath = path.join(__dirname, '../fixtures/inspector-network-resource/app.js.map'); const resourceText = fs.readFileSync(resourcePath, 'utf8'); +const embedPath = resourcePath.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); const script = ` const { NetworkResources } = require('node:inspector'); const fs = require('fs'); - NetworkResources.put('${resourceUrl}', fs.readFileSync('${resourcePath.replace(/\\/g, '\\').replace(/'/g, "\\'")}', 'utf8')); + NetworkResources.put('${resourceUrl}', fs.readFileSync('${embedPath}', 'utf8')); console.log('Network resource loaded:', '${resourceUrl}'); debugger; `; @@ -117,7 +118,7 @@ test('should load resource put from another thread', async () => { const script = ` const { NetworkResources } = require('node:inspector'); const fs = require('fs'); - NetworkResources.put('${resourceUrl}', fs.readFileSync('${resourcePath.replace(/\\/g, '\\').replace(/'/g, "\\'")}', 'utf8')); + NetworkResources.put('${resourceUrl}', fs.readFileSync('${embedPath}', 'utf8')); const { Worker } = require('worker_threads'); const worker = new Worker(\`${workerScript}\`, {eval: true}); `;