inspector: initial support for Network.loadNetworkResource

Fixes: https://github.com/nodejs/node/issues/57873
PR-URL: https://github.com/nodejs/node/pull/58077
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
Shima Ryuhei 2025-07-10 21:34:11 +09:00 committed by Antoine du Hamel
parent 9d073f32da
commit d02831ef73
No known key found for this signature in database
GPG key ID: 20B1A390B168D356
25 changed files with 613 additions and 26 deletions

View file

@ -1046,6 +1046,17 @@ passing a second `parentURL` argument for contextual resolution.
Previously gated the entire `import.meta.resolve` feature.
### `--experimental-inspector-network-resource`
<!-- YAML
added:
- REPLACEME
-->
> Stability: 1.1 - Active Development
Enable experimental support for inspector network resources.
### `--experimental-loader=module`
<!-- YAML

View file

@ -594,6 +594,43 @@ This feature is only available with the `--experimental-network-inspection` flag
Broadcasts the `Network.loadingFailed` event to connected frontends. This event indicates that
HTTP request has failed to load.
### `inspector.NetworkResources.put`
<!-- YAML
added:
- REPLACEME
-->
> 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

View file

@ -237,6 +237,9 @@ flag is no longer required as WASI is enabled by default.
.It Fl -experimental-wasm-modules
Enable experimental WebAssembly module 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.
.

View file

@ -39,6 +39,9 @@ const {
} = require('internal/validators');
const { isMainThread } = require('worker_threads');
const { _debugEnd } = internalBinding('process_methods');
const {
put,
} = require('internal/inspector/network_resources');
const {
Connection,
@ -221,6 +224,10 @@ const Network = {
dataReceived: (params) => broadcastToFrontend('Network.dataReceived', params),
};
const NetworkResources = {
put,
};
module.exports = {
open: inspectorOpen,
close: _debugEnd,
@ -229,4 +236,5 @@ module.exports = {
console,
Session,
Network,
NetworkResources,
};

View file

@ -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,
};

57
src/inspector/io_agent.cc Normal file
View file

@ -0,0 +1,57 @@
#include "io_agent.h"
#include <algorithm>
#include <iostream>
#include <string>
#include <string_view>
#include "crdtp/dispatch.h"
#include "inspector/network_resource_manager.h"
namespace node::inspector::protocol {
void IoAgent::Wire(UberDispatcher* dispatcher) {
frontend_ = std::make_shared<IO::Frontend>(dispatcher->channel());
IO::Dispatcher::wire(dispatcher, this);
}
DispatchResponse IoAgent::read(const String& in_handle,
std::optional<int> in_offset,
std::optional<int> in_size,
String* out_data,
bool* out_eof) {
std::string url = in_handle;
std::string txt = network_resource_manager_->Get(url);
std::string_view txt_view(txt);
int offset = 0;
bool offset_was_specified = false;
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.has_value()) {
size = *in_size;
}
if (static_cast<std::size_t>(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_[url] = offset + size;
}
} else {
*out_data = "";
*out_eof = true;
}
return DispatchResponse::Success();
}
DispatchResponse IoAgent::close(const String& in_handle) {
std::string url = in_handle;
network_resource_manager_->Erase(url);
return DispatchResponse::Success();
}
} // namespace node::inspector::protocol

30
src/inspector/io_agent.h Normal file
View file

@ -0,0 +1,30 @@
#ifndef SRC_INSPECTOR_IO_AGENT_H_
#define SRC_INSPECTOR_IO_AGENT_H_
#include <memory>
#include "inspector/network_resource_manager.h"
#include "node/inspector/protocol/IO.h"
namespace node::inspector::protocol {
class IoAgent : public IO::Backend {
public:
explicit IoAgent(
std::shared_ptr<NetworkResourceManager> network_resource_manager)
: network_resource_manager_(std::move(network_resource_manager)) {}
void Wire(UberDispatcher* dispatcher);
DispatchResponse read(const String& in_handle,
std::optional<int> in_offset,
std::optional<int> in_size,
String* out_data,
bool* out_eof) override;
DispatchResponse close(const String& in_handle) override;
private:
std::shared_ptr<IO::Frontend> frontend_;
std::unordered_map<std::string, int> offset_map_ =
{}; // Maps stream_id to offset
std::shared_ptr<NetworkResourceManager> network_resource_manager_;
};
} // namespace node::inspector::protocol
#endif // SRC_INSPECTOR_IO_AGENT_H_

View file

