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