inspector: enable Inspector JS API in workers

PR-URL: https://github.com/nodejs/node/pull/22769
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Eugene Ostroukhov 2018-09-08 19:45:10 -07:00
parent ab35194cb0
commit ab5f789e3f
21 changed files with 99 additions and 35 deletions

View File

@ -14,7 +14,7 @@ const util = require('util');
const { Connection, open, url } = process.binding('inspector'); const { Connection, open, url } = process.binding('inspector');
const { originalConsole } = require('internal/process/per_thread'); const { originalConsole } = require('internal/process/per_thread');
if (!Connection || !require('internal/worker').isMainThread) if (!Connection)
throw new ERR_INSPECTOR_NOT_AVAILABLE(); throw new ERR_INSPECTOR_NOT_AVAILABLE();
const connectionSymbol = Symbol('connectionProperty'); const connectionSymbol = Symbol('connectionProperty');

View File

@ -1,8 +1,6 @@
'use strict'; 'use strict';
// TODO(addaleax): Figure out how to integrate the inspector with workers. const hasInspector = process.config.variables.v8_enable_inspector === 1;
const hasInspector = process.config.variables.v8_enable_inspector === 1 &&
require('internal/worker').isMainThread;
const inspector = hasInspector ? require('inspector') : undefined; const inspector = hasInspector ? require('inspector') : undefined;
let session; let session;

View File

@ -17,6 +17,7 @@ const { MessagePort, MessageChannel } = internalBinding('messaging');
const { handle_onclose } = internalBinding('symbols'); const { handle_onclose } = internalBinding('symbols');
const { clearAsyncIdStack } = require('internal/async_hooks'); const { clearAsyncIdStack } = require('internal/async_hooks');
const { serializeError, deserializeError } = require('internal/error-serdes'); const { serializeError, deserializeError } = require('internal/error-serdes');
const { pathToFileURL } = require('url');
util.inherits(MessagePort, EventEmitter); util.inherits(MessagePort, EventEmitter);
@ -257,8 +258,9 @@ class Worker extends EventEmitter {
} }
} }
const url = options.eval ? null : pathToFileURL(filename);
// Set up the C++ handle for the worker, as well as some internal wiring. // Set up the C++ handle for the worker, as well as some internal wiring.
this[kHandle] = new WorkerImpl(); this[kHandle] = new WorkerImpl(url);
this[kHandle].onexit = (code) => this[kOnExit](code); this[kHandle].onexit = (code) => this[kOnExit](code);
this[kPort] = this[kHandle].messagePort; this[kPort] = this[kHandle].messagePort;
this[kPort].on('message', (data) => this[kOnMessage](data)); this[kPort].on('message', (data) => this[kOnMessage](data));

View File

@ -354,13 +354,6 @@ void MainThreadHandle::Reset() {
main_thread_ = nullptr; main_thread_ = nullptr;
} }
Agent* MainThreadHandle::GetInspectorAgent() {
Mutex::ScopedLock scoped_lock(block_lock_);
if (main_thread_ == nullptr)
return nullptr;
return main_thread_->inspector_agent();
}
std::unique_ptr<InspectorSessionDelegate> std::unique_ptr<InspectorSessionDelegate>
MainThreadHandle::MakeDelegateThreadSafe( MainThreadHandle::MakeDelegateThreadSafe(
std::unique_ptr<InspectorSessionDelegate> delegate) { std::unique_ptr<InspectorSessionDelegate> delegate) {

View File

@ -54,7 +54,6 @@ class MainThreadHandle : public std::enable_shared_from_this<MainThreadHandle> {
return ++next_object_id_; return ++next_object_id_;
} }
bool Post(std::unique_ptr<Request> request); bool Post(std::unique_ptr<Request> request);
Agent* GetInspectorAgent();
std::unique_ptr<InspectorSessionDelegate> MakeDelegateThreadSafe( std::unique_ptr<InspectorSessionDelegate> MakeDelegateThreadSafe(
std::unique_ptr<InspectorSessionDelegate> delegate); std::unique_ptr<InspectorSessionDelegate> delegate);
bool Expired(); bool Expired();

View File

@ -74,11 +74,14 @@ DispatchResponse TracingAgent::start(
if (categories_set.empty()) if (categories_set.empty())
return DispatchResponse::Error("At least one category should be enabled"); return DispatchResponse::Error("At least one category should be enabled");
trace_writer_ = env_->tracing_agent_writer()->agent()->AddClient( auto* writer = env_->tracing_agent_writer();
categories_set, if (writer != nullptr) {
std::unique_ptr<InspectorTraceWriter>( trace_writer_ = env_->tracing_agent_writer()->agent()->AddClient(
new InspectorTraceWriter(frontend_.get())), categories_set,
tracing::Agent::kIgnoreDefaultCategories); std::unique_ptr<InspectorTraceWriter>(
new InspectorTraceWriter(frontend_.get())),
tracing::Agent::kIgnoreDefaultCategories);
}
return DispatchResponse::OK(); return DispatchResponse::OK();
} }