@ -1,8 +1,14 @@
#include "network_agent.h"
#include <string>
#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 {
@ -202,9 +208,15 @@ std::unique_ptr<protocol::Network::Response> createResponseFromObject(
.build();
}
NetworkAgent::NetworkAgent(NetworkInspector* inspector,
v8_inspector::V8Inspector* v8_inspector)
: inspector_(inspector), v8_inspector_(v8_inspector) {
NetworkAgent::NetworkAgent(
NetworkInspector* inspector,
v8_inspector::V8Inspector* v8_inspector,
Environment* env,
std::shared_ptr<NetworkResourceManager> 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;
@ -329,10 +341,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<protocol::Network::LoadNetworkResourcePageResult>*
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.");
}
CHECK_NOT_NULL(network_resource_manager_);
std::string data = network_resource_manager_->Get(in_url);
bool found = !data.empty();
if (found) {
auto result = protocol::Network::LoadNetworkResourcePageResult::create()
.setSuccess(true)
.setStream(in_url)
.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<v8::Context> context,
v8::Local<v8::Object> params) {
protocol::String request_id;

View file

@ -1,9 +1,13 @@
#ifndef SRC_INSPECTOR_NETWORK_AGENT_H_
#define SRC_INSPECTOR_NETWORK_AGENT_H_
#include "env.h"
#include "io_agent.h"
#include "network_resource_manager.h"
#include "node/inspector/protocol/Network.h"
#include <map>
#include <memory>
#include <unordered_map>
namespace node {
@ -38,8 +42,11 @@ struct RequestEntry {
class NetworkAgent : public protocol::Network::Backend {
public:
explicit NetworkAgent(NetworkInspector* inspector,
v8_inspector::V8Inspector* v8_inspector);
explicit NetworkAgent(
NetworkInspector* inspector,
v8_inspector::V8Inspector* v8_inspector,
Environment* env,
std::shared_ptr<NetworkResourceManager> network_resource_manager);
void Wire(protocol::UberDispatcher* dispatcher);
@ -60,6 +67,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<protocol::Network::LoadNetworkResourcePageResult>*
out_resource) override;
void emitNotification(v8::Local<v8::Context> context,
const protocol::String& event,
v8::Local<v8::Object> params);
@ -89,6 +101,8 @@ class NetworkAgent : public protocol::Network::Backend {
v8::Local<v8::Object>);
std::unordered_map<protocol::String, EventNotifier> event_notifier_map_;
std::map<protocol::String, RequestEntry> requests_;
Environment* env_;
std::shared_ptr<NetworkResourceManager> network_resource_manager_;
};
} // namespace inspector

View file

@ -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<NetworkAgent>(this, v8_inspector);
NetworkInspector::NetworkInspector(
Environment* env,
v8_inspector::V8Inspector* v8_inspector,
std::shared_ptr<NetworkResourceManager> network_resource_manager)
: enabled_(false),
env_(env),
network_resource_manager_(std::move(network_resource_manager)) {
network_agent_ = std::make_unique<NetworkAgent>(
this, v8_inspector, env, network_resource_manager_);
}
NetworkInspector::~NetworkInspector() {
network_agent_.reset();

View file

@ -1,8 +1,10 @@
#ifndef SRC_INSPECTOR_NETWORK_INSPECTOR_H_
#define SRC_INSPECTOR_NETWORK_INSPECTOR_H_
#include <memory>
#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<NetworkResourceManager> network_resource_manager);
~NetworkInspector();
void Wire(protocol::UberDispatcher* dispatcher);
@ -32,6 +36,7 @@ class NetworkInspector {
bool enabled_;
Environment* env_;
std::unique_ptr<NetworkAgent> network_agent_;
std::shared_ptr<NetworkResourceManager> network_resource_manager_;
};
} // namespace inspector

View file

@ -0,0 +1,29 @@
#include "inspector/network_resource_manager.h"
#include <atomic>
#include <iostream>
#include <string>
#include <unordered_map>
namespace node {
namespace inspector {
void NetworkResourceManager::Put(const std::string& url,
const std::string& data) {
Mutex::ScopedLock lock(mutex_);
resources_[url] = data;
}
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 {};
}
void NetworkResourceManager::Erase(const std::string& stream_id) {
Mutex::ScopedLock lock(mutex_);
resources_.erase(stream_id);
}
} // namespace inspector
} // namespace node

View file

@ -0,0 +1,29 @@
// network_resource_manager.h
#ifndef SRC_INSPECTOR_NETWORK_RESOURCE_MANAGER_H_
#define SRC_INSPECTOR_NETWORK_RESOURCE_MANAGER_H_
#include <atomic>
#include <string>
#include <unordered_map>
#include "node_mutex.h"
namespace node {
namespace inspector {
class NetworkResourceManager {
public:
void Put(const std::string& url, const std::string& data);
std::string Get(const std::string& url);
// Erase resource and mapping by stream id
void Erase(const std::string& stream_id);
private:
std::unordered_map<std::string, std::string> resources_;
Mutex mutex_; // Protects access to resources_
};
} // namespace inspector
} // namespace node
#endif // SRC_INSPECTOR_NETWORK_RESOURCE_MANAGER_H_

View file

@ -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',

View file

@ -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

View file

@ -60,12 +60,14 @@ ParentInspectorHandle::ParentInspectorHandle(
const std::string& url,
std::shared_ptr<MainThreadHandle> parent_thread,
bool wait_for_connect,
const std::string& name)
const std::string& name,
std::shared_ptr<NetworkResourceManager> 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<ParentInspectorHandle> 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<NetworkResourceManager> network_resource_manager) {
bool wait = !delegates_waiting_on_start_.empty();
return std::make_unique<ParentInspectorHandle>(
thread_id, url, thread_, wait, name);
thread_id, url, thread_, wait, name, network_resource_manager);
}
void WorkerManager::RemoveAttachDelegate(int id) {

View file

@ -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<MainThreadHandle> parent_thread,
bool wait_for_connect,
const std::string& name);
ParentInspectorHandle(
uint64_t id,
const std::string& url,
std::shared_ptr<MainThreadHandle> parent_thread,
bool wait_for_connect,
const std::string& name,
std::shared_ptr<NetworkResourceManager> network_resource_manager);
~ParentInspectorHandle();
std::unique_ptr<ParentInspectorHandle> NewParentInspectorHandle(
uint64_t thread_id, const std::string& url, const std::string& name) {
return std::make_unique<ParentInspectorHandle>(
thread_id, url, parent_thread_, wait_, name);
thread_id, url, parent_thread_, wait_, name, network_resource_manager_);
}
void WorkerStarted(std::shared_ptr<MainThreadHandle> worker_thread,
bool waiting);
@ -74,6 +77,9 @@ class ParentInspectorHandle {
std::unique_ptr<inspector::InspectorSession> Connect(
std::unique_ptr<inspector::InspectorSessionDelegate> delegate,
bool prevent_shutdown);
std::shared_ptr<NetworkResourceManager> GetNetworkResourceManager() {
return network_resource_manager_;
}
private:
uint64_t id_;
@ -81,6 +87,7 @@ class ParentInspectorHandle {
std::shared_ptr<MainThreadHandle> parent_thread_;
bool wait_;
std::string name_;
std::shared_ptr<NetworkResourceManager> network_resource_manager_;
};
class WorkerManager : public std::enable_shared_from_this<WorkerManager> {
@ -89,7 +96,10 @@ class WorkerManager : public std::enable_shared_from_this<WorkerManager> {
: thread_(thread) {}
std::unique_ptr<ParentInspectorHandle> 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<NetworkResourceManager> network_resource_manager);
void WorkerStarted(uint64_t session_id, const WorkerInfo& info, bool waiting);
void WorkerFinished(uint64_t session_id);
std::unique_ptr<WorkerManagerEventHandle> SetAutoAttach(

View file

@ -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"
@ -238,8 +239,18 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
}
runtime_agent_ = std::make_unique<protocol::RuntimeAgent>();
runtime_agent_->Wire(node_dispatcher_.get());
network_inspector_ =
std::make_unique<NetworkInspector>(env, inspector.get());
if (env->options()->experimental_inspector_network_resource) {
io_agent_ = std::make_unique<protocol::IoAgent>(
env->inspector_agent()->GetNetworkResourceManager());
io_agent_->Wire(node_dispatcher_.get());
network_inspector_ = std::make_unique<NetworkInspector>(
env,
inspector.get(),
env->inspector_agent()->GetNetworkResourceManager());
} else {
network_inspector_ =
std::make_unique<NetworkInspector>(env, inspector.get(), nullptr);
}
network_inspector_->Wire(node_dispatcher_.get());
if (env->options()->experimental_worker_inspection) {
target_agent_ = std::make_shared<protocol::TargetAgent>();
@ -405,6 +416,7 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
std::unique_ptr<protocol::WorkerAgent> worker_agent_;
std::shared_ptr<protocol::TargetAgent> target_agent_;
std::unique_ptr<NetworkInspector> network_inspector_;
std::shared_ptr<protocol::IoAgent> io_agent_;
std::unique_ptr<InspectorSessionDelegate> delegate_;
std::unique_ptr<v8_inspector::V8InspectorSession> session_;
std::unique_ptr<UberDispatcher> node_dispatcher_;
@ -1153,7 +1165,8 @@ std::unique_ptr<ParentInspectorHandle> 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);
}
@ -1219,6 +1232,17 @@ std::shared_ptr<WorkerManager> Agent::GetWorkerManager() {
return client_->getWorkerManager();
}
std::shared_ptr<NetworkResourceManager> 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<NetworkResourceManager>();
return network_resource_manager_;
}
}
std::string Agent::GetWsUrl() const {
if (io_ == nullptr)
return "";

View file

@ -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<WorkerManager> GetWorkerManager();
inline Environment* env() const { return parent_env_; }
std::shared_ptr<NetworkResourceManager> GetNetworkResourceManager();
private:
void ToggleAsyncHook(v8::Isolate* isolate, v8::Local<v8::Function> 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<NetworkResourceManager> network_resource_manager_;
};
} // namespace inspector

