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