inspector: initial support storage inspection

PR-URL: https://github.com/nodejs/node/pull/61139
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Kohei Ueno <kohei.ueno119@gmail.com>
This commit is contained in:
Ryuhei Shima authored and GitHub committed 2026-01-23 14:51:48 +00:00
1 parent 5d39030b2e
commit ec564135f4
22 files changed
+912 -88

No files matched your search

+11
View File
@@ -1270,6 +1270,17 @@ added:
Use this flag to enable [ShadowRealm][] support.
### `--experimental-storage-inspection`
<!-- YAML
added:
- REPLACEME
-->
> Stability: 1.1 - Active Development
Enable experimental support for storage inspection
### `--experimental-test-coverage`
<!-- YAML
+97
View File
@@ -682,6 +682,103 @@ setNetworkResources().then(() => {
For more details, see the official CDP documentation: [Network.loadNetworkResource](https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-loadNetworkResource)
### `inspector.DOMStorage.domStorageItemAdded`
<!-- YAML
added:
- REPLACEME
-->
* `params` {Object}
* `storageId` {Object}
* `securityOrigin` {string}
* `storageKey` {string}
* `isLocalStorage` {boolean}
* `key` {string}
* `newValue` {string}
This feature is only available with the
`--experimental-storage-inspection` flag enabled.
Broadcasts the `DOMStorage.domStorageItemAdded` event to connected frontends.
This event indicates that a new item has been added to the storage.
### `inspector.DOMStorage.domStorageItemRemoved`
<!-- YAML
added:
- REPLACEME
-->
* `params` {Object}
* `storageId` {Object}
* `securityOrigin` {string}
* `storageKey` {string}
* `isLocalStorage` {boolean}
* `key` {string}
This feature is only available with the
`--experimental-storage-inspection` flag enabled.
Broadcasts the `DOMStorage.domStorageItemRemoved` event to connected frontends.
This event indicates that an item has been removed from the storage.
### `inspector.DOMStorage.domStorageItemUpdated`
<!-- YAML
added:
- REPLACEME
-->
* `params` {Object}
* `storageId` {Object}
* `securityOrigin` {string}
* `storageKey` {string}
* `isLocalStorage` {boolean}
* `key` {string}
* `oldValue` {string}
* `newValue` {string}
This feature is only available with the
`--experimental-storage-inspection` flag enabled.
Broadcasts the `DOMStorage.domStorageItemUpdated` event to connected frontends.
This event indicates that a storage item has been updated.
### `inspector.DOMStorage.domStorageItemsCleared`
<!-- YAML
added:
- REPLACEME
-->
* `params` {Object}
* `storageId` {Object}
* `securityOrigin` {string}
* `storageKey` {string}
* `isLocalStorage` {boolean}
This feature is only available with the
`--experimental-storage-inspection` flag enabled.
Broadcasts the `DOMStorage.domStorageItemsCleared` event to connected
frontends. This event indicates that all items have been cleared from the
storage.
### `inspector.DOMStorage.registerStorage`
<!-- YAML
added:
- REPLACEME
-->
* `params` {Object}
* `isLocalStorage` {boolean}
* `storageMap` {Object}
This feature is only available with the
`--experimental-storage-inspection` flag enabled.
## Support of breakpoints
The Chrome DevTools Protocol [`Debugger` domain][] allows an
+4
View File
@@ -237,6 +237,10 @@ Enable the experimental QUIC support.
.
.It Fl -experimental-inspector-network-resource
Enable experimental support for inspector network resources.
.It Fl -experimental-storage-inspection
Enable experimental support for storage inspection.
.
.It Fl -force-context-aware
Disable loading native addons that are not context-aware.
+11
View File
@@ -229,6 +229,16 @@ const NetworkResources = {
put,
};
const DOMStorage = {
domStorageItemAdded: (params) => broadcastToFrontend('DOMStorage.domStorageItemAdded', params),
domStorageItemRemoved: (params) => broadcastToFrontend('DOMStorage.domStorageItemRemoved', params),
domStorageItemUpdated: (params) => broadcastToFrontend('DOMStorage.domStorageItemUpdated', params),
domStorageItemsCleared: (params) => broadcastToFrontend('DOMStorage.domStorageItemsCleared', params),
// Pseudo-event: not part of the CDP DOMStorage domain.
// Call DOMStorageAgent::registerStorage in inspector/dom_storage_agent.cc.
registerStorage: (params) => broadcastToFrontend('DOMStorage.registerStorage', params),
};
module.exports = {
open: inspectorOpen,
close: _debugEnd,
@@ -238,4 +248,5 @@ module.exports = {
Session,
Network,
NetworkResources,
DOMStorage,
};
+248
View File
@@ -0,0 +1,248 @@
#include "dom_storage_agent.h"
#include "env-inl.h"
#include "inspector/inspector_object_utils.h"
#include "v8-isolate.h"
namespace node {
namespace inspector {
using v8::Array;
using v8::Context;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::Value;
std::unique_ptr<protocol::DOMStorage::StorageId> createStorageIdFromObject(
Local<Context> context, Local<Object> storage_id_obj) {
protocol::String security_origin;
if (!ObjectGetProtocolString(context, storage_id_obj, "securityOrigin")
.To(&security_origin)) {
return {};
}
bool is_local_storage =
ObjectGetBool(context, storage_id_obj, "isLocalStorage").FromMaybe(false);
protocol::String storageKey;
if (!ObjectGetProtocolString(context, storage_id_obj, "storageKey")
.To(&storageKey)) {
return {};
}
return protocol::DOMStorage::StorageId::create()
.setSecurityOrigin(security_origin)
.setIsLocalStorage(is_local_storage)
.setStorageKey(storageKey)
.build();
}
DOMStorageAgent::DOMStorageAgent(Environment* env) : env_(env) {}
DOMStorageAgent::~DOMStorageAgent() {}
void DOMStorageAgent::Wire(protocol::UberDispatcher* dispatcher) {
frontend_ =
std::make_unique<protocol::DOMStorage::Frontend>(dispatcher->channel());
protocol::DOMStorage::Dispatcher::wire(dispatcher, this);
addEventNotifier("domStorageItemAdded",
[this](v8::Local<v8::Context> ctx, v8::Local<v8::Object> p) {
this->domStorageItemAdded(ctx, p);
});
addEventNotifier("domStorageItemRemoved",
[this](v8::Local<v8::Context> ctx, v8::Local<v8::Object> p) {
this->domStorageItemRemoved(ctx, p);
});
addEventNotifier("domStorageItemUpdated",
[this](v8::Local<v8::Context> ctx, v8::Local<v8::Object> p) {
this->domStorageItemUpdated(ctx, p);
});
addEventNotifier("domStorageItemsCleared",
[this](v8::Local<v8::Context> ctx, v8::Local<v8::Object> p) {
this->domStorageItemsCleared(ctx, p);
});
addEventNotifier("registerStorage",
[this](v8::Local<v8::Context> ctx, v8::Local<v8::Object> p) {
this->registerStorage(ctx, p);
});
}
protocol::DispatchResponse DOMStorageAgent::enable() {
this->enabled_ = true;
return protocol::DispatchResponse::Success();
}
protocol::DispatchResponse DOMStorageAgent::disable() {
this->enabled_ = false;
return protocol::DispatchResponse::Success();
}
protocol::DispatchResponse DOMStorageAgent::getDOMStorageItems(
std::unique_ptr<protocol::DOMStorage::StorageId> storageId,
std::unique_ptr<protocol::Array<protocol::Array<protocol::String>>>*
items) {
if (!enabled_) {
return protocol::DispatchResponse::ServerError(
"DOMStorage domain is not enabled");
}
bool is_local_storage = storageId->getIsLocalStorage();
const std::unordered_map<std::string, std::string>& storage_map =
is_local_storage ? local_storage_map_ : session_storage_map_;
auto result =
std::make_unique<protocol::Array<protocol::Array<protocol::String>>>();
for (const auto& pair : storage_map) {
auto item = std::make_unique<protocol::Array<protocol::String>>();
item->push_back(pair.first);
item->push_back(pair.second);
result->push_back(std::move(item));
}
*items = std::move(result);
return protocol::DispatchResponse::Success();
}
protocol::DispatchResponse DOMStorageAgent::setDOMStorageItem(
std::unique_ptr<protocol::DOMStorage::StorageId> storageId,
const std::string& key,
const std::string& value) {
return protocol::DispatchResponse::ServerError("Not implemented");
}
protocol::DispatchResponse DOMStorageAgent::removeDOMStorageItem(
std::unique_ptr<protocol::DOMStorage::StorageId> storageId,
const std::string& key) {
return protocol::DispatchResponse::ServerError("Not implemented");
}
protocol::DispatchResponse DOMStorageAgent::clear(
std::unique_ptr<protocol::DOMStorage::StorageId> storageId) {
return protocol::DispatchResponse::ServerError("Not implemented");
}
void DOMStorageAgent::domStorageItemAdded(Local<Context> context,
Local<Object> params) {
Local<Object> storage_id_obj;
if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
return;
}
std::unique_ptr<protocol::DOMStorage::StorageId> storage_id =
createStorageIdFromObject(context, storage_id_obj);
if (!storage_id) {
return;
}
protocol::String key;
if (!ObjectGetProtocolString(context, params, "key").To(&key)) {
return;
}
protocol::String new_value;
if (!ObjectGetProtocolString(context, params, "newValue").To(&new_value)) {
return;
}
frontend_->domStorageItemAdded(std::move(storage_id), key, new_value);
}
void DOMStorageAgent::domStorageItemRemoved(Local<Context> context,
Local<Object> params) {
Local<Object> storage_id_obj;
if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
return;
}
std::unique_ptr<protocol::DOMStorage::StorageId> storage_id =
createStorageIdFromObject(context, storage_id_obj);
if (!storage_id) {
return;
}
protocol::String key;
if (!ObjectGetProtocolString(context, params, "key").To(&key)) {
return;
}
frontend_->domStorageItemRemoved(std::move(storage_id), key);
}
void DOMStorageAgent::domStorageItemUpdated(Local<Context> context,
Local<Object> params) {
Local<Object> storage_id_obj;
if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
return;
}
std::unique_ptr<protocol::DOMStorage::StorageId> storage_id =
createStorageIdFromObject(context, storage_id_obj);
if (!storage_id) {
return;
}
protocol::String key;
if (!ObjectGetProtocolString(context, params, "key").To(&key)) {
return;
}
protocol::String old_value;
if (!ObjectGetProtocolString(context, params, "oldValue").To(&old_value)) {
return;
}
protocol::String new_value;
if (!ObjectGetProtocolString(context, params, "newValue").To(&new_value)) {
return;
}
frontend_->domStorageItemUpdated(
std::move(storage_id), key, old_value, new_value);
}
void DOMStorageAgent::domStorageItemsCleared(Local<Context> context,
Local<Object> params) {
Local<Object> storage_id_obj;
if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
return;
}
std::unique_ptr<protocol::DOMStorage::StorageId> storage_id =
createStorageIdFromObject(context, storage_id_obj);
if (!storage_id) {
return;
}
frontend_->domStorageItemsCleared(std::move(storage_id));
}
void DOMStorageAgent::registerStorage(Local<Context> context,
Local<Object> params) {
Isolate* isolate = env_->isolate();
HandleScope handle_scope(isolate);
bool is_local_storage;
if (!ObjectGetBool(context, params, "isLocalStorage").To(&is_local_storage)) {
return;
}
Local<Object> storage_map_obj;
if (!ObjectGetObject(context, params, "storageMap")
.ToLocal(&storage_map_obj)) {
return;
}
std::unordered_map<std::string, std::string>& storage_map =
is_local_storage ? local_storage_map_ : session_storage_map_;
Local<Array> property_names;
if (!storage_map_obj->GetOwnPropertyNames(context).ToLocal(&property_names)) {
return;
}
uint32_t length = property_names->Length();
for (uint32_t i = 0; i < length; ++i) {
Local<Value> key_value;
if (!property_names->Get(context, i).ToLocal(&key_value)) {
return;
}
Local<Value> value_value;
if (!storage_map_obj->Get(context, key_value).ToLocal(&value_value)) {
return;
}
node::Utf8Value key_utf8(isolate, key_value);
node::Utf8Value value_utf8(isolate, value_value);
storage_map[*key_utf8] = *value_utf8;
}
}
bool DOMStorageAgent::canEmit(const std::string& domain) {
return domain == "DOMStorage";
}
} // namespace inspector
} // namespace node
+63
View File
@@ -0,0 +1,63 @@
#ifndef SRC_INSPECTOR_DOM_STORAGE_AGENT_H_
#define SRC_INSPECTOR_DOM_STORAGE_AGENT_H_
#include <string>
#include "env.h"
#include "node/inspector/protocol/DOMStorage.h"
#include "notification_emitter.h"
#include "v8.h"
namespace node {
namespace inspector {
class DOMStorageAgent : public protocol::DOMStorage::Backend,
public NotificationEmitter {
public:
explicit DOMStorageAgent(Environment* env);
~DOMStorageAgent() override;
void Wire(protocol::UberDispatcher* dispatcher);
protocol::DispatchResponse enable() override;
protocol::DispatchResponse disable() override;
protocol::DispatchResponse getDOMStorageItems(
std::unique_ptr<protocol::DOMStorage::StorageId> storageId,
std::unique_ptr<protocol::Array<protocol::Array<protocol::String>>>*
items) override;
protocol::DispatchResponse setDOMStorageItem(
std::unique_ptr<protocol::DOMStorage::StorageId> storageId,
const std::string& key,
const std::string& value) override;
protocol::DispatchResponse removeDOMStorageItem(
std::unique_ptr<protocol::DOMStorage::StorageId> storageId,
const std::string& key) override;
protocol::DispatchResponse clear(
std::unique_ptr<protocol::DOMStorage::StorageId> storageId) override;
void domStorageItemAdded(v8::Local<v8::Context> context,
v8::Local<v8::Object> params);
void domStorageItemRemoved(v8::Local<v8::Context> context,
v8::Local<v8::Object> params);
void domStorageItemUpdated(v8::Local<v8::Context> context,
v8::Local<v8::Object> params);
void domStorageItemsCleared(v8::Local<v8::Context> context,
v8::Local<v8::Object> params);
void registerStorage(v8::Local<v8::Context> context,
v8::Local<v8::Object> params);
bool canEmit(const std::string& domain) override;
DOMStorageAgent(const DOMStorageAgent&) = delete;
DOMStorageAgent& operator=(const DOMStorageAgent&) = delete;
private:
std::unique_ptr<protocol::DOMStorage::Frontend> frontend_;
std::unordered_map<std::string, std::string> local_storage_map_ = {};
std::unordered_map<std::string, std::string> session_storage_map_ = {};
bool enabled_ = false;
Environment* env_;
};
} // namespace inspector
} // namespace node
#endif // SRC_INSPECTOR_DOM_STORAGE_AGENT_H_
+67
View File
@@ -0,0 +1,67 @@
experimental domain DOMStorage
type SerializedStorageKey extends string
# DOM Storage identifier.
type StorageId extends object
properties
# Security origin for the storage.
optional string securityOrigin
# Represents a key by which DOM Storage keys its CachedStorageAreas
optional SerializedStorageKey storageKey
# Whether the storage is local storage (not session storage).
boolean isLocalStorage
# DOM Storage item.
type Item extends array of string
command clear
parameters
StorageId storageId
# Disables storage tracking, prevents storage events from being sent to the client.
command disable
# Enables storage tracking, storage events will now be delivered to the client.
command enable
command getDOMStorageItems
parameters
StorageId storageId
returns
array of Item entries
command removeDOMStorageItem
parameters
StorageId storageId
string key
command setDOMStorageItem
parameters
StorageId storageId
string key
string value
event domStorageItemAdded
parameters
StorageId storageId
string key
string newValue
event domStorageItemRemoved
parameters
StorageId storageId
string key
event domStorageItemUpdated
parameters
StorageId storageId
string key
string oldValue
string newValue
event domStorageItemsCleared
parameters
StorageId storageId
+7
View File
@@ -0,0 +1,7 @@
experimental domain Storage
type SerializedStorageKey extends string
experimental command getStorageKey
parameters
optional string frameId
returns
SerializedStorageKey storageKey
+102
View File
@@ -0,0 +1,102 @@
#include "inspector_object_utils.h"
#include "inspector/protocol_helper.h"
#include "util-inl.h"
using v8::Boolean;
using v8::Context;
using v8::EscapableHandleScope;
using v8::HandleScope;
using v8::Int32;
using v8::Isolate;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
namespace node {
namespace inspector {
// Get a protocol string property from the object.
Maybe<protocol::String> ObjectGetProtocolString(Local<Context> context,
Local<Object> object,
Local<String> property) {
HandleScope handle_scope(Isolate::GetCurrent());
Local<Value> value;
if (!object->Get(context, property).ToLocal(&value) || !value->IsString()) {
return Nothing<protocol::String>();
}
Local<String> str = value.As<String>();
return Just(ToProtocolString(Isolate::GetCurrent(), str));
}
// Get a protocol string property from the object.
Maybe<protocol::String> ObjectGetProtocolString(Local<Context> context,
Local<Object> object,
const char* property) {
HandleScope handle_scope(Isolate::GetCurrent());
return ObjectGetProtocolString(
context, object, OneByteString(Isolate::GetCurrent(), property));
}
// Get a protocol double property from the object.
Maybe<double> ObjectGetDouble(Local<Context> context,
Local<Object> object,
const char* property) {
HandleScope handle_scope(Isolate::GetCurrent());
Local<Value> value;
if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property))
.ToLocal(&value) ||
!value->IsNumber()) {
return Nothing<double>();
}
return Just(value.As<Number>()->Value());
}
// Get a protocol int property from the object.
Maybe<int> ObjectGetInt(Local<Context> context,
Local<Object> object,
const char* property) {
HandleScope handle_scope(Isolate::GetCurrent());
Local<Value> value;
if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property))
.ToLocal(&value) ||
!value->IsInt32()) {
return Nothing<int>();
}
return Just(value.As<Int32>()->Value());
}
// Get a protocol bool property from the object.
Maybe<bool> ObjectGetBool(Local<Context> context,
Local<Object> object,
const char* property) {
HandleScope handle_scope(Isolate::GetCurrent());
Local<Value> value;
if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property))
.ToLocal(&value) ||
!value->IsBoolean()) {
return Nothing<bool>();
}
return Just(value.As<Boolean>()->Value());
}
// Get an object property from the object.
MaybeLocal<Object> ObjectGetObject(Local<Context> context,
Local<Object> object,
const char* property) {
EscapableHandleScope handle_scope(Isolate::GetCurrent());
Local<Value> value;
if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property))
.ToLocal(&value) ||
!value->IsObject()) {
return {};
}
return handle_scope.Escape(value.As<Object>());
}
} // namespace inspector
} // namespace node
+38
View File
@@ -0,0 +1,38 @@
#ifndef SRC_INSPECTOR_INSPECTOR_OBJECT_UTILS_H_
#define SRC_INSPECTOR_INSPECTOR_OBJECT_UTILS_H_
#include <v8.h>
#include "node/inspector/protocol/Protocol.h"
namespace node {
namespace inspector {
v8::Maybe<protocol::String> ObjectGetProtocolString(
v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
v8::Local<v8::String> property);
v8::Maybe<protocol::String> ObjectGetProtocolString(
v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const char* property);
v8::Maybe<double> ObjectGetDouble(v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const char* property);
v8::Maybe<int> ObjectGetInt(v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const char* property);
v8::Maybe<bool> ObjectGetBool(v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const char* property);
v8::MaybeLocal<v8::Object> ObjectGetObject(v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const char* property);
} // namespace inspector
} // namespace node
#endif // SRC_INSPECTOR_INSPECTOR_OBJECT_UTILS_H_
+1 -83
View File
@@ -2,6 +2,7 @@
#include <string>
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "inspector/inspector_object_utils.h"
#include "inspector/network_resource_manager.h"
#include "inspector/protocol_helper.h"
#include "network_inspector.h"
@@ -14,98 +15,15 @@
namespace node {
namespace inspector {
using v8::EscapableHandleScope;
using v8::HandleScope;
using v8::Isolate;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Object;
using v8::Uint8Array;
using v8::Value;
constexpr size_t kDefaultMaxTotalBufferSize = 100 * 1024 * 1024; // 100MB
// Get a protocol string property from the object.
Maybe<protocol::String> ObjectGetProtocolString(v8::Local<v8::Context> context,
Local<Object> object,
Local<v8::String> property) {
HandleScope handle_scope(Isolate::GetCurrent());
Local<Value> value;
if (!object->Get(context, property).ToLocal(&value) || !value->IsString()) {
return Nothing<protocol::String>();
}
Local<v8::String> str = value.As<v8::String>();
return Just(ToProtocolString(Isolate::GetCurrent(), str));
}
// Get a protocol string property from the object.
Maybe<protocol::String> ObjectGetProtocolString(v8::Local<v8::Context> context,
Local<Object> object,
const char* property) {
HandleScope handle_scope(Isolate::GetCurrent());
return ObjectGetProtocolString(
context, object, OneByteString(Isolate::GetCurrent(), property));
}
// Get a protocol double property from the object.
Maybe<double> ObjectGetDouble(v8::Local<v8::Context> context,
Local<Object> object,
const char* property) {
HandleScope handle_scope(Isolate::GetCurrent());
Local<Value> value;
if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property))
.ToLocal(&value) ||
!value->IsNumber()) {
return Nothing<double>();
}
return Just(value.As<v8::Number>()->Value());
}
// Get a protocol int property from the object.
Maybe<int> ObjectGetInt(v8::Local<v8::Context> context,
Local<Object> object,
const char* property) {
HandleScope handle_scope(Isolate::GetCurrent());
Local<Value> value;
if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property))
.ToLocal(&value) ||
!value->IsInt32()) {
return Nothing<int>();
}
return Just(value.As<v8::Int32>()->Value());
}
// Get a protocol bool property from the object.
Maybe<bool> ObjectGetBool(v8::Local<v8::Context> context,
Local<Object> object,
const char* property) {
HandleScope handle_scope(Isolate::GetCurrent());
Local<Value> value;
if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property))
.ToLocal(&value) ||
!value->IsBoolean()) {
return Nothing<bool>();
}
return Just(value.As<v8::Boolean>()->Value());
}
// Get an object property from the object.
MaybeLocal<v8::Object> ObjectGetObject(v8::Local<v8::Context> context,
Local<Object> object,
const char* property) {
EscapableHandleScope handle_scope(Isolate::GetCurrent());
Local<Value> value;
if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property))
.ToLocal(&value) ||
!value->IsObject()) {
return {};
}
return handle_scope.Escape(value.As<v8::Object>());
}
// Create a protocol::Network::Headers from the v8 object.
std::unique_ptr<protocol::Network::Headers> createHeadersFromObject(
v8::Local<v8::Context> context, Local<Object> headers_obj) {
+14
View File
@@ -40,6 +40,14 @@
'src/inspector/io_agent.h',
'src/inspector/network_resource_manager.cc',
'src/inspector/network_resource_manager.h',
'src/inspector/dom_storage_agent.cc',
'src/inspector/dom_storage_agent.h',
'src/inspector/inspector_object_utils.cc',
'src/inspector/inspector_object_utils.h',
'src/inspector/storage_agent.h',
'src/inspector/storage_agent.cc',
'src/inspector/notification_emitter.h',
'src/inspector/notification_emitter.cc',
],
'node_inspector_generated_sources': [
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Forward.h',
@@ -57,6 +65,10 @@
'<(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',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/DOMStorage.h',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/DOMStorage.cpp',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Storage.cpp',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Storage.h',
],
'node_protocol_files': [
'<(protocol_tool_path)/lib/Forward_h.template',
@@ -80,6 +92,8 @@
'domain_node_tracing.pdl',
'domain_node_worker.pdl',
'domain_target.pdl',
'domain_dom_storage.pdl',
'domain_storage.pdl',
],
},
'defines': [
+2
View File
@@ -9,3 +9,5 @@ include domain_node_runtime.pdl
include domain_node_tracing.pdl
include domain_node_worker.pdl
include domain_target.pdl
include domain_dom_storage.pdl
include domain_storage.pdl
+24
View File
@@ -0,0 +1,24 @@
#include "notification_emitter.h"
#include "v8-inspector.h"
#include "v8.h"
namespace node {
namespace inspector {
NotificationEmitter::NotificationEmitter() {}
void NotificationEmitter::emitNotification(v8::Local<v8::Context> context,
const EventKey& event,
v8::Local<v8::Object> params) {
auto it = event_notifier_map_.find(event);
if (it != event_notifier_map_.end() && it->second) {
it->second(context, params);
}
}
void NotificationEmitter::addEventNotifier(const EventKey& event,
EventNotifier notifier) {
event_notifier_map_[event] = notifier;
}
} // namespace inspector
} // namespace node
+38
View File
@@ -0,0 +1,38 @@
#ifndef SRC_INSPECTOR_NOTIFICATION_EMITTER_H_
#define SRC_INSPECTOR_NOTIFICATION_EMITTER_H_
#include <string>
#include <unordered_map>
#include "v8.h"
namespace node {
namespace inspector {
class NotificationEmitter {
public:
using EventKey = std::string;
using EventNotifier =
std::function<void(v8::Local<v8::Context>, v8::Local<v8::Object>)>;
NotificationEmitter();
virtual ~NotificationEmitter() = default;
void emitNotification(v8::Local<v8::Context> context,
const EventKey& event,
v8::Local<v8::Object> params);
virtual bool canEmit(const std::string& domain) = 0;
NotificationEmitter(const NotificationEmitter&) = delete;
NotificationEmitter& operator=(const NotificationEmitter&) = delete;
protected:
void addEventNotifier(const EventKey& event, EventNotifier notifier);
private:
std::unordered_map<EventKey, EventNotifier> event_notifier_map_ = {};
};
} // namespace inspector
} // namespace node
#endif // SRC_INSPECTOR_NOTIFICATION_EMITTER_H_
+32
View File
@@ -0,0 +1,32 @@
#include "inspector/storage_agent.h"
#include <string>
#include "env-inl.h"
#include "node_url.h"
namespace node {
namespace inspector {
namespace protocol {
StorageAgent::StorageAgent(Environment* env) : env_(env) {}
StorageAgent::~StorageAgent() {}
void StorageAgent::Wire(protocol::UberDispatcher* dispatcher) {
frontend_ =
std::make_unique<protocol::Storage::Frontend>(dispatcher->channel());
protocol::Storage::Dispatcher::wire(dispatcher, this);
}
DispatchResponse StorageAgent::getStorageKey(
std::optional<protocol::String> frameId, protocol::String* storageKey) {
auto local_storage_file = env_->options()->localstorage_file;
*storageKey = node::url::FromFilePath(to_absolute_path(local_storage_file));
return protocol::DispatchResponse::Success();
}
std::string StorageAgent::to_absolute_path(const std::filesystem::path& input) {
std::filesystem::path abs =
std::filesystem::weakly_canonical(std::filesystem::absolute(input));
return abs.generic_string();
}
} // namespace protocol
} // namespace inspector
} // namespace node
+33
View File
@@ -0,0 +1,33 @@
#ifndef SRC_INSPECTOR_STORAGE_AGENT_H_
#define SRC_INSPECTOR_STORAGE_AGENT_H_
#include "env.h"
#include "node/inspector/protocol/Storage.h"
namespace node {
namespace inspector {
namespace protocol {
class StorageAgent : public protocol::Storage::Backend {
public:
explicit StorageAgent(Environment* env);
~StorageAgent() override;
void Wire(protocol::UberDispatcher* dispatcher);
DispatchResponse getStorageKey(std::optional<protocol::String> frameId,
protocol::String* storageKey) override;
StorageAgent(const StorageAgent&) = delete;
StorageAgent& operator=(const StorageAgent&) = delete;
private:
std::string to_absolute_path(const std::filesystem::path& input);
std::unique_ptr<protocol::Storage::Frontend> frontend_;
Environment* env_;
};
} // namespace protocol
} // namespace inspector
} // namespace node
#endif // SRC_INSPECTOR_STORAGE_AGENT_H_
+28 -4
View File
@@ -1,13 +1,18 @@
#include "inspector_agent.h"
#include <cstddef>
#include <memory>
#include "crdtp/json.h"
#include "env-inl.h"
#include "inspector/dom_storage_agent.h"
#include "inspector/io_agent.h"
#include "inspector/main_thread_interface.h"
#include "inspector/network_inspector.h"
#include "inspector/node_json.h"
#include "inspector/node_string.h"
#include "inspector/protocol_helper.h"
#include "inspector/runtime_agent.h"
#include "inspector/storage_agent.h"
#include "inspector/target_agent.h"
#include "inspector/tracing_agent.h"
#include "inspector/worker_agent.h"
@@ -225,6 +230,7 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
bool prevent_shutdown)
: delegate_(std::move(delegate)),
main_thread_(main_thread),
env_(env),
prevent_shutdown_(prevent_shutdown),
retaining_context_(false) {
session_ = inspector->connect(CONTEXT_GROUP_ID,
@@ -259,6 +265,12 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
target_agent_->Wire(node_dispatcher_.get());
target_agent_->listenWorker(worker_manager);
}
if (env->options()->experimental_storage_inspection) {
dom_storage_agent_ = std::make_unique<DOMStorageAgent>(env);
dom_storage_agent_->Wire(node_dispatcher_.get());
storage_agent_ = std::make_unique<protocol::StorageAgent>(env_);
storage_agent_->Wire(node_dispatcher_.get());
}
}
~ChannelImpl() override {
@@ -275,6 +287,14 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
if (target_agent_) {
target_agent_->reset();
}
if (storage_agent_) {
storage_agent_->disable();
storage_agent_.reset();
}
if (dom_storage_agent_) {
dom_storage_agent_->disable();
dom_storage_agent_.reset();
}
}
void emitNotificationFromBackend(v8::Local<v8::Context> context,
@@ -283,11 +303,13 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
std::string raw_event = protocol::StringUtil::StringViewToUtf8(event);
std::string domain_name = raw_event.substr(0, raw_event.find('.'));
std::string event_name = raw_event.substr(raw_event.find('.') + 1);
if (network_inspector_->canEmit(domain_name)) {
if (network_inspector_->canEmit(domain_name) &&
env_->options()->experimental_network_inspection) {
network_inspector_->emitNotification(
context, domain_name, event_name, params);
} else {
UNREACHABLE("Unknown domain for emitNotificationFromBackend");
} else if (dom_storage_agent_ && dom_storage_agent_->canEmit(domain_name) &&
env_->options()->experimental_storage_inspection) {
dom_storage_agent_->emitNotification(context, event_name, params);
}
}
@@ -418,11 +440,14 @@ 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::unique_ptr<DOMStorageAgent> dom_storage_agent_;
std::unique_ptr<protocol::StorageAgent> storage_agent_;
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_;
std::shared_ptr<MainThreadHandle> main_thread_;
Environment* env_;
bool prevent_shutdown_;
bool retaining_context_;
};
@@ -942,7 +967,6 @@ std::unique_ptr<InspectorSession> Agent::ConnectToMainThread(
void Agent::EmitProtocolEvent(v8::Local<v8::Context> context,
const StringView& event,
Local<Object> params) {
if (!env()->options()->experimental_network_inspection) return;
client_->emitNotification(context, event, params);
}
+3
View File
@@ -799,6 +799,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
AddOption("--experimental-network-inspection",
"experimental network inspection support",
&EnvironmentOptions::experimental_network_inspection);
AddOption("--experimental-storage-inspection",
"experimental storage inspection support",
&EnvironmentOptions::experimental_storage_inspection);
AddOption("--experimental-worker-inspection",
"experimental worker inspection support",
&EnvironmentOptions::experimental_worker_inspection);
+1
View File
@@ -176,6 +176,7 @@ class EnvironmentOptions : public Options {
bool cpu_prof = false;
bool experimental_network_inspection = false;
bool experimental_worker_inspection = false;
bool experimental_storage_inspection = false;
bool experimental_inspector_network_resource = false;
std::string heap_prof_dir;
std::string heap_prof_name;
@@ -0,0 +1,56 @@
// Flags: --inspect=0 --experimental-storage-inspection --localstorage-file=./localstorage.db
'use strict';
const common = require('../common');
const assert = require('assert');
common.skipIfInspectorDisabled();
const { DOMStorage, Session } = require('node:inspector/promises');
const { pathToFileURL } = require('node:url');
const path = require('node:path');
async function test() {
const session = new Session();
await session.connect();
await session.post('DOMStorage.enable');
const localStorageFileUrl =
pathToFileURL(path.join(process.cwd(), 'localstorage.db')).href;
const { storageKey } = await session.post('Storage.getStorageKey');
assert.strictEqual(
storageKey.toLowerCase(),
localStorageFileUrl.toLowerCase()
);
await checkStorage(true);
await checkStorage(false);
async function checkStorage(isLocalStorage) {
DOMStorage.registerStorage({
isLocalStorage,
storageMap: {
key1: 'value1',
key2: 'value2',
[1]: 2,
[true]: 'booleanKey',
},
});
const result = await session.post('DOMStorage.getDOMStorageItems', {
storageId: {
isLocalStorage,
securityOrigin: 'node-inspector://default-dom-storage',
},
});
const sortedEntries = result.entries.sort((a, b) => a[0].localeCompare(b[0]));
assert.deepStrictEqual(sortedEntries, [
['1', '2'],
['key1', 'value1'],
['key2', 'value2'],
['true', 'booleanKey'],
]);
}
}
test().then(common.mustCall());
@@ -1,4 +1,4 @@
// Flags: --inspect=0 --experimental-network-inspection
// Flags: --inspect=0 --experimental-network-inspection --experimental-storage-inspection
'use strict';
const common = require('../common');
@@ -116,6 +116,36 @@ const EXPECTED_EVENTS = {
}
},
],
DOMStorage: [
{
name: 'domStorageItemAdded',
params: {
storageId: {
securityOrigin: '',
isLocalStorage: true,
storageKey: 'node-inspector://default-dom-storage',
},
key: 'testKey',
newValue: 'testValue',
}
},
{
name: 'domStorageItemRemoved',
skip: true
},
{
name: 'domStorageItemUpdated',
skip: true
},
{
name: 'domStorageItemsCleared',
skip: true
},
{
name: 'registerStorage',
skip: true
},
]
};
@@ -146,6 +176,7 @@ for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) {
// Check that all events emit the expected parameters.
await session.post('Network.enable');
await session.post('DOMStorage.enable');
for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) {
for (const event of events) {
if (event.skip) {