From 0324529e0fa234b8102c1a6a1cde19c76a6fff82 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Thu, 13 Oct 2022 20:12:05 -0300 Subject: [PATCH] inspector: introduce inspector/promises API PR-URL: https://github.com/nodejs/node/pull/44250 Reviewed-By: Moshe Atlow Reviewed-By: Benjamin Gruenbaum Reviewed-By: Ruy Adorno Reviewed-By: Matteo Collina Reviewed-By: James M Snell Reviewed-By: Antoine du Hamel --- doc/api/inspector.md | 311 ++++++++++++++++++----- lib/inspector/promises.js | 22 ++ src/node_builtins.cc | 2 +- test/parallel/test-inspector-promises.js | 61 +++++ 4 files changed, 335 insertions(+), 61 deletions(-) create mode 100644 lib/inspector/promises.js create mode 100644 test/parallel/test-inspector-promises.js diff --git a/doc/api/inspector.md b/doc/api/inspector.md index 0b8ac27914bbae..d9a44929a62b87 100644 --- a/doc/api/inspector.md +++ b/doc/api/inspector.md @@ -11,94 +11,209 @@ inspector. It can be accessed using: -```js +```mjs +import * as inspector from 'node:inspector/promises'; +``` + +```cjs +const inspector = require('node:inspector/promises'); +``` + +or + +```mjs +import * as inspector from 'node:inspector'; +``` + +```cjs const inspector = require('node:inspector'); ``` -## `inspector.close()` +## Promises API + +> Stability: 1 - Experimental -Deactivate the inspector. Blocks until there are no active connections. +### Class: `inspector.Session` -## `inspector.console` +* Extends: {EventEmitter} -* {Object} An object to send messages to the remote inspector console. +The `inspector.Session` is used for dispatching messages to the V8 inspector +back-end and receiving message responses and notifications. + +#### `new inspector.Session()` + + + +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. + +#### Event: `'inspectorNotification'` + + + +* {Object} The notification message object + +Emitted when any notification from the V8 Inspector is received. ```js -require('node:inspector').console.log('a message'); +session.on('inspectorNotification', (message) => console.log(message.method)); +// Debugger.paused +// Debugger.resumed ``` -The inspector console does not have API parity with Node.js -console. +It is also possible to subscribe only to notifications with specific method: -## `inspector.open([port[, host[, wait]]])` +#### Event: ``; -* `port` {number} Port to listen on for inspector connections. Optional. - **Default:** what was specified on the CLI. -* `host` {string} Host to listen on for inspector connections. Optional. - **Default:** what was specified on the CLI. -* `wait` {boolean} Block until a client has connected. Optional. - **Default:** `false`. + -Activate inspector on host and port. Equivalent to -`node --inspect=[[host:]port]`, but can be done programmatically after node has -started. +* {Object} The notification message object -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. +Emitted when an inspector notification is received that has its method field set +to the `` value. -See the [security warning][] regarding the `host` -parameter usage. +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): -## `inspector.url()` +```js +session.on('Debugger.paused', ({ params }) => { + console.log(params.hitBreakpoints); +}); +// [ '/the/file/that/has/the/breakpoint.js:11:0' ] +``` -* Returns: {string|undefined} +#### `session.connect()` -Return the URL of the active inspector, or `undefined` if there is none. + -```console -$ node --inspect -p 'inspector.url()' -Debugger listening on ws://127.0.0.1:9229/166e272e-7a30-4d09-97ce-f1c012b43c34 -For help, see: https://nodejs.org/en/docs/inspector -ws://127.0.0.1:9229/166e272e-7a30-4d09-97ce-f1c012b43c34 +Connects a session to the inspector back-end. -$ node --inspect=localhost:3000 -p 'inspector.url()' -Debugger listening on ws://localhost:3000/51cf8d0e-3c36-4c59-8efd-54519839e56a -For help, see: https://nodejs.org/en/docs/inspector -ws://localhost:3000/51cf8d0e-3c36-4c59-8efd-54519839e56a +#### `session.connectToMainThread()` -$ node -p 'inspector.url()' -undefined -``` + -## `inspector.waitForDebugger()` +Connects a session to the main thread inspector back-end. An exception will +be thrown if this API was not called on a Worker thread. + +#### `session.disconnect()` -Blocks until a client (existing or connected later) has sent -`Runtime.runIfWaitingForDebugger` command. +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. -An exception will be thrown if there is no active inspector. +#### `session.post(method[, params])` -## Class: `inspector.Session` + + +* `method` {string} +* `params` {Object} +* Returns: {Promise} + +Posts a message to the inspector back-end. + +```mjs +import { Session } from 'node:inspector/promises'; +try { + const session = new Session(); + session.connect(); + const result = await session.post('Runtime.evaluate', { expression: '2 + 2' }); + console.log(result); +} catch (error) { + console.error(error); +} +// Output: { type: 'number', value: 4, description: '4' } +``` + +The latest version of the V8 inspector protocol is published on the +[Chrome DevTools Protocol Viewer][]. + +Node.js 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. + +#### Example usage + +Apart from the debugger, various V8 Profilers are available through the DevTools +protocol. + +##### CPU profiler + +Here's an example showing how to use the [CPU Profiler][]: + +```mjs +import { Session } from 'node:inspector/promises'; +import fs from 'node:fs'; +const session = new Session(); +session.connect(); + +await session.post('Profiler.enable'); +await session.post('Profiler.start'); +// Invoke business logic under measurement here... + +// some time later... +const { profile } = await session.post('Profiler.stop'); + +// Write profile to disk, upload, etc. +fs.writeFileSync('./profile.cpuprofile', JSON.stringify(profile)); +``` + +##### Heap profiler + +Here's an example showing how to use the [Heap Profiler][]: + +```mjs +import { Session } from 'node:inspector/promises'; +import fs from 'node:fs'; +const session = new Session(); + +const fd = fs.openSync('profile.heapsnapshot', 'w'); + +session.connect(); + +session.on('HeapProfiler.addHeapSnapshotChunk', (m) => { + fs.writeSync(fd, m.params.chunk); +}); + +const result = await session.post('HeapProfiler.takeHeapSnapshot', null); +console.log('HeapProfiler.takeHeapSnapshot done:', result); +session.disconnect(); +fs.closeSync(fd); +``` + +## Callback API + +### Class: `inspector.Session` * Extends: {EventEmitter} The `inspector.Session` is used for dispatching messages to the V8 inspector back-end and receiving message responses and notifications. -### `new inspector.Session()` +#### `new inspector.Session()` + +Deactivate the inspector. Blocks until there are no active connections. + +### `inspector.console` + +* {Object} An object to send messages to the remote inspector console. + +```js +require('node:inspector').console.log('a message'); +``` + +The inspector console does not have API parity with Node.js +console. + +### `inspector.open([port[, host[, wait]]])` + +* `port` {number} Port to listen on for inspector connections. Optional. + **Default:** what was specified on the CLI. +* `host` {string} Host to listen on for inspector connections. Optional. + **Default:** what was specified on the CLI. +* `wait` {boolean} Block until a client has connected. Optional. + **Default:** `false`. + +Activate inspector on host and port. Equivalent to +`node --inspect=[[host:]port]`, but can be done programmatically 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. + +See the [security warning][] regarding the `host` +parameter usage. + +### `inspector.url()` + +* Returns: {string|undefined} + +Return the URL of the active inspector, or `undefined` if there is none. + +```console +$ node --inspect -p 'inspector.url()' +Debugger listening on ws://127.0.0.1:9229/166e272e-7a30-4d09-97ce-f1c012b43c34 +For help, see: https://nodejs.org/en/docs/inspector +ws://127.0.0.1:9229/166e272e-7a30-4d09-97ce-f1c012b43c34 + +$ node --inspect=localhost:3000 -p 'inspector.url()' +Debugger listening on ws://localhost:3000/51cf8d0e-3c36-4c59-8efd-54519839e56a +For help, see: https://nodejs.org/en/docs/inspector +ws://localhost:3000/51cf8d0e-3c36-4c59-8efd-54519839e56a + +$ node -p 'inspector.url()' +undefined +``` + +### `inspector.waitForDebugger()` + + + +Blocks until a client (existing or connected later) has sent +`Runtime.runIfWaitingForDebugger` command. + +An exception will be thrown if there is no active inspector. + [CPU Profiler]: https://chromedevtools.github.io/devtools-protocol/v8/Profiler [Chrome DevTools Protocol Viewer]: https://chromedevtools.github.io/devtools-protocol/v8/ [Heap Profiler]: https://chromedevtools.github.io/devtools-protocol/v8/HeapProfiler diff --git a/lib/inspector/promises.js b/lib/inspector/promises.js new file mode 100644 index 00000000000000..cd13a2ea2d5126 --- /dev/null +++ b/lib/inspector/promises.js @@ -0,0 +1,22 @@ +'use strict'; + +const inspector = require('inspector'); +const { promisify } = require('util'); +const { FunctionPrototypeBind } = primordials; +class Session extends inspector.Session { + #post = promisify(FunctionPrototypeBind(super.post, this)); + /** + * Posts a message to the inspector back-end. + * @param {string} method + * @param {Record} [params] + * @returns {Promise} + */ + async post(method, params) { + return this.#post(method, params); + } +} + +module.exports = { + ...inspector, + Session, +}; diff --git a/src/node_builtins.cc b/src/node_builtins.cc index b8cee3b9e8bf48..768e248fc9f171 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -114,7 +114,7 @@ void BuiltinLoader::InitializeBuiltinCategories() { builtin_categories_.cannot_be_required = std::set { #if !HAVE_INSPECTOR - "inspector", "internal/util/inspector", + "inspector", "inspector/promises", "internal/util/inspector", #endif // !HAVE_INSPECTOR #if !NODE_USE_V8_PLATFORM || !defined(NODE_HAVE_I18N_SUPPORT) diff --git a/test/parallel/test-inspector-promises.js b/test/parallel/test-inspector-promises.js new file mode 100644 index 00000000000000..0fe297b9298605 --- /dev/null +++ b/test/parallel/test-inspector-promises.js @@ -0,0 +1,61 @@ +'use strict'; + +const common = require('../common'); +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const inspector = require('inspector/promises'); + +const { basename } = require('path'); +const currentFilename = basename(__filename); + +{ + // Ensure that inspector/promises has the same signature as inspector + assert.deepStrictEqual(Reflect.ownKeys(inspector), Reflect.ownKeys(require('inspector'))); +} + +(async () => { + { + // Ensure that session.post returns a valid promisified result + const session = new inspector.Session(); + session.connect(); + + await session.post('Profiler.enable'); + await session.post('Profiler.start'); + + const { + profile + } = await session.post('Profiler.stop'); + + const { + callFrame: { + url, + }, + } = profile.nodes.find(({ + callFrame, + }) => { + return callFrame.url.includes(currentFilename); + }); + session.disconnect(); + assert.deepStrictEqual(basename(url), currentFilename); + } + { + // Ensure that even if a post function is slower than another, Promise.all will get it in order + const session = new inspector.Session(); + session.connect(); + + const sum1 = session.post('Runtime.evaluate', { expression: '2 + 2' }); + const exp = 'new Promise((r) => setTimeout(() => r(6), 100))'; + const sum2 = session.post('Runtime.evaluate', { expression: exp, awaitPromise: true }); + const sum3 = session.post('Runtime.evaluate', { expression: '4 + 4' }); + + const results = (await Promise.all([ + sum1, + sum2, + sum3, + ])).map(({ result: { value } }) => value); + + session.disconnect(); + assert.deepStrictEqual(results, [ 4, 6, 8 ]); + } +})().then(common.mustCall());