From 9f0e80aa4df647a8bcb053aba852eb6b9210e68e Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 25 Apr 2021 23:27:15 +0200 Subject: [PATCH] debugger: refactor `internal/inspector/_inspect` to use more primordials PR-URL: https://github.com/nodejs/node/pull/38406 Reviewed-By: Rich Trott --- lib/internal/inspector/_inspect.js | 283 +++++++++++++++-------------- 1 file changed, 149 insertions(+), 134 deletions(-) diff --git a/lib/internal/inspector/_inspect.js b/lib/internal/inspector/_inspect.js index 487c8837752e8e..f695fd53f1fda6 100644 --- a/lib/internal/inspector/_inspect.js +++ b/lib/internal/inspector/_inspect.js @@ -20,14 +20,48 @@ * IN THE SOFTWARE. */ -// TODO(trott): enable ESLint -/* eslint-disable */ +// TODO(aduh95): remove restricted syntax errors +/* eslint-disable no-restricted-syntax */ 'use strict'; + +const { + ArrayPrototypeConcat, + ArrayPrototypeForEach, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePop, + ArrayPrototypeShift, + ArrayPrototypeSlice, + Error, + 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 { + setInterval, + setTimeout, +} = require('timers/promises'); +const { + AbortController, +} = require('internal/abort_controller'); + +// TODO(aduh95): remove console calls +const console = require('internal/console/global'); const { 0: InspectClient, 1: createRepl } = [ @@ -44,88 +78,72 @@ class StartupError extends Error { } } -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; + const ac = new AbortController(); + const { signal } = ac; - 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; + setTimeout(timeout).then(() => ac.abort()); + // eslint-disable-next-line no-unused-vars + for await (const _ of setInterval(retryDelay)) { + if (signal.aborted) { + throw new StartupError( + `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 +166,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 +195,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(), () => { + const repl = startRepl(); + this.repl = repl; + this.repl.on('exit', exitCodeZero); + this.paused = false; + }), (error) => process.nextTick(() => { throw error; })); } suspendReplWhile(fn) { @@ -197,16 +213,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 +233,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 setTimeout(1000); + } + } + this.stdout.write(' failed to connect, please retry\n'); + process.exit(1); } clearLine() { @@ -266,16 +273,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 +294,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 +347,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) {