View file

@ -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<Value>& args) {
args.GetReturnValue().Set(OneByteString(env->isolate(), url));
}
void PutNetworkResource(const v8::FunctionCallbackInfo<v8::Value>& 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<String>());
Utf8Value data(env->isolate(), args[1].As<String>());
env->inspector_agent()->GetNetworkResourceManager()->Put(*url, *data);
}
void Initialize(Local<Object> target, Local<Value> unused,
Local<Context> context, void* priv) {
Environment* env = Environment::GetCurrent(context);
@ -378,6 +391,7 @@ void Initialize(Local<Object> target, Local<Value> unused,
SetMethodNoSideEffect(context, target, "isEnabled", IsEnabled);
SetMethod(context, target, "emitProtocolEvent", EmitProtocolEvent);
SetMethod(context, target, "setupNetworkTracking", SetupNetworkTracking);
SetMethod(context, target, "putNetworkResource", PutNetworkResource);
Local<String> console_string = FIXED_ONE_BYTE_STRING(isolate, "console");
@ -420,6 +434,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(JSBindingsConnection<MainThreadConnection>::New);
registry->Register(JSBindingsConnection<MainThreadConnection>::Dispatch);
registry->Register(JSBindingsConnection<MainThreadConnection>::Disconnect);
registry->Register(PutNetworkResource);
}
} // namespace inspector

