mirror of https://github.com/nodejs/node.git
inspector: JavaScript bindings for the inspector
PR-URL: https://github.com/nodejs/node/pull/12263 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Sam Roberts <vieuxtech@gmail.com> Reviewed-By: Timothy Gu <timothygu99@gmail.com> Reviewed-By: Josh Gavant <josh.gavant@outlook.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
This commit is contained in:
parent
c0d858f8bb
commit
60390cd7fb
|
@ -24,6 +24,7 @@
|
|||
* [Globals](globals.html)
|
||||
* [HTTP](http.html)
|
||||
* [HTTPS](https.html)
|
||||
* [Inspector](inspector.html)
|
||||
* [Modules](modules.html)
|
||||
* [Net](net.html)
|
||||
* [OS](os.html)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
@include globals
|
||||
@include http
|
||||
@include https
|
||||
@include inspector
|
||||
@include modules
|
||||
@include net
|
||||
@include os
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
# Inspector
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
The `inspector` module provides an API for interacting with the V8 inspector.
|
||||
|
||||
It can be accessed using:
|
||||
|
||||
```js
|
||||
const inspector = require('inspector');
|
||||
```
|
||||
|
||||
## Class: inspector.Session
|
||||
|
||||
The `inspector.Session` is used for dispatching messages to the V8 inspector
|
||||
back-end and receiving message responses and notifications.
|
||||
|
||||
### Constructor: new inspector.Session()
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
Create a new instance of the `inspector.Session` class. The inspector session
|
||||
needs to be connected through [`session.connect()`][] before the messages
|
||||
can be dispatched to the inspector backend.
|
||||
|
||||
`inspector.Session` is an [`EventEmitter`][] with the following events:
|
||||
|
||||
### Event: 'inspectorNotification'
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* {Object} The notification message object
|
||||
|
||||
Emitted when any notification from the V8 Inspector is received.
|
||||
|
||||
```js
|
||||
session.on('inspectorNotification', (message) => console.log(message.method));
|
||||
// Debugger.paused
|
||||
// Debugger.resumed
|
||||
```
|
||||
|
||||
It is also possible to subscribe only to notifications with specific method:
|
||||
|
||||
### Event: <inspector-protocol-method>
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* {Object} The notification message object
|
||||
|
||||
Emitted when an inspector notification is received that has its method field set
|
||||
to the `<inspector-protocol-method>` value.
|
||||
|
||||
The following snippet installs a listener on the [`Debugger.paused`][]
|
||||
event, and prints the reason for program suspension whenever program
|
||||
execution is suspended (through breakpoints, for example):
|
||||
|
||||
```js
|
||||
session.on('Debugger.paused', ({params}) => console.log(params.hitBreakpoints));
|
||||
// [ '/node/test/inspector/test-bindings.js:11:0' ]
|
||||
```
|
||||
|
||||
### session.connect()
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
Connects a session to the inspector back-end. An exception will be thrown
|
||||
if there is already a connected session established either through the API or by
|
||||
a front-end connected to the Inspector WebSocket port.
|
||||
|
||||
### session.post(method[, params][, callback])
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* method {string}
|
||||
* params {Object}
|
||||
* callback {Function}
|
||||
|
||||
Posts a message to the inspector back-end. `callback` will be notified when
|
||||
a response is received. `callback` is a function that accepts two optional
|
||||
arguments - error and message-specific result.
|
||||
|
||||
```js
|
||||
session.post('Runtime.evaluate', {'expression': '2 + 2'},
|
||||
(error, {result}) => console.log(result.value));
|
||||
// Output: { type: 'number', value: 4, description: '4' }
|
||||
```
|
||||
|
||||
The latest version of the V8 inspector protocol is published on the
|
||||
[Chrome DevTools Protocol Viewer][].
|
||||
|
||||
Node inspector supports all the Chrome DevTools Protocol domains declared
|
||||
by V8. Chrome DevTools Protocol domain provides an interface for interacting
|
||||
with one of the runtime agents used to inspect the application state and listen
|
||||
to the run-time events.
|
||||
|
||||
### session.disconnect()
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
Immediately close the session. All pending message callbacks will be called
|
||||
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
|
||||
[Chrome DevTools Protocol Viewer]: https://chromedevtools.github.io/devtools-protocol/v8/
|
|
@ -0,0 +1,87 @@
|
|||
'use strict';
|
||||
|
||||
const connect = process.binding('inspector').connect;
|
||||
const EventEmitter = require('events');
|
||||
const util = require('util');
|
||||
|
||||
if (!connect)
|
||||
throw new Error('Inspector is not available');
|
||||
|
||||
const connectionSymbol = Symbol('connectionProperty');
|
||||
const messageCallbacksSymbol = Symbol('messageCallbacks');
|
||||
const nextIdSymbol = Symbol('nextId');
|
||||
const onMessageSymbol = Symbol('onMessage');
|
||||
|
||||
class Session extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this[connectionSymbol] = null;
|
||||
this[nextIdSymbol] = 1;
|
||||
this[messageCallbacksSymbol] = new Map();
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this[connectionSymbol])
|
||||
throw new Error('Already connected');
|
||||
this[connectionSymbol] =
|
||||
connect((message) => this[onMessageSymbol](message));
|
||||
}
|
||||
|
||||
[onMessageSymbol](message) {
|
||||
const parsed = JSON.parse(message);
|
||||
if (parsed.id) {
|
||||
const callback = this[messageCallbacksSymbol].get(parsed.id);
|
||||
this[messageCallbacksSymbol].delete(parsed.id);
|
||||
if (callback)
|
||||
callback(parsed.error || null, parsed.result || null);
|
||||
} else {
|
||||
this.emit(parsed.method, parsed);
|
||||
this.emit('inspectorNotification', parsed);
|
||||
}
|
||||
}
|
||||
|
||||
post(method, params, callback) {
|
||||
if (typeof method !== 'string')
|
||||
throw new TypeError(
|
||||
`"method" must be a string, got ${typeof method} instead`);
|
||||
if (!callback && util.isFunction(params)) {
|
||||
callback = params;
|
||||
params = null;
|
||||
}
|
||||
if (params && typeof params !== 'object')
|
||||
throw new TypeError(
|
||||
`"params" must be an object, got ${typeof params} instead`);
|
||||
if (callback && typeof callback !== 'function')
|
||||
throw new TypeError(
|
||||
`"callback" must be a function, got ${typeof callback} instead`);
|
||||
|
||||
if (!this[connectionSymbol])
|
||||
throw new Error('Session is not connected');
|
||||
const id = this[nextIdSymbol]++;
|
||||
const message = {id, method};
|
||||
if (params) {
|
||||
message['params'] = params;
|
||||
}
|
||||
if (callback) {
|
||||
this[messageCallbacksSymbol].set(id, callback);
|
||||
}
|
||||
this[connectionSymbol].dispatch(JSON.stringify(message));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (!this[connectionSymbol])
|
||||
return;
|
||||
this[connectionSymbol].disconnect();
|
||||
this[connectionSymbol] = null;
|
||||
const remainingCallbacks = this[messageCallbacksSymbol].values();
|
||||
for (const callback of remainingCallbacks) {
|
||||
process.nextTick(callback, new Error('Session was closed'));
|
||||
}
|
||||
this[messageCallbacksSymbol].clear();
|
||||
this[nextIdSymbol] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Session
|
||||
};
|
1
node.gyp
1
node.gyp
|
@ -43,6 +43,7 @@
|
|||
'lib/_http_outgoing.js',
|
||||
'lib/_http_server.js',
|
||||
'lib/https.js',
|
||||
'lib/inspector.js',
|
||||
'lib/module.js',
|
||||
'lib/net.js',
|
||||
'lib/os.js',
|
||||
|
|
|
@ -22,12 +22,17 @@ namespace node {
|
|||
namespace inspector {
|
||||
namespace {
|
||||
using v8::Context;
|
||||
using v8::External;
|
||||
using v8::Function;
|
||||
using v8::FunctionCallbackInfo;
|
||||
using v8::HandleScope;
|
||||
using v8::Isolate;
|
||||
using v8::Local;
|
||||
using v8::Maybe;
|
||||
using v8::MaybeLocal;
|
||||
using v8::NewStringType;
|
||||
using v8::Object;
|
||||
using v8::Persistent;
|
||||
using v8::String;
|
||||
using v8::Value;
|
||||
|
||||
|
@ -176,6 +181,143 @@ static int RegisterDebugSignalHandler() {
|
|||
}
|
||||
#endif // _WIN32
|
||||
|
||||
class JsBindingsSessionDelegate : public InspectorSessionDelegate {
|
||||
public:
|
||||
JsBindingsSessionDelegate(Environment* env,
|
||||
Local<Object> session,
|
||||
Local<Object> receiver,
|
||||
Local<Function> callback)
|
||||
: env_(env),
|
||||
session_(env->isolate(), session),
|
||||
receiver_(env->isolate(), receiver),
|
||||
callback_(env->isolate(), callback) {
|
||||
session_.SetWeak(this, JsBindingsSessionDelegate::Release,
|
||||
v8::WeakCallbackType::kParameter);
|
||||
}
|
||||
|
||||
virtual ~JsBindingsSessionDelegate() {
|
||||
session_.Reset();
|
||||
receiver_.Reset();
|
||||
callback_.Reset();
|
||||
}
|
||||
|
||||
bool WaitForFrontendMessage() override {
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnMessage(const v8_inspector::StringView& message) override {
|
||||
Isolate* isolate = env_->isolate();
|
||||
v8::HandleScope handle_scope(isolate);
|
||||
Context::Scope context_scope(env_->context());
|
||||
MaybeLocal<String> v8string =
|
||||
String::NewFromTwoByte(isolate, message.characters16(),
|
||||
NewStringType::kNormal, message.length());
|
||||
Local<Value> argument = v8string.ToLocalChecked().As<Value>();
|
||||
Local<Function> callback = callback_.Get(isolate);
|
||||
Local<Object> receiver = receiver_.Get(isolate);
|
||||
callback->Call(env_->context(), receiver, 1, &argument);
|
||||
}
|
||||
|
||||
void Disconnect() {
|
||||
Agent* agent = env_->inspector_agent();
|
||||
if (agent->delegate() == this)
|
||||
agent->Disconnect();
|
||||
}
|
||||
|
||||
private:
|
||||
static void Release(
|
||||
const v8::WeakCallbackInfo<JsBindingsSessionDelegate>& info) {
|
||||
info.SetSecondPassCallback(ReleaseSecondPass);
|
||||
info.GetParameter()->session_.Reset();
|
||||
}
|
||||
|
||||
static void ReleaseSecondPass(
|
||||
const v8::WeakCallbackInfo<JsBindingsSessionDelegate>& info) {
|
||||
JsBindingsSessionDelegate* delegate = info.GetParameter();
|
||||
delegate->Disconnect();
|
||||
delete delegate;
|
||||
}
|
||||
|
||||
Environment* env_;
|
||||
Persistent<Object> session_;
|
||||
Persistent<Object> receiver_;
|
||||
Persistent<Function> callback_;
|
||||
};
|
||||
|
||||
void SetDelegate(Environment* env, Local<Object> inspector,
|
||||
JsBindingsSessionDelegate* delegate) {
|
||||
inspector->SetPrivate(env->context(),
|
||||
env->inspector_delegate_private_symbol(),
|
||||
v8::External::New(env->isolate(), delegate));
|
||||
}
|
||||
|
||||
Maybe<JsBindingsSessionDelegate*> GetDelegate(
|
||||
const FunctionCallbackInfo<Value>& info) {
|
||||
Environment* env = Environment::GetCurrent(info);
|
||||
Local<Value> delegate;
|
||||
MaybeLocal<Value> maybe_delegate =
|
||||
info.This()->GetPrivate(env->context(),
|
||||
env->inspector_delegate_private_symbol());
|
||||
|
||||
if (maybe_delegate.ToLocal(&delegate)) {
|
||||
CHECK(delegate->IsExternal());
|
||||
void* value = delegate.As<External>()->Value();
|
||||
if (value != nullptr) {
|
||||
return v8::Just(static_cast<JsBindingsSessionDelegate*>(value));
|
||||
}
|
||||
}
|
||||
env->ThrowError("Inspector is not connected");
|
||||
return v8::Nothing<JsBindingsSessionDelegate*>();
|
||||
}
|
||||
|
||||
void Dispatch(const FunctionCallbackInfo<Value>& info) {
|
||||
Environment* env = Environment::GetCurrent(info);
|
||||
if (!info[0]->IsString()) {
|
||||
env->ThrowError("Inspector message must be a string");
|
||||
return;
|
||||
}
|
||||
Maybe<JsBindingsSessionDelegate*> maybe_delegate = GetDelegate(info);
|
||||
if (maybe_delegate.IsNothing())
|
||||
return;
|
||||
Agent* inspector = env->inspector_agent();
|
||||
CHECK_EQ(maybe_delegate.ToChecked(), inspector->delegate());
|
||||
inspector->Dispatch(ToProtocolString(env->isolate(), info[0])->string());
|
||||
}
|
||||
|
||||
void Disconnect(const FunctionCallbackInfo<Value>& info) {
|
||||
Environment* env = Environment::GetCurrent(info);
|
||||
Maybe<JsBindingsSessionDelegate*> delegate = GetDelegate(info);
|
||||
if (delegate.IsNothing()) {
|
||||
return;
|
||||
}
|
||||
delegate.ToChecked()->Disconnect();
|
||||
SetDelegate(env, info.This(), nullptr);
|
||||
delete delegate.ToChecked();
|
||||
}
|
||||
|
||||
void ConnectJSBindingsSession(const FunctionCallbackInfo<Value>& info) {
|
||||
Environment* env = Environment::GetCurrent(info);
|
||||
if (!info[0]->IsFunction()) {
|
||||
env->ThrowError("Message callback is required");
|
||||
return;
|
||||
}
|
||||
Agent* inspector = env->inspector_agent();
|
||||
if (inspector->delegate() != nullptr) {
|
||||
env->ThrowError("Session is already attached");
|
||||
return;
|
||||
}
|
||||
Local<Object> session = Object::New(env->isolate());
|
||||
env->SetMethod(session, "dispatch", Dispatch);
|
||||
env->SetMethod(session, "disconnect", Disconnect);
|
||||
info.GetReturnValue().Set(session);
|
||||
|
||||
JsBindingsSessionDelegate* delegate =
|
||||
new JsBindingsSessionDelegate(env, session, info.Holder(),
|
||||
info[0].As<Function>());
|
||||
inspector->Connect(delegate);
|
||||
SetDelegate(env, session, delegate);
|
||||
}
|
||||
|
||||
void InspectorConsoleCall(const v8::FunctionCallbackInfo<Value>& info) {
|
||||
Isolate* isolate = info.GetIsolate();
|
||||
HandleScope handle_scope(isolate);
|
||||
|
@ -229,7 +371,6 @@ void CallAndPauseOnStart(
|
|||
call_args.size(), call_args.data());
|
||||
args.GetReturnValue().Set(retval.ToLocalChecked());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Used in NodeInspectorClient::currentTimeMS() below.
|
||||
const int NANOS_PER_MSEC = 1000000;
|
||||
|
@ -284,6 +425,8 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel {
|
|||
std::unique_ptr<v8_inspector::V8InspectorSession> session_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class NodeInspectorClient : public v8_inspector::V8InspectorClient {
|
||||
public:
|
||||
NodeInspectorClient(node::Environment* env,
|
||||
|
@ -510,6 +653,14 @@ void Agent::RunMessageLoop() {
|
|||
inspector_->runMessageLoopOnPause(CONTEXT_GROUP_ID);
|
||||
}
|
||||
|
||||
InspectorSessionDelegate* Agent::delegate() {
|
||||
CHECK_NE(inspector_, nullptr);
|
||||
ChannelImpl* channel = inspector_->channel();
|
||||
if (channel == nullptr)
|
||||
return nullptr;
|
||||
return channel->delegate();
|
||||
}
|
||||
|
||||
void Agent::PauseOnNextJavascriptStatement(const std::string& reason) {
|
||||
ChannelImpl* channel = inspector_->channel();
|
||||
if (channel != nullptr)
|
||||
|
@ -524,6 +675,7 @@ void Agent::InitJSBindings(Local<Object> target, Local<Value> unused,
|
|||
env->SetMethod(target, "consoleCall", InspectorConsoleCall);
|
||||
if (agent->debug_options_.wait_for_connect())
|
||||
env->SetMethod(target, "callAndPauseOnStart", CallAndPauseOnStart);
|
||||
env->SetMethod(target, "connect", ConnectJSBindingsSession);
|
||||
}
|
||||
|
||||
void Agent::RequestIoStart() {
|
||||
|
|
|
@ -59,6 +59,7 @@ class Agent {
|
|||
void FatalException(v8::Local<v8::Value> error,
|
||||
v8::Local<v8::Message> message);
|
||||
void Connect(InspectorSessionDelegate* delegate);
|
||||
InspectorSessionDelegate* delegate();
|
||||
void Disconnect();
|
||||
void Dispatch(const v8_inspector::StringView& message);
|
||||
void RunMessageLoop();
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
'use strict';
|
||||
require('../common');
|
||||
const assert = require('assert');
|
||||
const inspector = require('inspector');
|
||||
const path = require('path');
|
||||
|
||||
// This test case will set a breakpoint 4 lines below
|
||||
function debuggedFunction() {
|
||||
let i;
|
||||
let accum = 0;
|
||||
for (i = 0; i < 5; i++) {
|
||||
accum += i;
|
||||
}
|
||||
return accum;
|
||||
}
|
||||
|
||||
let scopeCallback = null;
|
||||
|
||||
function checkScope(session, scopeId) {
|
||||
session.post('Runtime.getProperties', {
|
||||
'objectId': scopeId,
|
||||
'ownProperties': false,
|
||||
'accessorPropertiesOnly': false,
|
||||
'generatePreview': true
|
||||
}, scopeCallback);
|
||||
}
|
||||
|
||||
function debuggerPausedCallback(session, notification) {
|
||||
const params = notification['params'];
|
||||
const callFrame = params['callFrames'][0];
|
||||
const scopeId = callFrame['scopeChain'][0]['object']['objectId'];
|
||||
checkScope(session, scopeId);
|
||||
}
|
||||
|
||||
function testNoCrashWithExceptionInCallback() {
|
||||
// There is a deliberate exception in the callback
|
||||
const session = new inspector.Session();
|
||||
session.connect();
|
||||
const error = new Error('We expect this');
|
||||
assert.throws(() => {
|
||||
session.post('Console.enable', () => { throw error; });
|
||||
}, (e) => e === error);
|
||||
session.disconnect();
|
||||
}
|
||||
|
||||
function testSampleDebugSession() {
|
||||
let cur = 0;
|
||||
const failures = [];
|
||||
const expects = {
|
||||
i: [0, 1, 2, 3, 4],
|
||||
accum: [0, 0, 1, 3, 6]
|
||||
};
|
||||
scopeCallback = function(error, result) {
|
||||
const i = cur++;
|
||||
let v, actual, expected;
|
||||
for (v of result['result']) {
|
||||
actual = v['value']['value'];
|
||||
expected = expects[v['name']][i];
|
||||
if (actual !== expected) {
|
||||
failures.push('Iteration ' + i + ' variable: ' + v['name'] +
|
||||
' expected: ' + expected + ' actual: ' + actual);
|
||||
}
|
||||
}
|
||||
};
|
||||
const session = new inspector.Session();
|
||||
session.connect();
|
||||
let secondSessionOpened = false;
|
||||
const secondSession = new inspector.Session();
|
||||
try {
|
||||
secondSession.connect();
|
||||
secondSessionOpened = true;
|
||||
} catch (error) {
|
||||
// expected as the session already exists
|
||||
}
|
||||
assert.strictEqual(secondSessionOpened, false);
|
||||
session.on('Debugger.paused',
|
||||
(notification) => debuggerPausedCallback(session, notification));
|
||||
let cbAsSecondArgCalled = false;
|
||||
assert.throws(() => {
|
||||
session.post('Debugger.enable', function() {}, function() {});
|
||||
}, TypeError);
|
||||
session.post('Debugger.enable', () => cbAsSecondArgCalled = true);
|
||||
session.post('Debugger.setBreakpointByUrl', {
|
||||
'lineNumber': 11,
|
||||
'url': path.resolve(__dirname, __filename),
|
||||
'columnNumber': 0,
|
||||
'condition': ''
|
||||
});
|
||||
|
||||
debuggedFunction();
|
||||
assert.deepStrictEqual(cbAsSecondArgCalled, true);
|
||||
assert.deepStrictEqual(failures, []);
|
||||
assert.strictEqual(cur, 5);
|
||||
scopeCallback = null;
|
||||
session.disconnect();
|
||||
assert.throws(() => session.post('Debugger.enable'), (e) => !!e);
|
||||
}
|
||||
|
||||
testNoCrashWithExceptionInCallback();
|
||||
testSampleDebugSession();
|
||||
let breakpointHit = false;
|
||||
scopeCallback = () => (breakpointHit = true);
|
||||
debuggedFunction();
|
||||
assert.strictEqual(breakpointHit, false);
|
||||
testSampleDebugSession();
|
Loading…
Reference in New Issue