1'use strict'; 2 3const { 4 Array, 5 ArrayFrom, 6 ArrayPrototypeFilter, 7 ArrayPrototypeFind, 8 ArrayPrototypeForEach, 9 ArrayPrototypeIncludes, 10 ArrayPrototypeIndexOf, 11 ArrayPrototypeJoin, 12 ArrayPrototypeMap, 13 ArrayPrototypePush, 14 ArrayPrototypeSlice, 15 ArrayPrototypeSome, 16 ArrayPrototypeSplice, 17 Date, 18 FunctionPrototypeCall, 19 JSONStringify, 20 MathMax, 21 ObjectAssign, 22 ObjectDefineProperty, 23 ObjectKeys, 24 ObjectValues, 25 Promise, 26 PromisePrototypeThen, 27 PromiseResolve, 28 ReflectGetOwnPropertyDescriptor, 29 ReflectOwnKeys, 30 RegExpPrototypeExec, 31 SafeMap, 32 SafePromiseAllReturnArrayLike, 33 SafePromiseAllReturnVoid, 34 String, 35 StringFromCharCode, 36 StringPrototypeEndsWith, 37 StringPrototypeIncludes, 38 StringPrototypeRepeat, 39 StringPrototypeReplaceAll, 40 StringPrototypeSlice, 41 StringPrototypeSplit, 42 StringPrototypeStartsWith, 43 StringPrototypeToUpperCase, 44 StringPrototypeTrim, 45} = primordials; 46 47const { ERR_DEBUGGER_ERROR } = require('internal/errors').codes; 48 49const { validateString, validateNumber } = require('internal/validators'); 50 51const FS = require('fs'); 52const Path = require('path'); 53const Repl = require('repl'); 54const vm = require('vm'); 55const { fileURLToPath } = require('internal/url'); 56 57const { customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace } = require('internal/util'); 58const { inspect: utilInspect } = require('internal/util/inspect'); 59const debuglog = require('internal/util/debuglog').debuglog('inspect'); 60 61const SHORTCUTS = { 62 cont: 'c', 63 next: 'n', 64 step: 's', 65 out: 'o', 66 backtrace: 'bt', 67 setBreakpoint: 'sb', 68 clearBreakpoint: 'cb', 69 run: 'r', 70 exec: 'p', 71}; 72 73const HELP = StringPrototypeTrim(` 74run, restart, r Run the application or reconnect 75kill Kill a running application or disconnect 76 77cont, c Resume execution 78next, n Continue to next line in current file 79step, s Step into, potentially entering a function 80out, o Step out, leaving the current function 81backtrace, bt Print the current backtrace 82list Print the source around the current line where execution 83 is currently paused 84setContextLineNumber Set which lines to check for context 85setBreakpoint, sb Set a breakpoint 86clearBreakpoint, cb Clear a breakpoint 87breakpoints List all known breakpoints 88breakOnException Pause execution whenever an exception is thrown 89breakOnUncaught Pause execution whenever an exception isn't caught 90breakOnNone Don't pause on exceptions (this is the default) 91 92watch(expr) Start watching the given expression 93unwatch(expr) Stop watching an expression 94unwatch(index) Stop watching an expression at specific index from watch list 95watchers Print all watched expressions and their current values 96 97exec(expr), p(expr), exec expr, p expr 98 Evaluate the expression and print the value 99repl Enter a debug repl that works like exec 100 101scripts List application scripts that are currently loaded 102scripts(true) List all scripts (including node-internals) 103 104profile Start CPU profiling session. 105profileEnd Stop current CPU profiling session. 106profiles Array of completed CPU profiling sessions. 107profiles[n].save(filepath = 'node.cpuprofile') 108 Save CPU profiling session to disk as JSON. 109 110takeHeapSnapshot(filepath = 'node.heapsnapshot') 111 Take a heap snapshot and save to disk as JSON. 112`); 113 114const FUNCTION_NAME_PATTERN = /^(?:function\*? )?([^(\s]+)\(/; 115function extractFunctionName(description) { 116 const fnNameMatch = 117 RegExpPrototypeExec(FUNCTION_NAME_PATTERN, description); 118 return fnNameMatch ? `: ${fnNameMatch[1]}` : ''; 119} 120 121const { 122 builtinIds: PUBLIC_BUILTINS, 123} = internalBinding('builtins'); 124const NATIVES = internalBinding('natives'); 125function isNativeUrl(url) { 126 url = SideEffectFreeRegExpPrototypeSymbolReplace(/\.js$/, url, ''); 127 128 return StringPrototypeStartsWith(url, 'node:internal/') || 129 ArrayPrototypeIncludes(PUBLIC_BUILTINS, url) || 130 url in NATIVES || url === 'bootstrap_node'; 131} 132 133function getRelativePath(filenameOrURL) { 134 const dir = StringPrototypeSlice(Path.join(Path.resolve(), 'x'), 0, -1); 135 136 const filename = StringPrototypeStartsWith(filenameOrURL, 'file://') ? 137 fileURLToPath(filenameOrURL) : filenameOrURL; 138 139 // Change path to relative, if possible 140 if (StringPrototypeStartsWith(filename, dir)) { 141 return StringPrototypeSlice(filename, dir.length); 142 } 143 return filename; 144} 145 146// Adds spaces and prefix to number 147// maxN is a maximum number we should have space for 148function leftPad(n, prefix, maxN) { 149 const s = n.toString(); 150 const nchars = MathMax(2, String(maxN).length); 151 const nspaces = nchars - s.length; 152 153 return prefix + StringPrototypeRepeat(' ', nspaces) + s; 154} 155 156function markSourceColumn(sourceText, position, useColors) { 157 if (!sourceText) return ''; 158 159 const head = StringPrototypeSlice(sourceText, 0, position); 160 let tail = StringPrototypeSlice(sourceText, position); 161 162 // Colourize char if stdout supports colours 163 if (useColors) { 164 tail = SideEffectFreeRegExpPrototypeSymbolReplace(/(.+?)([^\w]|$)/, tail, 165 '\u001b[32m$1\u001b[39m$2'); 166 } 167 168 // Return source line with coloured char at `position` 169 return head + tail; 170} 171 172function extractErrorMessage(stack) { 173 if (!stack) return '<unknown>'; 174 const m = RegExpPrototypeExec(/^\w+: ([^\n]+)/, stack); 175 return m?.[1] ?? stack; 176} 177 178function convertResultToError(result) { 179 const { className, description } = result; 180 const err = new ERR_DEBUGGER_ERROR(extractErrorMessage(description)); 181 err.stack = description; 182 ObjectDefineProperty(err, 'name', { __proto__: null, value: className }); 183 return err; 184} 185 186class PropertyPreview { 187 constructor(attributes) { 188 ObjectAssign(this, attributes); 189 } 190 191 [customInspectSymbol](depth, opts) { 192 switch (this.type) { 193 case 'string': 194 case 'undefined': 195 return utilInspect(this.value, opts); 196 case 'number': 197 case 'boolean': 198 return opts.stylize(this.value, this.type); 199 case 'object': 200 case 'symbol': 201 if (this.subtype === 'date') { 202 return utilInspect(new Date(this.value), opts); 203 } 204 if (this.subtype === 'array') { 205 return opts.stylize(this.value, 'special'); 206 } 207 return opts.stylize(this.value, this.subtype || 'special'); 208 default: 209 return this.value; 210 } 211 } 212} 213 214class ObjectPreview { 215 constructor(attributes) { 216 ObjectAssign(this, attributes); 217 } 218 219 [customInspectSymbol](depth, opts) { 220 switch (this.type) { 221 case 'object': { 222 switch (this.subtype) { 223 case 'date': 224 return utilInspect(new Date(this.description), opts); 225 case 'null': 226 return utilInspect(null, opts); 227 case 'regexp': 228 return opts.stylize(this.description, 'regexp'); 229 case 'set': { 230 if (!this.entries) { 231 return `${this.description} ${this.overflow ? '{ ... }' : '{}'}`; 232 } 233 const values = ArrayPrototypeMap(this.entries, (entry) => 234 utilInspect(new ObjectPreview(entry.value), opts)); 235 return `${this.description} { ${ArrayPrototypeJoin(values, ', ')} }`; 236 } 237 case 'map': { 238 if (!this.entries) { 239 return `${this.description} ${this.overflow ? '{ ... }' : '{}'}`; 240 } 241 const mappings = ArrayPrototypeMap(this.entries, (entry) => { 242 const key = utilInspect(new ObjectPreview(entry.key), opts); 243 const value = utilInspect(new ObjectPreview(entry.value), opts); 244 return `${key} => ${value}`; 245 }); 246 return `${this.description} { ${ArrayPrototypeJoin(mappings, ', ')} }`; 247 } 248 case 'array': 249 case undefined: { 250 if (this.properties.length === 0) { 251 return this.subtype === 'array' ? '[]' : '{}'; 252 } 253 const props = ArrayPrototypeMap(this.properties, (prop, idx) => { 254 const value = utilInspect(new PropertyPreview(prop)); 255 if (prop.name === `${idx}`) return value; 256 return `${prop.name}: ${value}`; 257 }); 258 if (this.overflow) { 259 ArrayPrototypePush(props, '...'); 260 } 261 const singleLine = ArrayPrototypeJoin(props, ', '); 262 const propString = singleLine.length > 60 ? ArrayPrototypeJoin(props, ',\n ') : singleLine; 263 return this.subtype === 'array' ? `[ ${propString} ]` : `{ ${propString} }`; 264 } 265 default: 266 return this.description; 267 } 268 } 269 default: 270 return this.description; 271 } 272 } 273} 274 275class RemoteObject { 276 constructor(attributes) { 277 ObjectAssign(this, attributes); 278 if (this.type === 'number') { 279 this.value = 280 this.unserializableValue ? +this.unserializableValue : +this.value; 281 } 282 } 283 284 [customInspectSymbol](depth, opts) { 285 switch (this.type) { 286 case 'boolean': 287 case 'number': 288 case 'string': 289 case 'undefined': 290 return utilInspect(this.value, opts); 291 case 'symbol': 292 return opts.stylize(this.description, 'special'); 293 case 'function': { 294 const fnName = extractFunctionName(this.description); 295 const formatted = `[${this.className}${fnName}]`; 296 return opts.stylize(formatted, 'special'); 297 } 298 case 'object': 299 switch (this.subtype) { 300 case 'date': 301 return utilInspect(new Date(this.description), opts); 302 case 'null': 303 return utilInspect(null, opts); 304 case 'regexp': 305 return opts.stylize(this.description, 'regexp'); 306 case 'map': 307 case 'set': { 308 const preview = utilInspect(new ObjectPreview(this.preview), opts); 309 return `${this.description} ${preview}`; 310 } 311 default: 312 break; 313 } 314 if (this.preview) { 315 return utilInspect(new ObjectPreview(this.preview), opts); 316 } 317 return this.description; 318 default: 319 return this.description; 320 } 321 } 322 323 static fromEvalResult({ result, wasThrown }) { 324 if (wasThrown) return convertResultToError(result); 325 return new RemoteObject(result); 326 } 327} 328 329class ScopeSnapshot { 330 constructor(scope, properties) { 331 ObjectAssign(this, scope); 332 this.properties = new SafeMap(); 333 this.completionGroup = ArrayPrototypeMap(properties, (prop) => { 334 const value = new RemoteObject(prop.value); 335 this.properties.set(prop.name, value); 336 return prop.name; 337 }); 338 } 339 340 [customInspectSymbol](depth, opts) { 341 const type = StringPrototypeToUpperCase(this.type[0]) + 342 StringPrototypeSlice(this.type, 1); 343 const name = this.name ? `<${this.name}>` : ''; 344 const prefix = `${type}${name} `; 345 return SideEffectFreeRegExpPrototypeSymbolReplace(/^Map /, 346 utilInspect(this.properties, opts), 347 prefix); 348 } 349} 350 351function copyOwnProperties(target, source) { 352 ArrayPrototypeForEach( 353 ReflectOwnKeys(source), 354 (prop) => { 355 const desc = ReflectGetOwnPropertyDescriptor(source, prop); 356 ObjectDefineProperty(target, prop, desc); 357 }); 358} 359 360function aliasProperties(target, mapping) { 361 ArrayPrototypeForEach(ObjectKeys(mapping), (key) => { 362 const desc = ReflectGetOwnPropertyDescriptor(target, key); 363 ObjectDefineProperty(target, mapping[key], desc); 364 }); 365} 366 367function createRepl(inspector) { 368 const { Debugger, HeapProfiler, Profiler, Runtime } = inspector; 369 370 let repl; 371 372 // Things we want to keep around 373 const history = { control: [], debug: [] }; 374 const watchedExpressions = []; 375 const knownBreakpoints = []; 376 let heapSnapshotPromise = null; 377 let pauseOnExceptionState = 'none'; 378 let lastCommand; 379 380 // Things we need to reset when the app restarts 381 let knownScripts; 382 let currentBacktrace; 383 let selectedFrame; 384 let exitDebugRepl; 385 let contextLineNumber = 2; 386 387 function resetOnStart() { 388 knownScripts = {}; 389 currentBacktrace = null; 390 selectedFrame = null; 391 392 if (exitDebugRepl) exitDebugRepl(); 393 exitDebugRepl = null; 394 } 395 resetOnStart(); 396 397 const INSPECT_OPTIONS = { colors: inspector.stdout.isTTY }; 398 function inspect(value) { 399 return utilInspect(value, INSPECT_OPTIONS); 400 } 401 402 function print(value, addNewline = true) { 403 const text = typeof value === 'string' ? value : inspect(value); 404 return inspector.print(text, addNewline); 405 } 406 407 function getCurrentLocation() { 408 if (!selectedFrame) { 409 throw new ERR_DEBUGGER_ERROR('Requires execution to be paused'); 410 } 411 return selectedFrame.location; 412 } 413 414 function isCurrentScript(script) { 415 return selectedFrame && getCurrentLocation().scriptId === script.scriptId; 416 } 417 418 function formatScripts(displayNatives = false) { 419 function isVisible(script) { 420 if (displayNatives) return true; 421 return !script.isNative || isCurrentScript(script); 422 } 423 424 return ArrayPrototypeJoin(ArrayPrototypeMap( 425 ArrayPrototypeFilter(ObjectValues(knownScripts), isVisible), 426 (script) => { 427 const isCurrent = isCurrentScript(script); 428 const { isNative, url } = script; 429 const name = `${getRelativePath(url)}${isNative ? ' <native>' : ''}`; 430 return `${isCurrent ? '*' : ' '} ${script.scriptId}: ${name}`; 431 }), '\n'); 432 } 433 434 function listScripts(displayNatives = false) { 435 print(formatScripts(displayNatives)); 436 } 437 listScripts[customInspectSymbol] = function listWithoutInternal() { 438 return formatScripts(); 439 }; 440 441 const profiles = []; 442 class Profile { 443 constructor(data) { 444 this.data = data; 445 } 446 447 static createAndRegister({ profile }) { 448 const p = new Profile(profile); 449 ArrayPrototypePush(profiles, p); 450 return p; 451 } 452 453 [customInspectSymbol](depth, { stylize }) { 454 const { startTime, endTime } = this.data; 455 const MU = StringFromCharCode(956); 456 return stylize(`[Profile ${endTime - startTime}${MU}s]`, 'special'); 457 } 458 459 save(filename = 'node.cpuprofile') { 460 const absoluteFile = Path.resolve(filename); 461 const json = JSONStringify(this.data); 462 FS.writeFileSync(absoluteFile, json); 463 print('Saved profile to ' + absoluteFile); 464 } 465 } 466 467 class SourceSnippet { 468 constructor(location, delta, scriptSource) { 469 ObjectAssign(this, location); 470 this.scriptSource = scriptSource; 471 this.delta = delta; 472 } 473 474 [customInspectSymbol](depth, options) { 475 const { scriptId, lineNumber, columnNumber, delta, scriptSource } = this; 476 const start = MathMax(1, lineNumber - delta + 1); 477 const end = lineNumber + delta + 1; 478 479 const lines = StringPrototypeSplit(scriptSource, '\n'); 480 return ArrayPrototypeJoin( 481 ArrayPrototypeMap( 482 ArrayPrototypeSlice(lines, start - 1, end), 483 (lineText, offset) => { 484 const i = start + offset; 485 const isCurrent = i === (lineNumber + 1); 486 487 const markedLine = isCurrent ? 488 markSourceColumn(lineText, columnNumber, options.colors) : 489 lineText; 490 491 let isBreakpoint = false; 492 ArrayPrototypeForEach(knownBreakpoints, ({ location }) => { 493 if (!location) return; 494 if (scriptId === location.scriptId && 495 i === (location.lineNumber + 1)) { 496 isBreakpoint = true; 497 } 498 }); 499 500 let prefixChar = ' '; 501 if (isCurrent) { 502 prefixChar = '>'; 503 } else if (isBreakpoint) { 504 prefixChar = '*'; 505 } 506 return `${leftPad(i, prefixChar, end)} ${markedLine}`; 507 }), '\n'); 508 } 509 } 510 511 async function getSourceSnippet(location, delta = 5) { 512 const { scriptId } = location; 513 const { scriptSource } = await Debugger.getScriptSource({ scriptId }); 514 return new SourceSnippet(location, delta, scriptSource); 515 } 516 517 class CallFrame { 518 constructor(callFrame) { 519 ObjectAssign(this, callFrame); 520 } 521 522 loadScopes() { 523 return SafePromiseAllReturnArrayLike( 524 ArrayPrototypeFilter( 525 this.scopeChain, 526 (scope) => scope.type !== 'global', 527 ), 528 async (scope) => { 529 const { objectId } = scope.object; 530 const { result } = await Runtime.getProperties({ 531 objectId, 532 generatePreview: true, 533 }); 534 return new ScopeSnapshot(scope, result); 535 }); 536 } 537 538 list(delta = 5) { 539 return getSourceSnippet(this.location, delta); 540 } 541 } 542 543 class Backtrace extends Array { 544 [customInspectSymbol]() { 545 return ArrayPrototypeJoin( 546 ArrayPrototypeMap(this, (callFrame, idx) => { 547 const { 548 location: { scriptId, lineNumber, columnNumber }, 549 functionName, 550 } = callFrame; 551 const name = functionName || '(anonymous)'; 552 553 const script = knownScripts[scriptId]; 554 const relativeUrl = 555 (script && getRelativePath(script.url)) || '<unknown>'; 556 const frameLocation = 557 `${relativeUrl}:${lineNumber + 1}:${columnNumber}`; 558 559 return `#${idx} ${name} ${frameLocation}`; 560 }), '\n'); 561 } 562 563 static from(callFrames) { 564 return FunctionPrototypeCall( 565 ArrayFrom, 566 this, 567 callFrames, 568 (callFrame) => 569 (callFrame instanceof CallFrame ? 570 callFrame : 571 new CallFrame(callFrame)), 572 ); 573 } 574 } 575 576 function prepareControlCode(input) { 577 if (input === '\n') return lastCommand; 578 // Add parentheses: exec process.title => exec("process.title"); 579 const match = RegExpPrototypeExec(/^\s*(?:exec|p)\s+([^\n]*)/, input); 580 if (match) { 581 lastCommand = `exec(${JSONStringify(match[1])})`; 582 } else { 583 lastCommand = input; 584 } 585 return lastCommand; 586 } 587 588 async function evalInCurrentContext(code) { 589 // Repl asked for scope variables 590 if (code === '.scope') { 591 if (!selectedFrame) { 592 throw new ERR_DEBUGGER_ERROR('Requires execution to be paused'); 593 } 594 const scopes = await selectedFrame.loadScopes(); 595 return ArrayPrototypeMap(scopes, (scope) => scope.completionGroup); 596 } 597 598 if (selectedFrame) { 599 return PromisePrototypeThen(Debugger.evaluateOnCallFrame({ 600 callFrameId: selectedFrame.callFrameId, 601 expression: code, 602 objectGroup: 'node-inspect', 603 generatePreview: true, 604 }), RemoteObject.fromEvalResult); 605 } 606 return PromisePrototypeThen(Runtime.evaluate({ 607 expression: code, 608 objectGroup: 'node-inspect', 609 generatePreview: true, 610 }), RemoteObject.fromEvalResult); 611 } 612 613 function controlEval(input, context, filename, callback) { 614 debuglog('eval:', input); 615 function returnToCallback(error, result) { 616 debuglog('end-eval:', input, error); 617 callback(error, result); 618 } 619 620 try { 621 const code = prepareControlCode(input); 622 const result = vm.runInContext(code, context, filename); 623 624 const then = result?.then; 625 if (typeof then === 'function') { 626 FunctionPrototypeCall( 627 then, result, 628 (result) => returnToCallback(null, result), 629 returnToCallback, 630 ); 631 } else { 632 returnToCallback(null, result); 633 } 634 } catch (e) { 635 returnToCallback(e); 636 } 637 } 638 639 function debugEval(input, context, filename, callback) { 640 debuglog('eval:', input); 641 function returnToCallback(error, result) { 642 debuglog('end-eval:', input, error); 643 callback(error, result); 644 } 645 646 PromisePrototypeThen(evalInCurrentContext(input), 647 (result) => returnToCallback(null, result), 648 returnToCallback, 649 ); 650 } 651 652 async function formatWatchers(verbose = false) { 653 if (!watchedExpressions.length) { 654 return ''; 655 } 656 657 const inspectValue = (expr) => 658 PromisePrototypeThen(evalInCurrentContext(expr), undefined, 659 (error) => `<${error.message}>`); 660 const lastIndex = watchedExpressions.length - 1; 661 662 const values = await SafePromiseAllReturnArrayLike(watchedExpressions, inspectValue); 663 const lines = ArrayPrototypeMap(watchedExpressions, (expr, idx) => { 664 const prefix = `${leftPad(idx, ' ', lastIndex)}: ${expr} =`; 665 const value = inspect(values[idx]); 666 if (!StringPrototypeIncludes(value, '\n')) { 667 return `${prefix} ${value}`; 668 } 669 return `${prefix}\n ${StringPrototypeReplaceAll(value, '\n', '\n ')}`; 670 }); 671 const valueList = ArrayPrototypeJoin(lines, '\n'); 672 return verbose ? `Watchers:\n${valueList}\n` : valueList; 673 } 674 675 function watchers(verbose = false) { 676 return PromisePrototypeThen(formatWatchers(verbose), print); 677 } 678 679 // List source code 680 function list(delta = 5) { 681 if (!selectedFrame) { 682 throw new ERR_DEBUGGER_ERROR('Requires execution to be paused'); 683 } 684 return selectedFrame.list(delta).then(null, (error) => { 685 print("You can't list source code right now"); 686 throw error; 687 }); 688 } 689 690 function setContextLineNumber(delta = 2) { 691 if (!selectedFrame) { 692 throw new ERR_DEBUGGER_ERROR('Requires execution to be paused'); 693 } 694 validateNumber(delta, 'delta', 1); 695 contextLineNumber = delta; 696 print(`The contextLine has been changed to ${delta}.`); 697 } 698 699 function handleBreakpointResolved({ breakpointId, location }) { 700 const script = knownScripts[location.scriptId]; 701 const scriptUrl = script && script.url; 702 if (scriptUrl) { 703 ObjectAssign(location, { scriptUrl }); 704 } 705 const isExisting = ArrayPrototypeSome(knownBreakpoints, (bp) => { 706 if (bp.breakpointId === breakpointId) { 707 ObjectAssign(bp, { location }); 708 return true; 709 } 710 return false; 711 }); 712 if (!isExisting) { 713 ArrayPrototypePush(knownBreakpoints, { breakpointId, location }); 714 } 715 } 716 717 function listBreakpoints() { 718 if (!knownBreakpoints.length) { 719 print('No breakpoints yet'); 720 return; 721 } 722 723 function formatLocation(location) { 724 if (!location) return '<unknown location>'; 725 const script = knownScripts[location.scriptId]; 726 const scriptUrl = script ? script.url : location.scriptUrl; 727 return `${getRelativePath(scriptUrl)}:${location.lineNumber + 1}`; 728 } 729 const breaklist = ArrayPrototypeJoin( 730 ArrayPrototypeMap( 731 knownBreakpoints, 732 (bp, idx) => `#${idx} ${formatLocation(bp.location)}`), 733 '\n'); 734 print(breaklist); 735 } 736 737 function setBreakpoint(script, line, condition, silent) { 738 function registerBreakpoint({ breakpointId, actualLocation }) { 739 handleBreakpointResolved({ breakpointId, location: actualLocation }); 740 if (actualLocation && actualLocation.scriptId) { 741 if (!silent) return getSourceSnippet(actualLocation, 5); 742 } else { 743 print(`Warning: script '${script}' was not loaded yet.`); 744 } 745 return undefined; 746 } 747 748 // setBreakpoint(): set breakpoint at current location 749 if (script === undefined) { 750 return PromisePrototypeThen( 751 Debugger.setBreakpoint({ location: getCurrentLocation(), condition }), 752 registerBreakpoint); 753 } 754 755 // setBreakpoint(line): set breakpoint in current script at specific line 756 if (line === undefined && typeof script === 'number') { 757 const location = { 758 scriptId: getCurrentLocation().scriptId, 759 lineNumber: script - 1, 760 }; 761 return PromisePrototypeThen( 762 Debugger.setBreakpoint({ location, condition }), 763 registerBreakpoint); 764 } 765 766 validateString(script, 'script'); 767 768 // setBreakpoint('fn()'): Break when a function is called 769 if (StringPrototypeEndsWith(script, '()')) { 770 const debugExpr = `debug(${script.slice(0, -2)})`; 771 const debugCall = selectedFrame ? 772 Debugger.evaluateOnCallFrame({ 773 callFrameId: selectedFrame.callFrameId, 774 expression: debugExpr, 775 includeCommandLineAPI: true, 776 }) : 777 Runtime.evaluate({ 778 expression: debugExpr, 779 includeCommandLineAPI: true, 780 }); 781 return PromisePrototypeThen(debugCall, ({ result, wasThrown }) => { 782 if (wasThrown) return convertResultToError(result); 783 return undefined; // This breakpoint can't be removed the same way 784 }); 785 } 786 787 // setBreakpoint('scriptname') 788 let scriptId = null; 789 let ambiguous = false; 790 if (knownScripts[script]) { 791 scriptId = script; 792 } else { 793 ArrayPrototypeForEach(ObjectKeys(knownScripts), (id) => { 794 const scriptUrl = knownScripts[id].url; 795 if (scriptUrl && StringPrototypeIncludes(scriptUrl, script)) { 796 if (scriptId !== null) { 797 ambiguous = true; 798 } 799 scriptId = id; 800 } 801 }); 802 } 803 804 if (ambiguous) { 805 print('Script name is ambiguous'); 806 return undefined; 807 } 808 if (line <= 0) { 809 print('Line should be a positive value'); 810 return undefined; 811 } 812 813 if (scriptId !== null) { 814 const location = { scriptId, lineNumber: line - 1 }; 815 return PromisePrototypeThen( 816 Debugger.setBreakpoint({ location, condition }), 817 registerBreakpoint); 818 } 819 820 const escapedPath = SideEffectFreeRegExpPrototypeSymbolReplace(/([/\\.?*()^${}|[\]])/g, 821 script, '\\$1'); 822 const urlRegex = `^(.*[\\/\\\\])?${escapedPath}$`; 823 824 return PromisePrototypeThen( 825 Debugger.setBreakpointByUrl({ 826 urlRegex, 827 lineNumber: line - 1, 828 condition, 829 }), 830 (bp) => { 831 // TODO: handle bp.locations in case the regex matches existing files 832 if (!bp.location) { // Fake it for now. 833 ObjectAssign(bp, { 834 actualLocation: { 835 scriptUrl: `.*/${script}$`, 836 lineNumber: line - 1, 837 }, 838 }); 839 } 840 return registerBreakpoint(bp); 841 }); 842 } 843 844 function clearBreakpoint(url, line) { 845 const breakpoint = ArrayPrototypeFind(knownBreakpoints, ({ location }) => { 846 if (!location) return false; 847 const script = knownScripts[location.scriptId]; 848 if (!script) return false; 849 return ( 850 StringPrototypeIncludes(script.url, url) && 851 (location.lineNumber + 1) === line 852 ); 853 }); 854 if (!breakpoint) { 855 print(`Could not find breakpoint at ${url}:${line}`); 856 return PromiseResolve(); 857 } 858 return PromisePrototypeThen( 859 Debugger.removeBreakpoint({ breakpointId: breakpoint.breakpointId }), 860 () => { 861 const idx = ArrayPrototypeIndexOf(knownBreakpoints, breakpoint); 862 ArrayPrototypeSplice(knownBreakpoints, idx, 1); 863 }); 864 } 865 866 function restoreBreakpoints() { 867 const lastBreakpoints = ArrayPrototypeSplice(knownBreakpoints, 0); 868 const newBreakpoints = ArrayPrototypeMap( 869 ArrayPrototypeFilter(lastBreakpoints, 870 ({ location }) => !!location.scriptUrl), 871 ({ location }) => setBreakpoint(location.scriptUrl, 872 location.lineNumber + 1)); 873 if (!newBreakpoints.length) return PromiseResolve(); 874 return PromisePrototypeThen( 875 SafePromiseAllReturnVoid(newBreakpoints), 876 () => { 877 print(`${newBreakpoints.length} breakpoints restored.`); 878 }); 879 } 880 881 function setPauseOnExceptions(state) { 882 return PromisePrototypeThen( 883 Debugger.setPauseOnExceptions({ state }), 884 () => { 885 pauseOnExceptionState = state; 886 }); 887 } 888 889 Debugger.on('paused', ({ callFrames, reason /* , hitBreakpoints */ }) => { 890 if (process.env.NODE_INSPECT_RESUME_ON_START === '1' && 891 reason === 'Break on start') { 892 debuglog('Paused on start, but NODE_INSPECT_RESUME_ON_START' + 893 ' environment variable is set to 1, resuming'); 894 inspector.client.callMethod('Debugger.resume'); 895 return; 896 } 897 898 // Save execution context's data 899 currentBacktrace = Backtrace.from(callFrames); 900 selectedFrame = currentBacktrace[0]; 901 const { scriptId, lineNumber } = selectedFrame.location; 902 903 const breakType = reason === 'other' ? 'break' : reason; 904 const script = knownScripts[scriptId]; 905 const scriptUrl = script ? getRelativePath(script.url) : '[unknown]'; 906 907 const header = `${breakType} in ${scriptUrl}:${lineNumber + 1}`; 908 909 inspector.suspendReplWhile(() => 910 PromisePrototypeThen( 911 SafePromiseAllReturnArrayLike([formatWatchers(true), selectedFrame.list(contextLineNumber)]), 912 ({ 0: watcherList, 1: context }) => { 913 const breakContext = watcherList ? 914 `${watcherList}\n${inspect(context)}` : 915 inspect(context); 916 print(`${header}\n${breakContext}`); 917 })); 918 }); 919 920 function handleResumed() { 921 currentBacktrace = null; 922 selectedFrame = null; 923 } 924 925 Debugger.on('resumed', handleResumed); 926 927 Debugger.on('breakpointResolved', handleBreakpointResolved); 928 929 Debugger.on('scriptParsed', (script) => { 930 const { scriptId, url } = script; 931 if (url) { 932 knownScripts[scriptId] = { isNative: isNativeUrl(url), ...script }; 933 } 934 }); 935 936 Profiler.on('consoleProfileFinished', ({ profile }) => { 937 Profile.createAndRegister({ profile }); 938 print( 939 'Captured new CPU profile.\n' + 940 `Access it with profiles[${profiles.length - 1}]`, 941 ); 942 }); 943 944 function initializeContext(context) { 945 ArrayPrototypeForEach(inspector.domainNames, (domain) => { 946 ObjectDefineProperty(context, domain, { 947 __proto__: null, 948 value: inspector[domain], 949 enumerable: true, 950 configurable: true, 951 writeable: false, 952 }); 953 }); 954 955 copyOwnProperties(context, { 956 get help() { 957 return print(HELP); 958 }, 959 960 get run() { 961 return inspector.run(); 962 }, 963 964 get kill() { 965 return inspector.killChild(); 966 }, 967 968 get restart() { 969 return inspector.run(); 970 }, 971 972 get cont() { 973 handleResumed(); 974 return Debugger.resume(); 975 }, 976 977 get next() { 978 handleResumed(); 979 return Debugger.stepOver(); 980 }, 981 982 get step() { 983 handleResumed(); 984 return Debugger.stepInto(); 985 }, 986 987 get out() { 988 handleResumed(); 989 return Debugger.stepOut(); 990 }, 991 992 get pause() { 993 return Debugger.pause(); 994 }, 995 996 get backtrace() { 997 return currentBacktrace; 998 }, 999 1000 get breakpoints() { 1001 return listBreakpoints(); 1002 }, 1003 1004 exec(expr) { 1005 return evalInCurrentContext(expr); 1006 }, 1007 1008 get profile() { 1009 return Profiler.start(); 1010 }, 1011 1012 get profileEnd() { 1013 return PromisePrototypeThen(Profiler.stop(), 1014 Profile.createAndRegister); 1015 }, 1016 1017 get profiles() { 1018 return profiles; 1019 }, 1020 1021 takeHeapSnapshot(filename = 'node.heapsnapshot') { 1022 if (heapSnapshotPromise) { 1023 print( 1024 'Cannot take heap snapshot because another snapshot is in progress.', 1025 ); 1026 return heapSnapshotPromise; 1027 } 1028 heapSnapshotPromise = new Promise((resolve, reject) => { 1029 const absoluteFile = Path.resolve(filename); 1030 const writer = FS.createWriteStream(absoluteFile); 1031 let sizeWritten = 0; 1032 function onProgress({ done, total, finished }) { 1033 if (finished) { 1034 print('Heap snapshot prepared.'); 1035 } else { 1036 print(`Heap snapshot: ${done}/${total}`, false); 1037 } 1038 } 1039 1040 function onChunk({ chunk }) { 1041 sizeWritten += chunk.length; 1042 writer.write(chunk); 1043 print(`Writing snapshot: ${sizeWritten}`, false); 1044 } 1045 1046 function onResolve() { 1047 writer.end(() => { 1048 teardown(); 1049 print(`Wrote snapshot: ${absoluteFile}`); 1050 heapSnapshotPromise = null; 1051 resolve(); 1052 }); 1053 } 1054 1055 function onReject(error) { 1056 teardown(); 1057 reject(error); 1058 } 1059 1060 function teardown() { 1061 HeapProfiler.removeListener( 1062 'reportHeapSnapshotProgress', onProgress); 1063 HeapProfiler.removeListener('addHeapSnapshotChunk', onChunk); 1064 } 1065 1066 HeapProfiler.on('reportHeapSnapshotProgress', onProgress); 1067 HeapProfiler.on('addHeapSnapshotChunk', onChunk); 1068 1069 print('Heap snapshot: 0/0', false); 1070 PromisePrototypeThen( 1071 HeapProfiler.takeHeapSnapshot({ reportProgress: true }), 1072 onResolve, onReject); 1073 }); 1074 return heapSnapshotPromise; 1075 }, 1076 1077 get watchers() { 1078 return watchers(); 1079 }, 1080 1081 watch(expr) { 1082 validateString(expr, 'expression'); 1083 ArrayPrototypePush(watchedExpressions, expr); 1084 }, 1085 1086 unwatch(expr) { 1087 const index = ArrayPrototypeIndexOf(watchedExpressions, expr); 1088 1089 // Unwatch by expression 1090 // or 1091 // Unwatch by watcher number 1092 ArrayPrototypeSplice(watchedExpressions, 1093 index !== -1 ? index : +expr, 1); 1094 }, 1095 1096 get repl() { 1097 // Don't display any default messages 1098 const listeners = ArrayPrototypeSlice(repl.listeners('SIGINT')); 1099 repl.removeAllListeners('SIGINT'); 1100 1101 const oldContext = repl.context; 1102 1103 exitDebugRepl = () => { 1104 // Restore all listeners 1105 process.nextTick(() => { 1106 ArrayPrototypeForEach(listeners, (listener) => { 1107 repl.on('SIGINT', listener); 1108 }); 1109 }); 1110 1111 // Exit debug repl 1112 repl.eval = controlEval; 1113 1114 // Swap history 1115 history.debug = repl.history; 1116 repl.history = history.control; 1117 1118 repl.context = oldContext; 1119 repl.setPrompt('debug> '); 1120 repl.displayPrompt(); 1121 1122 repl.removeListener('SIGINT', exitDebugRepl); 1123 repl.removeListener('exit', exitDebugRepl); 1124 1125 exitDebugRepl = null; 1126 }; 1127 1128 // Exit debug repl on SIGINT 1129 repl.on('SIGINT', exitDebugRepl); 1130 1131 // Exit debug repl on repl exit 1132 repl.on('exit', exitDebugRepl); 1133 1134 // Set new 1135 repl.eval = debugEval; 1136 repl.context = {}; 1137 1138 // Swap history 1139 history.control = repl.history; 1140 repl.history = history.debug; 1141 1142 repl.setPrompt('> '); 1143 1144 print('Press Ctrl+C to leave debug repl'); 1145 return repl.displayPrompt(); 1146 }, 1147 1148 get version() { 1149 return PromisePrototypeThen(Runtime.evaluate({ 1150 expression: 'process.versions.v8', 1151 contextId: 1, 1152 returnByValue: true, 1153 }), ({ result }) => { 1154 print(result.value); 1155 }); 1156 }, 1157 1158 scripts: listScripts, 1159 1160 setBreakpoint, 1161 clearBreakpoint, 1162 setPauseOnExceptions, 1163 get breakOnException() { 1164 return setPauseOnExceptions('all'); 1165 }, 1166 get breakOnUncaught() { 1167 return setPauseOnExceptions('uncaught'); 1168 }, 1169 get breakOnNone() { 1170 return setPauseOnExceptions('none'); 1171 }, 1172 1173 list, 1174 setContextLineNumber, 1175 }); 1176 aliasProperties(context, SHORTCUTS); 1177 } 1178 1179 async function initAfterStart() { 1180 await Runtime.enable(); 1181 await Profiler.enable(); 1182 await Profiler.setSamplingInterval({ interval: 100 }); 1183 await Debugger.enable(); 1184 await Debugger.setAsyncCallStackDepth({ maxDepth: 0 }); 1185 await Debugger.setBlackboxPatterns({ patterns: [] }); 1186 await Debugger.setPauseOnExceptions({ state: pauseOnExceptionState }); 1187 await restoreBreakpoints(); 1188 return Runtime.runIfWaitingForDebugger(); 1189 } 1190 1191 return async function startRepl() { 1192 inspector.client.on('close', () => { 1193 resetOnStart(); 1194 }); 1195 inspector.client.on('ready', () => { 1196 initAfterStart(); 1197 }); 1198 1199 // Init once for the initial connection 1200 await initAfterStart(); 1201 1202 const replOptions = { 1203 prompt: 'debug> ', 1204 input: inspector.stdin, 1205 output: inspector.stdout, 1206 eval: controlEval, 1207 useGlobal: false, 1208 ignoreUndefined: true, 1209 }; 1210 1211 repl = Repl.start(replOptions); 1212 initializeContext(repl.context); 1213 repl.on('reset', initializeContext); 1214 1215 repl.defineCommand('interrupt', () => { 1216 // We want this for testing purposes where sending Ctrl+C can be tricky. 1217 repl.emit('SIGINT'); 1218 }); 1219 1220 return repl; 1221 }; 1222} 1223module.exports = createRepl; 1224