diff --git a/doc/api/cli.md b/doc/api/cli.md index 79fe3094ba2301..0a2016a745b0e5 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1122,6 +1122,17 @@ added: Enable experimental support for inspector network resources. +### `--experimental-inspector-storage-key=key` + + + +> Stability: 1.1 - Active Development + +Specify a inspector storage key used by the Node.js inspector when connecting to Chrome DevTools. + ### `--experimental-loader=module` + +* `params` {Object} + * `storageId` {Object} + * `securityOrigin` {string} + * `storageKey` {string} + * `isLocalStorage` {boolean} + * `key` {string} + * `newValue` {string} + +This feature is only available with the +`--experimental-inspector-storage-key=key` 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` + + + +* `params` {Object} + * `storageId` {Object} + * `securityOrigin` {string} + * `storageKey` {string} + * `isLocalStorage` {boolean} + * `key` {string} + +This feature is only available with the +`--experimental-inspector-storage-key=key` 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` + + + +* `params` {Object} + * `storageId` {Object} + * `securityOrigin` {string} + * `storageKey` {string} + * `isLocalStorage` {boolean} + * `key` {string} + * `oldValue` {string} + * `newValue` {string} + +This feature is only available with the +`--experimental-inspector-storage-key=key` flag enabled. + +Broadcasts the `DOMStorage.domStorageItemUpdated` event to connected frontends. +This event indicates that a storage item has been updated. + +### `inspector.DOMStorage.domStorageItemsCleared` + + + +* `params` {Object} + * `storageId` {Object} + * `securityOrigin` {string} + * `storageKey` {string} + * `isLocalStorage` {boolean} + +This feature is only available with the +`--experimental-inspector-storage-key=key` 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` + + + +* `params` {Object} + * `isLocalStorage` {boolean} + * `storageMap` {Object} + +This feature is only available with the +`--experimental-inspector-storage-key=key` flag enabled. + +Registers a storage map with the inspector. The registered storage entries are +reported when getDOMStorageItems is called. + ## Support of breakpoints The Chrome DevTools Protocol [`Debugger` domain][] allows an diff --git a/doc/node.1 b/doc/node.1 index 540ce661a0d182..97b0cde0e5a149 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -231,6 +231,10 @@ Enable the experimental QUIC support. . .It Fl -experimental-inspector-network-resource Enable experimental support for inspector network resources. + +.It Fl -experimental-inspector-storage-key +Specify a inspector storage key used by the Node.js inspector when connecting to Chrome DevTools. + . .It Fl -force-context-aware Disable loading native addons that are not context-aware. diff --git a/lib/inspector.js b/lib/inspector.js index a6b835bbc7d529..1c440794f3932f 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -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, }; diff --git a/src/inspector/dom_storage_agent.cc b/src/inspector/dom_storage_agent.cc new file mode 100644 index 00000000000000..7327cc73516653 --- /dev/null +++ b/src/inspector/dom_storage_agent.cc @@ -0,0 +1,238 @@ +#include "dom_storage_agent.h" + +#include "crdtp/dispatch.h" +#include "env-inl.h" +#include "inspector/inspector_object_utils.h" +#include "v8-isolate.h" + +namespace node { +namespace inspector { + +using v8::Array; +using v8::HandleScope; +using v8::Local; +using v8::Object; +using v8::Value; + +std::unique_ptr createStorageIdFromObject( + v8::Local context, v8::Local 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(dispatcher->channel()); + protocol::DOMStorage::Dispatcher::wire(dispatcher, this); + event_notifier_map_["domStorageItemAdded"] = + (EventNotifier)(&DOMStorageAgent::domStorageItemAdded); + event_notifier_map_["domStorageItemRemoved"] = + (EventNotifier)(&DOMStorageAgent::domStorageItemRemoved); + event_notifier_map_["domStorageItemUpdated"] = + (EventNotifier)(&DOMStorageAgent::domStorageItemUpdated); + event_notifier_map_["domStorageItemsCleared"] = + (EventNotifier)(&DOMStorageAgent::domStorageItemsCleared); + event_notifier_map_["registerStorage"] = + (EventNotifier)(&DOMStorageAgent::registerStorage); +} + +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 storageId, + std::unique_ptr>>* + items) { + if (!enabled_) { + return protocol::DispatchResponse::ServerError( + "DOMStorage domain is not enabled"); + } + bool is_local_storage = storageId->getIsLocalStorage(); + const std::unordered_map& storage_map = + is_local_storage ? local_storage_map_ : session_storage_map_; + auto result = + std::make_unique>>(); + for (const auto& pair : storage_map) { + auto item = std::make_unique>(); + 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 storageId, + const std::string& key, + const std::string& value) { + return protocol::DispatchResponse::ServerError("Not implemented"); +} + +protocol::DispatchResponse DOMStorageAgent::removeDOMStorageItem( + std::unique_ptr storageId, + const std::string& key) { + return protocol::DispatchResponse::ServerError("Not implemented"); +} + +protocol::DispatchResponse DOMStorageAgent::clear( + std::unique_ptr storageId) { + return protocol::DispatchResponse::ServerError("Not implemented"); +} + +void DOMStorageAgent::domStorageItemAdded(v8::Local context, + v8::Local params) { + Local storage_id_obj; + if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + return; + } + + std::unique_ptr 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(v8::Local context, + v8::Local params) { + Local storage_id_obj; + if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + return; + } + std::unique_ptr 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(v8::Local context, + v8::Local params) { + Local storage_id_obj; + if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + return; + } + + std::unique_ptr 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(v8::Local context, + v8::Local params) { + Local storage_id_obj; + if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + return; + } + std::unique_ptr storage_id = + createStorageIdFromObject(context, storage_id_obj); + + if (!storage_id) { + return; + } + frontend_->domStorageItemsCleared(std::move(storage_id)); +} + +void DOMStorageAgent::registerStorage(v8::Local context, + v8::Local params) { + v8::Isolate* isolate = env_->isolate(); + HandleScope handle_scope(isolate); + bool is_local_storage; + if (!ObjectGetBool(context, params, "isLocalStorage").To(&is_local_storage)) { + return; + } + Local storage_map_obj; + if (!ObjectGetObject(context, params, "storageMap") + .ToLocal(&storage_map_obj)) { + return; + } + std::unordered_map& storage_map = + is_local_storage ? local_storage_map_ : session_storage_map_; + Local 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 key_value; + if (!property_names->Get(context, i).ToLocal(&key_value)) { + return; + } + Local 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 diff --git a/src/inspector/dom_storage_agent.h b/src/inspector/dom_storage_agent.h new file mode 100644 index 00000000000000..7d1d84c61f6499 --- /dev/null +++ b/src/inspector/dom_storage_agent.h @@ -0,0 +1,63 @@ +#ifndef SRC_INSPECTOR_DOM_STORAGE_AGENT_H_ +#define SRC_INSPECTOR_DOM_STORAGE_AGENT_H_ + +#include +#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 storageId, + std::unique_ptr>>* + items) override; + protocol::DispatchResponse setDOMStorageItem( + std::unique_ptr storageId, + const std::string& key, + const std::string& value) override; + protocol::DispatchResponse removeDOMStorageItem( + std::unique_ptr storageId, + const std::string& key) override; + protocol::DispatchResponse clear( + std::unique_ptr storageId) override; + + void domStorageItemAdded(v8::Local context, + v8::Local params); + void domStorageItemRemoved(v8::Local context, + v8::Local params); + void domStorageItemUpdated(v8::Local context, + v8::Local params); + void domStorageItemsCleared(v8::Local context, + v8::Local params); + void registerStorage(v8::Local context, + v8::Local params); + bool canEmit(const std::string& domain) override; + + DOMStorageAgent(const DOMStorageAgent&) = delete; + DOMStorageAgent& operator=(const DOMStorageAgent&) = delete; + + private: + std::unique_ptr frontend_; + std::unordered_map local_storage_map_; + std::unordered_map session_storage_map_; + bool enabled_; + Environment* env_; +}; + +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_DOM_STORAGE_AGENT_H_ diff --git a/src/inspector/domain_dom_storage.pdl b/src/inspector/domain_dom_storage.pdl new file mode 100644 index 00000000000000..64993819533d9c --- /dev/null +++ b/src/inspector/domain_dom_storage.pdl @@ -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 + + diff --git a/src/inspector/domain_storage.pdl b/src/inspector/domain_storage.pdl new file mode 100644 index 00000000000000..837def94db6e70 --- /dev/null +++ b/src/inspector/domain_storage.pdl @@ -0,0 +1,7 @@ +experimental domain Storage + type SerializedStorageKey extends string + experimental command getStorageKey + parameters + optional string frameId + returns + SerializedStorageKey storageKey diff --git a/src/inspector/inspector_object_utils.cc b/src/inspector/inspector_object_utils.cc new file mode 100644 index 00000000000000..1b1af23adbe56d --- /dev/null +++ b/src/inspector/inspector_object_utils.cc @@ -0,0 +1,100 @@ +#include "inspector_object_utils.h" +#include "inspector/protocol_helper.h" +#include "util-inl.h" + +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::Value; + +namespace node { +namespace inspector { +// Get a protocol string property from the object. +Maybe ObjectGetProtocolString(Local context, + Local object, + Local property) { + HandleScope handle_scope(Isolate::GetCurrent()); + Local value; + if (!object->Get(context, property).ToLocal(&value) || !value->IsString()) { + return Nothing(); + } + Local str = value.As(); + return Just(ToProtocolString(Isolate::GetCurrent(), str)); +} + +// Get a protocol string property from the object. +Maybe ObjectGetProtocolString(Local context, + Local 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 ObjectGetDouble(Local context, + Local object, + const char* property) { + HandleScope handle_scope(Isolate::GetCurrent()); + Local value; + if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) + .ToLocal(&value) || + !value->IsNumber()) { + return Nothing(); + } + return Just(value.As()->Value()); +} + +// Get a protocol int property from the object. +Maybe ObjectGetInt(Local context, + Local object, + const char* property) { + HandleScope handle_scope(Isolate::GetCurrent()); + Local value; + if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) + .ToLocal(&value) || + !value->IsInt32()) { + return Nothing(); + } + return Just(value.As()->Value()); +} + +// Get a protocol bool property from the object. +Maybe ObjectGetBool(Local context, + Local object, + const char* property) { + HandleScope handle_scope(Isolate::GetCurrent()); + Local value; + if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) + .ToLocal(&value) || + !value->IsBoolean()) { + return Nothing(); + } + return Just(value.As()->Value()); +} + +// Get an object property from the object. +MaybeLocal ObjectGetObject(Local context, + Local object, + const char* property) { + EscapableHandleScope handle_scope(Isolate::GetCurrent()); + Local value; + if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) + .ToLocal(&value) || + !value->IsObject()) { + return {}; + } + return handle_scope.Escape(value.As()); +} + +} // namespace inspector +} // namespace node diff --git a/src/inspector/inspector_object_utils.h b/src/inspector/inspector_object_utils.h new file mode 100644 index 00000000000000..f69933a8bb55c4 --- /dev/null +++ b/src/inspector/inspector_object_utils.h @@ -0,0 +1,38 @@ +#ifndef SRC_INSPECTOR_INSPECTOR_OBJECT_UTILS_H_ +#define SRC_INSPECTOR_INSPECTOR_OBJECT_UTILS_H_ + +#include +#include "node/inspector/protocol/Protocol.h" + +namespace node { +namespace inspector { + +v8::Maybe ObjectGetProtocolString( + v8::Local context, + v8::Local object, + v8::Local property); + +v8::Maybe ObjectGetProtocolString( + v8::Local context, + v8::Local object, + const char* property); + +v8::Maybe ObjectGetDouble(v8::Local context, + v8::Local object, + const char* property); + +v8::Maybe ObjectGetInt(v8::Local context, + v8::Local object, + const char* property); + +v8::Maybe ObjectGetBool(v8::Local context, + v8::Local object, + const char* property); + +v8::MaybeLocal ObjectGetObject(v8::Local context, + v8::Local object, + const char* property); + +} // namespace inspector +} // namespace node +#endif // SRC_INSPECTOR_INSPECTOR_OBJECT_UTILS_H_ diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index 45b9cc2af591c6..bca1413b85af03 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -2,6 +2,7 @@ #include #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 ObjectGetProtocolString(v8::Local context, - Local object, - Local property) { - HandleScope handle_scope(Isolate::GetCurrent()); - Local value; - if (!object->Get(context, property).ToLocal(&value) || !value->IsString()) { - return Nothing(); - } - Local str = value.As(); - return Just(ToProtocolString(Isolate::GetCurrent(), str)); -} - -// Get a protocol string property from the object. -Maybe ObjectGetProtocolString(v8::Local context, - Local 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 ObjectGetDouble(v8::Local context, - Local object, - const char* property) { - HandleScope handle_scope(Isolate::GetCurrent()); - Local value; - if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) - .ToLocal(&value) || - !value->IsNumber()) { - return Nothing(); - } - return Just(value.As()->Value()); -} - -// Get a protocol int property from the object. -Maybe ObjectGetInt(v8::Local context, - Local object, - const char* property) { - HandleScope handle_scope(Isolate::GetCurrent()); - Local value; - if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) - .ToLocal(&value) || - !value->IsInt32()) { - return Nothing(); - } - return Just(value.As()->Value()); -} - -// Get a protocol bool property from the object. -Maybe ObjectGetBool(v8::Local context, - Local object, - const char* property) { - HandleScope handle_scope(Isolate::GetCurrent()); - Local value; - if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) - .ToLocal(&value) || - !value->IsBoolean()) { - return Nothing(); - } - return Just(value.As()->Value()); -} - -// Get an object property from the object. -MaybeLocal ObjectGetObject(v8::Local context, - Local object, - const char* property) { - EscapableHandleScope handle_scope(Isolate::GetCurrent()); - Local value; - if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) - .ToLocal(&value) || - !value->IsObject()) { - return {}; - } - return handle_scope.Escape(value.As()); -} - // Create a protocol::Network::Headers from the v8 object. std::unique_ptr createHeadersFromObject( v8::Local context, Local headers_obj) { diff --git a/src/inspector/node_inspector.gypi b/src/inspector/node_inspector.gypi index cde02a28d03dfa..e3e2fc140db7c3 100644 --- a/src/inspector/node_inspector.gypi +++ b/src/inspector/node_inspector.gypi @@ -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': [ diff --git a/src/inspector/node_protocol.pdl b/src/inspector/node_protocol.pdl index 87b3d93d39c245..8cd8f5efd93d7f 100644 --- a/src/inspector/node_protocol.pdl +++ b/src/inspector/node_protocol.pdl @@ -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 diff --git a/src/inspector/notification_emitter.cc b/src/inspector/notification_emitter.cc new file mode 100644 index 00000000000000..5e66a2b25950ed --- /dev/null +++ b/src/inspector/notification_emitter.cc @@ -0,0 +1,20 @@ +#include "notification_emitter.h" +#include "v8-inspector.h" +#include "v8.h" + +namespace node { +namespace inspector { + +NotificationEmitter::NotificationEmitter() {} + +void NotificationEmitter::emitNotification(v8::Local context, + const std::string& event, + v8::Local params) { + auto it = event_notifier_map_.find(event); + if (it != event_notifier_map_.end()) { + (this->*(it->second))(context, params); + } +} + +} // namespace inspector +} // namespace node diff --git a/src/inspector/notification_emitter.h b/src/inspector/notification_emitter.h new file mode 100644 index 00000000000000..426f2bf7f78713 --- /dev/null +++ b/src/inspector/notification_emitter.h @@ -0,0 +1,34 @@ +#ifndef SRC_INSPECTOR_NOTIFICATION_EMITTER_H_ +#define SRC_INSPECTOR_NOTIFICATION_EMITTER_H_ + +#include +#include +#include "node/inspector/protocol/Protocol.h" +#include "v8.h" + +namespace node { +namespace inspector { + +class NotificationEmitter { + public: + NotificationEmitter(); + virtual ~NotificationEmitter() = default; + + void emitNotification(v8::Local context, + const protocol::String& event, + v8::Local params); + virtual bool canEmit(const std::string& domain) = 0; + + NotificationEmitter(const NotificationEmitter&) = delete; + NotificationEmitter& operator=(const NotificationEmitter&) = delete; + + protected: + using EventNotifier = void (NotificationEmitter::*)( + v8::Local context, v8::Local); + std::unordered_map event_notifier_map_; +}; + +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_NOTIFICATION_EMITTER_H_ diff --git a/src/inspector/storage_agent.cc b/src/inspector/storage_agent.cc new file mode 100644 index 00000000000000..3935831952413c --- /dev/null +++ b/src/inspector/storage_agent.cc @@ -0,0 +1,28 @@ +#include "inspector/storage_agent.h" +#include +#include "env-inl.h" +#include "inspector/protocol_helper.h" +#include "util-inl.h" +#include "v8-isolate.h" +#include "v8-local-handle.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(dispatcher->channel()); + protocol::Storage::Dispatcher::wire(dispatcher, this); +} +DispatchResponse StorageAgent::getStorageKey( + std::optional frameId, protocol::String* storageKey) { + *storageKey = env_->options()->experimental_inspector_storage_key; + return protocol::DispatchResponse::Success(); +} + +} // namespace protocol +} // namespace inspector +} // namespace node diff --git a/src/inspector/storage_agent.h b/src/inspector/storage_agent.h new file mode 100644 index 00000000000000..455d6a1140b4ef --- /dev/null +++ b/src/inspector/storage_agent.h @@ -0,0 +1,34 @@ +#ifndef SRC_INSPECTOR_STORAGE_AGENT_H_ +#define SRC_INSPECTOR_STORAGE_AGENT_H_ + +#include +#include "env.h" +#include "inspector/notification_emitter.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 frameId, + protocol::String* storageKey) override; + + StorageAgent(const StorageAgent&) = delete; + StorageAgent& operator=(const StorageAgent&) = delete; + + private: + std::unique_ptr frontend_; + Environment* env_; +}; +} // namespace protocol +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_STORAGE_AGENT_H_ diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index f62eb36b57542a..1304310a3ad1a1 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -1,13 +1,18 @@ #include "inspector_agent.h" +#include +#include #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_inspector_storage_key.empty() == false) { + dom_storage_agent_ = std::make_unique(env); + dom_storage_agent_->Wire(node_dispatcher_.get()); + storage_agent_ = std::make_unique(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 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_inspector_storage_key.empty()) { + dom_storage_agent_->emitNotification(context, event_name, params); } } @@ -418,11 +440,14 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, std::unique_ptr worker_agent_; std::shared_ptr target_agent_; std::unique_ptr network_inspector_; + std::unique_ptr dom_storage_agent_; + std::unique_ptr storage_agent_; std::shared_ptr io_agent_; std::unique_ptr delegate_; std::unique_ptr session_; std::unique_ptr node_dispatcher_; std::shared_ptr main_thread_; + Environment* env_; bool prevent_shutdown_; bool retaining_context_; }; @@ -942,7 +967,6 @@ std::unique_ptr Agent::ConnectToMainThread( void Agent::EmitProtocolEvent(v8::Local context, const StringView& event, Local params) { - if (!env()->options()->experimental_network_inspection) return; client_->emitNotification(context, event, params); } diff --git a/src/node_options.cc b/src/node_options.cc index 959a5df163b609..e02efa81c2ccb6 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -783,6 +783,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--experimental-network-inspection", "experimental network inspection support", &EnvironmentOptions::experimental_network_inspection); + AddOption("--experimental-inspector-storage-key", + "experimental storage inspection support", + &EnvironmentOptions::experimental_inspector_storage_key); AddOption("--experimental-worker-inspection", "experimental worker inspection support", &EnvironmentOptions::experimental_worker_inspection); diff --git a/src/node_options.h b/src/node_options.h index 9afdeb983d6b26..e3d3d34d35fad5 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -176,6 +176,7 @@ class EnvironmentOptions : public Options { bool cpu_prof = false; bool experimental_network_inspection = false; bool experimental_worker_inspection = false; + std::string experimental_inspector_storage_key; bool experimental_inspector_network_resource = false; std::string heap_prof_dir; std::string heap_prof_name; diff --git a/test/parallel/test-inspector-dom-storage.js b/test/parallel/test-inspector-dom-storage.js new file mode 100644 index 00000000000000..20585b1742b625 --- /dev/null +++ b/test/parallel/test-inspector-dom-storage.js @@ -0,0 +1,59 @@ +// Flags: --inspect=0 --experimental-inspector-storage-key=node-inspector://default-dom-storage +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { DOMStorage, Session } = require('node:inspector/promises'); + +common.skipIfInspectorDisabled(); + +async function test() { + const session = new Session(); + await session.connect(); + + // Check disabled before enable + await session + .post('DOMStorage.getDOMStorageItems', { + storageId: { + isLocalStorage: true, + securityOrigin: 'node-inspector://default-dom-storage', + }, + }) + .catch( + common.mustCall((err) => { + assert.ok(err.message.includes('DOMStorage domain is not enabled')); + }) + ); + + await session.post('DOMStorage.enable'); + + 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()); diff --git a/test/parallel/test-inspector-emit-protocol-event.js b/test/parallel/test-inspector-emit-protocol-event.js index 3125287c3a53d5..110cf5584aba81 100644 --- a/test/parallel/test-inspector-emit-protocol-event.js +++ b/test/parallel/test-inspector-emit-protocol-event.js @@ -1,4 +1,4 @@ -// Flags: --inspect=0 --experimental-network-inspection +// Flags: --inspect=0 --experimental-network-inspection --experimental-inspector-storage-key=node-inspector://default-dom-storage '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 @@ const runAsyncTest = async () => { // 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) {