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