1'use strict'; 2 3const { 4 ArrayPrototypeForEach, 5 ArrayPrototypeJoin, 6 ArrayPrototypeMap, 7 ArrayPrototypePop, 8 ArrayPrototypePushApply, 9 ArrayPrototypeShift, 10 ArrayPrototypeSlice, 11 FunctionPrototypeBind, 12 Number, 13 Promise, 14 PromisePrototypeThen, 15 PromiseResolve, 16 Proxy, 17 RegExpPrototypeExec, 18 RegExpPrototypeSymbolSplit, 19 StringPrototypeEndsWith, 20 StringPrototypeSplit, 21} = primordials; 22 23const { spawn } = require('child_process'); 24const { EventEmitter } = require('events'); 25const net = require('net'); 26const util = require('util'); 27const { 28 setInterval: pSetInterval, 29 setTimeout: pSetTimeout, 30} = require('timers/promises'); 31const { 32 AbortController, 33} = require('internal/abort_controller'); 34 35const { 0: InspectClient, 1: createRepl } = 36 [ 37 require('internal/debugger/inspect_client'), 38 require('internal/debugger/inspect_repl'), 39 ]; 40 41const debuglog = util.debuglog('inspect'); 42 43const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes; 44 45async function portIsFree(host, port, timeout = 3000) { 46 if (port === 0) return; // Binding to a random port. 47 48 const retryDelay = 150; 49 const ac = new AbortController(); 50 const { signal } = ac; 51 52 pSetTimeout(timeout).then(() => ac.abort()); 53 54 const asyncIterator = pSetInterval(retryDelay); 55 while (true) { 56 await asyncIterator.next(); 57 if (signal.aborted) { 58 throw new ERR_DEBUGGER_STARTUP_ERROR( 59 `Timeout (${timeout}) waiting for ${host}:${port} to be free`); 60 } 61 const error = await new Promise((resolve) => { 62 const socket = net.connect(port, host); 63 socket.on('error', resolve); 64 socket.on('connect', () => { 65 socket.end(); 66 resolve(); 67 }); 68 }); 69 if (error?.code === 'ECONNREFUSED') { 70 return; 71 } 72 } 73} 74 75const debugRegex = /Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\//; 76async function runScript(script, scriptArgs, inspectHost, inspectPort, 77 childPrint) { 78 await portIsFree(inspectHost, inspectPort); 79 const args = [`--inspect-brk=${inspectPort}`, script]; 80 ArrayPrototypePushApply(args, scriptArgs); 81 const child = spawn(process.execPath, args); 82 child.stdout.setEncoding('utf8'); 83 child.stderr.setEncoding('utf8'); 84 child.stdout.on('data', (chunk) => childPrint(chunk, 'stdout')); 85 child.stderr.on('data', (chunk) => childPrint(chunk, 'stderr')); 86 87 let output = ''; 88 return new Promise((resolve) => { 89 function waitForListenHint(text) { 90 output += text; 91 const debug = RegExpPrototypeExec(debugRegex, output); 92 if (debug) { 93 const host = debug[1]; 94 const port = Number(debug[2]); 95 child.stderr.removeListener('data', waitForListenHint); 96 resolve([child, port, host]); 97 } 98 } 99 100 child.stderr.on('data', waitForListenHint); 101 }); 102} 103 104function createAgentProxy(domain, client) { 105 const agent = new EventEmitter(); 106 agent.then = (then, _catch) => { 107 // TODO: potentially fetch the protocol and pretty-print it here. 108 const descriptor = { 109 [util.inspect.custom](depth, { stylize }) { 110 return stylize(`[Agent ${domain}]`, 'special'); 111 }, 112 }; 113 return PromisePrototypeThen(PromiseResolve(descriptor), then, _catch); 114 }; 115 116 return new Proxy(agent, { 117 __proto__: null, 118 get(target, name) { 119 if (name in target) return target[name]; 120 return function callVirtualMethod(params) { 121 return client.callMethod(`${domain}.${name}`, params); 122 }; 123 }, 124 }); 125} 126 127class NodeInspector { 128 constructor(options, stdin, stdout) { 129 this.options = options; 130 this.stdin = stdin; 131 this.stdout = stdout; 132 133 this.paused = true; 134 this.child = null; 135 136 if (options.script) { 137 this._runScript = FunctionPrototypeBind( 138 runScript, null, 139 options.script, 140 options.scriptArgs, 141 options.host, 142 options.port, 143 FunctionPrototypeBind(this.childPrint, this)); 144 } else { 145 this._runScript = 146 () => PromiseResolve([null, options.port, options.host]); 147 } 148 149 this.client = new InspectClient(); 150 151 this.domainNames = ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime']; 152 ArrayPrototypeForEach(this.domainNames, (domain) => { 153 this[domain] = createAgentProxy(domain, this.client); 154 }); 155 this.handleDebugEvent = (fullName, params) => { 156 const { 0: domain, 1: name } = StringPrototypeSplit(fullName, '.'); 157 if (domain in this) { 158 this[domain].emit(name, params); 159 } 160 }; 161 this.client.on('debugEvent', this.handleDebugEvent); 162 const startRepl = createRepl(this); 163 164 // Handle all possible exits 165 process.on('exit', () => this.killChild()); 166 const exitCodeZero = () => process.exit(0); 167 process.once('SIGTERM', exitCodeZero); 168 process.once('SIGHUP', exitCodeZero); 169 170 (async () => { 171 try { 172 await this.run(); 173 const repl = await startRepl(); 174 this.repl = repl; 175 this.repl.on('exit', exitCodeZero); 176 this.paused = false; 177 } catch (error) { 178 process.nextTick(() => { throw error; }); 179 } 180 })(); 181 } 182 183 suspendReplWhile(fn) { 184 if (this.repl) { 185 this.repl.pause(); 186 } 187 this.stdin.pause(); 188 this.paused = true; 189 return (async () => { 190 try { 191 await fn(); 192 this.paused = false; 193 if (this.repl) { 194 this.repl.resume(); 195 this.repl.displayPrompt(); 196 } 197 this.stdin.resume(); 198 } catch (error) { 199 process.nextTick(() => { throw error; }); 200 } 201 })(); 202 } 203 204 killChild() { 205 this.client.reset(); 206 if (this.child) { 207 this.child.kill(); 208 this.child = null; 209 } 210 } 211 212 async run() { 213 this.killChild(); 214 215 const { 0: child, 1: port, 2: host } = await this._runScript(); 216 this.child = child; 217 218 this.print(`connecting to ${host}:${port} ..`, false); 219 for (let attempt = 0; attempt < 5; attempt++) { 220 debuglog('connection attempt #%d', attempt); 221 this.stdout.write('.'); 222 try { 223 await this.client.connect(port, host); 224 debuglog('connection established'); 225 this.stdout.write(' ok\n'); 226 return; 227 } catch (error) { 228 debuglog('connect failed', error); 229 await pSetTimeout(1000); 230 } 231 } 232 this.stdout.write(' failed to connect, please retry\n'); 233 process.exit(1); 234 } 235 236 clearLine() { 237 if (this.stdout.isTTY) { 238 this.stdout.cursorTo(0); 239 this.stdout.clearLine(1); 240 } else { 241 this.stdout.write('\b'); 242 } 243 } 244 245 print(text, appendNewline = false) { 246 this.clearLine(); 247 this.stdout.write(appendNewline ? `${text}\n` : text); 248 } 249 250 #stdioBuffers = { stdout: '', stderr: '' }; 251 childPrint(text, which) { 252 const lines = RegExpPrototypeSymbolSplit( 253 /\r\n|\r|\n/g, 254 this.#stdioBuffers[which] + text); 255 256 this.#stdioBuffers[which] = ''; 257 258 if (lines[lines.length - 1] !== '') { 259 this.#stdioBuffers[which] = ArrayPrototypePop(lines); 260 } 261 262 const textToPrint = ArrayPrototypeJoin( 263 ArrayPrototypeMap(lines, (chunk) => `< ${chunk}`), 264 '\n'); 265 266 if (lines.length) { 267 this.print(textToPrint, true); 268 if (!this.paused) { 269 this.repl.displayPrompt(true); 270 } 271 } 272 273 if (StringPrototypeEndsWith( 274 textToPrint, 275 'Waiting for the debugger to disconnect...\n', 276 )) { 277 this.killChild(); 278 } 279 } 280} 281 282function parseArgv(args) { 283 const target = ArrayPrototypeShift(args); 284 let host = '127.0.0.1'; 285 let port = 9229; 286 let isRemote = false; 287 let script = target; 288 let scriptArgs = args; 289 290 const hostMatch = RegExpPrototypeExec(/^([^:]+):(\d+)$/, target); 291 const portMatch = RegExpPrototypeExec(/^--port=(\d+)$/, target); 292 293 if (hostMatch) { 294 // Connecting to remote debugger 295 host = hostMatch[1]; 296 port = Number(hostMatch[2]); 297 isRemote = true; 298 script = null; 299 } else if (portMatch) { 300 // Start on custom port 301 port = Number(portMatch[1]); 302 script = args[0]; 303 scriptArgs = ArrayPrototypeSlice(args, 1); 304 } else if (args.length === 1 && RegExpPrototypeExec(/^\d+$/, args[0]) !== null && 305 target === '-p') { 306 // Start debugger against a given pid 307 const pid = Number(args[0]); 308 try { 309 process._debugProcess(pid); 310 } catch (e) { 311 if (e.code === 'ESRCH') { 312 process.stderr.write(`Target process: ${pid} doesn't exist.\n`); 313 process.exit(1); 314 } 315 throw e; 316 } 317 script = null; 318 isRemote = true; 319 } 320 321 return { 322 host, port, isRemote, script, scriptArgs, 323 }; 324} 325 326function startInspect(argv = ArrayPrototypeSlice(process.argv, 2), 327 stdin = process.stdin, 328 stdout = process.stdout) { 329 if (argv.length < 1) { 330 const invokedAs = `${process.argv0} ${process.argv[1]}`; 331 332 process.stderr.write(`Usage: ${invokedAs} script.js\n` + 333 ` ${invokedAs} <host>:<port>\n` + 334 ` ${invokedAs} --port=<port> Use 0 for random port assignment\n` + 335 ` ${invokedAs} -p <pid>\n`); 336 process.exit(1); 337 } 338 339 const options = parseArgv(argv); 340 const inspector = new NodeInspector(options, stdin, stdout); 341 342 stdin.resume(); 343 344 function handleUnexpectedError(e) { 345 if (e.code !== 'ERR_DEBUGGER_STARTUP_ERROR') { 346 process.stderr.write('There was an internal error in Node.js. ' + 347 'Please report this bug.\n' + 348 `${e.message}\n${e.stack}\n`); 349 } else { 350 process.stderr.write(e.message); 351 process.stderr.write('\n'); 352 } 353 if (inspector.child) inspector.child.kill(); 354 process.exit(1); 355 } 356 357 process.on('uncaughtException', handleUnexpectedError); 358} 359exports.start = startInspect; 360