inspector: allow --inspect=host:port from js

PR-URL: https://github.com/nodejs/node/pull/13228
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Sam Roberts 2017-05-25 19:00:24 -07:00
parent dcfbbacba8
commit 2791b360c1
9 changed files with 193 additions and 3 deletions

View File

@ -10,6 +10,30 @@ It can be accessed using:
const inspector = require('inspector'); 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 ## Class: inspector.Session
The `inspector.Session` is used for dispatching messages to the V8 inspector 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 messages again. Reconnected session will lose all inspector state, such as
enabled agents or configured breakpoints. enabled agents or configured breakpoints.
[`session.connect()`]: #sessionconnect [`session.connect()`]: #sessionconnect
[`Debugger.paused`]: https://chromedevtools.github.io/devtools-protocol/v8/Debugger/#event-paused [`Debugger.paused`]: https://chromedevtools.github.io/devtools-protocol/v8/Debugger/#event-paused
[`EventEmitter`]: events.html#events_class_eventemitter [`EventEmitter`]: events.html#events_class_eventemitter

View File

@ -1,8 +1,8 @@
'use strict'; 'use strict';
const connect = process.binding('inspector').connect;
const EventEmitter = require('events'); const EventEmitter = require('events');
const util = require('util'); const util = require('util');
const { connect, open, url } = process.binding('inspector');
if (!connect) if (!connect)
throw new Error('Inspector is not available'); throw new Error('Inspector is not available');
@ -83,5 +83,8 @@ class Session extends EventEmitter {
} }
module.exports = { module.exports = {
open: (port, host, wait) => open(port, host, !!wait),
close: process._debugEnd,
url: url,
Session Session
}; };

View File

@ -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. // Ignore failure, SIGUSR1 won't work, but that should not block node start.
StartDebugSignalHandler(); StartDebugSignalHandler();
if (options.inspector_enabled()) { if (options.inspector_enabled()) {
// This will return false if listen failed on the inspector port.
return StartIoThread(options.wait_for_connect()); return StartIoThread(options.wait_for_connect());
} }
return true; return true;
@ -666,6 +667,50 @@ void Agent::PauseOnNextJavascriptStatement(const std::string& reason) {
channel->schedulePauseOnNextStatement(reason); channel->schedulePauseOnNextStatement(reason);
} }
void Open(const FunctionCallbackInfo<Value>& 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<int>(port));
}
if (args.Length() > 1 && args[1]->IsString()) {
node::Utf8Value host(env->isolate(), args[1].As<String>());
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<Value>& args) {
Environment* env = Environment::GetCurrent(args);
inspector::Agent* agent = env->inspector_agent();
inspector::InspectorIo* io = agent->io();
if (!io) return;
std::vector<std::string> 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 // static
void Agent::InitInspector(Local<Object> target, Local<Value> unused, void Agent::InitInspector(Local<Object> target, Local<Value> unused,
Local<Context> context, void* priv) { Local<Context> context, void* priv) {
@ -675,11 +720,13 @@ void Agent::InitInspector(Local<Object> target, Local<Value> unused,
if (agent->debug_options_.wait_for_connect()) if (agent->debug_options_.wait_for_connect())
env->SetMethod(target, "callAndPauseOnStart", CallAndPauseOnStart); env->SetMethod(target, "callAndPauseOnStart", CallAndPauseOnStart);
env->SetMethod(target, "connect", ConnectJSBindingsSession); env->SetMethod(target, "connect", ConnectJSBindingsSession);
env->SetMethod(target, "open", Open);
env->SetMethod(target, "url", Url);
} }
void Agent::RequestIoThreadStart() { void Agent::RequestIoThreadStart() {
// We need to attempt to interrupt V8 flow (in case Node is running // 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) // for IO events)
uv_async_send(&start_io_thread_async); uv_async_send(&start_io_thread_async);
v8::Isolate* isolate = parent_env_->isolate(); v8::Isolate* isolate = parent_env_->isolate();

View File

@ -95,6 +95,8 @@ class Agent {
// Calls StartIoThread() from off the main thread. // Calls StartIoThread() from off the main thread.
void RequestIoThreadStart(); void RequestIoThreadStart();
DebugOptions& options() { return debug_options_; }
private: private:
node::Environment* parent_env_; node::Environment* parent_env_;
std::unique_ptr<NodeInspectorClient> client_; std::unique_ptr<NodeInspectorClient> client_;

View File

@ -354,6 +354,10 @@ void InspectorIo::PostIncomingMessage(InspectorAction action, int session_id,
NotifyMessageReceived(); NotifyMessageReceived();
} }
std::vector<std::string> InspectorIo::GetTargetIds() const {
return delegate_ ? delegate_->GetTargetIds() : std::vector<std::string>();
}
void InspectorIo::WaitForFrontendMessageWhilePaused() { void InspectorIo::WaitForFrontendMessageWhilePaused() {
dispatching_messages_ = false; dispatching_messages_ = false;
Mutex::ScopedLock scoped_lock(state_lock_); Mutex::ScopedLock scoped_lock(state_lock_);

View File

@ -72,6 +72,8 @@ class InspectorIo {
} }
int port() const { return port_; } int port() const { return port_; }
std::string host() const { return options_.host_name(); }
std::vector<std::string> GetTargetIds() const;
private: private:
template <typename Action> template <typename Action>
@ -152,7 +154,6 @@ class InspectorIo {
std::string script_name_; std::string script_name_;
std::string script_path_; std::string script_path_;
const std::string id_;
const bool wait_for_connect_; const bool wait_for_connect_;
int port_; int port_;

View File

@ -264,6 +264,9 @@ static struct {
#if HAVE_INSPECTOR #if HAVE_INSPECTOR
bool StartInspector(Environment *env, const char* script_path, bool StartInspector(Environment *env, const char* script_path,
const node::DebugOptions& options) { 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); return env->inspector_agent()->Start(platform_, script_path, options);
} }

View File

@ -21,6 +21,7 @@ class DebugOptions {
} }
bool wait_for_connect() const { return break_first_line_; } bool wait_for_connect() const { return break_first_line_; }
std::string host_name() const { return host_name_; } std::string host_name() const { return host_name_; }
void set_host_name(std::string host_name) { host_name_ = host_name; }
int port() const; int port() const;
void set_port(int port) { port_ = port; } void set_port(int port) { port_ = port; }

View File

@ -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()});
});
}