View file

@ -650,6 +650,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 "

View file

@ -172,6 +172,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;

View file

@ -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"
}

View file

@ -0,0 +1,181 @@
// 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 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('${embedPath}', 'utf8'));
console.log('Network resource loaded:', '${resourceUrl}');
debugger;
`;
async function setupSessionAndPauseAtEvalLastLine(script) {
const instance = new NodeInstance([
'--inspect-wait=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('Debugger.paused');
return { instance, session };
}
test('should load and stream a static network resource using loadNetworkResource and IO.read', async () => {
const { session } = await setupSessionAndPauseAtEvalLastLine(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;
assert.strictEqual(content, resourceText);
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 setupSessionAndPauseAtEvalLastLine(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 setupSessionAndPauseAtEvalLastLine(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 setupSessionAndPauseAtEvalLastLine(script);
const { resource } = await session.send({
method: 'Network.loadNetworkResource',
params: { url: resourceUrl },
});
assert(resource.success);
assert(resource.stream);
let result = await session.send({ method: 'IO.read', params: { handle: resource.stream, size: 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, resourceText.slice(5, 10));
result = await session.send({ method: 'IO.read', params: { handle: resource.stream, offset: 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();
});
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('${embedPath}', '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' });
await session.waitForNotification('Target.targetCreated');
await session.send({ method: 'Target.setAutoAttach', params: { autoAttach: true, waitForDebuggerOnStart: true } });
let sessionId;
await session.waitForNotification((notification) => {
if (notification.method === 'Target.attachedToTarget') {
sessionId = notification.params.sessionId;
return true;
}
return false;
});
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;
assert.strictEqual(content, resourceText);
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 });
}
});

View file

@ -33,4 +33,5 @@ export interface InspectorBinding {
console: Console;
Connection: InspectorConnectionConstructor;
MainThreadConnection: InspectorConnectionConstructor;
putNetworkResource: (url: string, resource: string) => void;
}