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