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