diff --git a/doc/api/errors.md b/doc/api/errors.md index 7828d5a688e5c1..7b6b406de0fa54 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -876,6 +876,22 @@ An unknown cipher was specified. An unknown Diffie-Hellman group name was given. See [`crypto.getDiffieHellman()`][] for a list of valid group names. + +### `ERR_DEBUGGER_ERROR` + + +An error occurred with the [debugger][]. + + +### `ERR_DEBUGGER_STARTUP_ERROR` + + +The [debugger][] timed out waiting for the required host/port to be free. + ### `ERR_DIR_CLOSED` @@ -2599,6 +2615,7 @@ closed. [`util.getSystemErrorName(error.errno)`]: util.md#util_util_getsystemerrorname_err [`zlib`]: zlib.md [crypto digest algorithm]: crypto.md#crypto_crypto_gethashes +[debugger]: debugger.md [define a custom subpath]: packages.md#packages_subpath_exports [domains]: domain.md [event emitter-based]: events.md#events_class_eventemitter diff --git a/lib/internal/debugger/_inspect.js b/lib/internal/debugger/_inspect.js index f0bd9331d086c7..c8e38512bb8297 100644 --- a/lib/internal/debugger/_inspect.js +++ b/lib/internal/debugger/_inspect.js @@ -1,33 +1,45 @@ -/* - * 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 { + ArrayPrototypeConcat, + ArrayPrototypeForEach, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePop, + ArrayPrototypeShift, + ArrayPrototypeSlice, + FunctionPrototypeBind, + Number, + Promise, + PromisePrototypeCatch, + PromisePrototypeThen, + PromiseResolve, + Proxy, + RegExpPrototypeSymbolMatch, + RegExpPrototypeSymbolSplit, + RegExpPrototypeTest, + StringPrototypeEndsWith, + StringPrototypeSplit, +} = primordials; + const { spawn } = require('child_process'); const { EventEmitter } = require('events'); const net = require('net'); const util = require('util'); +const { + AbortController, +} = require('internal/abort_controller'); + +const pSetTimeout = util.promisify(require('timers').setTimeout); +async function* pSetInterval(delay) { + while (true) { + await pSetTimeout(delay); + yield; + } +} + +// TODO(aduh95): remove console calls +const console = require('internal/console/global'); const { 0: InspectClient, 1: createRepl } = [ @@ -37,95 +49,75 @@ const { 0: InspectClient, 1: createRepl } = const debuglog = util.debuglog('inspect'); -class StartupError extends Error { - constructor(message) { - super(message); - this.name = 'StartupError'; - } -} +const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes; -function portIsFree(host, port, timeout = 9999) { - if (port === 0) return Promise.resolve(); // Binding to a random port. +async function portIsFree(host, port, timeout = 9999) { + if (port === 0) return; // 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); + const ac = new AbortController(); + const { signal } = ac; - function pingPort() { - if (didTimeOut) return; + pSetTimeout(timeout).then(() => ac.abort()); + const asyncIterator = pSetInterval(retryDelay); + while (true) { + await asyncIterator.next(); + if (signal.aborted) { + throw new ERR_DEBUGGER_STARTUP_ERROR( + `Timeout (${timeout}) waiting for ${host}:${port} to be free`); + } + const error = await new Promise((resolve) => { 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(); - }); + socket.on('error', resolve); + socket.on('connect', resolve); + }); + if (error?.code === 'ECONNREFUSED') { + return; } - 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', (chunk) => childPrint(chunk, 'stdout')); - child.stderr.on('data', (chunk) => childPrint(chunk, 'stderr')); - - 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); - }); - }); +const debugRegex = /Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\//; +async function runScript(script, scriptArgs, inspectHost, inspectPort, + childPrint) { + await portIsFree(inspectHost, inspectPort); + const args = ArrayPrototypeConcat( + [`--inspect-brk=${inspectPort}`, script], + scriptArgs); + const child = spawn(process.execPath, args); + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stdout.on('data', (chunk) => childPrint(chunk, 'stdout')); + child.stderr.on('data', (chunk) => childPrint(chunk, 'stderr')); + + let output = ''; + return new Promise((resolve) => { + function waitForListenHint(text) { + output += text; + const debug = RegExpPrototypeSymbolMatch(debugRegex, output); + if (debug) { + const host = debug[1]; + const port = Number(debug[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) => { + agent.then = (then, _catch) => { // 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 PromisePrototypeThen(PromiseResolve(descriptor), then, _catch); }; return new Proxy(agent, { @@ -148,25 +140,26 @@ class NodeInspector { this.child = null; if (options.script) { - this._runScript = runScript.bind(null, - options.script, - options.scriptArgs, - options.host, - options.port, - this.childPrint.bind(this)); + this._runScript = FunctionPrototypeBind( + runScript, null, + options.script, + options.scriptArgs, + options.host, + options.port, + FunctionPrototypeBind(this.childPrint, this)); } else { this._runScript = - () => Promise.resolve([null, options.port, options.host]); + () => PromiseResolve([null, options.port, options.host]); } this.client = new InspectClient(); this.domainNames = ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime']; - this.domainNames.forEach((domain) => { + ArrayPrototypeForEach(this.domainNames, (domain) => { this[domain] = createAgentProxy(domain, this.client); }); this.handleDebugEvent = (fullName, params) => { - const { 0: domain, 1: name } = fullName.split('.'); + const { 0: domain, 1: name } = StringPrototypeSplit(fullName, '.'); if (domain in this) { this[domain].emit(name, params); } @@ -176,19 +169,16 @@ class NodeInspector { // 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; })); + const exitCodeZero = () => process.exit(0); + process.once('SIGTERM', exitCodeZero); + process.once('SIGHUP', exitCodeZero); + + PromisePrototypeCatch(PromisePrototypeThen(this.run(), async () => { + const repl = await startRepl(); + this.repl = repl; + this.repl.on('exit', exitCodeZero); + this.paused = false; + }), (error) => process.nextTick(() => { throw error; })); } suspendReplWhile(fn) { @@ -197,16 +187,16 @@ class NodeInspector { } this.stdin.pause(); this.paused = true; - return new Promise((resolve) => { + return PromisePrototypeCatch(PromisePrototypeThen(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; })); + }), (error) => process.nextTick(() => { throw error; })); } killChild() { @@ -217,37 +207,28 @@ class NodeInspector { } } - run() { + async 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\n'); - }, (error) => { - debuglog('connect failed', error); - // If it's failed to connect 5 times then print failed message - if (connectionAttempts >= 5) { - this.stdout.write(' failed to connect, please retry\n'); - process.exit(1); - } - - return new Promise((resolve) => setTimeout(resolve, 1000)) - .then(attemptConnect); - }); - }; - - this.print(`connecting to ${host}:${port} ..`, false); - return attemptConnect(); - }); + const { 0: child, 1: port, 2: host } = await this._runScript(); + this.child = child; + + this.print(`connecting to ${host}:${port} ..`, false); + for (let attempt = 0; attempt < 5; attempt++) { + debuglog('connection attempt #%d', attempt); + this.stdout.write('.'); + try { + await this.client.connect(port, host); + debuglog('connection established'); + this.stdout.write(' ok\n'); + return; + } catch (error) { + debuglog('connect failed', error); + await pSetTimeout(1000); + } + } + this.stdout.write(' failed to connect, please retry\n'); + process.exit(1); } clearLine() { @@ -266,16 +247,19 @@ class NodeInspector { #stdioBuffers = { stdout: '', stderr: '' }; childPrint(text, which) { - const lines = (this.#stdioBuffers[which] + text) - .split(/\r\n|\r|\n/g); + const lines = RegExpPrototypeSymbolSplit( + /\r\n|\r|\n/g, + this.#stdioBuffers[which] + text); this.#stdioBuffers[which] = ''; if (lines[lines.length - 1] !== '') { - this.#stdioBuffers[which] = lines.pop(); + this.#stdioBuffers[which] = ArrayPrototypePop(lines); } - const textToPrint = lines.map((chunk) => `< ${chunk}`).join('\n'); + const textToPrint = ArrayPrototypeJoin( + ArrayPrototypeMap(lines, (chunk) => `< ${chunk}`), + '\n'); if (lines.length) { this.print(textToPrint, true); @@ -284,36 +268,41 @@ class NodeInspector { } } - if (textToPrint.endsWith('Waiting for the debugger to disconnect...\n')) { + if (StringPrototypeEndsWith( + textToPrint, + 'Waiting for the debugger to disconnect...\n' + )) { this.killChild(); } } } -function parseArgv([target, ...args]) { +function parseArgv(args) { + const target = ArrayPrototypeShift(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+)$/); + const hostMatch = RegExpPrototypeSymbolMatch(/^([^:]+):(\d+)$/, target); + const portMatch = RegExpPrototypeSymbolMatch(/^--port=(\d+)$/, target); if (hostMatch) { // Connecting to remote debugger host = hostMatch[1]; - port = parseInt(hostMatch[2], 10); + port = Number(hostMatch[2]); isRemote = true; script = null; } else if (portMatch) { // Start on custom port - port = parseInt(portMatch[1], 10); + port = Number(portMatch[1]); script = args[0]; - scriptArgs = args.slice(1); - } else if (args.length === 1 && /^\d+$/.test(args[0]) && target === '-p') { + scriptArgs = ArrayPrototypeSlice(args, 1); + } else if (args.length === 1 && RegExpPrototypeTest(/^\d+$/, args[0]) && + target === '-p') { // Start debugger against a given pid - const pid = parseInt(args[0], 10); + const pid = Number(args[0]); try { process._debugProcess(pid); } catch (e) { @@ -332,7 +321,7 @@ function parseArgv([target, ...args]) { }; } -function startInspect(argv = process.argv.slice(2), +function startInspect(argv = ArrayPrototypeSlice(process.argv, 2), stdin = process.stdin, stdout = process.stdout) { if (argv.length < 1) { @@ -340,6 +329,7 @@ function startInspect(argv = process.argv.slice(2), console.error(`Usage: ${invokedAs} script.js`); console.error(` ${invokedAs} :`); + console.error(` ${invokedAs} --port=`); console.error(` ${invokedAs} -p `); process.exit(1); } @@ -350,7 +340,7 @@ function startInspect(argv = process.argv.slice(2), stdin.resume(); function handleUnexpectedError(e) { - if (!(e instanceof StartupError)) { + if (e.code !== 'ERR_DEBUGGER_STARTUP_ERROR') { console.error('There was an internal error in Node.js. ' + 'Please report this bug.'); console.error(e.message); diff --git a/lib/internal/debugger/inspect_client.js b/lib/internal/debugger/inspect_client.js index 831a7fda1cdff1..8fc9011ca888ed 100644 --- a/lib/internal/debugger/inspect_client.js +++ b/lib/internal/debugger/inspect_client.js @@ -1,36 +1,22 @@ -/* - * 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 { + ArrayPrototypePush, + ErrorCaptureStackTrace, + FunctionPrototypeBind, + JSONParse, + JSONStringify, + ObjectKeys, + Promise, +} = primordials; + const Buffer = require('buffer').Buffer; +const { ERR_DEBUGGER_ERROR } = require('internal/errors').codes; const { EventEmitter } = require('events'); const http = require('http'); const URL = require('url'); -const util = require('util'); -const debuglog = util.debuglog('inspect'); +const debuglog = require('internal/util/debuglog').debuglog('inspect'); const kOpCodeText = 0x1; const kOpCodeClose = 0x8; @@ -49,14 +35,10 @@ 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}`); + const err = new ERR_DEBUGGER_ERROR(`${message} - ${data}`); err.code = code; - Error.captureStackTrace(err, unpackError); + ErrorCaptureStackTrace(err, unpackError); return err; } @@ -116,14 +98,14 @@ function decodeFrameHybi17(data) { const masked = (secondByte & kMaskBit) !== 0; const compressed = reserved1; if (compressed) { - throw new Error('Compressed frames not supported'); + throw new ERR_DEBUGGER_ERROR('Compressed frames not supported'); } if (!final || reserved2 || reserved3) { - throw new Error('Only compression extension is supported'); + throw new ERR_DEBUGGER_ERROR('Only compression extension is supported'); } if (masked) { - throw new Error('Masked server frame - not supported'); + throw new ERR_DEBUGGER_ERROR('Masked server frame - not supported'); } let closed = false; @@ -134,7 +116,7 @@ function decodeFrameHybi17(data) { case kOpCodeText: break; default: - throw new Error(`Unsupported op code ${opCode}`); + throw new ERR_DEBUGGER_ERROR(`Unsupported op code ${opCode}`); } let payloadLength = secondByte & kPayloadLengthMask; @@ -169,7 +151,7 @@ function decodeFrameHybi17(data) { class Client extends EventEmitter { constructor() { super(); - this.handleChunk = this._handleChunk.bind(this); + this.handleChunk = FunctionPrototypeBind(this._handleChunk, this); this._port = undefined; this._host = undefined; @@ -198,11 +180,13 @@ class Client extends EventEmitter { debuglog('< %s', payloadStr); const lastChar = payloadStr[payloadStr.length - 1]; if (payloadStr[0] !== '{' || lastChar !== '}') { - throw new Error(`Payload does not look like JSON: ${payloadStr}`); + throw new ERR_DEBUGGER_ERROR( + `Payload does not look like JSON: ${payloadStr}` + ); } let payload; try { - payload = JSON.parse(payloadStr); + payload = JSONParse(payloadStr); } catch (parseError) { parseError.string = payloadStr; throw parseError; @@ -219,7 +203,7 @@ class Client extends EventEmitter { this.emit('debugEvent', method, params); this.emit(method, params); } else { - throw new Error(`Unsupported response: ${payloadStr}`); + throw new ERR_DEBUGGER_ERROR(`Unsupported response: ${payloadStr}`); } } } @@ -241,15 +225,15 @@ class Client extends EventEmitter { callMethod(method, params) { return new Promise((resolve, reject) => { if (!this._socket) { - reject(new Error('Use `run` to start the app again.')); + reject(new ERR_DEBUGGER_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); + else resolve(ObjectKeys(result).length ? result : undefined); }; - const json = JSON.stringify(data); + const json = JSONStringify(data); debuglog('> %s', json); this._socket.write(encodeFrameHybi17(Buffer.from(json))); }); @@ -269,19 +253,22 @@ class Client extends EventEmitter { function parseChunks() { const resBody = Buffer.concat(chunks).toString(); if (httpRes.statusCode !== 200) { - reject(new Error(`Unexpected ${httpRes.statusCode}: ${resBody}`)); + reject(new ERR_DEBUGGER_ERROR( + `Unexpected ${httpRes.statusCode}: ${resBody}` + )); return; } try { - resolve(JSON.parse(resBody)); - } catch (parseError) { - reject(new Error(`Response didn't contain JSON: ${resBody}`)); - + resolve(JSONParse(resBody)); + } catch { + reject(new ERR_DEBUGGER_ERROR( + `Response didn't contain JSON: ${resBody}` + )); } } httpRes.on('error', reject); - httpRes.on('data', (chunk) => chunks.push(chunk)); + httpRes.on('data', (chunk) => ArrayPrototypePush(chunks, chunk)); httpRes.on('end', parseChunks); } @@ -290,17 +277,16 @@ class Client extends EventEmitter { }); } - connect(port, host) { + async connect(port, host) { this._port = port; this._host = host; - return this._discoverWebsocketPath() - .then((urlPath) => this._connectWebsocket(urlPath)); + const urlPath = await this._discoverWebsocketPath(); + return this._connectWebsocket(urlPath); } - _discoverWebsocketPath() { - return this._fetchJSON('/json') - .then(({ 0: { webSocketDebuggerUrl } }) => - URL.parse(webSocketDebuggerUrl).path); + async _discoverWebsocketPath() { + const { 0: { webSocketDebuggerUrl } } = await this._fetchJSON('/json'); + return URL.parse(webSocketDebuggerUrl).path; } _connectWebsocket(urlPath) { diff --git a/lib/internal/debugger/inspect_repl.js b/lib/internal/debugger/inspect_repl.js index 128ae697105a3d..5393cb32718300 100644 --- a/lib/internal/debugger/inspect_repl.js +++ b/lib/internal/debugger/inspect_repl.js @@ -1,37 +1,66 @@ -/* - * 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 */ +/* eslint-disable getter-return */ 'use strict'; + +const { + Array, + ArrayFrom, + ArrayPrototypeFilter, + ArrayPrototypeFind, + ArrayPrototypeForEach, + ArrayPrototypeIncludes, + ArrayPrototypeIndexOf, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePush, + ArrayPrototypeSlice, + ArrayPrototypeSome, + ArrayPrototypeSplice, + Date, + FunctionPrototypeCall, + JSONStringify, + MathMax, + ObjectAssign, + ObjectDefineProperty, + ObjectKeys, + ObjectValues, + Promise, + PromiseAll, + PromisePrototypeCatch, + PromisePrototypeThen, + PromiseResolve, + ReflectGetOwnPropertyDescriptor, + ReflectOwnKeys, + RegExpPrototypeSymbolMatch, + RegExpPrototypeSymbolReplace, + SafeArrayIterator, + SafeMap, + String, + StringFromCharCode, + StringPrototypeEndsWith, + StringPrototypeIncludes, + StringPrototypeRepeat, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, + StringPrototypeToUpperCase, + StringPrototypeTrim, +} = primordials; + +const { ERR_DEBUGGER_ERROR } = require('internal/errors').codes; + +const { validateString } = require('internal/validators'); + 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 { fileURLToPath } = require('internal/url'); -const debuglog = util.debuglog('inspect'); +const { customInspectSymbol } = require('internal/util'); +const { inspect: utilInspect } = require('internal/util/inspect'); +const debuglog = require('internal/util/debuglog').debuglog('inspect'); const SHORTCUTS = { cont: 'c', @@ -44,7 +73,7 @@ const SHORTCUTS = { run: 'r', }; -const HELP = ` +const HELP = StringPrototypeTrim(` run, restart, r Run the application or reconnect kill Kill a running application or disconnect @@ -81,100 +110,95 @@ profiles[n].save(filepath = 'node.cpuprofile') 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); + const fnNameMatch = + RegExpPrototypeSymbolMatch(FUNCTION_NAME_PATTERN, description); return fnNameMatch ? `: ${fnNameMatch[1]}` : ''; } -const PUBLIC_BUILTINS = require('module').builtinModules; -const NATIVES = PUBLIC_BUILTINS ? internalBinding('natives') : {}; +const { + moduleIds: PUBLIC_BUILTINS, +} = internalBinding('native_module'); +const NATIVES = internalBinding('natives'); function isNativeUrl(url) { - url = url.replace(/\.js$/, ''); - if (PUBLIC_BUILTINS) { - if (url.startsWith('node:internal/') || PUBLIC_BUILTINS.includes(url)) - return true; - } + url = RegExpPrototypeSymbolReplace(/\.js$/, url, ''); - return url in NATIVES || url === 'bootstrap_node'; + return StringPrototypeStartsWith(url, 'node:internal/') || + ArrayPrototypeIncludes(PUBLIC_BUILTINS, url) || + url in NATIVES || url === 'bootstrap_node'; } function getRelativePath(filenameOrURL) { - const dir = Path.join(Path.resolve(), 'x').slice(0, -1); + const dir = StringPrototypeSlice(Path.join(Path.resolve(), 'x'), 0, -1); - const filename = filenameOrURL.startsWith('file://') ? + const filename = StringPrototypeStartsWith(filenameOrURL, 'file://') ? fileURLToPath(filenameOrURL) : filenameOrURL; // Change path to relative, if possible - if (filename.indexOf(dir) === 0) { - return filename.slice(dir.length); + if (StringPrototypeStartsWith(filename, dir)) { + return StringPrototypeSlice(filename, 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; + const nchars = MathMax(2, String(maxN).length); + const nspaces = nchars - s.length; - return prefix + ' '.repeat(nspaces) + s; + return prefix + StringPrototypeRepeat(' ', nspaces) + s; } function markSourceColumn(sourceText, position, useColors) { if (!sourceText) return ''; - const head = sourceText.slice(0, position); - let tail = sourceText.slice(position); + const head = StringPrototypeSlice(sourceText, 0, position); + let tail = StringPrototypeSlice(sourceText, position); // Colourize char if stdout supports colours if (useColors) { - tail = tail.replace(/(.+?)([^\w]|$)/, '\u001b[32m$1\u001b[39m$2'); + tail = RegExpPrototypeSymbolReplace(/(.+?)([^\w]|$)/, tail, + '\u001b[32m$1\u001b[39m$2'); } // Return source line with coloured char at `position` - return [head, tail].join(''); + return head + tail; } function extractErrorMessage(stack) { if (!stack) return ''; - const m = stack.match(/^\w+: ([^\n]+)/); - return m ? m[1] : stack; + const m = RegExpPrototypeSymbolMatch(/^\w+: ([^\n]+)/, stack); + return m?.[1] ?? stack; } function convertResultToError(result) { const { className, description } = result; - const err = new Error(extractErrorMessage(description)); + const err = new ERR_DEBUGGER_ERROR(extractErrorMessage(description)); err.stack = description; - Object.defineProperty(err, 'name', { value: className }); + ObjectDefineProperty(err, 'name', { value: className }); return err; } class RemoteObject { constructor(attributes) { - Object.assign(this, attributes); + ObjectAssign(this, attributes); if (this.type === 'number') { this.value = this.unserializableValue ? +this.unserializableValue : +this.value; } } - [util.inspect.custom](depth, opts) { + [customInspectSymbol](depth, opts) { function formatProperty(prop) { switch (prop.type) { case 'string': case 'undefined': - return util.inspect(prop.value, opts); + return utilInspect(prop.value, opts); case 'number': case 'boolean': @@ -183,7 +207,7 @@ class RemoteObject { case 'object': case 'symbol': if (prop.subtype === 'date') { - return util.inspect(new Date(prop.value), opts); + return utilInspect(new Date(prop.value), opts); } if (prop.subtype === 'array') { return opts.stylize(prop.value, 'special'); @@ -199,7 +223,7 @@ class RemoteObject { case 'number': case 'string': case 'undefined': - return util.inspect(this.value, opts); + return utilInspect(this.value, opts); case 'symbol': return opts.stylize(this.description, 'special'); @@ -213,10 +237,10 @@ class RemoteObject { case 'object': switch (this.subtype) { case 'date': - return util.inspect(new Date(this.description), opts); + return utilInspect(new Date(this.description), opts); case 'null': - return util.inspect(null, opts); + return utilInspect(null, opts); case 'regexp': return opts.stylize(this.description, 'regexp'); @@ -225,18 +249,21 @@ class RemoteObject { break; } if (this.preview) { - const props = this.preview.properties - .map((prop, idx) => { + const props = ArrayPrototypeMap( + this.preview.properties, + (prop, idx) => { const value = formatProperty(prop); if (prop.name === `${idx}`) return value; return `${prop.name}: ${value}`; }); if (this.preview.overflow) { - props.push('...'); + ArrayPrototypePush(props, '...'); } - const singleLine = props.join(', '); + const singleLine = ArrayPrototypeJoin(props, ', '); const propString = - singleLine.length > 60 ? props.join(',\n ') : singleLine; + singleLine.length > 60 ? + ArrayPrototypeJoin(props, ',\n ') : + singleLine; return this.subtype === 'array' ? `[ ${propString} ]` : `{ ${propString} }`; @@ -256,34 +283,39 @@ class RemoteObject { class ScopeSnapshot { constructor(scope, properties) { - Object.assign(this, scope); - this.properties = new Map(properties.map((prop) => { + ObjectAssign(this, scope); + this.properties = new SafeMap(); + this.completionGroup = ArrayPrototypeMap(properties, (prop) => { const value = new RemoteObject(prop.value); - return [prop.name, value]; - })); - this.completionGroup = properties.map((prop) => prop.name); + this.properties.set(prop.name, value); + return prop.name; + }); } - [util.inspect.custom](depth, opts) { - const type = `${this.type[0].toUpperCase()}${this.type.slice(1)}`; + [customInspectSymbol](depth, opts) { + const type = StringPrototypeToUpperCase(this.type[0]) + + StringPrototypeSlice(this.type, 1); const name = this.name ? `<${this.name}>` : ''; const prefix = `${type}${name} `; - return util.inspect(this.properties, opts) - .replace(/^Map /, prefix); + return RegExpPrototypeSymbolReplace(/^Map /, + utilInspect(this.properties, opts), + prefix); } } function copyOwnProperties(target, source) { - Object.getOwnPropertyNames(source).forEach((prop) => { - const descriptor = Object.getOwnPropertyDescriptor(source, prop); - Object.defineProperty(target, prop, descriptor); - }); + ArrayPrototypeForEach( + ReflectOwnKeys(source), + (prop) => { + const desc = ReflectGetOwnPropertyDescriptor(source, prop); + ObjectDefineProperty(target, prop, desc); + }); } function aliasProperties(target, mapping) { - Object.keys(mapping).forEach((key) => { - const descriptor = Object.getOwnPropertyDescriptor(target, key); - Object.defineProperty(target, mapping[key], descriptor); + ArrayPrototypeForEach(ObjectKeys(mapping), (key) => { + const desc = ReflectGetOwnPropertyDescriptor(target, key); + ObjectDefineProperty(target, mapping[key], desc); }); } @@ -317,7 +349,7 @@ function createRepl(inspector) { const INSPECT_OPTIONS = { colors: inspector.stdout.isTTY }; function inspect(value) { - return util.inspect(value, INSPECT_OPTIONS); + return utilInspect(value, INSPECT_OPTIONS); } function print(value, addNewline = true) { @@ -327,7 +359,7 @@ function createRepl(inspector) { function getCurrentLocation() { if (!selectedFrame) { - throw new Error('Requires execution to be paused'); + throw new ERR_DEBUGGER_ERROR('Requires execution to be paused'); } return selectedFrame.location; } @@ -342,22 +374,20 @@ function createRepl(inspector) { return !script.isNative || isCurrentScript(script); } - return Object.keys(knownScripts) - .map((scriptId) => knownScripts[scriptId]) - .filter(isVisible) - .map((script) => { + return ArrayPrototypeJoin(ArrayPrototypeMap( + ArrayPrototypeFilter(ObjectValues(knownScripts), isVisible), + (script) => { const isCurrent = isCurrentScript(script); const { isNative, url } = script; const name = `${getRelativePath(url)}${isNative ? ' ' : ''}`; return `${isCurrent ? '*' : ' '} ${script.scriptId}: ${name}`; - }) - .join('\n'); + }), '\n'); } function listScripts(displayNatives = false) { print(formatScripts(displayNatives)); } - listScripts[util.inspect.custom] = function listWithoutInternal() { + listScripts[customInspectSymbol] = function listWithoutInternal() { return formatScripts(); }; @@ -369,18 +399,19 @@ function createRepl(inspector) { static createAndRegister({ profile }) { const p = new Profile(profile); - profiles.push(p); + ArrayPrototypePush(profiles, p); return p; } - [util.inspect.custom](depth, { stylize }) { + [customInspectSymbol](depth, { stylize }) { const { startTime, endTime } = this.data; - return stylize(`[Profile ${endTime - startTime}μs]`, 'special'); + const MU = StringFromCharCode(956); + return stylize(`[Profile ${endTime - startTime}${MU}s]`, 'special'); } save(filename = 'node.cpuprofile') { const absoluteFile = Path.resolve(filename); - const json = JSON.stringify(this.data); + const json = JSONStringify(this.data); FS.writeFileSync(absoluteFile, json); print('Saved profile to ' + absoluteFile); } @@ -388,68 +419,75 @@ function createRepl(inspector) { class SourceSnippet { constructor(location, delta, scriptSource) { - Object.assign(this, location); + ObjectAssign(this, location); this.scriptSource = scriptSource; this.delta = delta; } - [util.inspect.custom](depth, options) { + [customInspectSymbol](depth, options) { const { scriptId, lineNumber, columnNumber, delta, scriptSource } = this; - const start = Math.max(1, lineNumber - delta + 1); + const start = MathMax(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 && + const lines = StringPrototypeSplit(scriptSource, '\n'); + return ArrayPrototypeJoin( + ArrayPrototypeMap( + ArrayPrototypeSlice(lines, start - 1, end), + (lineText, offset) => { + const i = start + offset; + const isCurrent = i === (lineNumber + 1); + + const markedLine = isCurrent ? + markSourceColumn(lineText, columnNumber, options.colors) : + lineText; + + let isBreakpoint = false; + ArrayPrototypeForEach(knownBreakpoints, ({ location }) => { + if (!location) return; + if (scriptId === location.scriptId && i === (location.lineNumber + 1)) { - isBreakpoint = true; - } - }); + isBreakpoint = true; + } + }); - let prefixChar = ' '; - if (isCurrent) { - prefixChar = '>'; - } else if (isBreakpoint) { - prefixChar = '*'; - } - return `${leftPad(i, prefixChar, end)} ${markedLine}`; - }).join('\n'); + let prefixChar = ' '; + if (isCurrent) { + prefixChar = '>'; + } else if (isBreakpoint) { + prefixChar = '*'; + } + return `${leftPad(i, prefixChar, end)} ${markedLine}`; + }), '\n'); } } - function getSourceSnippet(location, delta = 5) { + async function getSourceSnippet(location, delta = 5) { const { scriptId } = location; - return Debugger.getScriptSource({ scriptId }) - .then(({ scriptSource }) => - new SourceSnippet(location, delta, scriptSource)); + const { scriptSource } = await Debugger.getScriptSource({ scriptId }); + return new SourceSnippet(location, delta, scriptSource); } class CallFrame { constructor(callFrame) { - Object.assign(this, callFrame); + ObjectAssign(this, callFrame); } loadScopes() { - return Promise.all( - this.scopeChain - .filter((scope) => scope.type !== 'global') - .map((scope) => { + return PromiseAll( + new SafeArrayIterator(ArrayPrototypeMap( + ArrayPrototypeFilter( + this.scopeChain, + (scope) => scope.type !== 'global' + ), + async (scope) => { const { objectId } = scope.object; - return Runtime.getProperties({ + const { result } = await Runtime.getProperties({ objectId, generatePreview: true, - }).then(({ result }) => new ScopeSnapshot(scope, result)); + }); + return new ScopeSnapshot(scope, result); }) + ) ); } @@ -459,70 +497,73 @@ function createRepl(inspector) { } 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 = + [customInspectSymbol]() { + return ArrayPrototypeJoin( + ArrayPrototypeMap(this, (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 = + const frameLocation = `${relativeUrl}:${lineNumber + 1}:${columnNumber}`; - return `#${idx} ${name} ${frameLocation}`; - }).join('\n'); + return `#${idx} ${name} ${frameLocation}`; + }), '\n'); } static from(callFrames) { - return super.from(Array.from(callFrames).map((callFrame) => { - if (callFrame instanceof CallFrame) { - return callFrame; - } - return new CallFrame(callFrame); - })); + return FunctionPrototypeCall( + ArrayFrom, + this, + callFrames, + (callFrame) => + (callFrame instanceof CallFrame ? + callFrame : + new CallFrame(callFrame)) + ); } } function prepareControlCode(input) { if (input === '\n') return lastCommand; // Add parentheses: exec process.title => exec("process.title"); - const match = input.match(/^\s*exec\s+([^\n]*)/); + const match = RegExpPrototypeSymbolMatch(/^\s*exec\s+([^\n]*)/, input); if (match) { - lastCommand = `exec(${JSON.stringify(match[1])})`; + lastCommand = `exec(${JSONStringify(match[1])})`; } else { lastCommand = input; } return lastCommand; } - function evalInCurrentContext(code) { + async function evalInCurrentContext(code) { // Repl asked for scope variables if (code === '.scope') { if (!selectedFrame) { - return Promise.reject(new Error('Requires execution to be paused')); + throw new ERR_DEBUGGER_ERROR('Requires execution to be paused'); } - return selectedFrame.loadScopes().then((scopes) => { - return scopes.map((scope) => scope.completionGroup); - }); + const scopes = await selectedFrame.loadScopes(); + return ArrayPrototypeMap(scopes, (scope) => scope.completionGroup); } if (selectedFrame) { - return Debugger.evaluateOnCallFrame({ + return PromisePrototypeThen(Debugger.evaluateOnCallFrame({ callFrameId: selectedFrame.callFrameId, expression: code, objectGroup: 'node-inspect', generatePreview: true, - }).then(RemoteObject.fromEvalResult); + }), RemoteObject.fromEvalResult); } - return Runtime.evaluate({ + return PromisePrototypeThen(Runtime.evaluate({ expression: code, objectGroup: 'node-inspect', generatePreview: true, - }).then(RemoteObject.fromEvalResult); + }), RemoteObject.fromEvalResult); } function controlEval(input, context, filename, callback) { @@ -536,11 +577,16 @@ function createRepl(inspector) { const code = prepareControlCode(input); const result = vm.runInContext(code, context, filename); - if (result && typeof result.then === 'function') { - toCallback(result, returnToCallback); - return; + const then = result?.then; + if (typeof then === 'function') { + FunctionPrototypeCall( + then, result, + (result) => returnToCallback(null, result), + returnToCallback + ); + } else { + returnToCallback(null, result); } - returnToCallback(null, result); } catch (e) { returnToCallback(e); } @@ -553,76 +599,63 @@ function createRepl(inspector) { 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); - } + PromisePrototypeThen(evalInCurrentContext(input), + (result) => returnToCallback(null, result), + returnToCallback + ); } - function formatWatchers(verbose = false) { + async function formatWatchers(verbose = false) { if (!watchedExpressions.length) { - return Promise.resolve(''); + return ''; } const inspectValue = (expr) => - evalInCurrentContext(expr) - // .then(formatValue) - .catch((error) => `<${error.message}>`); + PromisePrototypeCatch(evalInCurrentContext(expr), + (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]); - 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; - }); + const values = await PromiseAll(new SafeArrayIterator( + ArrayPrototypeMap(watchedExpressions, inspectValue))); + const lines = ArrayPrototypeMap(watchedExpressions, (expr, idx) => { + const prefix = `${leftPad(idx, ' ', lastIndex)}: ${expr} =`; + const value = inspect(values[idx]); + if (!StringPrototypeIncludes(value, '\n')) { + return `${prefix} ${value}`; + } + return `${prefix}\n ${RegExpPrototypeSymbolReplace(/\n/g, value, '\n ')}`; + }); + const valueList = ArrayPrototypeJoin(lines, '\n'); + return verbose ? `Watchers:\n${valueList}\n` : valueList; } function watchers(verbose = false) { - return formatWatchers(verbose).then(print); + return PromisePrototypeThen(formatWatchers(verbose), 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; - }); + 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 }); + ObjectAssign(location, { scriptUrl }); } - const isExisting = knownBreakpoints.some((bp) => { + const isExisting = ArrayPrototypeSome(knownBreakpoints, (bp) => { if (bp.breakpointId === breakpointId) { - Object.assign(bp, { location }); + ObjectAssign(bp, { location }); return true; } return false; }); if (!isExisting) { - knownBreakpoints.push({ breakpointId, location }); + ArrayPrototypePush(knownBreakpoints, { breakpointId, location }); } } @@ -638,9 +671,11 @@ function createRepl(inspector) { 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'); + const breaklist = ArrayPrototypeJoin( + ArrayPrototypeMap( + knownBreakpoints, + (bp, idx) => `#${idx} ${formatLocation(bp.location)}`), + '\n'); print(breaklist); } @@ -657,9 +692,9 @@ function createRepl(inspector) { // setBreakpoint(): set breakpoint at current location if (script === undefined) { - return Debugger - .setBreakpoint({ location: getCurrentLocation(), condition }) - .then(registerBreakpoint); + return PromisePrototypeThen( + Debugger.setBreakpoint({ location: getCurrentLocation(), condition }), + registerBreakpoint); } // setBreakpoint(line): set breakpoint in current script at specific line @@ -668,16 +703,15 @@ function createRepl(inspector) { scriptId: getCurrentLocation().scriptId, lineNumber: script - 1, }; - return Debugger.setBreakpoint({ location, condition }) - .then(registerBreakpoint); + return PromisePrototypeThen( + Debugger.setBreakpoint({ location, condition }), + registerBreakpoint); } - if (typeof script !== 'string') { - throw new TypeError(`setBreakpoint() expects a string, got ${script}`); - } + validateString(script, 'script'); // setBreakpoint('fn()'): Break when a function is called - if (script.endsWith('()')) { + if (StringPrototypeEndsWith(script, '()')) { const debugExpr = `debug(${script.slice(0, -2)})`; const debugCall = selectedFrame ? Debugger.evaluateOnCallFrame({ @@ -689,7 +723,7 @@ function createRepl(inspector) { expression: debugExpr, includeCommandLineAPI: true, }); - return debugCall.then(({ result, wasThrown }) => { + return PromisePrototypeThen(debugCall, ({ result, wasThrown }) => { if (wasThrown) return convertResultToError(result); return undefined; // This breakpoint can't be removed the same way }); @@ -701,15 +735,15 @@ function createRepl(inspector) { if (knownScripts[script]) { scriptId = script; } else { - for (const id of Object.keys(knownScripts)) { + ArrayPrototypeForEach(ObjectKeys(knownScripts), (id) => { const scriptUrl = knownScripts[id].url; - if (scriptUrl && scriptUrl.indexOf(script) !== -1) { + if (scriptUrl && StringPrototypeIncludes(scriptUrl, script)) { if (scriptId !== null) { ambiguous = true; } scriptId = id; } - } + }); } if (ambiguous) { @@ -723,19 +757,25 @@ function createRepl(inspector) { if (scriptId !== null) { const location = { scriptId, lineNumber: line - 1 }; - return Debugger.setBreakpoint({ location, condition }) - .then(registerBreakpoint); + return PromisePrototypeThen( + Debugger.setBreakpoint({ location, condition }), + registerBreakpoint); } - const escapedPath = script.replace(/([/\\.?*()^${}|[\]])/g, '\\$1'); + const escapedPath = RegExpPrototypeSymbolReplace(/([/\\.?*()^${}|[\]])/g, + script, '\\$1'); const urlRegex = `^(.*[\\/\\\\])?${escapedPath}$`; - return Debugger - .setBreakpointByUrl({ urlRegex, lineNumber: line - 1, condition }) - .then((bp) => { + return PromisePrototypeThen( + Debugger.setBreakpointByUrl({ + urlRegex, + lineNumber: line - 1, + condition, + }), + (bp) => { // TODO: handle bp.locations in case the regex matches existing files if (!bp.location) { // Fake it for now. - Object.assign(bp, { + ObjectAssign(bp, { actualLocation: { scriptUrl: `.*/${script}$`, lineNumber: line - 1, @@ -747,41 +787,46 @@ function createRepl(inspector) { } function clearBreakpoint(url, line) { - const breakpoint = knownBreakpoints.find(({ location }) => { + const breakpoint = ArrayPrototypeFind(knownBreakpoints, ({ location }) => { if (!location) return false; const script = knownScripts[location.scriptId]; if (!script) return false; return ( - script.url.indexOf(url) !== -1 && (location.lineNumber + 1) === line + StringPrototypeIncludes(script.url, url) && + (location.lineNumber + 1) === line ); }); if (!breakpoint) { print(`Could not find breakpoint at ${url}:${line}`); - return Promise.resolve(); + return PromiseResolve(); } - return Debugger.removeBreakpoint({ breakpointId: breakpoint.breakpointId }) - .then(() => { - const idx = knownBreakpoints.indexOf(breakpoint); - knownBreakpoints.splice(idx, 1); + return PromisePrototypeThen( + Debugger.removeBreakpoint({ breakpointId: breakpoint.breakpointId }), + () => { + const idx = ArrayPrototypeIndexOf(knownBreakpoints, breakpoint); + ArrayPrototypeSplice(knownBreakpoints, 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.`); - }); + const lastBreakpoints = ArrayPrototypeSplice(knownBreakpoints, 0); + const newBreakpoints = ArrayPrototypeMap( + ArrayPrototypeFilter(lastBreakpoints, + ({ location }) => !!location.scriptUrl), + ({ location }) => setBreakpoint(location.scriptUrl, + location.lineNumber + 1)); + if (!newBreakpoints.length) return PromiseResolve(); + return PromisePrototypeThen( + PromiseAll(new SafeArrayIterator(newBreakpoints)), + (results) => { + print(`${results.length} breakpoints restored.`); + }); } function setPauseOnExceptions(state) { - return Debugger.setPauseOnExceptions({ state }) - .then(() => { + return PromisePrototypeThen( + Debugger.setPauseOnExceptions({ state }), + () => { pauseOnExceptionState = state; }); } @@ -807,13 +852,13 @@ function createRepl(inspector) { const header = `${breakType} in ${scriptUrl}:${lineNumber + 1}`; inspector.suspendReplWhile(() => - Promise.all([formatWatchers(true), selectedFrame.list(2)]) - .then(({ 0: watcherList, 1: context }) => { - if (watcherList) { - return `${watcherList}\n${inspect(context)}`; - } - return inspect(context); - }).then((breakContext) => { + PromisePrototypeThen( + PromiseAll(new SafeArrayIterator( + [formatWatchers(true), selectedFrame.list(2)])), + ({ 0: watcherList, 1: context }) => { + const breakContext = watcherList ? + `${watcherList}\n${inspect(context)}` : + inspect(context); print(`${header}\n${breakContext}`); })); }); @@ -836,15 +881,15 @@ function createRepl(inspector) { Profiler.on('consoleProfileFinished', ({ profile }) => { Profile.createAndRegister({ profile }); - print([ - 'Captured new CPU profile.', - `Access it with profiles[${profiles.length - 1}]`, - ].join('\n')); + print( + 'Captured new CPU profile.\n' + + `Access it with profiles[${profiles.length - 1}]` + ); }); function initializeContext(context) { - inspector.domainNames.forEach((domain) => { - Object.defineProperty(context, domain, { + ArrayPrototypeForEach(inspector.domainNames, (domain) => { + ObjectDefineProperty(context, domain, { value: inspector[domain], enumerable: true, configurable: true, @@ -910,8 +955,8 @@ function createRepl(inspector) { }, get profileEnd() { - return Profiler.stop() - .then(Profile.createAndRegister); + return PromisePrototypeThen(Profiler.stop(), + Profile.createAndRegister); }, get profiles() { @@ -960,8 +1005,9 @@ function createRepl(inspector) { HeapProfiler.on('addHeapSnapshotChunk', onChunk); print('Heap snapshot: 0/0', false); - HeapProfiler.takeHeapSnapshot({ reportProgress: true }) - .then(onResolve, onReject); + PromisePrototypeThen( + HeapProfiler.takeHeapSnapshot({ reportProgress: true }), + onResolve, onReject); }); }, @@ -970,21 +1016,22 @@ function createRepl(inspector) { }, watch(expr) { - watchedExpressions.push(expr); + ArrayPrototypePush(watchedExpressions, expr); }, unwatch(expr) { - const index = watchedExpressions.indexOf(expr); + const index = ArrayPrototypeIndexOf(watchedExpressions, expr); // Unwatch by expression // or // Unwatch by watcher number - watchedExpressions.splice(index !== -1 ? index : +expr, 1); + ArrayPrototypeSplice(watchedExpressions, + index !== -1 ? index : +expr, 1); }, get repl() { // Don't display any default messages - const listeners = repl.listeners('SIGINT').slice(0); + const listeners = ArrayPrototypeSlice(repl.listeners('SIGINT')); repl.removeAllListeners('SIGINT'); const oldContext = repl.context; @@ -992,7 +1039,7 @@ function createRepl(inspector) { exitDebugRepl = () => { // Restore all listeners process.nextTick(() => { - listeners.forEach((listener) => { + ArrayPrototypeForEach(listeners, (listener) => { repl.on('SIGINT', listener); }); }); @@ -1030,16 +1077,16 @@ function createRepl(inspector) { repl.setPrompt('> '); - print('Press Ctrl + C to leave debug repl'); + print('Press Ctrl+C to leave debug repl'); repl.displayPrompt(); }, get version() { - return Runtime.evaluate({ + return PromisePrototypeThen(Runtime.evaluate({ expression: 'process.versions.v8', contextId: 1, returnByValue: true, - }).then(({ result }) => { + }), ({ result }) => { print(result.value); }); }, @@ -1064,20 +1111,20 @@ function createRepl(inspector) { aliasProperties(context, SHORTCUTS); } - function initAfterStart() { - return Runtime.enable() - .then(() => Profiler.enable()) - .then(() => Profiler.setSamplingInterval({ interval: 100 })) - .then(() => Debugger.enable()) - .then(() => Debugger.setPauseOnExceptions({ state: 'none' })) - .then(() => Debugger.setAsyncCallStackDepth({ maxDepth: 0 })) - .then(() => Debugger.setBlackboxPatterns({ patterns: [] })) - .then(() => Debugger.setPauseOnExceptions({ state: pauseOnExceptionState })) - .then(() => restoreBreakpoints()) - .then(() => Runtime.runIfWaitingForDebugger()); + async function initAfterStart() { + await Runtime.enable(); + await Profiler.enable(); + await Profiler.setSamplingInterval({ interval: 100 }); + await Debugger.enable(); + await Debugger.setPauseOnExceptions({ state: 'none' }); + await Debugger.setAsyncCallStackDepth({ maxDepth: 0 }); + await Debugger.setBlackboxPatterns({ patterns: [] }); + await Debugger.setPauseOnExceptions({ state: pauseOnExceptionState }); + await restoreBreakpoints(); + return Runtime.runIfWaitingForDebugger(); } - return function startRepl() { + return async function startRepl() { inspector.client.on('close', () => { resetOnStart(); }); @@ -1085,6 +1132,9 @@ function createRepl(inspector) { initAfterStart(); }); + // Init once for the initial connection + await initAfterStart(); + const replOptions = { prompt: 'debug> ', input: inspector.stdin, @@ -1099,13 +1149,10 @@ function createRepl(inspector) { repl.on('reset', initializeContext); repl.defineCommand('interrupt', () => { - // We want this for testing purposes where sending CTRL-C can be tricky. + // 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; }; } diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 31f569f5e57e6a..441c4f2f317171 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -831,6 +831,8 @@ E('ERR_CRYPTO_SCRYPT_INVALID_PARAMETER', 'Invalid scrypt parameter', Error); E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error); // Switch to TypeError. The current implementation does not seem right. E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error); +E('ERR_DEBUGGER_ERROR', '%s', Error); +E('ERR_DEBUGGER_STARTUP_ERROR', '%s', Error); E('ERR_DIR_CLOSED', 'Directory handle was closed', Error); E('ERR_DIR_CONCURRENT_OPERATION', 'Cannot do synchronous work on directory handle with concurrent ' + diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 4ff756a0ab5c3f..7313c7ac847cd6 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -438,7 +438,7 @@ function trySelf(parentPath, request) { const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; function resolveExports(nmPath, request) { // The implementation's behavior is meant to mirror resolution in ESM. - const [, name, expansion = ''] = + const { 1: name, 2: expansion = '' } = StringPrototypeMatch(request, EXPORTS_PATTERN) || []; if (!name) return; diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 9121cb388fccc2..0e1a7eb7b6a779 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -12,6 +12,7 @@ const { PromisePrototypeCatch, ReflectApply, RegExpPrototypeTest, + SafeArrayIterator, SafeSet, StringPrototypeIncludes, StringPrototypeMatch, @@ -76,9 +77,9 @@ class ModuleJob { }); if (promises !== undefined) - await PromiseAll(promises); + await PromiseAll(new SafeArrayIterator(promises)); - return PromiseAll(dependencyJobs); + return PromiseAll(new SafeArrayIterator(dependencyJobs)); }; // Promise for the list of all dependencyJobs. this.linked = link(); @@ -106,8 +107,8 @@ class ModuleJob { } jobsInGraph.add(moduleJob); const dependencyJobs = await moduleJob.linked; - return PromiseAll( - ArrayPrototypeMap(dependencyJobs, addJobsToDependencyGraph)); + return PromiseAll(new SafeArrayIterator( + ArrayPrototypeMap(dependencyJobs, addJobsToDependencyGraph))); }; await addJobsToDependencyGraph(this); diff --git a/lib/internal/per_context/primordials.js b/lib/internal/per_context/primordials.js index a4ea444ef7d832..135afd4d60ce56 100644 --- a/lib/internal/per_context/primordials.js +++ b/lib/internal/per_context/primordials.js @@ -263,6 +263,9 @@ primordials.SafeWeakSet = makeSafe( // Refs: https://tc39.es/ecma262/#sec-%typedarray%-intrinsic-object [ { name: 'TypedArray', original: Reflect.getPrototypeOf(Uint8Array) }, + { name: 'ArrayIterator', original: { + prototype: Reflect.getPrototypeOf(Array.prototype[Symbol.iterator]()), + } }, { name: 'StringIterator', original: { prototype: Reflect.getPrototypeOf(String.prototype[Symbol.iterator]()), } }, @@ -274,6 +277,10 @@ primordials.SafeWeakSet = makeSafe( copyPrototype(original.prototype, primordials, `${name}Prototype`); }); +primordials.SafeArrayIterator = createSafeIterator( + primordials.ArrayPrototypeSymbolIterator, + primordials.ArrayIteratorPrototypeNext +); primordials.SafeStringIterator = createSafeIterator( primordials.StringPrototypeSymbolIterator, primordials.StringIteratorPrototypeNext diff --git a/lib/internal/util/debuglog.js b/lib/internal/util/debuglog.js index a3b8a84772054e..46d3ed5613246e 100644 --- a/lib/internal/util/debuglog.js +++ b/lib/internal/util/debuglog.js @@ -6,6 +6,7 @@ const { ObjectDefineProperty, RegExp, RegExpPrototypeTest, + SafeArrayIterator, StringPrototypeToUpperCase } = primordials; @@ -78,7 +79,7 @@ function debuglog(set, cb) { debug = debuglogImpl(enabled, set); if (typeof cb === 'function') cb(debug); - debug(...args); + debug(...new SafeArrayIterator(args)); }; let enabled; let test = () => { @@ -86,7 +87,7 @@ function debuglog(set, cb) { test = () => enabled; return enabled; }; - const logger = (...args) => debug(...args); + const logger = (...args) => debug(...new SafeArrayIterator(args)); ObjectDefineProperty(logger, 'enabled', { get() { return test(); diff --git a/test/parallel/test-debug-usage.js b/test/parallel/test-debug-usage.js index eb9594f236b35c..fcf2d8edb944e0 100644 --- a/test/parallel/test-debug-usage.js +++ b/test/parallel/test-debug-usage.js @@ -11,7 +11,7 @@ child.stderr.setEncoding('utf8'); const expectedLines = [ /^\(node:\d+\) \[DEP0068\] DeprecationWarning:/, - /Usage: .*node.* debug script\.js\r?\n .*node.* debug :\r?\n .*node.* debug -p \r?\n$/, + /Usage: .*node.* debug script\.js\r?\n .*node.* debug :\r?\n .*node.* debug --port=\r?\n .*node.* debug -p \r?\n$/, ]; let actualUsageMessage = ''; diff --git a/test/sequential/test-debugger-exec.js b/test/sequential/test-debugger-exec.js index 15e7e212cf2466..f47eaaa5a6f62d 100644 --- a/test/sequential/test-debugger-exec.js +++ b/test/sequential/test-debugger-exec.js @@ -31,7 +31,7 @@ const assert = require('assert'); .then(() => { assert.match( cli.output, - /Press Ctrl \+ C to leave debug repl\n+> /, + /Press Ctrl\+C to leave debug repl\n+> /, 'shows hint for how to leave repl'); assert.doesNotMatch(cli.output, /debug>/, 'changes the repl style'); })