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');
```
## 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

View File

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

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.
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<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
void Agent::InitInspector(Local<Object> target, Local<Value> unused,
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())
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();

View File

@ -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<NodeInspectorClient> client_;

View File

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

View File

@ -72,6 +72,8 @@ class InspectorIo {
}
int port() const { return port_; }
std::string host() const { return options_.host_name(); }
std::vector<std::string> GetTargetIds() const;
private:
template <typename Action>
@ -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_;

View File

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

View File

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

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