Skip to content

Commit 2791b36

Browse files
committed
inspector: allow --inspect=host:port from js
PR-URL: #13228 Reviewed-By: Michael Dawson <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent dcfbbac commit 2791b36

9 files changed

+193
-3
lines changed

doc/api/inspector.md

+25
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@ It can be accessed using:
1010
const inspector = require('inspector');
1111
```
1212

13+
## inspector.open([port[, host[, wait]]])
14+
15+
* port {number} Port to listen on for inspector connections. Optional,
16+
defaults to what was specified on the CLI.
17+
* host {string} Host to listen on for inspector connections. Optional,
18+
defaults to what was specified on the CLI.
19+
* wait {boolean} Block until a client has connected. Optional, defaults
20+
to false.
21+
22+
Activate inspector on host and port. Equivalent to `node
23+
--inspect=[[host:]port]`, but can be done programatically after node has
24+
started.
25+
26+
If wait is `true`, will block until a client has connected to the inspect port
27+
and flow control has been passed to the debugger client.
28+
29+
### inspector.close()
30+
31+
Deactivate the inspector. Blocks until there are no active connections.
32+
33+
### inspector.url()
34+
35+
Return the URL of the active inspector, or `undefined` if there is none.
36+
1337
## Class: inspector.Session
1438

1539
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
110134
messages again. Reconnected session will lose all inspector state, such as
111135
enabled agents or configured breakpoints.
112136

137+
113138
[`session.connect()`]: #sessionconnect
114139
[`Debugger.paused`]: https://chromedevtools.github.io/devtools-protocol/v8/Debugger/#event-paused
115140
[`EventEmitter`]: events.html#events_class_eventemitter

lib/inspector.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use strict';
22

3-
const connect = process.binding('inspector').connect;
43
const EventEmitter = require('events');
54
const util = require('util');
5+
const { connect, open, url } = process.binding('inspector');
66

77
if (!connect)
88
throw new Error('Inspector is not available');
@@ -83,5 +83,8 @@ class Session extends EventEmitter {
8383
}
8484

8585
module.exports = {
86+
open: (port, host, wait) => open(port, host, !!wait),
87+
close: process._debugEnd,
88+
url: url,
8689
Session
8790
};

src/inspector_agent.cc

+48-1
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ bool Agent::Start(v8::Platform* platform, const char* path,
562562
// Ignore failure, SIGUSR1 won't work, but that should not block node start.
563563
StartDebugSignalHandler();
564564
if (options.inspector_enabled()) {
565+
// This will return false if listen failed on the inspector port.
565566
return StartIoThread(options.wait_for_connect());
566567
}
567568
return true;
@@ -666,6 +667,50 @@ void Agent::PauseOnNextJavascriptStatement(const std::string& reason) {
666667
channel->schedulePauseOnNextStatement(reason);
667668
}
668669

670+
void Open(const FunctionCallbackInfo<Value>& args) {
671+
Environment* env = Environment::GetCurrent(args);
672+
inspector::Agent* agent = env->inspector_agent();
673+
bool wait_for_connect = false;
674+
675+
if (args.Length() > 0 && args[0]->IsUint32()) {
676+
uint32_t port = args[0]->Uint32Value();
677+
agent->options().set_port(static_cast<int>(port));
678+
}
679+
680+
if (args.Length() > 1 && args[1]->IsString()) {
681+
node::Utf8Value host(env->isolate(), args[1].As<String>());
682+
agent->options().set_host_name(*host);
683+
}
684+
685+
if (args.Length() > 2 && args[2]->IsBoolean()) {
686+
wait_for_connect = args[2]->BooleanValue();
687+
}
688+
689+
agent->StartIoThread(wait_for_connect);
690+
}
691+
692+
void Url(const FunctionCallbackInfo<Value>& args) {
693+
Environment* env = Environment::GetCurrent(args);
694+
inspector::Agent* agent = env->inspector_agent();
695+
inspector::InspectorIo* io = agent->io();
696+
697+
if (!io) return;
698+
699+
std::vector<std::string> ids = io->GetTargetIds();
700+
701+
if (ids.empty()) return;
702+
703+
std::string url = "ws://";
704+
url += io->host();
705+
url += ":";
706+
url += std::to_string(io->port());
707+
url += "/";
708+
url += ids[0];
709+
710+
args.GetReturnValue().Set(OneByteString(env->isolate(), url.c_str()));
711+
}
712+
713+
669714
// static
670715
void Agent::InitInspector(Local<Object> target, Local<Value> unused,
671716
Local<Context> context, void* priv) {
@@ -675,11 +720,13 @@ void Agent::InitInspector(Local<Object> target, Local<Value> unused,
675720
if (agent->debug_options_.wait_for_connect())
676721
env->SetMethod(target, "callAndPauseOnStart", CallAndPauseOnStart);
677722
env->SetMethod(target, "connect", ConnectJSBindingsSession);
723+
env->SetMethod(target, "open", Open);
724+
env->SetMethod(target, "url", Url);
678725
}
679726

680727
void Agent::RequestIoThreadStart() {
681728
// We need to attempt to interrupt V8 flow (in case Node is running
682-
// continuous JS code) and to wake up libuv thread (in case Node is wating
729+
// continuous JS code) and to wake up libuv thread (in case Node is waiting
683730
// for IO events)
684731
uv_async_send(&start_io_thread_async);
685732
v8::Isolate* isolate = parent_env_->isolate();

src/inspector_agent.h

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ class Agent {
9595
// Calls StartIoThread() from off the main thread.
9696
void RequestIoThreadStart();
9797

98+
DebugOptions& options() { return debug_options_; }
99+
98100
private:
99101
node::Environment* parent_env_;
100102
std::unique_ptr<NodeInspectorClient> client_;

src/inspector_io.cc

+4
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,10 @@ void InspectorIo::PostIncomingMessage(InspectorAction action, int session_id,
354354
NotifyMessageReceived();
355355
}
356356

357+
std::vector<std::string> InspectorIo::GetTargetIds() const {
358+
return delegate_ ? delegate_->GetTargetIds() : std::vector<std::string>();
359+
}
360+
357361
void InspectorIo::WaitForFrontendMessageWhilePaused() {
358362
dispatching_messages_ = false;
359363
Mutex::ScopedLock scoped_lock(state_lock_);

src/inspector_io.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ class InspectorIo {
7272
}
7373

7474
int port() const { return port_; }
75+
std::string host() const { return options_.host_name(); }
76+
std::vector<std::string> GetTargetIds() const;
7577

7678
private:
7779
template <typename Action>
@@ -152,7 +154,6 @@ class InspectorIo {
152154

153155
std::string script_name_;
154156
std::string script_path_;
155-
const std::string id_;
156157
const bool wait_for_connect_;
157158
int port_;
158159

src/node.cc

+3
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ static struct {
264264
#if HAVE_INSPECTOR
265265
bool StartInspector(Environment *env, const char* script_path,
266266
const node::DebugOptions& options) {
267+
// Inspector agent can't fail to start, but if it was configured to listen
268+
// right away on the websocket port and fails to bind/etc, this will return
269+
// false.
267270
return env->inspector_agent()->Start(platform_, script_path, options);
268271
}
269272

src/node_debug_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class DebugOptions {
2121
}
2222
bool wait_for_connect() const { return break_first_line_; }
2323
std::string host_name() const { return host_name_; }
24+
void set_host_name(std::string host_name) { host_name_ = host_name; }
2425
int port() const;
2526
void set_port(int port) { port_ = port; }
2627

test/parallel/test-inspector-open.js

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
// Test inspector open()/close()/url() API. It uses ephemeral ports so can be
5+
// run safely in parallel.
6+
7+
const assert = require('assert');
8+
const fork = require('child_process').fork;
9+
const net = require('net');
10+
const url = require('url');
11+
12+
common.skipIfInspectorDisabled();
13+
14+
if (process.env.BE_CHILD)
15+
return beChild();
16+
17+
const child = fork(__filename, {env: {BE_CHILD: 1}});
18+
19+
child.once('message', common.mustCall((msg) => {
20+
assert.strictEqual(msg.cmd, 'started');
21+
22+
child.send({cmd: 'open', args: [0]});
23+
child.once('message', common.mustCall(firstOpen));
24+
}));
25+
26+
let firstPort;
27+
28+
function firstOpen(msg) {
29+
assert.strictEqual(msg.cmd, 'url');
30+
const port = url.parse(msg.url).port;
31+
ping(port, (err) => {
32+
assert.ifError(err);
33+
// Inspector is already open, and won't be reopened, so args don't matter.
34+
child.send({cmd: 'open', args: []});
35+
child.once('message', common.mustCall(tryToOpenWhenOpen));
36+
firstPort = port;
37+
});
38+
}
39+
40+
function tryToOpenWhenOpen(msg) {
41+
assert.strictEqual(msg.cmd, 'url');
42+
const port = url.parse(msg.url).port;
43+
// Reopen didn't do anything, the port was already open, and has not changed.
44+
assert.strictEqual(port, firstPort);
45+
ping(port, (err) => {
46+
assert.ifError(err);
47+
child.send({cmd: 'close'});
48+
child.once('message', common.mustCall(closeWhenOpen));
49+
});
50+
}
51+
52+
function closeWhenOpen(msg) {
53+
assert.strictEqual(msg.cmd, 'url');
54+
assert.strictEqual(msg.url, undefined);
55+
ping(firstPort, (err) => {
56+
assert(err);
57+
child.send({cmd: 'close'});
58+
child.once('message', common.mustCall(tryToCloseWhenClosed));
59+
});
60+
}
61+
62+
function tryToCloseWhenClosed(msg) {
63+
assert.strictEqual(msg.cmd, 'url');
64+
assert.strictEqual(msg.url, undefined);
65+
child.send({cmd: 'open', args: []});
66+
child.once('message', common.mustCall(reopenAfterClose));
67+
}
68+
69+
function reopenAfterClose(msg) {
70+
assert.strictEqual(msg.cmd, 'url');
71+
const port = url.parse(msg.url).port;
72+
assert.notStrictEqual(port, firstPort);
73+
ping(port, (err) => {
74+
assert.ifError(err);
75+
process.exit();
76+
});
77+
}
78+
79+
function ping(port, callback) {
80+
net.connect(port)
81+
.on('connect', function() { close(this); })
82+
.on('error', function(err) { close(this, err); });
83+
84+
function close(self, err) {
85+
self.end();
86+
self.on('close', () => callback(err));
87+
}
88+
}
89+
90+
function beChild() {
91+
const inspector = require('inspector');
92+
93+
process.send({cmd: 'started'});
94+
95+
process.on('message', (msg) => {
96+
if (msg.cmd === 'open') {
97+
inspector.open(...msg.args);
98+
}
99+
if (msg.cmd === 'close') {
100+
inspector.close();
101+
}
102+
process.send({cmd: 'url', url: inspector.url()});
103+
});
104+
}

0 commit comments

Comments
 (0)