Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jun 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]]])

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe document the signatures, a la session.post

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, I'll do

* 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,
Copy link
Contributor

Choose a reason for hiding this comment

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

Re: #9659 I now think this should have been a URL not a string...

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() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it not worth moving this to a fixture?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

code is used nowhere else, and is intimately tied to this specific test

Copy link
Contributor

Choose a reason for hiding this comment

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

Ack

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