summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEugene Ostroukhov <eostroukhov@google.com>2018-09-08 19:45:10 -0700
committerEugene Ostroukhov <eostroukhov@google.com>2018-09-18 09:01:33 -0700
commitf28c6f7eef58e7c3133bb2cd457d05b986194ba3 (patch)
treeb8e8583ff735a3b0721831af51c4f92d967849f1
parentba0b4e43e442926bfb9389a42aa7393f91e6748a (diff)
downloadandroid-node-v8-f28c6f7eef58e7c3133bb2cd457d05b986194ba3.tar.gz
android-node-v8-f28c6f7eef58e7c3133bb2cd457d05b986194ba3.tar.bz2
android-node-v8-f28c6f7eef58e7c3133bb2cd457d05b986194ba3.zip
inspector: workers debugging
Introduce a NodeTarget inspector domain modelled after ChromeDevTools Target domain. It notifies inspector frontend attached to a main V8 isolate when workers are starting and allows passing messages to inspectors on their isolates. All inspector functionality is enabled on worker isolates. PR-URL: https://github.com/nodejs/node/pull/21364 Reviewed-By: Aleksei Koziatinskii <ak239spb@gmail.com> Reviewed-By: Jan Krems <jan.krems@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
-rw-r--r--node.gyp6
-rw-r--r--src/inspector/node_protocol.pdl55
-rw-r--r--src/inspector/node_protocol_config.json7
-rw-r--r--src/inspector/worker_agent.cc154
-rw-r--r--src/inspector/worker_agent.h39
-rw-r--r--src/inspector/worker_inspector.cc128
-rw-r--r--src/inspector/worker_inspector.h98
-rw-r--r--src/inspector_agent.cc48
-rw-r--r--src/inspector_agent.h8
-rw-r--r--src/node_worker.cc14
-rw-r--r--test/parallel/test-worker-debug.js228
11 files changed, 775 insertions, 10 deletions
diff --git a/node.gyp b/node.gyp
index fb1c674e8e..d52748ec2b 100644
--- a/node.gyp
+++ b/node.gyp
@@ -509,14 +509,18 @@
'src/inspector_socket.cc',
'src/inspector_socket_server.cc',
'src/inspector/main_thread_interface.cc',
+ 'src/inspector/worker_inspector.cc',
'src/inspector/node_string.cc',
+ 'src/inspector/worker_agent.cc',
'src/inspector/tracing_agent.cc',
'src/inspector_agent.h',
'src/inspector_io.h',
'src/inspector_socket.h',
'src/inspector_socket_server.h',
'src/inspector/main_thread_interface.h',
+ 'src/inspector/worker_inspector.h',
'src/inspector/node_string.h',
+ 'src/inspector/worker_agent.h',
'src/inspector/tracing_agent.h',
'<@(node_inspector_generated_sources)'
],
@@ -1066,6 +1070,8 @@
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Forward.h',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Protocol.cpp',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Protocol.h',
+ '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeWorker.cpp',
+ '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeWorker.h',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeTracing.cpp',
'<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeTracing.h',
],
diff --git a/src/inspector/node_protocol.pdl b/src/inspector/node_protocol.pdl
index 27b3d814c8..9fb9f1c55f 100644
--- a/src/inspector/node_protocol.pdl
+++ b/src/inspector/node_protocol.pdl
@@ -37,3 +37,58 @@ experimental domain NodeTracing
# Signals that tracing is stopped and there is no trace buffers pending flush, all data were
# delivered via dataCollected events.
event tracingComplete
+
+# Support for sending messages to Node worker Inspector instances.
+experimental domain NodeWorker
+
+ type WorkerID extends string
+
+ # Unique identifier of attached debugging session.
+ type SessionID extends string
+
+ type WorkerInfo extends object
+ properties
+ WorkerID workerId
+ string type
+ string title
+ string url
+
+ # Sends protocol message over session with given id.
+ command sendMessageToWorker
+ parameters
+ string message
+ # Identifier of the session.
+ SessionID sessionId
+
+ # Instructs the inspector to attach to running workers. Will also attach to new workers
+ # as they start
+ command enable
+ parameters
+ # Whether to new workers should be paused until the frontend sends `Runtime.runIfWaitingForDebugger`
+ # message to run them.
+ boolean waitForDebuggerOnStart
+
+ # Detaches from all running workers and disables attaching to new workers as they are started.
+ command disable
+
+ # Issued when attached to a worker.
+ event attachedToWorker
+ parameters
+ # Identifier assigned to the session used to send/receive messages.
+ SessionID sessionId
+ WorkerInfo workerInfo
+ boolean waitingForDebugger
+
+ # Issued when detached from the worker.
+ event detachedFromWorker
+ parameters
+ # Detached session identifier.
+ SessionID sessionId
+
+ # Notifies about a new protocol message received from the session
+ # (session ID is provided in attachedToWorker notification).
+ event receivedMessageFromWorker
+ parameters
+ # Identifier of a session which sends a message.
+ SessionID sessionId
+ string message
diff --git a/src/inspector/node_protocol_config.json b/src/inspector/node_protocol_config.json
index 7cea20ae93..4ef3785606 100644
--- a/src/inspector/node_protocol_config.json
+++ b/src/inspector/node_protocol_config.json
@@ -3,12 +3,7 @@
"path": "node_protocol.json",
"package": "src/node/inspector/protocol",
"output": "node/inspector/protocol",
- "namespace": ["node", "inspector", "protocol"],
- "options": [
- {
- "domain": "NodeTracing"
- }
- ]
+ "namespace": ["node", "inspector", "protocol"]
},
"exported": {
"package": "include/inspector",
diff --git a/src/inspector/worker_agent.cc b/src/inspector/worker_agent.cc
new file mode 100644
index 0000000000..fccd6d57a5
--- /dev/null
+++ b/src/inspector/worker_agent.cc
@@ -0,0 +1,154 @@
+#include "worker_agent.h"
+
+#include "main_thread_interface.h"
+#include "worker_inspector.h"
+
+namespace node {
+namespace inspector {
+namespace protocol {
+
+class NodeWorkers
+ : public std::enable_shared_from_this<NodeWorkers> {
+ public:
+ explicit NodeWorkers(std::weak_ptr<NodeWorker::Frontend> frontend,
+ std::shared_ptr<MainThreadHandle> thread)
+ : frontend_(frontend), thread_(thread) {}
+ void WorkerCreated(const std::string& title,
+ const std::string& url,
+ bool waiting,
+ std::shared_ptr<MainThreadHandle> target);
+ void Receive(const std::string& id, const std::string& message);
+ void Send(const std::string& id, const std::string& message);
+ void Detached(const std::string& id);
+
+ private:
+ std::weak_ptr<NodeWorker::Frontend> frontend_;
+ std::shared_ptr<MainThreadHandle> thread_;
+ std::unordered_map<std::string, std::unique_ptr<InspectorSession>> sessions_;
+ int next_target_id_ = 0;
+};
+
+namespace {
+class AgentWorkerInspectorDelegate : public WorkerDelegate {
+ public:
+ explicit AgentWorkerInspectorDelegate(std::shared_ptr<NodeWorkers> workers)
+ : workers_(workers) {}
+
+ void WorkerCreated(const std::string& title,
+ const std::string& url,
+ bool waiting,
+ std::shared_ptr<MainThreadHandle> target) override {
+ workers_->WorkerCreated(title, url, waiting, target);
+ }
+
+ private:
+ std::shared_ptr<NodeWorkers> workers_;
+};
+
+class ParentInspectorSessionDelegate : public InspectorSessionDelegate {
+ public:
+ ParentInspectorSessionDelegate(const std::string& id,
+ std::shared_ptr<NodeWorkers> workers)
+ : id_(id), workers_(workers) {}
+
+ ~ParentInspectorSessionDelegate() override {
+ workers_->Detached(id_);
+ }
+
+ void SendMessageToFrontend(const v8_inspector::StringView& msg) override {
+ std::string message = protocol::StringUtil::StringViewToUtf8(msg);
+ workers_->Send(id_, message);
+ }
+
+ private:
+ std::string id_;
+ std::shared_ptr<NodeWorkers> workers_;
+};
+
+std::unique_ptr<NodeWorker::WorkerInfo> WorkerInfo(const std::string& id,
+ const std::string& title,
+ const std::string& url) {
+ return NodeWorker::WorkerInfo::create()
+ .setWorkerId(id)
+ .setTitle(title)
+ .setUrl(url)
+ .setType("worker").build();
+}
+} // namespace
+
+WorkerAgent::WorkerAgent(std::weak_ptr<WorkerManager> manager)
+ : manager_(manager) {}
+
+
+void WorkerAgent::Wire(UberDispatcher* dispatcher) {
+ frontend_.reset(new NodeWorker::Frontend(dispatcher->channel()));
+ NodeWorker::Dispatcher::wire(dispatcher, this);
+ auto manager = manager_.lock();
+ CHECK_NOT_NULL(manager);
+ workers_ =
+ std::make_shared<NodeWorkers>(frontend_, manager->MainThread());
+}
+
+DispatchResponse WorkerAgent::sendMessageToWorker(const String& message,
+ const String& sessionId) {
+ workers_->Receive(sessionId, message);
+ return DispatchResponse::OK();
+}
+
+DispatchResponse WorkerAgent::enable(bool waitForDebuggerOnStart) {
+ auto manager = manager_.lock();
+ if (!manager) {
+ return DispatchResponse::OK();
+ }
+ if (!event_handle_) {
+ std::unique_ptr<AgentWorkerInspectorDelegate> delegate(
+ new AgentWorkerInspectorDelegate(workers_));
+ event_handle_ = manager->SetAutoAttach(std::move(delegate));
+ }
+ event_handle_->SetWaitOnStart(waitForDebuggerOnStart);
+ return DispatchResponse::OK();
+}
+
+DispatchResponse WorkerAgent::disable() {
+ event_handle_.reset();
+ return DispatchResponse::OK();
+}
+
+void NodeWorkers::WorkerCreated(const std::string& title,
+ const std::string& url,
+ bool waiting,
+ std::shared_ptr<MainThreadHandle> target) {
+ auto frontend = frontend_.lock();
+ if (!frontend)
+ return;
+ std::string id = std::to_string(++next_target_id_);
+ auto delegate = thread_->MakeDelegateThreadSafe(
+ std::unique_ptr<InspectorSessionDelegate>(
+ new ParentInspectorSessionDelegate(id, shared_from_this())));
+ sessions_[id] = target->Connect(std::move(delegate), true);
+ frontend->attachedToWorker(id, WorkerInfo(id, title, url), waiting);
+}
+
+void NodeWorkers::Send(const std::string& id, const std::string& message) {
+ auto frontend = frontend_.lock();
+ if (frontend)
+ frontend->receivedMessageFromWorker(id, message);
+}
+
+void NodeWorkers::Receive(const std::string& id, const std::string& message) {
+ auto it = sessions_.find(id);
+ if (it != sessions_.end())
+ it->second->Dispatch(Utf8ToStringView(message)->string());
+}
+
+void NodeWorkers::Detached(const std::string& id) {
+ if (sessions_.erase(id) == 0)
+ return;
+ auto frontend = frontend_.lock();
+ if (frontend) {
+ frontend->detachedFromWorker(id);
+ }
+}
+} // namespace protocol
+} // namespace inspector
+} // namespace node
diff --git a/src/inspector/worker_agent.h b/src/inspector/worker_agent.h
new file mode 100644
index 0000000000..402c719416
--- /dev/null
+++ b/src/inspector/worker_agent.h
@@ -0,0 +1,39 @@
+#ifndef SRC_INSPECTOR_WORKER_AGENT_H_
+#define SRC_INSPECTOR_WORKER_AGENT_H_
+
+#include "node/inspector/protocol/NodeWorker.h"
+#include "v8.h"
+
+
+namespace node {
+namespace inspector {
+class WorkerManagerEventHandle;
+class WorkerManager;
+
+namespace protocol {
+class NodeWorkers;
+
+class WorkerAgent : public NodeWorker::Backend {
+ public:
+ explicit WorkerAgent(std::weak_ptr<WorkerManager> manager);
+ ~WorkerAgent() override = default;
+
+ void Wire(UberDispatcher* dispatcher);
+
+ DispatchResponse sendMessageToWorker(const String& message,
+ const String& sessionId) override;
+
+ DispatchResponse enable(bool waitForDebuggerOnStart) override;
+ DispatchResponse disable() override;
+
+ private:
+ std::shared_ptr<NodeWorker::Frontend> frontend_;
+ std::weak_ptr<WorkerManager> manager_;
+ std::unique_ptr<WorkerManagerEventHandle> event_handle_;
+ std::shared_ptr<NodeWorkers> workers_;
+};
+} // namespace protocol
+} // namespace inspector
+} // namespace node
+
+#endif // SRC_INSPECTOR_WORKER_AGENT_H_
diff --git a/src/inspector/worker_inspector.cc b/src/inspector/worker_inspector.cc
new file mode 100644
index 0000000000..52e71a562d
--- /dev/null
+++ b/src/inspector/worker_inspector.cc
@@ -0,0 +1,128 @@
+#include "worker_inspector.h"
+
+#include "main_thread_interface.h"
+
+namespace node {
+namespace inspector {
+namespace {
+
+class WorkerStartedRequest : public Request {
+ public:
+ WorkerStartedRequest(
+ int id,
+ const std::string& url,
+ std::shared_ptr<node::inspector::MainThreadHandle> worker_thread,
+ bool waiting)
+ : id_(id),
+ info_(BuildWorkerTitle(id), url, worker_thread),
+ waiting_(waiting) {}
+ void Call(MainThreadInterface* thread) override {
+ auto manager = thread->inspector_agent()->GetWorkerManager();
+ manager->WorkerStarted(id_, info_, waiting_);
+ }
+
+ private:
+ static std::string BuildWorkerTitle(int id) {
+ return "Worker " + std::to_string(id);
+ }
+
+ int id_;
+ WorkerInfo info_;
+ bool waiting_;
+};
+
+
+void Report(const std::unique_ptr<WorkerDelegate>& delegate,
+ const WorkerInfo& info, bool waiting) {
+ if (info.worker_thread)
+ delegate->WorkerCreated(info.title, info.url, waiting, info.worker_thread);
+}
+
+class WorkerFinishedRequest : public Request {
+ public:
+ explicit WorkerFinishedRequest(int worker_id) : worker_id_(worker_id) {}
+
+ void Call(MainThreadInterface* thread) override {
+ thread->inspector_agent()->GetWorkerManager()->WorkerFinished(worker_id_);
+ }
+
+ private:
+ int worker_id_;
+};
+} // namespace
+
+
+ParentInspectorHandle::ParentInspectorHandle(
+ int id, const std::string& url,
+ std::shared_ptr<MainThreadHandle> parent_thread, bool wait_for_connect)
+ : id_(id), url_(url), parent_thread_(parent_thread),
+ wait_(wait_for_connect) {}
+
+ParentInspectorHandle::~ParentInspectorHandle() {
+ parent_thread_->Post(
+ std::unique_ptr<Request>(new WorkerFinishedRequest(id_)));
+}
+
+void ParentInspectorHandle::WorkerStarted(
+ std::shared_ptr<MainThreadHandle> worker_thread, bool waiting) {
+ std::unique_ptr<Request> request(
+ new WorkerStartedRequest(id_, url_, worker_thread, waiting));
+ parent_thread_->Post(std::move(request));
+}
+
+void WorkerManager::WorkerFinished(int session_id) {
+ children_.erase(session_id);
+}
+
+void WorkerManager::WorkerStarted(int session_id,
+ const WorkerInfo& info,
+ bool waiting) {
+ if (info.worker_thread->Expired())
+ return;
+ children_.emplace(session_id, info);
+ for (const auto& delegate : delegates_) {
+ Report(delegate.second, info, waiting);
+ }
+}
+
+std::unique_ptr<ParentInspectorHandle>
+WorkerManager::NewParentHandle(int thread_id, const std::string& url) {
+ bool wait = !delegates_waiting_on_start_.empty();
+ return std::unique_ptr<ParentInspectorHandle>(
+ new ParentInspectorHandle(thread_id, url, thread_, wait));
+}
+
+void WorkerManager::RemoveAttachDelegate(int id) {
+ delegates_.erase(id);
+ delegates_waiting_on_start_.erase(id);
+}
+
+std::unique_ptr<WorkerManagerEventHandle> WorkerManager::SetAutoAttach(
+ std::unique_ptr<WorkerDelegate> attach_delegate) {
+ int id = ++next_delegate_id_;
+ delegates_[id] = std::move(attach_delegate);
+ const auto& delegate = delegates_[id];
+ for (const auto& worker : children_) {
+ // Waiting is only reported when a worker is started, same as browser
+ Report(delegate, worker.second, false);
+ }
+ return std::unique_ptr<WorkerManagerEventHandle>(
+ new WorkerManagerEventHandle(shared_from_this(), id));
+}
+
+void WorkerManager::SetWaitOnStartForDelegate(int id, bool wait) {
+ if (wait)
+ delegates_waiting_on_start_.insert(id);
+ else
+ delegates_waiting_on_start_.erase(id);
+}
+
+void WorkerManagerEventHandle::SetWaitOnStart(bool wait_on_start) {
+ manager_->SetWaitOnStartForDelegate(id_, wait_on_start);
+}
+
+WorkerManagerEventHandle::~WorkerManagerEventHandle() {
+ manager_->RemoveAttachDelegate(id_);
+}
+} // namespace inspector
+} // namespace node
diff --git a/src/inspector/worker_inspector.h b/src/inspector/worker_inspector.h
new file mode 100644
index 0000000000..e3c96cf62f
--- /dev/null
+++ b/src/inspector/worker_inspector.h
@@ -0,0 +1,98 @@
+#ifndef SRC_INSPECTOR_WORKER_INSPECTOR_H_
+#define SRC_INSPECTOR_WORKER_INSPECTOR_H_
+
+#if !HAVE_INSPECTOR
+#error("This header can only be used when inspector is enabled")
+#endif
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+
+namespace node {
+namespace inspector {
+class MainThreadHandle;
+class WorkerManager;
+
+class WorkerDelegate {
+ public:
+ virtual void WorkerCreated(const std::string& title,
+ const std::string& url,
+ bool waiting,
+ std::shared_ptr<MainThreadHandle> worker) = 0;
+};
+
+class WorkerManagerEventHandle {
+ public:
+ explicit WorkerManagerEventHandle(std::shared_ptr<WorkerManager> manager,
+ int id)
+ : manager_(manager), id_(id) {}
+ void SetWaitOnStart(bool wait_on_start);
+ ~WorkerManagerEventHandle();
+
+ private:
+ std::shared_ptr<WorkerManager> manager_;
+ int id_;
+};
+
+struct WorkerInfo {
+ WorkerInfo(const std::string& target_title,
+ const std::string& target_url,
+ std::shared_ptr<MainThreadHandle> worker_thread)
+ : title(target_title),
+ url(target_url),
+ worker_thread(worker_thread) {}
+ std::string title;
+ std::string url;
+ std::shared_ptr<MainThreadHandle> worker_thread;
+};
+
+class ParentInspectorHandle {
+ public:
+ ParentInspectorHandle(int id, const std::string& url,
+ std::shared_ptr<MainThreadHandle> parent_thread,
+ bool wait_for_connect);
+ ~ParentInspectorHandle();
+ void WorkerStarted(std::shared_ptr<MainThreadHandle> worker_thread,
+ bool waiting);
+ bool WaitForConnect() {
+ return wait_;
+ }
+
+ private:
+ int id_;
+ std::string url_;
+ std::shared_ptr<MainThreadHandle> parent_thread_;
+ bool wait_;
+};
+
+class WorkerManager : public std::enable_shared_from_this<WorkerManager> {
+ public:
+ explicit WorkerManager(std::shared_ptr<MainThreadHandle> thread)
+ : thread_(thread) {}
+
+ std::unique_ptr<ParentInspectorHandle> NewParentHandle(
+ int thread_id, const std::string& url);
+ void WorkerStarted(int session_id, const WorkerInfo& info, bool waiting);
+ void WorkerFinished(int session_id);
+ std::unique_ptr<WorkerManagerEventHandle> SetAutoAttach(
+ std::unique_ptr<WorkerDelegate> attach_delegate);
+ void SetWaitOnStartForDelegate(int id, bool wait);
+ void RemoveAttachDelegate(int id);
+ std::shared_ptr<MainThreadHandle> MainThread() {
+ return thread_;
+ }
+
+ private:
+ std::shared_ptr<MainThreadHandle> thread_;
+ std::unordered_map<int, WorkerInfo> children_;
+ std::unordered_map<int, std::unique_ptr<WorkerDelegate>> delegates_;
+ // If any one needs it, workers stop for all
+ std::unordered_set<int> delegates_waiting_on_start_;
+ int next_delegate_id_ = 0;
+};
+} // namespace inspector
+} // namespace node
+
+#endif // SRC_INSPECTOR_WORKER_INSPECTOR_H_
diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc
index 63b9266353..ebb7b7d5bc 100644
--- a/src/inspector_agent.cc
+++ b/src/inspector_agent.cc
@@ -4,6 +4,8 @@
#include "inspector/main_thread_interface.h"
#include "inspector/node_string.h"
#include "inspector/tracing_agent.h"
+#include "inspector/worker_agent.h"
+#include "inspector/worker_inspector.h"
#include "node/inspector/protocol/Protocol.h"
#include "node_internals.h"
#include "node_url.h"
@@ -201,6 +203,7 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
public:
explicit ChannelImpl(Environment* env,
const std::unique_ptr<V8Inspector>& inspector,
+ std::shared_ptr<WorkerManager> worker_manager,
std::unique_ptr<InspectorSessionDelegate> delegate,
bool prevent_shutdown)
: delegate_(std::move(delegate)),
@@ -209,11 +212,15 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
node_dispatcher_.reset(new protocol::UberDispatcher(this));
tracing_agent_.reset(new protocol::TracingAgent(env));
tracing_agent_->Wire(node_dispatcher_.get());
+ worker_agent_.reset(new protocol::WorkerAgent(worker_manager));
+ worker_agent_->Wire(node_dispatcher_.get());
}
virtual ~ChannelImpl() {
tracing_agent_->disable();
tracing_agent_.reset(); // Dispose before the dispatchers
+ worker_agent_->disable();
+ worker_agent_.reset(); // Dispose before the dispatchers
}
std::string dispatchProtocolMessage(const StringView& message) {
@@ -273,6 +280,7 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
}
std::unique_ptr<protocol::TracingAgent> tracing_agent_;
+ std::unique_ptr<protocol::WorkerAgent> worker_agent_;
std::unique_ptr<InspectorSessionDelegate> delegate_;
std::unique_ptr<v8_inspector::V8InspectorSession> session_;
std::unique_ptr<protocol::UberDispatcher> node_dispatcher_;
@@ -469,7 +477,8 @@ class NodeInspectorClient : public V8InspectorClient {
// TODO(addaleax): Revert back to using make_unique once we get issues
// with CI resolved (i.e. revert the patch that added this comment).
channels_[session_id].reset(
- new ChannelImpl(env_, client_, std::move(delegate), prevent_shutdown));
+ new ChannelImpl(env_, client_, getWorkerManager(),
+ std::move(delegate), prevent_shutdown));
return session_id;
}
@@ -589,6 +598,14 @@ class NodeInspectorClient : public V8InspectorClient {
return interface_->GetHandle();
}
+ std::shared_ptr<WorkerManager> getWorkerManager() {
+ if (worker_manager_ == nullptr) {
+ worker_manager_ =
+ std::make_shared<WorkerManager>(getThreadHandle());
+ }
+ return worker_manager_;
+ }
+
bool IsActive() {
return !channels_.empty();
}
@@ -646,6 +663,7 @@ class NodeInspectorClient : public V8InspectorClient {
bool waiting_for_io_shutdown_ = false;
// Allows accessing Inspector from non-main threads
std::unique_ptr<MainThreadInterface> interface_;
+ std::shared_ptr<WorkerManager> worker_manager_;
};
Agent::Agent(Environment* env)
@@ -680,7 +698,10 @@ bool Agent::Start(const std::string& path,
}
bool wait_for_connect = options->wait_for_connect();
- if (!options->inspector_enabled || !StartIoThread()) {
+ if (parent_handle_) {
+ wait_for_connect = parent_handle_->WaitForConnect();
+ parent_handle_->WorkerStarted(client_->getThreadHandle(), wait_for_connect);
+ } else if (!options->inspector_enabled || !StartIoThread()) {
return false;
}
if (wait_for_connect) {
@@ -727,7 +748,9 @@ std::unique_ptr<InspectorSession> Agent::Connect(
void Agent::WaitForDisconnect() {
CHECK_NOT_NULL(client_);
- if (client_->hasConnectedSessions()) {
+ bool is_worker = parent_handle_ != nullptr;
+ parent_handle_.reset();
+ if (client_->hasConnectedSessions() && !is_worker) {
fprintf(stderr, "Waiting for the debugger to disconnect...\n");
fflush(stderr);
}
@@ -842,7 +865,11 @@ void Agent::ContextCreated(Local<Context> context, const ContextInfo& info) {
}
bool Agent::WillWaitForConnect() {
- return debug_options_->wait_for_connect();
+ if (debug_options_->wait_for_connect())
+ return true;
+ if (parent_handle_)
+ return parent_handle_->WaitForConnect();
+ return false;
}
bool Agent::IsActive() {
@@ -851,11 +878,24 @@ bool Agent::IsActive() {
return io_ != nullptr || client_->IsActive();
}
+void Agent::AddWorkerInspector(int thread_id,
+ const std::string& url,
+ Agent* agent) {
+ CHECK_NOT_NULL(client_);
+ agent->parent_handle_ =
+ client_->getWorkerManager()->NewParentHandle(thread_id, url);
+}
+
void Agent::WaitForConnect() {
CHECK_NOT_NULL(client_);
client_->waitForFrontend();
}
+std::shared_ptr<WorkerManager> Agent::GetWorkerManager() {
+ CHECK_NOT_NULL(client_);
+ return client_->getWorkerManager();
+}
+
SameThreadInspectorSession::~SameThreadInspectorSession() {
auto client = client_.lock();
if (client)
diff --git a/src/inspector_agent.h b/src/inspector_agent.h
index 79ae8d4cd9..e926ccaa92 100644
--- a/src/inspector_agent.h
+++ b/src/inspector_agent.h
@@ -26,7 +26,9 @@ struct ContextInfo;
namespace inspector {
class InspectorIo;
+class ParentInspectorHandle;
class NodeInspectorClient;
+class WorkerManager;
class InspectorSession {
public:
@@ -82,6 +84,8 @@ class Agent {
void EnableAsyncHook();
void DisableAsyncHook();
+ void AddWorkerInspector(int thread_id, const std::string& url, Agent* agent);
+
// Called to create inspector sessions that can be used from the main thread.
// The inspector responds by using the delegate to send messages back.
std::unique_ptr<InspectorSession> Connect(
@@ -103,6 +107,9 @@ class Agent {
std::shared_ptr<DebugOptions> options() { return debug_options_; }
void ContextCreated(v8::Local<v8::Context> context, const ContextInfo& info);
+ // Interface for interacting with inspectors in worker threads
+ std::shared_ptr<WorkerManager> GetWorkerManager();
+
private:
void ToggleAsyncHook(v8::Isolate* isolate,
const node::Persistent<v8::Function>& fn);
@@ -112,6 +119,7 @@ class Agent {
std::shared_ptr<NodeInspectorClient> client_;
// Interface for transports, e.g. WebSocket server
std::unique_ptr<InspectorIo> io_;
+ std::unique_ptr<ParentInspectorHandle> parent_handle_;
std::string path_;
std::shared_ptr<DebugOptions> debug_options_;
diff --git a/src/node_worker.cc b/src/node_worker.cc
index 1a90e3a64f..209b7a4d09 100644
--- a/src/node_worker.cc
+++ b/src/node_worker.cc
@@ -40,6 +40,14 @@ void StartWorkerInspector(Environment* child, const std::string& url) {
child->inspector_agent()->Start(url, nullptr, false);
}
+void AddWorkerInspector(Environment* parent,
+ Environment* child,
+ int id,
+ const std::string& url) {
+ parent->inspector_agent()->AddWorkerInspector(id, url,
+ child->inspector_agent());
+}
+
void WaitForWorkerInspectorToStop(Environment* child) {
child->inspector_agent()->WaitForDisconnect();
child->inspector_agent()->Stop();
@@ -48,6 +56,10 @@ void WaitForWorkerInspectorToStop(Environment* child) {
#else
// No-ops
void StartWorkerInspector(Environment* child, const std::string& url) {}
+void AddWorkerInspector(Environment* parent,
+ Environment* child,
+ int id,
+ const std::string& url) {}
void WaitForWorkerInspectorToStop(Environment* child) {}
#endif
@@ -115,6 +127,8 @@ Worker::Worker(Environment* env, Local<Object> wrap, const std::string& url)
env_->Start(std::vector<std::string>{},
std::vector<std::string>{},
env->profiler_idle_notifier_started());
+ // Done while on the parent thread
+ AddWorkerInspector(env, env_.get(), thread_id_, url_);
}
// The new isolate won't be bothered on this thread again.
diff --git a/test/parallel/test-worker-debug.js b/test/parallel/test-worker-debug.js
new file mode 100644
index 0000000000..8691879f14
--- /dev/null
+++ b/test/parallel/test-worker-debug.js
@@ -0,0 +1,228 @@
+// Flags: --experimental-worker
+'use strict';
+const common = require('../common');
+
+common.skipIfInspectorDisabled();
+
+const assert = require('assert');
+const EventEmitter = require('events');
+const { Session } = require('inspector');
+const { pathToFileURL } = require('url');
+const {
+ Worker, isMainThread, parentPort, workerData
+} = require('worker_threads');
+
+
+const workerMessage = 'This is a message from a worker';
+
+function waitForMessage() {
+ return new Promise((resolve) => {
+ parentPort.once('message', resolve);
+ });
+}
+
+// This is at the top so line numbers change less often
+if (!isMainThread) {
+ if (workerData === 1) {
+ console.log(workerMessage);
+ debugger; // eslint-disable-line no-debugger
+ } else if (workerData === 2) {
+ parentPort.postMessage('running');
+ waitForMessage();
+ }
+ return;
+}
+
+function doPost(session, method, params) {
+ return new Promise((resolve, reject) => {
+ session.post(method, params, (error, result) => {
+ if (error)
+ reject(JSON.stringify(error));
+ else
+ resolve(result);
+ });
+ });
+}
+
+function waitForEvent(emitter, event) {
+ return new Promise((resolve) => emitter.once(event, resolve));
+}
+
+function waitForWorkerAttach(session) {
+ return waitForEvent(session, 'NodeWorker.attachedToWorker')
+ .then(({ params }) => params);
+}
+
+async function waitForWorkerDetach(session, id) {
+ let sessionId;
+ do {
+ const { params } =
+ await waitForEvent(session, 'NodeWorker.detachedFromWorker');
+ sessionId = params.sessionId;
+ } while (sessionId !== id);
+}
+
+function runWorker(id, workerCallback = () => {}) {
+ return new Promise((resolve, reject) => {
+ const worker = new Worker(__filename, { workerData: id });
+ workerCallback(worker);
+ worker.on('error', reject);
+ worker.on('exit', resolve);
+ });
+}
+
+class WorkerSession extends EventEmitter {
+ constructor(parentSession, id) {
+ super();
+ this._parentSession = parentSession;
+ this._id = id;
+ this._requestCallbacks = new Map();
+ this._nextCommandId = 1;
+ this._parentSession.on('NodeWorker.receivedMessageFromWorker',
+ ({ params }) => {
+ if (params.sessionId === this._id)
+ this._processMessage(JSON.parse(params.message));
+ });
+ }
+
+ _processMessage(message) {
+ if (message.id === undefined) {
+ // console.log(JSON.stringify(message));
+ this.emit('inspectorNotification', message);
+ this.emit(message.method, message);
+ return;
+ }
+ const callback = this._requestCallbacks.get(message.id);
+ if (callback) {
+ this._requestCallbacks.delete(message.id);
+ if (message.error)
+ callback[1](message.error.message);
+ else
+ callback[0](message.result);
+ }
+ }
+
+ async waitForBreakAfterCommand(command, script, line) {
+ const notificationPromise = waitForEvent(this, 'Debugger.paused');
+ this.post(command);
+ const notification = await notificationPromise;
+ const callFrame = notification.params.callFrames[0];
+ assert.strictEqual(callFrame.url, pathToFileURL(script).toString());
+ assert.strictEqual(callFrame.location.lineNumber, line);
+ }
+
+ post(method, parameters) {
+ const msg = {
+ id: this._nextCommandId++,
+ method
+ };
+ if (parameters)
+ msg.params = parameters;
+
+ return new Promise((resolve, reject) => {
+ this._requestCallbacks.set(msg.id, [resolve, reject]);
+ this._parentSession.post('NodeWorker.sendMessageToWorker', {
+ sessionId: this._id, message: JSON.stringify(msg)
+ });
+ });
+ }
+}
+
+async function testBasicWorkerDebug(session, post) {
+ /*
+ 1. Do 'enble' with waitForDebuggerOnStart = true
+ 2. Run worker. It should break on start.
+ 3. Enable Runtime (to get console message) and Debugger. Resume.
+ 4. Breaks on the 'debugger' statement. Resume.
+ 5. Console message recieved, worker runs to a completion.
+ 6. contextCreated/contextDestroyed had been properly dispatched
+ */
+ console.log('Test basic debug scenario');
+ await post('NodeWorker.enable', { waitForDebuggerOnStart: true });
+ const attached = waitForWorkerAttach(session);
+ const worker = runWorker(1);
+ const { sessionId, waitingForDebugger } = await attached;
+ assert.strictEqual(waitingForDebugger, true);
+ const detached = waitForWorkerDetach(session, sessionId);
+ const workerSession = new WorkerSession(session, sessionId);
+ const contextEvents = Promise.all([
+ waitForEvent(workerSession, 'Runtime.executionContextCreated'),
+ waitForEvent(workerSession, 'Runtime.executionContextDestroyed')
+ ]);
+ const consolePromise = waitForEvent(workerSession, 'Runtime.consoleAPICalled')
+ .then((notification) => notification.params.args[0].value);
+ await workerSession.post('Debugger.enable');
+ await workerSession.post('Runtime.enable');
+ await workerSession.waitForBreakAfterCommand(
+ 'Runtime.runIfWaitingForDebugger', __filename, 2);
+ await workerSession.waitForBreakAfterCommand(
+ 'Debugger.resume', __filename, 27); // V8 line number is zero-based
+ assert.strictEqual(await consolePromise, workerMessage);
+ workerSession.post('Debugger.resume');
+ await Promise.all([worker, detached, contextEvents]);
+}
+
+async function testNoWaitOnStart(session, post) {
+ console.log('Test disabled waitForDebuggerOnStart');
+ await post('NodeWorker.enable', { waitForDebuggerOnStart: false });
+ let worker;
+ const promise = waitForWorkerAttach(session);
+ const exitPromise = runWorker(2, (w) => { worker = w; });
+ const { waitingForDebugger } = await promise;
+ assert.strictEqual(waitingForDebugger, false);
+ worker.postMessage('resume');
+ await exitPromise;
+}
+
+async function testTwoWorkers(session, post) {
+ console.log('Test attach to a running worker and then start a new one');
+ await post('NodeWorker.disable');
+ let okToAttach = false;
+ const worker1attached = waitForWorkerAttach(session).then((notification) => {
+ assert.strictEqual(okToAttach, true);
+ return notification;
+ });
+
+ let worker1Exited;
+ const worker = await new Promise((resolve, reject) => {
+ worker1Exited = runWorker(2, resolve);
+ }).then((worker) => new Promise(
+ (resolve) => worker.once('message', () => resolve(worker))));
+ okToAttach = true;
+ await post('NodeWorker.enable', { waitForDebuggerOnStart: true });
+ const { waitingForDebugger: worker1Waiting } = await worker1attached;
+ assert.strictEqual(worker1Waiting, false);
+
+ const worker2Attached = waitForWorkerAttach(session);
+ let worker2Done = false;
+ const worker2Exited = runWorker(1)
+ .then(() => assert.strictEqual(worker2Done, true));
+ const worker2AttachInfo = await worker2Attached;
+ assert.strictEqual(worker2AttachInfo.waitingForDebugger, true);
+ worker2Done = true;
+
+ const workerSession = new WorkerSession(session, worker2AttachInfo.sessionId);
+ workerSession.post('Runtime.runIfWaitingForDebugger');
+ worker.postMessage('resume');
+ await Promise.all([worker1Exited, worker2Exited]);
+}
+
+async function test() {
+ const session = new Session();
+ session.connect();
+ const post = doPost.bind(null, session);
+
+ await testBasicWorkerDebug(session, post);
+
+ console.log('Test disabling attach to workers');
+ await post('NodeWorker.disable');
+ await runWorker(1);
+
+ await testNoWaitOnStart(session, post);
+ await testTwoWorkers(session, post);
+
+ session.disconnect();
+ console.log('Test done');
+}
+
+test();