From fc1c7785e20c7b581701e6bc7fd831be7eb58272 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sat, 6 Apr 2019 07:49:00 +0800 Subject: [PATCH] inspector: introduce inspector.SyncSession This patch introduces a synchronous inspector session that dispatches the message and returns the result synchronously to the user. --- lib/inspector.js | 79 +++++++++++++++++++++- lib/internal/util/debuglog.js | 2 +- src/inspector_js_api.cc | 20 ++++-- test/parallel/test-inspect-sync-session.js | 64 ++++++++++++++++++ 4 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 test/parallel/test-inspect-sync-session.js diff --git a/lib/inspector.js b/lib/inspector.js index 793e63ff3942aa..00350a2c9e6c51 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -1,7 +1,7 @@ 'use strict'; const { JSON } = primordials; - +const assert = require('internal/assert'); const { ERR_INSPECTOR_ALREADY_CONNECTED, ERR_INSPECTOR_CLOSED, @@ -11,6 +11,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_INVALID_CALLBACK } = require('internal/errors').codes; +const debug = require('internal/util/debuglog').debuglog('inspector'); const { hasInspector } = internalBinding('config'); if (!hasInspector) @@ -25,6 +26,7 @@ const connectionSymbol = Symbol('connectionProperty'); const messageCallbacksSymbol = Symbol('messageCallbacks'); const nextIdSymbol = Symbol('nextId'); const onMessageSymbol = Symbol('onMessage'); +const resultSymbol = Symbol('result'); class Session extends EventEmitter { constructor() { @@ -109,6 +111,78 @@ class Session extends EventEmitter { } } +// The synchronicity of this class depends on: +// - ChannelImpl::sendProtocolResponse is synchronous +// - InspectorSessionDelegate::sendMessageToFrontend is synchronous +// - JSBindingsConnection::OnMessage is synchronous +// when the Connection is instantiated with is_sync = true. +class SyncSession extends EventEmitter { + constructor() { + super(); + this[connectionSymbol] = null; + this[nextIdSymbol] = 1; + this[resultSymbol] = undefined; + } + + connect() { + if (this[connectionSymbol]) + throw new ERR_INSPECTOR_ALREADY_CONNECTED('The inspector session'); + const connection = + new Connection(this[onMessageSymbol].bind(this), true); + if (connection.sessionAttached) { + throw new ERR_INSPECTOR_ALREADY_CONNECTED('Another inspector session'); + } + this[connectionSymbol] = connection; + } + + [onMessageSymbol](message) { + const parsed = JSON.parse(message); + if (parsed.error) { + throw (new ERR_INSPECTOR_COMMAND(parsed.error.code, + parsed.error.message)); + } + debug(`received message #${parsed.id}:`, parsed); + if (parsed.id) { + this[resultSymbol] = parsed.result; + } else { + this.emit(parsed.method, parsed); + this.emit('inspectorNotification', parsed); + } + } + + post(method, params) { + validateString(method, 'method'); + if (params && typeof params !== 'object') { + throw new ERR_INVALID_ARG_TYPE('params', 'Object', params); + } + if (!this[connectionSymbol]) { + throw new ERR_INSPECTOR_NOT_CONNECTED(); + } + const id = this[nextIdSymbol]++; + const message = { id, method }; + if (params) { + message.params = params; + } + this[resultSymbol] = undefined; + // [onMessageSymbol] is supposed to be called synchronously here + // to store the result in this[resultSymbol]. + debug(`dispatching message #${id}:`, message); + this[connectionSymbol].dispatch(JSON.stringify(message)); + const result = this[resultSymbol]; + assert(result !== undefined); + this[resultSymbol] = undefined; + return result; + } + + disconnect() { + if (!this[connectionSymbol]) + return; + this[connectionSymbol].disconnect(); + this[connectionSymbol] = null; + this[nextIdSymbol] = 1; + } +} + module.exports = { open: (port, host, wait) => open(port, host, !!wait), close: process._debugEnd, @@ -116,5 +190,6 @@ module.exports = { // This is dynamically added during bootstrap, // where the console from the VM is still available console: require('internal/util/inspector').consoleFromVM, - Session + Session, + SyncSession }; diff --git a/lib/internal/util/debuglog.js b/lib/internal/util/debuglog.js index 769328ac9d8453..e92f3ad2bd5b98 100644 --- a/lib/internal/util/debuglog.js +++ b/lib/internal/util/debuglog.js @@ -39,7 +39,7 @@ function debuglog(set) { emitWarningIfNeeded(set); debugs[set] = function debug(...args) { const msg = format(...args); - console.error('%s %d: %s', set, pid, msg); + process.stderr.write(format('%s %d: %s\n', set, pid, msg)); }; } else { debugs[set] = function debug() {}; diff --git a/src/inspector_js_api.cc b/src/inspector_js_api.cc index 8b55146dbe555f..ef563f9c8a7dd6 100644 --- a/src/inspector_js_api.cc +++ b/src/inspector_js_api.cc @@ -61,25 +61,34 @@ class JSBindingsConnection : public AsyncWrap { JSBindingsConnection* connection_; }; - JSBindingsConnection(Environment* env, + JSBindingsConnection(bool is_sync, + Environment* env, Local wrap, Local callback) - : AsyncWrap(env, wrap, PROVIDER_INSPECTORJSBINDING), - callback_(env->isolate(), callback) { + : AsyncWrap(env, wrap, PROVIDER_INSPECTORJSBINDING), + callback_(env->isolate(), callback) { Agent* inspector = env->inspector_agent(); session_ = inspector->Connect(std::make_unique( env, this), false); } void OnMessage(Local value) { - MakeCallback(callback_.Get(env()->isolate()), 1, &value); + if (is_sync) { + // The callback in JS land would store the result synchronously + // to return to user later. + USE(callback_.Get(env()->isolate()) + ->Call(env()->context(), v8::Null(env()->isolate()), 1, &value)); + } else { + MakeCallback(callback_.Get(env()->isolate()), 1, &value); + } } static void New(const FunctionCallbackInfo& info) { Environment* env = Environment::GetCurrent(info); CHECK(info[0]->IsFunction()); Local callback = info[0].As(); - new JSBindingsConnection(env, info.This(), callback); + bool is_sync = info[1]->IsTrue(); + new JSBindingsConnection(is_sync, env, info.This(), callback); } void Disconnect() { @@ -117,6 +126,7 @@ class JSBindingsConnection : public AsyncWrap { private: std::unique_ptr session_; Persistent callback_; + bool is_sync = false; }; static bool InspectorEnabled(Environment* env) { diff --git a/test/parallel/test-inspect-sync-session.js b/test/parallel/test-inspect-sync-session.js new file mode 100644 index 00000000000000..35bfbdee9fcd34 --- /dev/null +++ b/test/parallel/test-inspect-sync-session.js @@ -0,0 +1,64 @@ +'use strict'; + +// This tests that inspector.SyncSession() works. + +const common = require('../common'); +common.skipIfInspectorDisabled(); + +const fixtures = require('../common/fixtures'); +const inspector = require('inspector'); +const assert = require('assert'); +const session = new inspector.SyncSession(); +const { pathToFileURL } = require('url'); + +session.connect(); + +// Test Profiler +session.post('Profiler.enable'); +session.post('Profiler.start'); + +// Test HeapProfiler +session.post('HeapProfiler.enable'); +session.post('HeapProfiler.startSampling'); + +// Test Runtime +session.post('Runtime.enable'); +let res = session.post('Runtime.evaluate', { expression: '1 + 2' }); +assert.deepStrictEqual(res, { + result: { type: 'number', value: 3, description: '3' } +}); + +// Test Debug +session.post('Debugger.enable'); +{ + const scripts = new Map(); + session.on('Debugger.scriptParsed', common.mustCallAtLeast((info) => { + scripts.set(info.params.url, info.params.scriptId); + })); + // Trigger Debugger.scriptParsed + const filepath = fixtures.path('empty.js'); + const url = pathToFileURL(filepath); + require(filepath); + assert(scripts.has(url.href)); +} + +{ + session.post('Debugger.setSkipAllPauses', { skip: false }); + let callFrames; + session.once('Debugger.paused', common.mustCall((obj) => { + callFrames = obj.params.callFrames; + })); + session.post('Debugger.pause'); + assert.strictEqual(callFrames[0].url, 'inspector.js'); + assert.strictEqual(callFrames[0].this.className, 'SyncSession'); +} + +// Test Profiler +res = session.post('Profiler.stop'); +assert(Array.isArray(res.profile.nodes)); + +// Test HeapProfiler +res = session.post('HeapProfiler.stopSampling'); +assert(Array.isArray(res.profile.samples)); + +session.disconnect();