diff --git a/doc/api/inspector.md b/doc/api/inspector.md index 73c735efd3b4b2..3336874b949269 100644 --- a/doc/api/inspector.md +++ b/doc/api/inspector.md @@ -10,6 +10,30 @@ It can be accessed using: const inspector = require('inspector'); ``` +## inspector.open([port[, host[, wait]]]) + +* port {number} Port to listen on for inspector connections. Optional, + defaults to what was specified on the CLI. +* host {string} Host to listen on for inspector connections. Optional, + defaults to what was specified on the CLI. +* wait {boolean} Block until a client has connected. Optional, defaults + to false. + +Activate inspector on host and port. Equivalent to `node +--inspect=[[host:]port]`, but can be done programatically after node has +started. + +If wait is `true`, will block until a client has connected to the inspect port +and flow control has been passed to the debugger client. + +### inspector.close() + +Deactivate the inspector. Blocks until there are no active connections. + +### inspector.url() + +Return the URL of the active inspector, or `undefined` if there is none. + ## Class: inspector.Session The `inspector.Session` is used for dispatching messages to the V8 inspector @@ -110,6 +134,7 @@ with an error. [`session.connect()`] will need to be called to be able to send messages again. Reconnected session will lose all inspector state, such as enabled agents or configured breakpoints. + [`session.connect()`]: #sessionconnect [`Debugger.paused`]: https://chromedevtools.github.io/devtools-protocol/v8/Debugger/#event-paused [`EventEmitter`]: events.html#events_class_eventemitter diff --git a/lib/inspector.js b/lib/inspector.js index 1edc9fc3beebeb..e4c401a9951da7 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -1,8 +1,8 @@ 'use strict'; -const connect = process.binding('inspector').connect; const EventEmitter = require('events'); const util = require('util'); +const { connect, open, url } = process.binding('inspector'); if (!connect) throw new Error('Inspector is not available'); @@ -83,5 +83,8 @@ class Session extends EventEmitter { } module.exports = { + open: (port, host, wait) => open(port, host, !!wait), + close: process._debugEnd, + url: url, Session }; diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 8269c9e097055c..948719ed702c5a 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -562,6 +562,7 @@ bool Agent::Start(v8::Platform* platform, const char* path, // Ignore failure, SIGUSR1 won't work, but that should not block node start. StartDebugSignalHandler(); if (options.inspector_enabled()) { + // This will return false if listen failed on the inspector port. return StartIoThread(options.wait_for_connect()); } return true; @@ -666,6 +667,50 @@ void Agent::PauseOnNextJavascriptStatement(const std::string& reason) { channel->schedulePauseOnNextStatement(reason); } +void Open(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + inspector::Agent* agent = env->inspector_agent(); + bool wait_for_connect = false; + + if (args.Length() > 0 && args[0]->IsUint32()) { + uint32_t port = args[0]->Uint32Value(); + agent->options().set_port(static_cast(port)); + } + + if (args.Length() > 1 && args[1]->IsString()) { + node::Utf8Value host(env->isolate(), args[1].As()); + agent->options().set_host_name(*host); + } + + if (args.Length() > 2 && args[2]->IsBoolean()) { + wait_for_connect = args[2]->BooleanValue(); + } + + agent->StartIoThread(wait_for_connect); +} + +void Url(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + inspector::Agent* agent = env->inspector_agent(); + inspector::InspectorIo* io = agent->io(); + + if (!io) return; + + std::vector ids = io->GetTargetIds(); + + if (ids.empty()) return; + + std::string url = "ws://"; + url += io->host(); + url += ":"; + url += std::to_string(io->port()); + url += "/"; + url += ids[0]; + + args.GetReturnValue().Set(OneByteString(env->isolate(), url.c_str())); +} + + // static void Agent::InitInspector(Local target, Local unused, Local context, void* priv) { @@ -675,11 +720,13 @@ void Agent::InitInspector(Local target, Local unused, if (agent->debug_options_.wait_for_connect()) env->SetMethod(target, "callAndPauseOnStart", CallAndPauseOnStart); env->SetMethod(target, "connect", ConnectJSBindingsSession); + env->SetMethod(target, "open", Open); + env->SetMethod(target, "url", Url); } void Agent::RequestIoThreadStart() { // We need to attempt to interrupt V8 flow (in case Node is running - // continuous JS code) and to wake up libuv thread (in case Node is wating + // continuous JS code) and to wake up libuv thread (in case Node is waiting // for IO events) uv_async_send(&start_io_thread_async); v8::Isolate* isolate = parent_env_->isolate(); diff --git a/src/inspector_agent.h b/src/inspector_agent.h index 367b460d203342..80967212cd7aef 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -95,6 +95,8 @@ class Agent { // Calls StartIoThread() from off the main thread. void RequestIoThreadStart(); + DebugOptions& options() { return debug_options_; } + private: node::Environment* parent_env_; std::unique_ptr client_; diff --git a/src/inspector_io.cc b/src/inspector_io.cc index a558205a186c82..69eed62ab4729a 100644 --- a/src/inspector_io.cc +++ b/src/inspector_io.cc @@ -354,6 +354,10 @@ void InspectorIo::PostIncomingMessage(InspectorAction action, int session_id, NotifyMessageReceived(); } +std::vector InspectorIo::GetTargetIds() const { + return delegate_ ? delegate_->GetTargetIds() : std::vector(); +} + void InspectorIo::WaitForFrontendMessageWhilePaused() { dispatching_messages_ = false; Mutex::ScopedLock scoped_lock(state_lock_); diff --git a/src/inspector_io.h b/src/inspector_io.h index 9ea1e0a785f878..6ef2ea54c4745d 100644 --- a/src/inspector_io.h +++ b/src/inspector_io.h @@ -72,6 +72,8 @@ class InspectorIo { } int port() const { return port_; } + std::string host() const { return options_.host_name(); } + std::vector GetTargetIds() const; private: template @@ -152,7 +154,6 @@ class InspectorIo { std::string script_name_; std::string script_path_; - const std::string id_; const bool wait_for_connect_; int port_; diff --git a/src/node.cc b/src/node.cc index 3a7e4049fbf516..68f134ffe99b79 100644 --- a/src/node.cc +++ b/src/node.cc @@ -264,6 +264,9 @@ static struct { #if HAVE_INSPECTOR bool StartInspector(Environment *env, const char* script_path, const node::DebugOptions& options) { + // Inspector agent can't fail to start, but if it was configured to listen + // right away on the websocket port and fails to bind/etc, this will return + // false. return env->inspector_agent()->Start(platform_, script_path, options); } diff --git a/src/node_debug_options.h b/src/node_debug_options.h index 6fdd30384fc3e8..99364f40989f6a 100644 --- a/src/node_debug_options.h +++ b/src/node_debug_options.h @@ -21,6 +21,7 @@ class DebugOptions { } bool wait_for_connect() const { return break_first_line_; } std::string host_name() const { return host_name_; } + void set_host_name(std::string host_name) { host_name_ = host_name; } int port() const; void set_port(int port) { port_ = port; } diff --git a/test/parallel/test-inspector-open.js b/test/parallel/test-inspector-open.js new file mode 100644 index 00000000000000..bc7d15a554aa06 --- /dev/null +++ b/test/parallel/test-inspector-open.js @@ -0,0 +1,104 @@ +'use strict'; +const common = require('../common'); + +// Test inspector open()/close()/url() API. It uses ephemeral ports so can be +// run safely in parallel. + +const assert = require('assert'); +const fork = require('child_process').fork; +const net = require('net'); +const url = require('url'); + +common.skipIfInspectorDisabled(); + +if (process.env.BE_CHILD) + return beChild(); + +const child = fork(__filename, {env: {BE_CHILD: 1}}); + +child.once('message', common.mustCall((msg) => { + assert.strictEqual(msg.cmd, 'started'); + + child.send({cmd: 'open', args: [0]}); + child.once('message', common.mustCall(firstOpen)); +})); + +let firstPort; + +function firstOpen(msg) { + assert.strictEqual(msg.cmd, 'url'); + const port = url.parse(msg.url).port; + ping(port, (err) => { + assert.ifError(err); + // Inspector is already open, and won't be reopened, so args don't matter. + child.send({cmd: 'open', args: []}); + child.once('message', common.mustCall(tryToOpenWhenOpen)); + firstPort = port; + }); +} + +function tryToOpenWhenOpen(msg) { + assert.strictEqual(msg.cmd, 'url'); + const port = url.parse(msg.url).port; + // Reopen didn't do anything, the port was already open, and has not changed. + assert.strictEqual(port, firstPort); + ping(port, (err) => { + assert.ifError(err); + child.send({cmd: 'close'}); + child.once('message', common.mustCall(closeWhenOpen)); + }); +} + +function closeWhenOpen(msg) { + assert.strictEqual(msg.cmd, 'url'); + assert.strictEqual(msg.url, undefined); + ping(firstPort, (err) => { + assert(err); + child.send({cmd: 'close'}); + child.once('message', common.mustCall(tryToCloseWhenClosed)); + }); +} + +function tryToCloseWhenClosed(msg) { + assert.strictEqual(msg.cmd, 'url'); + assert.strictEqual(msg.url, undefined); + child.send({cmd: 'open', args: []}); + child.once('message', common.mustCall(reopenAfterClose)); +} + +function reopenAfterClose(msg) { + assert.strictEqual(msg.cmd, 'url'); + const port = url.parse(msg.url).port; + assert.notStrictEqual(port, firstPort); + ping(port, (err) => { + assert.ifError(err); + process.exit(); + }); +} + +function ping(port, callback) { + net.connect(port) + .on('connect', function() { close(this); }) + .on('error', function(err) { close(this, err); }); + + function close(self, err) { + self.end(); + self.on('close', () => callback(err)); + } +} + +function beChild() { + const inspector = require('inspector'); + + process.send({cmd: 'started'}); + + process.on('message', (msg) => { + if (msg.cmd === 'open') { + inspector.open(...msg.args); + } + if (msg.cmd === 'close') { + inspector.close(); + } + process.send({cmd: 'url', url: inspector.url()}); + }); +}