Skip to content

Commit

Permalink
inspector: allow --inspect=host:port from js
Browse files Browse the repository at this point in the history
PR-URL: #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>
  • Loading branch information
sam-github committed Jun 6, 2017
1 parent dcfbbac commit 2791b36
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 3 deletions.
25 changes: 25 additions & 0 deletions doc/api/inspector.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion lib/inspector.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -83,5 +83,8 @@ class Session extends EventEmitter {
}

module.exports = {
open: (port, host, wait) => open(port, host, !!wait),
close: process._debugEnd,
url: url,
Session
};
49 changes: 48 additions & 1 deletion src/inspector_agent.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions src/inspector_agent.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand Down
4 changes: 4 additions & 0 deletions src/inspector_io.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
Expand Down
3 changes: 2 additions & 1 deletion src/inspector_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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_;

Expand Down
3 changes: 3 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
1 change: 1 addition & 0 deletions src/node_debug_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
104 changes: 104 additions & 0 deletions test/parallel/test-inspector-open.js
Original file line number Diff line number Diff line change
@@ -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()});
});
}

1 comment on commit 2791b36

@TimothyGu
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hindsight:

Fixes: #11496

Please sign in to comment.