From 6603a936e33834d2c4ee7915db17570516244dd5 Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Thu, 8 Apr 2021 03:49:53 -0700 Subject: [PATCH] debugger: move node-inspect to internal library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit node-inspect developers have agreed to move node-inspect into core rather than vendor it as a dependency. Refs: https://github.com/nodejs/node/discussions/36481 PR-URL: https://github.com/nodejs/node/pull/38161 Backport-PR-URL: https://github.com/nodejs/node/pull/38858 Reviewed-By: Matteo Collina Reviewed-By: Jan Krems Reviewed-By: Colin Ihrig Reviewed-By: Stephen Belanger Reviewed-By: Gerhard Stöbich Reviewed-By: Michaël Zasso --- lib/internal/inspector/_inspect.js | 369 +++++++ lib/internal/inspector/inspect_client.js | 355 +++++++ lib/internal/inspector/inspect_repl.js | 1112 ++++++++++++++++++++++ lib/internal/main/inspect.js | 2 +- node.gyp | 6 +- 5 files changed, 1840 insertions(+), 4 deletions(-) create mode 100644 lib/internal/inspector/_inspect.js create mode 100644 lib/internal/inspector/inspect_client.js create mode 100644 lib/internal/inspector/inspect_repl.js diff --git a/lib/internal/inspector/_inspect.js b/lib/internal/inspector/_inspect.js new file mode 100644 index 00000000000000..df4e53979db8fd --- /dev/null +++ b/lib/internal/inspector/_inspect.js @@ -0,0 +1,369 @@ +/* + * Copyright Node.js contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +// TODO(trott): enable ESLint +/* eslint-disable */ + +'use strict'; +const { spawn } = require('child_process'); +const { EventEmitter } = require('events'); +const net = require('net'); +const util = require('util'); + +const runAsStandalone = typeof __dirname !== 'undefined'; + +const { 0: InspectClient, 1: createRepl } = + runAsStandalone ? + // This copy of node-inspect is on-disk, relative paths make sense. + [ + require('./inspect_client'), + require('./inspect_repl'), + ] : + // This copy of node-inspect is built into the node executable. + [ + require('internal/inspector/inspect_client'), + require('internal/inspector/inspect_repl'), + ]; + +const debuglog = util.debuglog('inspect'); + +class StartupError extends Error { + constructor(message) { + super(message); + this.name = 'StartupError'; + } +} + +function portIsFree(host, port, timeout = 9999) { + if (port === 0) return Promise.resolve(); // Binding to a random port. + + const retryDelay = 150; + let didTimeOut = false; + + return new Promise((resolve, reject) => { + setTimeout(() => { + didTimeOut = true; + reject(new StartupError( + `Timeout (${timeout}) waiting for ${host}:${port} to be free`)); + }, timeout); + + function pingPort() { + if (didTimeOut) return; + + const socket = net.connect(port, host); + let didRetry = false; + function retry() { + if (!didRetry && !didTimeOut) { + didRetry = true; + setTimeout(pingPort, retryDelay); + } + } + + socket.on('error', (error) => { + if (error.code === 'ECONNREFUSED') { + resolve(); + } else { + retry(); + } + }); + socket.on('connect', () => { + socket.destroy(); + retry(); + }); + } + pingPort(); + }); +} + +function runScript(script, scriptArgs, inspectHost, inspectPort, childPrint) { + return portIsFree(inspectHost, inspectPort) + .then(() => { + return new Promise((resolve) => { + const needDebugBrk = process.version.match(/^v(6|7)\./); + const args = (needDebugBrk ? + ['--inspect', `--debug-brk=${inspectPort}`] : + [`--inspect-brk=${inspectPort}`]) + .concat([script], scriptArgs); + const child = spawn(process.execPath, args); + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stdout.on('data', childPrint); + child.stderr.on('data', childPrint); + + let output = ''; + function waitForListenHint(text) { + output += text; + if (/Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\//.test(output)) { + const host = RegExp.$1; + const port = Number.parseInt(RegExp.$2); + child.stderr.removeListener('data', waitForListenHint); + resolve([child, port, host]); + } + } + + child.stderr.on('data', waitForListenHint); + }); + }); +} + +function createAgentProxy(domain, client) { + const agent = new EventEmitter(); + agent.then = (...args) => { + // TODO: potentially fetch the protocol and pretty-print it here. + const descriptor = { + [util.inspect.custom](depth, { stylize }) { + return stylize(`[Agent ${domain}]`, 'special'); + }, + }; + return Promise.resolve(descriptor).then(...args); + }; + + return new Proxy(agent, { + get(target, name) { + if (name in target) return target[name]; + return function callVirtualMethod(params) { + return client.callMethod(`${domain}.${name}`, params); + }; + }, + }); +} + +class NodeInspector { + constructor(options, stdin, stdout) { + this.options = options; + this.stdin = stdin; + this.stdout = stdout; + + this.paused = true; + this.child = null; + + if (options.script) { + this._runScript = runScript.bind(null, + options.script, + options.scriptArgs, + options.host, + options.port, + this.childPrint.bind(this)); + } else { + this._runScript = + () => Promise.resolve([null, options.port, options.host]); + } + + this.client = new InspectClient(); + + this.domainNames = ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime']; + this.domainNames.forEach((domain) => { + this[domain] = createAgentProxy(domain, this.client); + }); + this.handleDebugEvent = (fullName, params) => { + const { 0: domain, 1: name } = fullName.split('.'); + if (domain in this) { + this[domain].emit(name, params); + } + }; + this.client.on('debugEvent', this.handleDebugEvent); + const startRepl = createRepl(this); + + // Handle all possible exits + process.on('exit', () => this.killChild()); + process.once('SIGTERM', process.exit.bind(process, 0)); + process.once('SIGHUP', process.exit.bind(process, 0)); + + this.run() + .then(() => startRepl()) + .then((repl) => { + this.repl = repl; + this.repl.on('exit', () => { + process.exit(0); + }); + this.paused = false; + }) + .then(null, (error) => process.nextTick(() => { throw error; })); + } + + suspendReplWhile(fn) { + if (this.repl) { + this.repl.pause(); + } + this.stdin.pause(); + this.paused = true; + return new Promise((resolve) => { + resolve(fn()); + }).then(() => { + this.paused = false; + if (this.repl) { + this.repl.resume(); + this.repl.displayPrompt(); + } + this.stdin.resume(); + }).then(null, (error) => process.nextTick(() => { throw error; })); + } + + killChild() { + this.client.reset(); + if (this.child) { + this.child.kill(); + this.child = null; + } + } + + run() { + this.killChild(); + + return this._runScript().then(({ 0: child, 1: port, 2: host }) => { + this.child = child; + + let connectionAttempts = 0; + const attemptConnect = () => { + ++connectionAttempts; + debuglog('connection attempt #%d', connectionAttempts); + this.stdout.write('.'); + return this.client.connect(port, host) + .then(() => { + debuglog('connection established'); + this.stdout.write(' ok'); + }, (error) => { + debuglog('connect failed', error); + // If it's failed to connect 10 times then print failed message + if (connectionAttempts >= 10) { + this.stdout.write(' failed to connect, please retry\n'); + process.exit(1); + } + + return new Promise((resolve) => setTimeout(resolve, 500)) + .then(attemptConnect); + }); + }; + + this.print(`connecting to ${host}:${port} ..`, true); + return attemptConnect(); + }); + } + + clearLine() { + if (this.stdout.isTTY) { + this.stdout.cursorTo(0); + this.stdout.clearLine(1); + } else { + this.stdout.write('\b'); + } + } + + print(text, oneline = false) { + this.clearLine(); + this.stdout.write(oneline ? text : `${text}\n`); + } + + childPrint(text) { + this.print( + text.toString() + .split(/\r\n|\r|\n/g) + .filter((chunk) => !!chunk) + .map((chunk) => `< ${chunk}`) + .join('\n') + ); + if (!this.paused) { + this.repl.displayPrompt(true); + } + if (/Waiting for the debugger to disconnect\.\.\.\n$/.test(text)) { + this.killChild(); + } + } +} + +function parseArgv([target, ...args]) { + let host = '127.0.0.1'; + let port = 9229; + let isRemote = false; + let script = target; + let scriptArgs = args; + + const hostMatch = target.match(/^([^:]+):(\d+)$/); + const portMatch = target.match(/^--port=(\d+)$/); + + if (hostMatch) { + // Connecting to remote debugger + host = hostMatch[1]; + port = parseInt(hostMatch[2], 10); + isRemote = true; + script = null; + } else if (portMatch) { + // Start on custom port + port = parseInt(portMatch[1], 10); + script = args[0]; + scriptArgs = args.slice(1); + } else if (args.length === 1 && /^\d+$/.test(args[0]) && target === '-p') { + // Start debugger against a given pid + const pid = parseInt(args[0], 10); + try { + process._debugProcess(pid); + } catch (e) { + if (e.code === 'ESRCH') { + console.error(`Target process: ${pid} doesn't exist.`); + process.exit(1); + } + throw e; + } + script = null; + isRemote = true; + } + + return { + host, port, isRemote, script, scriptArgs, + }; +} + +function startInspect(argv = process.argv.slice(2), + stdin = process.stdin, + stdout = process.stdout) { + if (argv.length < 1) { + const invokedAs = runAsStandalone ? + 'node-inspect' : + `${process.argv0} ${process.argv[1]}`; + + console.error(`Usage: ${invokedAs} script.js`); + console.error(` ${invokedAs} :`); + console.error(` ${invokedAs} -p `); + process.exit(1); + } + + const options = parseArgv(argv); + const inspector = new NodeInspector(options, stdin, stdout); + + stdin.resume(); + + function handleUnexpectedError(e) { + if (!(e instanceof StartupError)) { + console.error('There was an internal error in Node.js. ' + + 'Please report this bug.'); + console.error(e.message); + console.error(e.stack); + } else { + console.error(e.message); + } + if (inspector.child) inspector.child.kill(); + process.exit(1); + } + + process.on('uncaughtException', handleUnexpectedError); +} +exports.start = startInspect; diff --git a/lib/internal/inspector/inspect_client.js b/lib/internal/inspector/inspect_client.js new file mode 100644 index 00000000000000..6010f4ac09389d --- /dev/null +++ b/lib/internal/inspector/inspect_client.js @@ -0,0 +1,355 @@ +/* + * Copyright Node.js contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +// TODO(trott): enable ESLint +/* eslint-disable */ + +'use strict'; +const Buffer = require('buffer').Buffer; +const { EventEmitter } = require('events'); +const http = require('http'); +const URL = require('url'); +const util = require('util'); + +const debuglog = util.debuglog('inspect'); + +const kOpCodeText = 0x1; +const kOpCodeClose = 0x8; + +const kFinalBit = 0x80; +const kReserved1Bit = 0x40; +const kReserved2Bit = 0x20; +const kReserved3Bit = 0x10; +const kOpCodeMask = 0xF; +const kMaskBit = 0x80; +const kPayloadLengthMask = 0x7F; + +const kMaxSingleBytePayloadLength = 125; +const kMaxTwoBytePayloadLength = 0xFFFF; +const kTwoBytePayloadLengthField = 126; +const kEightBytePayloadLengthField = 127; +const kMaskingKeyWidthInBytes = 4; + +function isEmpty(obj) { + return Object.keys(obj).length === 0; +} + +function unpackError({ code, message, data }) { + const err = new Error(`${message} - ${data}`); + err.code = code; + Error.captureStackTrace(err, unpackError); + return err; +} + +function encodeFrameHybi17(payload) { + var i; + + const dataLength = payload.length; + + let singleByteLength; + let additionalLength; + if (dataLength > kMaxTwoBytePayloadLength) { + singleByteLength = kEightBytePayloadLengthField; + additionalLength = Buffer.alloc(8); + let remaining = dataLength; + for (i = 0; i < 8; ++i) { + additionalLength[7 - i] = remaining & 0xFF; + remaining >>= 8; + } + } else if (dataLength > kMaxSingleBytePayloadLength) { + singleByteLength = kTwoBytePayloadLengthField; + additionalLength = Buffer.alloc(2); + additionalLength[0] = (dataLength & 0xFF00) >> 8; + additionalLength[1] = dataLength & 0xFF; + } else { + additionalLength = Buffer.alloc(0); + singleByteLength = dataLength; + } + + const header = Buffer.from([ + kFinalBit | kOpCodeText, + kMaskBit | singleByteLength, + ]); + + const mask = Buffer.alloc(4); + const masked = Buffer.alloc(dataLength); + for (i = 0; i < dataLength; ++i) { + masked[i] = payload[i] ^ mask[i % kMaskingKeyWidthInBytes]; + } + + return Buffer.concat([header, additionalLength, mask, masked]); +} + +function decodeFrameHybi17(data) { + const dataAvailable = data.length; + const notComplete = { closed: false, payload: null, rest: data }; + let payloadOffset = 2; + if ((dataAvailable - payloadOffset) < 0) return notComplete; + + const firstByte = data[0]; + const secondByte = data[1]; + + const final = (firstByte & kFinalBit) !== 0; + const reserved1 = (firstByte & kReserved1Bit) !== 0; + const reserved2 = (firstByte & kReserved2Bit) !== 0; + const reserved3 = (firstByte & kReserved3Bit) !== 0; + const opCode = firstByte & kOpCodeMask; + const masked = (secondByte & kMaskBit) !== 0; + const compressed = reserved1; + if (compressed) { + throw new Error('Compressed frames not supported'); + } + if (!final || reserved2 || reserved3) { + throw new Error('Only compression extension is supported'); + } + + if (masked) { + throw new Error('Masked server frame - not supported'); + } + + let closed = false; + switch (opCode) { + case kOpCodeClose: + closed = true; + break; + case kOpCodeText: + break; + default: + throw new Error(`Unsupported op code ${opCode}`); + } + + let payloadLength = secondByte & kPayloadLengthMask; + switch (payloadLength) { + case kTwoBytePayloadLengthField: + payloadOffset += 2; + payloadLength = (data[2] << 8) + data[3]; + break; + + case kEightBytePayloadLengthField: + payloadOffset += 8; + payloadLength = 0; + for (var i = 0; i < 8; ++i) { + payloadLength <<= 8; + payloadLength |= data[2 + i]; + } + break; + + default: + // Nothing. We already have the right size. + } + if ((dataAvailable - payloadOffset - payloadLength) < 0) return notComplete; + + const payloadEnd = payloadOffset + payloadLength; + return { + payload: data.slice(payloadOffset, payloadEnd), + rest: data.slice(payloadEnd), + closed, + }; +} + +class Client extends EventEmitter { + constructor() { + super(); + this.handleChunk = this._handleChunk.bind(this); + + this._port = undefined; + this._host = undefined; + + this.reset(); + } + + _handleChunk(chunk) { + this._unprocessed = Buffer.concat([this._unprocessed, chunk]); + + while (this._unprocessed.length > 2) { + const { + closed, + payload: payloadBuffer, + rest + } = decodeFrameHybi17(this._unprocessed); + this._unprocessed = rest; + + if (closed) { + this.reset(); + return; + } + if (payloadBuffer === null || payloadBuffer.length === 0) break; + + const payloadStr = payloadBuffer.toString(); + debuglog('< %s', payloadStr); + const lastChar = payloadStr[payloadStr.length - 1]; + if (payloadStr[0] !== '{' || lastChar !== '}') { + throw new Error(`Payload does not look like JSON: ${payloadStr}`); + } + let payload; + try { + payload = JSON.parse(payloadStr); + } catch (parseError) { + parseError.string = payloadStr; + throw parseError; + } + + const { id, method, params, result, error } = payload; + if (id) { + const handler = this._pending[id]; + if (handler) { + delete this._pending[id]; + handler(error, result); + } + } else if (method) { + this.emit('debugEvent', method, params); + this.emit(method, params); + } else { + throw new Error(`Unsupported response: ${payloadStr}`); + } + } + } + + reset() { + if (this._http) { + this._http.destroy(); + } + this._http = null; + this._lastId = 0; + this._socket = null; + this._pending = {}; + this._unprocessed = Buffer.alloc(0); + } + + callMethod(method, params) { + return new Promise((resolve, reject) => { + if (!this._socket) { + reject(new Error('Use `run` to start the app again.')); + return; + } + const data = { id: ++this._lastId, method, params }; + this._pending[data.id] = (error, result) => { + if (error) reject(unpackError(error)); + else resolve(isEmpty(result) ? undefined : result); + }; + const json = JSON.stringify(data); + debuglog('> %s', json); + this._socket.write(encodeFrameHybi17(Buffer.from(json))); + }); + } + + _fetchJSON(urlPath) { + return new Promise((resolve, reject) => { + const httpReq = http.get({ + host: this._host, + port: this._port, + path: urlPath, + }); + + const chunks = []; + + function onResponse(httpRes) { + function parseChunks() { + const resBody = Buffer.concat(chunks).toString(); + if (httpRes.statusCode !== 200) { + reject(new Error(`Unexpected ${httpRes.statusCode}: ${resBody}`)); + return; + } + try { + resolve(JSON.parse(resBody)); + } catch (parseError) { + reject(new Error(`Response didn't contain JSON: ${resBody}`)); + + } + } + + httpRes.on('error', reject); + httpRes.on('data', (chunk) => chunks.push(chunk)); + httpRes.on('end', parseChunks); + } + + httpReq.on('error', reject); + httpReq.on('response', onResponse); + }); + } + + connect(port, host) { + this._port = port; + this._host = host; + return this._discoverWebsocketPath() + .then((urlPath) => this._connectWebsocket(urlPath)); + } + + _discoverWebsocketPath() { + return this._fetchJSON('/json') + .then(({ 0: { webSocketDebuggerUrl } }) => + URL.parse(webSocketDebuggerUrl).path); + } + + _connectWebsocket(urlPath) { + this.reset(); + + const key1 = require('crypto').randomBytes(16).toString('base64'); + debuglog('request websocket', key1); + + const httpReq = this._http = http.request({ + host: this._host, + port: this._port, + path: urlPath, + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': key1, + 'Sec-WebSocket-Version': '13', + }, + }); + httpReq.on('error', (e) => { + this.emit('error', e); + }); + httpReq.on('response', (httpRes) => { + if (httpRes.statusCode >= 400) { + process.stderr.write(`Unexpected HTTP code: ${httpRes.statusCode}\n`); + httpRes.pipe(process.stderr); + } else { + httpRes.pipe(process.stderr); + } + }); + + const handshakeListener = (res, socket) => { + // TODO: we *could* validate res.headers[sec-websocket-accept] + debuglog('websocket upgrade'); + + this._socket = socket; + socket.on('data', this.handleChunk); + socket.on('close', () => { + this.emit('close'); + }); + + this.emit('ready'); + }; + + return new Promise((resolve, reject) => { + this.once('error', reject); + this.once('ready', resolve); + + httpReq.on('upgrade', handshakeListener); + httpReq.end(); + }); + } +} + +module.exports = Client; diff --git a/lib/internal/inspector/inspect_repl.js b/lib/internal/inspector/inspect_repl.js new file mode 100644 index 00000000000000..7d5b019e28506d --- /dev/null +++ b/lib/internal/inspector/inspect_repl.js @@ -0,0 +1,1112 @@ +/* + * Copyright Node.js contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +// TODO(trott): enable ESLint +/* eslint-disable */ + +'use strict'; +const FS = require('fs'); +const Path = require('path'); +const Repl = require('repl'); +const util = require('util'); +const vm = require('vm'); +const fileURLToPath = require('url').fileURLToPath; + +const debuglog = util.debuglog('inspect'); + +const SHORTCUTS = { + cont: 'c', + next: 'n', + step: 's', + out: 'o', + backtrace: 'bt', + setBreakpoint: 'sb', + clearBreakpoint: 'cb', + run: 'r', +}; + +const HELP = ` +run, restart, r Run the application or reconnect +kill Kill a running application or disconnect + +cont, c Resume execution +next, n Continue to next line in current file +step, s Step into, potentially entering a function +out, o Step out, leaving the current function +backtrace, bt Print the current backtrace +list Print the source around the current line where execution + is currently paused + +setBreakpoint, sb Set a breakpoint +clearBreakpoint, cb Clear a breakpoint +breakpoints List all known breakpoints +breakOnException Pause execution whenever an exception is thrown +breakOnUncaught Pause execution whenever an exception isn't caught +breakOnNone Don't pause on exceptions (this is the default) + +watch(expr) Start watching the given expression +unwatch(expr) Stop watching an expression +watchers Print all watched expressions and their current values + +exec(expr) Evaluate the expression and print the value +repl Enter a debug repl that works like exec + +scripts List application scripts that are currently loaded +scripts(true) List all scripts (including node-internals) + +profile Start CPU profiling session. +profileEnd Stop current CPU profiling session. +profiles Array of completed CPU profiling sessions. +profiles[n].save(filepath = 'node.cpuprofile') + Save CPU profiling session to disk as JSON. + +takeHeapSnapshot(filepath = 'node.heapsnapshot') + Take a heap snapshot and save to disk as JSON. +`.trim(); + +const FUNCTION_NAME_PATTERN = /^(?:function\*? )?([^(\s]+)\(/; +function extractFunctionName(description) { + const fnNameMatch = description.match(FUNCTION_NAME_PATTERN); + return fnNameMatch ? `: ${fnNameMatch[1]}` : ''; +} + +const PUBLIC_BUILTINS = require('module').builtinModules; +const NATIVES = PUBLIC_BUILTINS ? process.binding('natives') : {}; +function isNativeUrl(url) { + url = url.replace(/\.js$/, ''); + if (PUBLIC_BUILTINS) { + if (url.startsWith('internal/') || PUBLIC_BUILTINS.includes(url)) + return true; + } + + return url in NATIVES || url === 'bootstrap_node'; +} + +function getRelativePath(filenameOrURL) { + const dir = Path.join(Path.resolve(), 'x').slice(0, -1); + + const filename = filenameOrURL.startsWith('file://') ? + fileURLToPath(filenameOrURL) : filenameOrURL; + + // Change path to relative, if possible + if (filename.indexOf(dir) === 0) { + return filename.slice(dir.length); + } + return filename; +} + +function toCallback(promise, callback) { + function forward(...args) { + process.nextTick(() => callback(...args)); + } + promise.then(forward.bind(null, null), forward); +} + +// Adds spaces and prefix to number +// maxN is a maximum number we should have space for +function leftPad(n, prefix, maxN) { + const s = n.toString(); + const nchars = Math.max(2, String(maxN).length) + 1; + const nspaces = nchars - s.length - 1; + + return prefix + ' '.repeat(nspaces) + s; +} + +function markSourceColumn(sourceText, position, useColors) { + if (!sourceText) return ''; + + const head = sourceText.slice(0, position); + let tail = sourceText.slice(position); + + // Colourize char if stdout supports colours + if (useColors) { + tail = tail.replace(/(.+?)([^\w]|$)/, '\u001b[32m$1\u001b[39m$2'); + } + + // Return source line with coloured char at `position` + return [head, tail].join(''); +} + +function extractErrorMessage(stack) { + if (!stack) return ''; + const m = stack.match(/^\w+: ([^\n]+)/); + return m ? m[1] : stack; +} + +function convertResultToError(result) { + const { className, description } = result; + const err = new Error(extractErrorMessage(description)); + err.stack = description; + Object.defineProperty(err, 'name', { value: className }); + return err; +} + +class RemoteObject { + constructor(attributes) { + Object.assign(this, attributes); + if (this.type === 'number') { + this.value = + this.unserializableValue ? +this.unserializableValue : +this.value; + } + } + + [util.inspect.custom](depth, opts) { + function formatProperty(prop) { + switch (prop.type) { + case 'string': + case 'undefined': + return util.inspect(prop.value, opts); + + case 'number': + case 'boolean': + return opts.stylize(prop.value, prop.type); + + case 'object': + case 'symbol': + if (prop.subtype === 'date') { + return util.inspect(new Date(prop.value), opts); + } + if (prop.subtype === 'array') { + return opts.stylize(prop.value, 'special'); + } + return opts.stylize(prop.value, prop.subtype || 'special'); + + default: + return prop.value; + } + } + switch (this.type) { + case 'boolean': + case 'number': + case 'string': + case 'undefined': + return util.inspect(this.value, opts); + + case 'symbol': + return opts.stylize(this.description, 'special'); + + case 'function': { + const fnName = extractFunctionName(this.description); + const formatted = `[${this.className}${fnName}]`; + return opts.stylize(formatted, 'special'); + } + + case 'object': + switch (this.subtype) { + case 'date': + return util.inspect(new Date(this.description), opts); + + case 'null': + return util.inspect(null, opts); + + case 'regexp': + return opts.stylize(this.description, 'regexp'); + + default: + break; + } + if (this.preview) { + const props = this.preview.properties + .map((prop, idx) => { + const value = formatProperty(prop); + if (prop.name === `${idx}`) return value; + return `${prop.name}: ${value}`; + }); + if (this.preview.overflow) { + props.push('...'); + } + const singleLine = props.join(', '); + const propString = + singleLine.length > 60 ? props.join(',\n ') : singleLine; + + return this.subtype === 'array' ? + `[ ${propString} ]` : `{ ${propString} }`; + } + return this.description; + + default: + return this.description; + } + } + + static fromEvalResult({ result, wasThrown }) { + if (wasThrown) return convertResultToError(result); + return new RemoteObject(result); + } +} + +class ScopeSnapshot { + constructor(scope, properties) { + Object.assign(this, scope); + this.properties = new Map(properties.map((prop) => { + const value = new RemoteObject(prop.value); + return [prop.name, value]; + })); + this.completionGroup = properties.map((prop) => prop.name); + } + + [util.inspect.custom](depth, opts) { + const type = `${this.type[0].toUpperCase()}${this.type.slice(1)}`; + const name = this.name ? `<${this.name}>` : ''; + const prefix = `${type}${name} `; + return util.inspect(this.properties, opts) + .replace(/^Map /, prefix); + } +} + +function copyOwnProperties(target, source) { + Object.getOwnPropertyNames(source).forEach((prop) => { + const descriptor = Object.getOwnPropertyDescriptor(source, prop); + Object.defineProperty(target, prop, descriptor); + }); +} + +function aliasProperties(target, mapping) { + Object.keys(mapping).forEach((key) => { + const descriptor = Object.getOwnPropertyDescriptor(target, key); + Object.defineProperty(target, mapping[key], descriptor); + }); +} + +function createRepl(inspector) { + const { Debugger, HeapProfiler, Profiler, Runtime } = inspector; + + let repl; + + // Things we want to keep around + const history = { control: [], debug: [] }; + const watchedExpressions = []; + const knownBreakpoints = []; + let pauseOnExceptionState = 'none'; + let lastCommand; + + // Things we need to reset when the app restarts + let knownScripts; + let currentBacktrace; + let selectedFrame; + let exitDebugRepl; + + function resetOnStart() { + knownScripts = {}; + currentBacktrace = null; + selectedFrame = null; + + if (exitDebugRepl) exitDebugRepl(); + exitDebugRepl = null; + } + resetOnStart(); + + const INSPECT_OPTIONS = { colors: inspector.stdout.isTTY }; + function inspect(value) { + return util.inspect(value, INSPECT_OPTIONS); + } + + function print(value, oneline = false) { + const text = typeof value === 'string' ? value : inspect(value); + return inspector.print(text, oneline); + } + + function getCurrentLocation() { + if (!selectedFrame) { + throw new Error('Requires execution to be paused'); + } + return selectedFrame.location; + } + + function isCurrentScript(script) { + return selectedFrame && getCurrentLocation().scriptId === script.scriptId; + } + + function formatScripts(displayNatives = false) { + function isVisible(script) { + if (displayNatives) return true; + return !script.isNative || isCurrentScript(script); + } + + return Object.keys(knownScripts) + .map((scriptId) => knownScripts[scriptId]) + .filter(isVisible) + .map((script) => { + const isCurrent = isCurrentScript(script); + const { isNative, url } = script; + const name = `${getRelativePath(url)}${isNative ? ' ' : ''}`; + return `${isCurrent ? '*' : ' '} ${script.scriptId}: ${name}`; + }) + .join('\n'); + } + function listScripts(displayNatives = false) { + print(formatScripts(displayNatives)); + } + listScripts[util.inspect.custom] = function listWithoutInternal() { + return formatScripts(); + }; + + const profiles = []; + class Profile { + constructor(data) { + this.data = data; + } + + static createAndRegister({ profile }) { + const p = new Profile(profile); + profiles.push(p); + return p; + } + + [util.inspect.custom](depth, { stylize }) { + const { startTime, endTime } = this.data; + return stylize(`[Profile ${endTime - startTime}μs]`, 'special'); + } + + save(filename = 'node.cpuprofile') { + const absoluteFile = Path.resolve(filename); + const json = JSON.stringify(this.data); + FS.writeFileSync(absoluteFile, json); + print('Saved profile to ' + absoluteFile); + } + } + + class SourceSnippet { + constructor(location, delta, scriptSource) { + Object.assign(this, location); + this.scriptSource = scriptSource; + this.delta = delta; + } + + [util.inspect.custom](depth, options) { + const { scriptId, lineNumber, columnNumber, delta, scriptSource } = this; + const start = Math.max(1, lineNumber - delta + 1); + const end = lineNumber + delta + 1; + + const lines = scriptSource.split('\n'); + return lines.slice(start - 1, end).map((lineText, offset) => { + const i = start + offset; + const isCurrent = i === (lineNumber + 1); + + const markedLine = isCurrent + ? markSourceColumn(lineText, columnNumber, options.colors) + : lineText; + + let isBreakpoint = false; + knownBreakpoints.forEach(({ location }) => { + if (!location) return; + if (scriptId === location.scriptId && + i === (location.lineNumber + 1)) { + isBreakpoint = true; + } + }); + + let prefixChar = ' '; + if (isCurrent) { + prefixChar = '>'; + } else if (isBreakpoint) { + prefixChar = '*'; + } + return `${leftPad(i, prefixChar, end)} ${markedLine}`; + }).join('\n'); + } + } + + function getSourceSnippet(location, delta = 5) { + const { scriptId } = location; + return Debugger.getScriptSource({ scriptId }) + .then(({ scriptSource }) => + new SourceSnippet(location, delta, scriptSource)); + } + + class CallFrame { + constructor(callFrame) { + Object.assign(this, callFrame); + } + + loadScopes() { + return Promise.all( + this.scopeChain + .filter((scope) => scope.type !== 'global') + .map((scope) => { + const { objectId } = scope.object; + return Runtime.getProperties({ + objectId, + generatePreview: true, + }).then(({ result }) => new ScopeSnapshot(scope, result)); + }) + ); + } + + list(delta = 5) { + return getSourceSnippet(this.location, delta); + } + } + + class Backtrace extends Array { + [util.inspect.custom]() { + return this.map((callFrame, idx) => { + const { + location: { scriptId, lineNumber, columnNumber }, + functionName + } = callFrame; + const name = functionName || '(anonymous)'; + + const script = knownScripts[scriptId]; + const relativeUrl = + (script && getRelativePath(script.url)) || ''; + const frameLocation = + `${relativeUrl}:${lineNumber + 1}:${columnNumber}`; + + return `#${idx} ${name} ${frameLocation}`; + }).join('\n'); + } + + static from(callFrames) { + return super.from(Array.from(callFrames).map((callFrame) => { + if (callFrame instanceof CallFrame) { + return callFrame; + } + return new CallFrame(callFrame); + })); + } + } + + function prepareControlCode(input) { + if (input === '\n') return lastCommand; + // exec process.title => exec("process.title"); + const match = input.match(/^\s*exec\s+([^\n]*)/); + if (match) { + lastCommand = `exec(${JSON.stringify(match[1])})`; + } else { + lastCommand = input; + } + return lastCommand; + } + + function evalInCurrentContext(code) { + // Repl asked for scope variables + if (code === '.scope') { + if (!selectedFrame) { + return Promise.reject(new Error('Requires execution to be paused')); + } + return selectedFrame.loadScopes().then((scopes) => { + return scopes.map((scope) => scope.completionGroup); + }); + } + + if (selectedFrame) { + return Debugger.evaluateOnCallFrame({ + callFrameId: selectedFrame.callFrameId, + expression: code, + objectGroup: 'node-inspect', + generatePreview: true, + }).then(RemoteObject.fromEvalResult); + } + return Runtime.evaluate({ + expression: code, + objectGroup: 'node-inspect', + generatePreview: true, + }).then(RemoteObject.fromEvalResult); + } + + function controlEval(input, context, filename, callback) { + debuglog('eval:', input); + function returnToCallback(error, result) { + debuglog('end-eval:', input, error); + callback(error, result); + } + + try { + const code = prepareControlCode(input); + const result = vm.runInContext(code, context, filename); + + if (result && typeof result.then === 'function') { + toCallback(result, returnToCallback); + return; + } + returnToCallback(null, result); + } catch (e) { + returnToCallback(e); + } + } + + function debugEval(input, context, filename, callback) { + debuglog('eval:', input); + function returnToCallback(error, result) { + debuglog('end-eval:', input, error); + callback(error, result); + } + + try { + const result = evalInCurrentContext(input); + + if (result && typeof result.then === 'function') { + toCallback(result, returnToCallback); + return; + } + returnToCallback(null, result); + } catch (e) { + returnToCallback(e); + } + } + + function formatWatchers(verbose = false) { + if (!watchedExpressions.length) { + return Promise.resolve(''); + } + + const inspectValue = (expr) => + evalInCurrentContext(expr) + // .then(formatValue) + .catch((error) => `<${error.message}>`); + const lastIndex = watchedExpressions.length - 1; + + return Promise.all(watchedExpressions.map(inspectValue)) + .then((values) => { + const lines = watchedExpressions + .map((expr, idx) => { + const prefix = `${leftPad(idx, ' ', lastIndex)}: ${expr} =`; + const value = inspect(values[idx], { colors: true }); + if (value.indexOf('\n') === -1) { + return `${prefix} ${value}`; + } + return `${prefix}\n ${value.split('\n').join('\n ')}`; + }); + return lines.join('\n'); + }) + .then((valueList) => { + return verbose ? `Watchers:\n${valueList}\n` : valueList; + }); + } + + function watchers(verbose = false) { + return formatWatchers(verbose).then(print); + } + + // List source code + function list(delta = 5) { + return selectedFrame.list(delta) + .then(null, (error) => { + print('You can\'t list source code right now'); + throw error; + }); + } + + function handleBreakpointResolved({ breakpointId, location }) { + const script = knownScripts[location.scriptId]; + const scriptUrl = script && script.url; + if (scriptUrl) { + Object.assign(location, { scriptUrl }); + } + const isExisting = knownBreakpoints.some((bp) => { + if (bp.breakpointId === breakpointId) { + Object.assign(bp, { location }); + return true; + } + return false; + }); + if (!isExisting) { + knownBreakpoints.push({ breakpointId, location }); + } + } + + function listBreakpoints() { + if (!knownBreakpoints.length) { + print('No breakpoints yet'); + return; + } + + function formatLocation(location) { + if (!location) return ''; + const script = knownScripts[location.scriptId]; + const scriptUrl = script ? script.url : location.scriptUrl; + return `${getRelativePath(scriptUrl)}:${location.lineNumber + 1}`; + } + const breaklist = knownBreakpoints + .map((bp, idx) => `#${idx} ${formatLocation(bp.location)}`) + .join('\n'); + print(breaklist); + } + + function setBreakpoint(script, line, condition, silent) { + function registerBreakpoint({ breakpointId, actualLocation }) { + handleBreakpointResolved({ breakpointId, location: actualLocation }); + if (actualLocation && actualLocation.scriptId) { + if (!silent) return getSourceSnippet(actualLocation, 5); + } else { + print(`Warning: script '${script}' was not loaded yet.`); + } + return undefined; + } + + // setBreakpoint(): set breakpoint at current location + if (script === undefined) { + return Debugger + .setBreakpoint({ location: getCurrentLocation(), condition }) + .then(registerBreakpoint); + } + + // setBreakpoint(line): set breakpoint in current script at specific line + if (line === undefined && typeof script === 'number') { + const location = { + scriptId: getCurrentLocation().scriptId, + lineNumber: script - 1, + }; + return Debugger.setBreakpoint({ location, condition }) + .then(registerBreakpoint); + } + + if (typeof script !== 'string') { + throw new TypeError(`setBreakpoint() expects a string, got ${script}`); + } + + // setBreakpoint('fn()'): Break when a function is called + if (script.endsWith('()')) { + const debugExpr = `debug(${script.slice(0, -2)})`; + const debugCall = selectedFrame + ? Debugger.evaluateOnCallFrame({ + callFrameId: selectedFrame.callFrameId, + expression: debugExpr, + includeCommandLineAPI: true, + }) + : Runtime.evaluate({ + expression: debugExpr, + includeCommandLineAPI: true, + }); + return debugCall.then(({ result, wasThrown }) => { + if (wasThrown) return convertResultToError(result); + return undefined; // This breakpoint can't be removed the same way + }); + } + + // setBreakpoint('scriptname') + let scriptId = null; + let ambiguous = false; + if (knownScripts[script]) { + scriptId = script; + } else { + for (const id of Object.keys(knownScripts)) { + const scriptUrl = knownScripts[id].url; + if (scriptUrl && scriptUrl.indexOf(script) !== -1) { + if (scriptId !== null) { + ambiguous = true; + } + scriptId = id; + } + } + } + + if (ambiguous) { + print('Script name is ambiguous'); + return undefined; + } + if (line <= 0) { + print('Line should be a positive value'); + return undefined; + } + + if (scriptId !== null) { + const location = { scriptId, lineNumber: line - 1 }; + return Debugger.setBreakpoint({ location, condition }) + .then(registerBreakpoint); + } + + const escapedPath = script.replace(/([/\\.?*()^${}|[\]])/g, '\\$1'); + const urlRegex = `^(.*[\\/\\\\])?${escapedPath}$`; + + return Debugger + .setBreakpointByUrl({ urlRegex, lineNumber: line - 1, condition }) + .then((bp) => { + // TODO: handle bp.locations in case the regex matches existing files + if (!bp.location) { // Fake it for now. + Object.assign(bp, { + actualLocation: { + scriptUrl: `.*/${script}$`, + lineNumber: line - 1, + }, + }); + } + return registerBreakpoint(bp); + }); + } + + function clearBreakpoint(url, line) { + const breakpoint = knownBreakpoints.find(({ location }) => { + if (!location) return false; + const script = knownScripts[location.scriptId]; + if (!script) return false; + return ( + script.url.indexOf(url) !== -1 && (location.lineNumber + 1) === line + ); + }); + if (!breakpoint) { + print(`Could not find breakpoint at ${url}:${line}`); + return Promise.resolve(); + } + return Debugger.removeBreakpoint({ breakpointId: breakpoint.breakpointId }) + .then(() => { + const idx = knownBreakpoints.indexOf(breakpoint); + knownBreakpoints.splice(idx, 1); + }); + } + + function restoreBreakpoints() { + const lastBreakpoints = knownBreakpoints.slice(); + knownBreakpoints.length = 0; + const newBreakpoints = lastBreakpoints + .filter(({ location }) => !!location.scriptUrl) + .map(({ location }) => + setBreakpoint(location.scriptUrl, location.lineNumber + 1)); + if (!newBreakpoints.length) return Promise.resolve(); + return Promise.all(newBreakpoints).then((results) => { + print(`${results.length} breakpoints restored.`); + }); + } + + function setPauseOnExceptions(state) { + return Debugger.setPauseOnExceptions({ state }) + .then(() => { + pauseOnExceptionState = state; + }); + } + + Debugger.on('paused', ({ callFrames, reason /* , hitBreakpoints */ }) => { + if (process.env.NODE_INSPECT_RESUME_ON_START === '1' && + reason === 'Break on start') { + debuglog('Paused on start, but NODE_INSPECT_RESUME_ON_START' + + ' environment variable is set to 1, resuming'); + inspector.client.callMethod('Debugger.resume'); + return; + } + + // Save execution context's data + currentBacktrace = Backtrace.from(callFrames); + selectedFrame = currentBacktrace[0]; + const { scriptId, lineNumber } = selectedFrame.location; + + const breakType = reason === 'other' ? 'break' : reason; + const script = knownScripts[scriptId]; + const scriptUrl = script ? getRelativePath(script.url) : '[unknown]'; + + const header = `${breakType} in ${scriptUrl}:${lineNumber + 1}`; + + inspector.suspendReplWhile(() => + Promise.all([formatWatchers(true), selectedFrame.list(2)]) + .then(([watcherList, context]) => { + if (watcherList) { + return `${watcherList}\n${inspect(context)}`; + } + return inspect(context); + }).then((breakContext) => { + print(`${header}\n${breakContext}`); + })); + }); + + function handleResumed() { + currentBacktrace = null; + selectedFrame = null; + } + + Debugger.on('resumed', handleResumed); + + Debugger.on('breakpointResolved', handleBreakpointResolved); + + Debugger.on('scriptParsed', (script) => { + const { scriptId, url } = script; + if (url) { + knownScripts[scriptId] = Object.assign({ + isNative: isNativeUrl(url), + }, script); + } + }); + + Profiler.on('consoleProfileFinished', ({ profile }) => { + Profile.createAndRegister({ profile }); + print([ + 'Captured new CPU profile.', + `Access it with profiles[${profiles.length - 1}]` + ].join('\n')); + }); + + function initializeContext(context) { + inspector.domainNames.forEach((domain) => { + Object.defineProperty(context, domain, { + value: inspector[domain], + enumerable: true, + configurable: true, + writeable: false, + }); + }); + + copyOwnProperties(context, { + get help() { + print(HELP); + }, + + get run() { + return inspector.run(); + }, + + get kill() { + return inspector.killChild(); + }, + + get restart() { + return inspector.run(); + }, + + get cont() { + handleResumed(); + return Debugger.resume(); + }, + + get next() { + handleResumed(); + return Debugger.stepOver(); + }, + + get step() { + handleResumed(); + return Debugger.stepInto(); + }, + + get out() { + handleResumed(); + return Debugger.stepOut(); + }, + + get pause() { + return Debugger.pause(); + }, + + get backtrace() { + return currentBacktrace; + }, + + get breakpoints() { + return listBreakpoints(); + }, + + exec(expr) { + return evalInCurrentContext(expr); + }, + + get profile() { + return Profiler.start(); + }, + + get profileEnd() { + return Profiler.stop() + .then(Profile.createAndRegister); + }, + + get profiles() { + return profiles; + }, + + takeHeapSnapshot(filename = 'node.heapsnapshot') { + return new Promise((resolve, reject) => { + const absoluteFile = Path.resolve(filename); + const writer = FS.createWriteStream(absoluteFile); + let sizeWritten = 0; + function onProgress({ done, total, finished }) { + if (finished) { + print('Heap snaphost prepared.'); + } else { + print(`Heap snapshot: ${done}/${total}`, true); + } + } + function onChunk({ chunk }) { + sizeWritten += chunk.length; + writer.write(chunk); + print(`Writing snapshot: ${sizeWritten}`, true); + } + function onResolve() { + writer.end(() => { + teardown(); + print(`Wrote snapshot: ${absoluteFile}`); + resolve(); + }); + } + function onReject(error) { + teardown(); + reject(error); + } + function teardown() { + HeapProfiler.removeListener( + 'reportHeapSnapshotProgress', onProgress); + HeapProfiler.removeListener('addHeapSnapshotChunk', onChunk); + } + + HeapProfiler.on('reportHeapSnapshotProgress', onProgress); + HeapProfiler.on('addHeapSnapshotChunk', onChunk); + + print('Heap snapshot: 0/0', true); + HeapProfiler.takeHeapSnapshot({ reportProgress: true }) + .then(onResolve, onReject); + }); + }, + + get watchers() { + return watchers(); + }, + + watch(expr) { + watchedExpressions.push(expr); + }, + + unwatch(expr) { + const index = watchedExpressions.indexOf(expr); + + // Unwatch by expression + // or + // Unwatch by watcher number + watchedExpressions.splice(index !== -1 ? index : +expr, 1); + }, + + get repl() { + // Don't display any default messages + const listeners = repl.listeners('SIGINT').slice(0); + repl.removeAllListeners('SIGINT'); + + const oldContext = repl.context; + + exitDebugRepl = () => { + // Restore all listeners + process.nextTick(() => { + listeners.forEach((listener) => { + repl.on('SIGINT', listener); + }); + }); + + // Exit debug repl + repl.eval = controlEval; + + // Swap history + history.debug = repl.history; + repl.history = history.control; + + repl.context = oldContext; + repl.setPrompt('debug> '); + repl.displayPrompt(); + + repl.removeListener('SIGINT', exitDebugRepl); + repl.removeListener('exit', exitDebugRepl); + + exitDebugRepl = null; + }; + + // Exit debug repl on SIGINT + repl.on('SIGINT', exitDebugRepl); + + // Exit debug repl on repl exit + repl.on('exit', exitDebugRepl); + + // Set new + repl.eval = debugEval; + repl.context = {}; + + // Swap history + history.control = repl.history; + repl.history = history.debug; + + repl.setPrompt('> '); + + print('Press Ctrl + C to leave debug repl'); + repl.displayPrompt(); + }, + + get version() { + return Runtime.evaluate({ + expression: 'process.versions.v8', + contextId: 1, + returnByValue: true, + }).then(({ result }) => { + print(result.value); + }); + }, + + scripts: listScripts, + + setBreakpoint, + clearBreakpoint, + setPauseOnExceptions, + get breakOnException() { + return setPauseOnExceptions('all'); + }, + get breakOnUncaught() { + return setPauseOnExceptions('uncaught'); + }, + get breakOnNone() { + return setPauseOnExceptions('none'); + }, + + list, + }); + aliasProperties(context, SHORTCUTS); + } + + function initAfterStart() { + const setupTasks = [ + Runtime.enable(), + Profiler.enable(), + Profiler.setSamplingInterval({ interval: 100 }), + Debugger.enable(), + Debugger.setPauseOnExceptions({ state: 'none' }), + Debugger.setAsyncCallStackDepth({ maxDepth: 0 }), + Debugger.setBlackboxPatterns({ patterns: [] }), + Debugger.setPauseOnExceptions({ state: pauseOnExceptionState }), + restoreBreakpoints(), + Runtime.runIfWaitingForDebugger(), + ]; + return Promise.all(setupTasks); + } + + return function startRepl() { + inspector.client.on('close', () => { + resetOnStart(); + }); + inspector.client.on('ready', () => { + initAfterStart(); + }); + + const replOptions = { + prompt: 'debug> ', + input: inspector.stdin, + output: inspector.stdout, + eval: controlEval, + useGlobal: false, + ignoreUndefined: true, + }; + + repl = Repl.start(replOptions); + initializeContext(repl.context); + repl.on('reset', initializeContext); + + repl.defineCommand('interrupt', () => { + // We want this for testing purposes where sending CTRL-C can be tricky. + repl.emit('SIGINT'); + }); + + // Init once for the initial connection + initAfterStart(); + + return repl; + }; +} +module.exports = createRepl; diff --git a/lib/internal/main/inspect.js b/lib/internal/main/inspect.js index f6777fe852bcfc..10e7cbf2c2b48a 100644 --- a/lib/internal/main/inspect.js +++ b/lib/internal/main/inspect.js @@ -18,5 +18,5 @@ markBootstrapComplete(); // Start the debugger agent. process.nextTick(() => { - require('internal/deps/node-inspect/lib/_inspect').start(); + require('internal/inspector/_inspect').start(); }); diff --git a/node.gyp b/node.gyp index 59ba758e73fecc..e199ac99ce7799 100644 --- a/node.gyp +++ b/node.gyp @@ -148,6 +148,9 @@ 'lib/internal/heap_utils.js', 'lib/internal/histogram.js', 'lib/internal/idna.js', + 'lib/internal/inspector/_inspect.js', + 'lib/internal/inspector/inspect_client.js', + 'lib/internal/inspector/inspect_repl.js', 'lib/internal/inspector_async_hook.js', 'lib/internal/js_stream_socket.js', 'lib/internal/linkedlist.js', @@ -250,9 +253,6 @@ 'deps/v8/tools/tickprocessor.js', 'deps/v8/tools/SourceMap.js', 'deps/v8/tools/tickprocessor-driver.js', - 'deps/node-inspect/lib/_inspect.js', - 'deps/node-inspect/lib/internal/inspect_client.js', - 'deps/node-inspect/lib/internal/inspect_repl.js', 'deps/acorn/acorn/dist/acorn.js', 'deps/acorn/acorn-walk/dist/walk.js', 'deps/acorn-plugins/acorn-class-fields/index.js',