• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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