diff --git a/doc/api/errors.md b/doc/api/errors.md
index d7b5de448ff8..d94ff49a3503 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1004,6 +1004,12 @@ The `inspector` module is not available for use.
While using the `inspector` module, an attempt was made to use the inspector
before it was connected.
+
+### ERR_INSPECTOR_NO_DOMAIN_OVERRIDE
+
+The `ERR_INSPECTOR_NO_DOMAIN_OVERRIDE` error code indicates an attemp to
+override one of the V8 built in inspector protocol domains.
+
### ERR_INVALID_ARG_TYPE
diff --git a/doc/guides/extending-inspector-protocol.md b/doc/guides/extending-inspector-protocol.md
new file mode 100644
index 000000000000..0ab75e74825d
--- /dev/null
+++ b/doc/guides/extending-inspector-protocol.md
@@ -0,0 +1,77 @@
+# Extending Node.js Inspector Protocol
+
+Node.js Inspector Protocol (based on the [Chrome DevTools](https://chromedevtools.github.io/devtools-protocol/v8/))
+is designed to be easily extensible by the implementers. This document provides
+details on such extension.
+
+## Terminology
+
+* *Inspector session* - message exchange between protocol client and Node.js
+* *Protocol handler* - object with a lifetime of the inspector session that
+interacts with the protocol client.
+* *Protocol domain* - 'API unit' of the inspector protocol. Defines message
+formats for specific and related functionality. Node.js exposes protocol
+domains provided by the V8. These include `Runtime`, `Debugger`, `Profiler` and
+`HeapProfiler`.
+* *Message* - a JSON string passed between Node.js backend and an inspector
+protocol client. Message types are: request, response and notification. Protocol
+client only sends out requests. Messages are asynchronous and client should not
+assume response will be sent immediately after serving the request.
+* *Method field* - a mandatory field in request and notification messages.
+This field value includes *domain* and *method* strings separated by a dot
+(e.g. `Debugger.pause`).
+
+## API
+
+`internal/inspector_node_domains` module provides API for extending inspector
+protocol exposed by the Node.js
+
+### inspector_node_domains.registerDomainHandlerClass(domain, constructor)
+
+* domain {string} - a JavaScript identifier string that serves as a unique
+identifier for the messages that target this domain. Domains should be unique.
+It is not permitted to override built-in Node.js domains.
+* constructor {ES6 class|constructor function} - creates a protocol handler that
+will receive messages for the specified domain. This function should accept a
+single callback argument. This callback can be used by the handler to send
+notifications to protocol clients. Callback accepts two arguments - *method* and
+optional *parameters* object. This callback can be called multiple times.
+
+### Handler method
+
+Protocol dispatcher will call handler method with the name that was specified in
+the *method field*. Handler method will receive message *params* object as an
+argument. Handler method can return one of the following:
+* Nothing. Empty response will be sent to the client.
+* Object. This object will be sent to the client as *params* field of
+the response.
+* Throw exception. Error response will be sent to the client. Exception message
+will be included in the response. In the case thrown object does not have the
+message field, entire object will be included in the response.
+* Function that accepts a callback argument. This function will be invoked
+immediately by the dispatcher and enables asynchronous processing of
+the inspector message.
+
+### [SessionTerminatedSymbol] handler method
+
+This method is invoked when inspector session is terminated and should perform
+all necessary cleanup.
+
+## Things to keep in mind
+
+Regular event loop is interrupted to process the protocol message. This is to
+ensure that tooling is available even while the application is running a
+continuous body of code (e.g. infinite loop) or is waiting for IO events.
+
+This means that the handler implementation should be careful when relying on
+async APIs (e.g. promise resolution) as those events may never happen if
+the user code is stuck in the infinite loop or if the JavaScript VM is paused on
+a breakpoint.
+
+## Being a good citizen
+
+* Any state should not outlive the session state. E.g. event collection should
+be disabled, buffers should be freed.
+* Handlers should never be initiating the exchange. E.g. handlers should not
+send any notifications before the client explicitly requests them. Convention
+is to only send notification after the client invokes '*Domain*.enable' method.
diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js
index d8ebd9af968c..a837bf19e6f3 100644
--- a/lib/internal/bootstrap_node.js
+++ b/lib/internal/bootstrap_node.js
@@ -62,6 +62,7 @@
NativeModule.require('internal/trace_events_async_hooks').setup();
NativeModule.require('internal/inspector_async_hook').setup();
+ NativeModule.require('internal/inspector_node_domains').setup();
// Do not initialize channel in debugger agent, it deletes env variable
// and the main thread won't see it.
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 619732df5d6f..513ec7acc61b 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -367,6 +367,7 @@ E('ERR_INSPECTOR_ALREADY_CONNECTED', 'The inspector is already connected');
E('ERR_INSPECTOR_CLOSED', 'Session was closed');
E('ERR_INSPECTOR_NOT_AVAILABLE', 'Inspector is not available');
E('ERR_INSPECTOR_NOT_CONNECTED', 'Session is not connected');
+E('ERR_INSPECTOR_NO_DOMAIN_OVERRIDE', 'Cannot override V8 protocol domain');
E('ERR_INVALID_ARG_TYPE', invalidArgType);
E('ERR_INVALID_ARG_VALUE', (name, value) =>
`The value "${String(value)}" is invalid for argument "${name}"`);
diff --git a/lib/internal/inspector_node_domains.js b/lib/internal/inspector_node_domains.js
new file mode 100644
index 000000000000..869cfc084851
--- /dev/null
+++ b/lib/internal/inspector_node_domains.js
@@ -0,0 +1,444 @@
+'use strict';
+
+const assert = require('assert');
+const errors = require('internal/errors');
+const EventEmitter = require('events');
+const util = require('util');
+
+const { registerDispatcherFactory } = process.binding('inspector');
+
+const domainHandlerClasses = new Map();
+const SessionTerminatedSymbol = Symbol('SessionTerminatedSymbol');
+// V8 does not provide a way to obtain this list programmatically
+const V8_DOMAINS = [ 'Runtime', 'Debugger', 'Profiler', 'HeapProfiler',
+ 'Console', 'Schema' ];
+
+function createNotificationChannel(session, domain) {
+ return function(method, params) {
+ session.sendMessageToFrontend(JSON.stringify({
+ method: `${domain}.${method}`,
+ params
+ }));
+ };
+}
+
+function createResponseCallback(session, id) {
+ let firstCall = true;
+ return function(error, result) {
+ assert.ok(firstCall, 'Callback should only be called once');
+ firstCall = false;
+ const response = { id };
+ if (error)
+ response['error'] = error;
+ else
+ response['result'] = result || {};
+ session.sendMessageToFrontend(JSON.stringify(response));
+ };
+}
+
+function parseSuppressingError(message) {
+ try {
+ return JSON.parse(message);
+ } catch (error) {
+ process.emitWarning(error);
+ return {};
+ }
+}
+
+let targetIdCounter = 1;
+let sessionIdCounter = 1;
+
+const targetsAgent = new class extends EventEmitter {
+ constructor() {
+ super();
+ this._targets = new Map();
+ }
+
+ add(info, attachCallback) {
+ info.targetId = `target_${targetIdCounter++}`;
+ this._targets.set(info.targetId, { info, attachCallback });
+ this.emit('targetCreated', info);
+ return {
+ id: info.targetId, // This is for test only
+ destroyed: () => this._targetDestroyed(info),
+ updated: this._targetUpdated.bind(this, info)
+ };
+ }
+
+ attach(targetId, attachedCallback, messageCallback) {
+ const target = this._targets.get(targetId);
+ if (!target)
+ throw new Error(`Target ${targetId} was not found`);
+ target.attachCallback(attachedCallback, messageCallback);
+ }
+
+ targets() {
+ return Array.from(this._targets.values()).map(({ info }) => info);
+ }
+
+ getTarget(id) {
+ const target = this._targets.get(id);
+ return target && target.info;
+ }
+
+ _targetDestroyed(target) {
+ if (this._targets.delete(target.targetId)) {
+ this.emit('targetDestroyed', target);
+ }
+ }
+
+ _targetUpdated(info, type, title, url) {
+ let updated = false;
+ if (type && info.type !== type) {
+ updated = true;
+ info.type = type;
+ }
+ if (title && info.title !== title) {
+ updated = true;
+ info.title = title;
+ }
+ if (url && info.url !== url) {
+ updated = true;
+ info.url = url;
+ }
+ if (updated)
+ this.emit('targetInfoChanged', info);
+ }
+}();
+
+class TargetDomainHandler {
+ constructor(notificationCallback) {
+ this._notificationCallback = notificationCallback;
+ this._discoverTargets = false;
+ this._autoAttach = false;
+ this._waitForDebuggerOnStart = false;
+ this._listeners = [
+ this._addListener('targetCreated',
+ (target) => this._onTargetCreated(target)),
+ this._addListener('targetDestroyed',
+ (target) => this._onTargetDestroyed(target))
+ ];
+ this._discoveryListeners = [];
+
+ this._sessions = new Map();
+ }
+
+ setDiscoverTargets({ discover }) {
+ discover = !!discover;
+ if (discover === this._discoverTargets)
+ return;
+ this._discoverTargets = discover;
+ if (this._discoverTargets) {
+ this._discoveryListeners = [
+ this._forwardEventAsNotification('targetInfoChanged', true)
+ ];
+ } else {
+ for (const remover of this._discoveryListeners) {
+ remover();
+ }
+ this._discoveryListeners = [];
+ }
+ }
+
+ setAutoAttach({ autoAttach, waitForDebuggerOnStart }) {
+ this._autoAttach = !!autoAttach;
+ this._waitForDebuggerOnStart = !!waitForDebuggerOnStart;
+ if (this._autoAttach)
+ return this._attachToAllTargets.bind(this);
+ else
+ return this._detachFromAllTargets.bind(this);
+ }
+
+ getTargets() {
+ return targetsAgent.targets()
+ .map((target) => this._toProtocolTarget(target));
+ }
+
+ getTargetInfo({ targetId }) {
+ this._checkArgument('targetId', targetId);
+ const target = targetsAgent.getTarget(targetId);
+ if (target)
+ return this._toProtocolTarget(target);
+ else
+ throw new Error(`Target ${targetId} was not found`);
+ }
+
+ attachToTarget({ targetId }) {
+ this._checkArgument('targetId', targetId);
+ return this._attachToTarget.bind(this, targetId);
+ }
+
+ detachFromTarget({ sessionId }) {
+ return this._detachSession.bind(this, sessionId);
+ }
+
+ sendMessageToTarget({ sessionId, message }) {
+ this._checkArgument('message', message);
+ return (callback) => {
+ this._getSessionChecked(sessionId).messageCallback(message, callback);
+ };
+ }
+
+ setAttachToFrames() {
+ // Not implemented, browser specific functionality
+ }
+
+ _checkArgument(name, value) {
+ if (!value)
+ throw new Error(`Argument ${name} was not provided`);
+ }
+
+ _attachToAllTargets(callback) {
+ let count = 1;
+ function decrease() {
+ if ((--count) === 0) {
+ callback();
+ }
+ }
+ for (const target of targetsAgent.targets()) {
+ count++;
+ // Note that we don't care if attach fails
+ try {
+ this._attachToTarget(target.targetId, decrease);
+ } catch (e) {
+ decrease();
+ }
+ }
+ decrease();
+ }
+
+ _detachFromAllTargets(callback) {
+ let count = 1;
+ function decrease() {
+ if ((--count) === 0) {
+ callback();
+ }
+ }
+ for (const sessionId of this._sessions.keys()) {
+ count++;
+ try {
+ this._detachSession(sessionId, decrease);
+ } catch (e) {
+ decrease();
+ }
+ }
+ decrease();
+ }
+
+ _attachToTarget(targetId, callback) {
+ const sessionId = `${targetId}:${sessionIdCounter++}`;
+ const attachedCb = this._onSessionAttached.bind(this, targetId, sessionId,
+ callback);
+ const messageCb = this._onMessage.bind(this, sessionId);
+ targetsAgent.attach(targetId, attachedCb, messageCb);
+ }
+
+ _detachSession(sessionId, callback) {
+ const { targetId, detachCallback } = this._getSessionChecked(sessionId);
+ const target = this._toProtocolTarget(targetsAgent.getTarget(targetId));
+ detachCallback((error) => {
+ this._sessionDetached(sessionId, target);
+ callback(error);
+ });
+ }
+
+ _getSessionChecked(sessionId) {
+ this._checkArgument('sessionId', sessionId);
+ const session = this._sessions.get(sessionId);
+ if (!session)
+ throw new Error(`Session ${sessionId} was not found`);
+ return session;
+ }
+
+ _onMessage(sessionId, message) {
+ const session = this._sessions.get(sessionId);
+ if (!session)
+ return;
+ if (message === null) {
+ this._sessionDetached(sessionId,
+ targetsAgent.getTarget(session.targetId));
+ } else {
+ this._notificationCallback(
+ 'receivedMessageFromTarget', { sessionId, message });
+ }
+
+ }
+
+ _onTargetCreated(target) {
+ if (this._discoverTargets) {
+ const targetInfo = this._toProtocolTarget(target);
+ this._notificationCallback('targetCreated', { targetInfo });
+ }
+ if (this._autoAttach) {
+ this._attachToTarget(target.targetId, () => {});
+ }
+ }
+
+ _onTargetDestroyed(target) {
+ for (const sessionId of this._sessionsForTarget(target.targetId)) {
+ this._sessionDetached(sessionId, target);
+ }
+ if (this._discoverTargets) {
+ const targetInfo = this._toProtocolTarget(target);
+ this._notificationCallback('targetDestroyed', { targetInfo });
+ }
+ }
+
+ _onSessionAttached(targetId, sessionId, callback, error, waitingForDebugger,
+ messageCallback, detachCallback) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ const session = { targetId, detachCallback, messageCallback };
+ this._sessions.set(sessionId, session);
+ const targetInfo = this._toProtocolTarget(targetsAgent.getTarget(targetId));
+ if (this._sessionsForTarget(targetId).length === 1)
+ this._sendTargetInfoChanged(targetInfo);
+ this._notificationCallback('attachedToTarget',
+ { targetInfo, sessionId, waitingForDebugger });
+ callback(null, { sessionId });
+ }
+
+ _sendTargetInfoChanged(targetInfo) {
+ this._notificationCallback('targetInfoChanged', { targetInfo });
+ }
+
+ _sessionDetached(sessionId, target) {
+ if (!this._sessions.has(sessionId))
+ return;
+ const { targetId } = this._sessions.get(sessionId);
+ this._sessions.delete(sessionId);
+ this._notificationCallback('detachedFromTarget', { sessionId });
+ if (this._sessionsForTarget(targetId).length === 0) {
+ this._sendTargetInfoChanged(this._toProtocolTarget(target));
+ }
+ }
+
+ _removeListeners() {
+ for (const remover of this._listeners) {
+ remover();
+ }
+ }
+
+ _forwardEventAsNotification(event, wrap) {
+ const listener = (data) => {
+ const targetInfo = this._toProtocolTarget(data);
+ const object = wrap ? { targetInfo } : targetInfo;
+ this._notificationCallback(event, object);
+ };
+ return this._addListener(event, listener);
+ }
+
+ _addListener(event, listener) {
+ targetsAgent.addListener(event, listener);
+ return () => {
+ targetsAgent.removeListener(event, listener);
+ };
+ }
+
+ _toProtocolTarget(target) {
+ const attached = this._sessionsForTarget(target.targetId).length > 0;
+ return Object.assign({ attached }, target);
+ }
+
+ _sessionsForTarget(tid) {
+ return Array.from(this._sessions.entries())
+ .filter(([, { targetId }]) => targetId === tid)
+ .map(([ agentSessionId ]) => agentSessionId);
+ }
+
+ [SessionTerminatedSymbol]() {
+ for (const { detachCallback } of this._sessions.values())
+ detachCallback(() => {});
+ this._removeListeners();
+ this._listeners.forEach((cancel) => cancel());
+ }
+}
+
+class Dispatcher {
+ constructor(session) {
+ this._session = session;
+ this._domainHandlers = new Map();
+ }
+
+ dispatch(message) {
+ if (domainHandlerClasses.size === 0 && this._domainHandlers.size === 0) {
+ return false;
+ }
+
+ if (message === null) {
+ this._shutdown();
+ return;
+ }
+ const parsed = parseSuppressingError(message);
+ const [ domain, method, tail ] =
+ parsed.method ? parsed.method.split('.') : [];
+
+ if (!domain || !method || !method.match(/^[a-zA-Z]\w*$/) || tail)
+ return false;
+
+ const handler = this._getOrCreateHandler(domain);
+
+ if (!handler || !util.isFunction(handler[method])) {
+ return false;
+ }
+
+ const callback = createResponseCallback(this._session, parsed.id);
+ try {
+ const result = handler[method](parsed.params);
+ if (util.isFunction(result)) {
+ result(callback);
+ } else {
+ callback(null, result);
+ }
+ } catch (e) {
+ callback(e.message || e, null);
+ }
+ return true;
+ }
+
+ _getOrCreateHandler(domain) {
+ let handler = this._domainHandlers.get(domain);
+ if (handler)
+ return handler;
+ var ctor = domainHandlerClasses.get(domain);
+ if (!ctor)
+ return null;
+ handler = new ctor(createNotificationChannel(this._session, domain));
+ this._domainHandlers.set(domain, handler);
+ return handler;
+ }
+
+ _shutdown() {
+ for (const handler of this._domainHandlers.values()) {
+ if (handler[SessionTerminatedSymbol])
+ handler[SessionTerminatedSymbol]();
+ }
+ this._domainHandlers.clear();
+ }
+
+ static create(session) {
+ const dispatcher = new Dispatcher(session);
+ return dispatcher.dispatch.bind(dispatcher);
+ }
+}
+
+function newTarget(type, title, url, attachCallback) {
+ return targetsAgent.add({ type, title, url }, attachCallback);
+}
+
+function registerDomainHandlerClass(domain, factory) {
+ if (V8_DOMAINS.includes('domain'))
+ throw errors.Error('ERR_INSPECTOR_NO_DOMAIN_OVERRIDE');
+ domainHandlerClasses.set(domain, factory);
+}
+
+function setup() {
+ registerDomainHandlerClass('Target', TargetDomainHandler);
+ registerDispatcherFactory(Dispatcher.create);
+}
+
+module.exports = {
+ newTarget, registerDomainHandlerClass, setup, SessionTerminatedSymbol
+};
diff --git a/node.gyp b/node.gyp
index ed28b4a6ce19..608541fffd13 100644
--- a/node.gyp
+++ b/node.gyp
@@ -100,6 +100,7 @@
'lib/internal/fs.js',
'lib/internal/http.js',
'lib/internal/inspector_async_hook.js',
+ 'lib/internal/inspector_node_domains.js',
'lib/internal/linkedlist.js',
'lib/internal/loader/Loader.js',
'lib/internal/loader/ModuleMap.js',
diff --git a/src/async_wrap.h b/src/async_wrap.h
index ec9e162ca79b..d1e444e719aa 100644
--- a/src/async_wrap.h
+++ b/src/async_wrap.h
@@ -76,7 +76,8 @@ namespace node {
#if HAVE_INSPECTOR
#define NODE_ASYNC_INSPECTOR_PROVIDER_TYPES(V) \
- V(INSPECTORJSBINDING)
+ V(INSPECTORJSBINDING) \
+ V(INSPECTORJSDISPATCHER)
#else
#define NODE_ASYNC_INSPECTOR_PROVIDER_TYPES(V)
#endif // HAVE_INSPECTOR
diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc
index 875c12efa023..709aa32152bb 100644
--- a/src/inspector_agent.cc
+++ b/src/inspector_agent.cc
@@ -186,55 +186,6 @@ static int StartDebugSignalHandler() {
const int NANOS_PER_MSEC = 1000000;
const int CONTEXT_GROUP_ID = 1;
-class ChannelImpl final : public v8_inspector::V8Inspector::Channel {
- public:
- explicit ChannelImpl(V8Inspector* inspector,
- InspectorSessionDelegate* delegate)
- : delegate_(delegate) {
- session_ = inspector->connect(1, this, StringView());
- }
-
- virtual ~ChannelImpl() {}
-
- void dispatchProtocolMessage(const StringView& message) {
- session_->dispatchProtocolMessage(message);
- }
-
- bool waitForFrontendMessage() {
- return delegate_->WaitForFrontendMessageWhilePaused();
- }
-
- void schedulePauseOnNextStatement(const std::string& reason) {
- std::unique_ptr buffer = Utf8ToStringView(reason);
- session_->schedulePauseOnNextStatement(buffer->string(), buffer->string());
- }
-
- InspectorSessionDelegate* delegate() {
- return delegate_;
- }
-
- private:
- void sendResponse(
- int callId,
- std::unique_ptr message) override {
- sendMessageToFrontend(message->string());
- }
-
- void sendNotification(
- std::unique_ptr message) override {
- sendMessageToFrontend(message->string());
- }
-
- void flushProtocolNotifications() override { }
-
- void sendMessageToFrontend(const StringView& message) {
- delegate_->SendMessageToFrontend(message);
- }
-
- InspectorSessionDelegate* const delegate_;
- std::unique_ptr session_;
-};
-
class InspectorTimer {
public:
InspectorTimer(uv_loop_t* loop,
@@ -296,6 +247,74 @@ class InspectorTimerHandle {
};
} // namespace
+class ChannelImpl final : public v8_inspector::V8Inspector::Channel {
+ public:
+ ChannelImpl(V8Inspector* inspector, InspectorSessionDelegate* delegate)
+ : delegate_(delegate), dispatcher_factory_(nullptr) {
+ session_ = inspector->connect(1, this, StringView());
+ }
+
+ ~ChannelImpl() override {
+ resetDispatcher();
+ }
+
+ void dispatchProtocolMessage(const StringView& message) {
+ if (v8_inspector::V8InspectorSession::canDispatchMethod(message) ||
+ !dispatchToNodeDispatcher(message)) {
+ session_->dispatchProtocolMessage(message);
+ }
+ }
+
+ bool waitForFrontendMessage() {
+ return delegate_->WaitForFrontendMessageWhilePaused();
+ }
+
+ void schedulePauseOnNextStatement(const std::string& reason) {
+ std::unique_ptr buffer = Utf8ToStringView(reason);
+ session_->schedulePauseOnNextStatement(buffer->string(), buffer->string());
+ }
+
+ void setDispatcherFactory(MessageDispatcherFactory* factory) {
+ resetDispatcher();
+ dispatcher_factory_ = factory;
+ }
+
+ void resetDispatcher() {
+ dispatcher_.reset();
+ }
+
+ void sendMessageToFrontend(const StringView& message) {
+ delegate_->SendMessageToFrontend(message);
+ }
+
+ private:
+ bool dispatchToNodeDispatcher(const StringView& message) {
+ if (dispatcher_ == nullptr && dispatcher_factory_ != nullptr) {
+ dispatcher_ = dispatcher_factory_->Create(
+ std::unique_ptr(new DispatcherSession(this)));
+ }
+ return dispatcher_ != nullptr && dispatcher_->HandleMessage(message);
+ }
+
+ void sendResponse(
+ int callId,
+ std::unique_ptr message) override {
+ sendMessageToFrontend(message->string());
+ }
+
+ void sendNotification(
+ std::unique_ptr message) override {
+ sendMessageToFrontend(message->string());
+ }
+
+ void flushProtocolNotifications() override { }
+
+ InspectorSessionDelegate* const delegate_;
+ std::unique_ptr session_;
+ std::unique_ptr dispatcher_;
+ MessageDispatcherFactory* dispatcher_factory_;
+};
+
class NodeInspectorClient : public V8InspectorClient {
public:
NodeInspectorClient(node::Environment* env, node::NodePlatform* platform)
@@ -346,22 +365,23 @@ class NodeInspectorClient : public V8InspectorClient {
terminated_ = true;
}
- void connectFrontend(InspectorSessionDelegate* delegate) {
- CHECK_EQ(channel_, nullptr);
- channel_ = std::unique_ptr(
- new ChannelImpl(client_.get(), delegate));
+ std::unique_ptr connectFrontend(
+ InspectorSessionDelegate* delegate) {
+ if (channel_)
+ return nullptr;
+ channel_.reset(new ChannelImpl(client_.get(), delegate));
+ channel_->setDispatcherFactory(dispatcher_factory_.get());
+ return std::unique_ptr(
+ new InspectorSession(this, channel_.get()));
}
- void disconnectFrontend() {
+ void disconnectFrontend(ChannelImpl* channel) {
+ if (channel_.get() != channel)
+ return;
quitMessageLoopOnPause();
channel_.reset();
}
- void dispatchMessageFromFrontend(const StringView& message) {
- CHECK_NE(channel_, nullptr);
- channel_->dispatchProtocolMessage(message);
- }
-
Local ensureDefaultContextInGroup(int contextGroupId) override {
return env_->context();
}
@@ -447,6 +467,13 @@ class NodeInspectorClient : public V8InspectorClient {
client_->allAsyncTasksCanceled();
}
+ void RegisterDispatcherFactory(
+ std::unique_ptr factory) {
+ dispatcher_factory_ = std::move(factory);
+ if (channel_)
+ channel_->setDispatcherFactory(dispatcher_factory_.get());
+ }
+
private:
node::Environment* env_;
node::NodePlatform* platform_;
@@ -455,8 +482,26 @@ class NodeInspectorClient : public V8InspectorClient {
std::unique_ptr client_;
std::unique_ptr channel_;
std::unordered_map timers_;
+ std::unique_ptr dispatcher_factory_;
};
+InspectorSession::~InspectorSession() {
+ client_->disconnectFrontend(channel_);
+}
+
+void InspectorSession::Dispatch(const v8_inspector::StringView& message) {
+ channel_->dispatchProtocolMessage(message);
+}
+
+DispatcherSession::~DispatcherSession() {
+ channel_->resetDispatcher();
+}
+
+void DispatcherSession::SendMessageToFrontend(
+ const v8_inspector::StringView& message) {
+ channel_->sendMessageToFrontend(message);
+}
+
Agent::Agent(Environment* env) : parent_env_(env),
client_(nullptr),
platform_(nullptr),
@@ -542,9 +587,10 @@ void Agent::Stop() {
}
}
-void Agent::Connect(InspectorSessionDelegate* delegate) {
+std::unique_ptr Agent::Connect(
+ InspectorSessionDelegate* delegate) {
enabled_ = true;
- client_->connectFrontend(delegate);
+ return client_->connectFrontend(delegate);
}
bool Agent::IsConnected() {
@@ -566,29 +612,11 @@ void Agent::FatalException(Local error, Local message) {
WaitForDisconnect();
}
-void Agent::Dispatch(const StringView& message) {
- CHECK_NE(client_, nullptr);
- client_->dispatchMessageFromFrontend(message);
-}
-
-void Agent::Disconnect() {
- CHECK_NE(client_, nullptr);
- client_->disconnectFrontend();
-}
-
void Agent::RunMessageLoop() {
CHECK_NE(client_, nullptr);
client_->runMessageLoopOnPause(CONTEXT_GROUP_ID);
}
-InspectorSessionDelegate* Agent::delegate() {
- CHECK_NE(client_, nullptr);
- ChannelImpl* channel = client_->channel();
- if (channel == nullptr)
- return nullptr;
- return channel->delegate();
-}
-
void Agent::PauseOnNextJavascriptStatement(const std::string& reason) {
ChannelImpl* channel = client_->channel();
if (channel != nullptr)
@@ -690,6 +718,11 @@ bool Agent::IsWaitingForConnect() {
return debug_options_.wait_for_connect();
}
+void Agent::RegisterDispatcherFactory(
+ std::unique_ptr factory) {
+ client_->RegisterDispatcherFactory(std::move(factory));
+}
+
} // namespace inspector
} // namespace node
diff --git a/src/inspector_agent.h b/src/inspector_agent.h
index 7211f5a2a49a..c07e8b2040bb 100644
--- a/src/inspector_agent.h
+++ b/src/inspector_agent.h
@@ -23,6 +23,21 @@ class Environment;
namespace inspector {
+class ChannelImpl;
+class InspectorIo;
+class NodeInspectorClient;
+
+class InspectorSession {
+ public:
+ InspectorSession(NodeInspectorClient* client, ChannelImpl* channel)
+ : channel_(channel), client_(client) { }
+ ~InspectorSession();
+ void Dispatch(const v8_inspector::StringView& message);
+ private:
+ ChannelImpl* channel_;
+ NodeInspectorClient* client_;
+};
+
class InspectorSessionDelegate {
public:
virtual ~InspectorSessionDelegate() = default;
@@ -31,8 +46,27 @@ class InspectorSessionDelegate {
= 0;
};
-class InspectorIo;
-class NodeInspectorClient;
+class DispatcherSession {
+ public:
+ explicit DispatcherSession(ChannelImpl* channel) : channel_(channel) {}
+ ~DispatcherSession();
+ void SendMessageToFrontend(const v8_inspector::StringView& message);
+ private:
+ ChannelImpl* channel_;
+};
+
+class MessageDispatcher {
+ public:
+ virtual ~MessageDispatcher() = default;
+ virtual bool HandleMessage(const v8_inspector::StringView& message) = 0;
+};
+
+class MessageDispatcherFactory {
+ public:
+ virtual ~MessageDispatcherFactory() = default;
+ virtual std::unique_ptr Create(
+ std::unique_ptr session) = 0;
+};
class Agent {
public:
@@ -67,13 +101,13 @@ class Agent {
v8::Local enable_function,
v8::Local disable_function);
+ void RegisterDispatcherFactory(
+ std::unique_ptr factory);
+
// These methods are called by the WS protocol and JS binding to create
// inspector sessions. The inspector responds by using the delegate to send
// messages back.
- void Connect(InspectorSessionDelegate* delegate);
- void Disconnect();
- void Dispatch(const v8_inspector::StringView& message);
- InspectorSessionDelegate* delegate();
+ std::unique_ptr Connect(InspectorSessionDelegate* delegate);
void RunMessageLoop();
bool enabled() { return enabled_; }
diff --git a/src/inspector_io.cc b/src/inspector_io.cc
index 538cbab3c9fe..76b624f89de2 100644
--- a/src/inspector_io.cc
+++ b/src/inspector_io.cc
@@ -121,11 +121,18 @@ std::unique_ptr Utf8ToStringView(const std::string& message) {
class IoSessionDelegate : public InspectorSessionDelegate {
public:
- explicit IoSessionDelegate(InspectorIo* io) : io_(io) { }
+ explicit IoSessionDelegate(Agent* agent, InspectorIo* io)
+ : io_(io), session_(agent->Connect(this)) { }
bool WaitForFrontendMessageWhilePaused() override;
void SendMessageToFrontend(const v8_inspector::StringView& message) override;
+ bool IsConnected() const { return !!session_; }
+ void Dispatch(const StringView& message) {
+ CHECK_NE(session_, nullptr);
+ session_->Dispatch(message);
+ }
private:
InspectorIo* io_;
+ std::unique_ptr session_;
};
// Passed to InspectorSocketServer to handle WS inspector protocol events,
@@ -382,9 +389,10 @@ void InspectorIo::DispatchMessages() {
session_id_ = std::get<1>(task);
state_ = State::kConnected;
fprintf(stderr, "Debugger attached.\n");
- session_delegate_ = std::unique_ptr(
- new IoSessionDelegate(this));
- parent_env_->inspector_agent()->Connect(session_delegate_.get());
+ session_delegate_.reset(
+ new IoSessionDelegate(parent_env_->inspector_agent(), this));
+ if (!session_delegate_->IsConnected())
+ session_delegate_.reset();
break;
case InspectorAction::kEndSession:
CHECK_NE(session_delegate_, nullptr);
@@ -393,11 +401,11 @@ void InspectorIo::DispatchMessages() {
} else {
state_ = State::kAccepting;
}
- parent_env_->inspector_agent()->Disconnect();
session_delegate_.reset();
break;
case InspectorAction::kSendMessage:
- parent_env_->inspector_agent()->Dispatch(message);
+ if (session_delegate_)
+ session_delegate_->Dispatch(message);
break;
}
}
diff --git a/src/inspector_io.h b/src/inspector_io.h
index 7c15466eed91..76a99d03dc64 100644
--- a/src/inspector_io.h
+++ b/src/inspector_io.h
@@ -47,6 +47,8 @@ enum class TransportAction {
kStop
};
+class IoSessionDelegate;
+
class InspectorIo {
public:
InspectorIo(node::Environment* env, v8::Platform* platform,
@@ -143,7 +145,7 @@ class InspectorIo {
// Note that this will live while the async is being closed - likely, past
// the parent object lifespan
std::pair* main_thread_req_;
- std::unique_ptr session_delegate_;
+ std::unique_ptr session_delegate_;
v8::Platform* platform_;
// Message queues
diff --git a/src/inspector_js_api.cc b/src/inspector_js_api.cc
index 428d8391f258..9ad76a47c0f5 100644
--- a/src/inspector_js_api.cc
+++ b/src/inspector_js_api.cc
@@ -32,6 +32,171 @@ std::unique_ptr ToProtocolString(Isolate* isolate,
return StringBuffer::create(StringView(*buffer, buffer.length()));
}
+class JSDispatcher;
+
+class JSDispatcherInterface : private AsyncWrap {
+ public:
+ JSDispatcherInterface(Environment* env, Local