View File

@ -190,6 +190,12 @@ static int StartDebugSignalHandler() {
const int CONTEXT_GROUP_ID = 1; const int CONTEXT_GROUP_ID = 1;
std::string GetWorkerLabel(node::Environment* env) {
std::ostringstream result;
result << "Worker[" << env->thread_id() << "]";
return result.str();
}
class ChannelImpl final : public v8_inspector::V8Inspector::Channel, class ChannelImpl final : public v8_inspector::V8Inspector::Channel,
public protocol::FrontendChannel { public protocol::FrontendChannel {
public: public:
@ -393,10 +399,13 @@ bool IsFilePath(const std::string& path) {
class NodeInspectorClient : public V8InspectorClient { class NodeInspectorClient : public V8InspectorClient {
public: public:
explicit NodeInspectorClient(node::Environment* env) : env_(env) { explicit NodeInspectorClient(node::Environment* env, bool is_main)
: env_(env), is_main_(is_main) {
client_ = V8Inspector::create(env->isolate(), this); client_ = V8Inspector::create(env->isolate(), this);
// TODO(bnoordhuis) Make name configurable from src/node.cc. // TODO(bnoordhuis) Make name configurable from src/node.cc.
ContextInfo info(GetHumanReadableProcessName()); std::string name =
is_main_ ? GetHumanReadableProcessName() : GetWorkerLabel(env);
ContextInfo info(name);
info.is_default = true; info.is_default = true;
contextCreated(env->context(), info); contextCreated(env->context(), info);
} }
@ -625,6 +634,7 @@ class NodeInspectorClient : public V8InspectorClient {
} }
node::Environment* env_; node::Environment* env_;
bool is_main_;
bool running_nested_loop_ = false; bool running_nested_loop_ = false;
std::unique_ptr<V8Inspector> client_; std::unique_ptr<V8Inspector> client_;
std::unordered_map<int, std::unique_ptr<ChannelImpl>> channels_; std::unordered_map<int, std::unique_ptr<ChannelImpl>> channels_;
@ -642,13 +652,23 @@ Agent::Agent(Environment* env)
: parent_env_(env), : parent_env_(env),
debug_options_(env->options()->debug_options) {} debug_options_(env->options()->debug_options) {}
Agent::~Agent() = default; Agent::~Agent() {
if (start_io_thread_async.data == this) {
start_io_thread_async.data = nullptr;
// This is global, will never get freed
uv_close(reinterpret_cast<uv_handle_t*>(&start_io_thread_async), nullptr);
}
}
bool Agent::Start(const std::string& path, bool Agent::Start(const std::string& path,
std::shared_ptr<DebugOptions> options) { std::shared_ptr<DebugOptions> options,
bool is_main) {
if (options == nullptr) {
options = std::make_shared<DebugOptions>();
}
path_ = path; path_ = path;
debug_options_ = options; debug_options_ = options;
client_ = std::make_shared<NodeInspectorClient>(parent_env_); client_ = std::make_shared<NodeInspectorClient>(parent_env_, is_main);
if (parent_env_->is_main_thread()) { if (parent_env_->is_main_thread()) {
CHECK_EQ(0, uv_async_init(parent_env_->event_loop(), CHECK_EQ(0, uv_async_init(parent_env_->event_loop(),
&start_io_thread_async, &start_io_thread_async,

View File

@ -47,7 +47,9 @@ class Agent {
~Agent(); ~Agent();
// Create client_, may create io_ if option enabled // Create client_, may create io_ if option enabled
bool Start(const std::string& path, std::shared_ptr<DebugOptions> options); bool Start(const std::string& path,
std::shared_ptr<DebugOptions> options,
bool is_worker);
// Stop and destroy io_ // Stop and destroy io_
void Stop(); void Stop();

View File

@ -327,7 +327,7 @@ static struct {
// right away on the websocket port and fails to bind/etc, this will return // right away on the websocket port and fails to bind/etc, this will return
// false. // false.
return env->inspector_agent()->Start( return env->inspector_agent()->Start(
script_path == nullptr ? "" : script_path, options); script_path == nullptr ? "" : script_path, options, true);
} }
bool InspectorStarted(Environment* env) { bool InspectorStarted(Environment* env) {

View File

@ -35,10 +35,26 @@ namespace {
uint64_t next_thread_id = 1; uint64_t next_thread_id = 1;
Mutex next_thread_id_mutex; Mutex next_thread_id_mutex;
#if NODE_USE_V8_PLATFORM && HAVE_INSPECTOR
void StartWorkerInspector(Environment* child, const std::string& url) {
child->inspector_agent()->Start(url, nullptr, false);
}
void WaitForWorkerInspectorToStop(Environment* child) {
child->inspector_agent()->WaitForDisconnect();
child->inspector_agent()->Stop();
}
#else
// No-ops
void StartWorkerInspector(Environment* child, const std::string& url) {}
void WaitForWorkerInspectorToStop(Environment* child) {}
#endif
} // anonymous namespace } // anonymous namespace
Worker::Worker(Environment* env, Local<Object> wrap) Worker::Worker(Environment* env, Local<Object> wrap, const std::string& url)
: AsyncWrap(env, wrap, AsyncWrap::PROVIDER_WORKER) { : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_WORKER), url_(url) {
// Generate a new thread id. // Generate a new thread id.
{ {
Mutex::ScopedLock next_thread_id_lock(next_thread_id_mutex); Mutex::ScopedLock next_thread_id_lock(next_thread_id_mutex);
@ -155,6 +171,7 @@ void Worker::Run() {
env_->async_hooks()->pop_async_id(1); env_->async_hooks()->pop_async_id(1);
Debug(this, "Loaded environment for worker %llu", thread_id_); Debug(this, "Loaded environment for worker %llu", thread_id_);
StartWorkerInspector(env_.get(), url_);
} }
{ {
@ -215,6 +232,7 @@ void Worker::Run() {
env_->stop_sub_worker_contexts(); env_->stop_sub_worker_contexts();
env_->RunCleanup(); env_->RunCleanup();
RunAtExit(env_.get()); RunAtExit(env_.get());
WaitForWorkerInspectorToStop(env_.get());
{ {
Mutex::ScopedLock stopped_lock(stopped_mutex_); Mutex::ScopedLock stopped_lock(stopped_mutex_);
@ -351,7 +369,15 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
return; return;
} }
new Worker(env, args.This()); std::string url;
// Argument might be a string or URL
if (args.Length() == 1 && !args[0]->IsNullOrUndefined()) {
Utf8Value value(
args.GetIsolate(),
args[0]->ToString(env->context()).FromMaybe(v8::Local<v8::String>()));
url.append(value.out(), value.length());
}
new Worker(env, args.This(), url);
} }
void Worker::StartThread(const FunctionCallbackInfo<Value>& args) { void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {

View File

@ -12,7 +12,7 @@ namespace worker {
// A worker thread, as represented in its parent thread. // A worker thread, as represented in its parent thread.
class Worker : public AsyncWrap { class Worker : public AsyncWrap {
public: public:
Worker(Environment* env, v8::Local<v8::Object> wrap); Worker(Environment* env, v8::Local<v8::Object> wrap, const std::string& url);
~Worker(); ~Worker();
// Run the worker. This is only called from the worker thread. // Run the worker. This is only called from the worker thread.
@ -52,6 +52,7 @@ class Worker : public AsyncWrap {
uv_loop_t loop_; uv_loop_t loop_;
DeleteFnPtr<IsolateData, FreeIsolateData> isolate_data_; DeleteFnPtr<IsolateData, FreeIsolateData> isolate_data_;
DeleteFnPtr<Environment, FreeEnvironment> env_; DeleteFnPtr<Environment, FreeEnvironment> env_;
const std::string url_;
v8::Isolate* isolate_ = nullptr; v8::Isolate* isolate_ = nullptr;
DeleteFnPtr<ArrayBufferAllocator, FreeArrayBufferAllocator> DeleteFnPtr<ArrayBufferAllocator, FreeArrayBufferAllocator>
array_buffer_allocator_; array_buffer_allocator_;

View File

@ -361,6 +361,11 @@ was disabled at compile time.
Skip the rest of the tests in the current file when the Node.js executable Skip the rest of the tests in the current file when the Node.js executable
was compiled with a pointer size smaller than 64 bits. was compiled with a pointer size smaller than 64 bits.
### skipIfWorker()
Skip the rest of the tests in the current file when not running on a main
thread.
## ArrayStream Module ## ArrayStream Module
The `ArrayStream` module provides a simple `Stream` that pushes elements from The `ArrayStream` module provides a simple `Stream` that pushes elements from

View File

@ -614,10 +614,6 @@ function skipIfInspectorDisabled() {
if (process.config.variables.v8_enable_inspector === 0) { if (process.config.variables.v8_enable_inspector === 0) {
skip('V8 inspector is disabled'); skip('V8 inspector is disabled');
} }
if (!isMainThread) {
// TODO(addaleax): Fix me.
skip('V8 inspector is not available in Workers');
}
} }
function skipIf32Bits() { function skipIf32Bits() {
@ -626,6 +622,12 @@ function skipIf32Bits() {
} }
} }
function skipIfWorker() {
if (!isMainThread) {
skip('This test only works on a main thread');
}
}
function getArrayBufferViews(buf) { function getArrayBufferViews(buf) {
const { buffer, byteOffset, byteLength } = buf; const { buffer, byteOffset, byteLength } = buf;
@ -744,6 +746,7 @@ module.exports = {
skipIf32Bits, skipIf32Bits,
skipIfEslintMissing, skipIfEslintMissing,
skipIfInspectorDisabled, skipIfInspectorDisabled,
skipIfWorker,
get localhostIPv6() { return '::1'; }, get localhostIPv6() { return '::1'; },

View File

@ -11,4 +11,4 @@ const list = process.moduleLoadList.slice();
const assert = require('assert'); const assert = require('assert');
assert(list.length <= 74, list); assert(list.length <= 75, list);

View File

@ -3,6 +3,7 @@
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();
common.skipIfWorker();
// Assert that even when started with `--inspect=0` workers are assigned // Assert that even when started with `--inspect=0` workers are assigned
// consecutive (i.e. deterministically predictable) debug ports // consecutive (i.e. deterministically predictable) debug ports

View File

@ -3,6 +3,7 @@
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();
common.skipIfWorker(); // https://github.com/nodejs/node/issues/22767
const assert = require('assert'); const assert = require('assert');
const { Session } = require('inspector'); const { Session } = require('inspector');

View File

@ -3,6 +3,7 @@
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();
common.skipIfWorker(); // https://github.com/nodejs/node/issues/22767
const assert = require('assert'); const assert = require('assert');
const { performance } = require('perf_hooks'); const { performance } = require('perf_hooks');

View File

@ -10,6 +10,8 @@ common.skipIfInspectorDisabled();
if (common.isWindows) if (common.isWindows)
common.skip('test does not apply to Windows'); common.skip('test does not apply to Windows');
common.skipIfWorker(); // Worker inspector never has a server running
common.expectWarning('Warning', common.expectWarning('Warning',
'process.on(SIGPROF) is reserved while debugging', 'process.on(SIGPROF) is reserved while debugging',
common.noWarnCode); common.noWarnCode);

View File

@ -6,6 +6,8 @@ common.skipIf32Bits();
const { NodeInstance } = require('../common/inspector-helper.js'); const { NodeInstance } = require('../common/inspector-helper.js');
const assert = require('assert'); const assert = require('assert');
common.skipIfWorker(); // Signal starts a server for a main thread inspector
const script = ` const script = `
process._rawDebug('Waiting until a signal enables the inspector...'); process._rawDebug('Waiting until a signal enables the inspector...');
let waiting = setInterval(waitUntilDebugged, 50); let waiting = setInterval(waitUntilDebugged, 50);

View File

@ -33,7 +33,11 @@ async function testContextCreatedAndDestroyed() {
// the PID. // the PID.
strictEqual(name.includes(`[${process.pid}]`), true); strictEqual(name.includes(`[${process.pid}]`), true);
} else { } else {
strictEqual(`${process.argv0}[${process.pid}]`, name); let expects = `${process.argv0}[${process.pid}]`;
if (!common.isMainThread) {
expects = `Worker[${require('worker_threads').threadId}]`;
}
strictEqual(expects, name);
} }
strictEqual(origin, '', strictEqual(origin, '',
JSON.stringify(contextCreated)); JSON.stringify(contextCreated));

View File

@ -3,6 +3,7 @@
const common = require('../common'); const common = require('../common');
common.skipIfInspectorDisabled(); common.skipIfInspectorDisabled();
common.skipIfWorker();
const assert = require('assert'); const assert = require('assert');
const cluster = require('cluster'); const cluster = require('cluster');