• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypeConcat,
5  ArrayPrototypeForEach,
6  ArrayPrototypeJoin,
7  ArrayPrototypeMap,
8  ArrayPrototypePop,
9  ArrayPrototypeShift,
10  ArrayPrototypeSlice,
11  FunctionPrototypeBind,
12  Number,
13  Promise,
14  PromisePrototypeCatch,
15  PromisePrototypeThen,
16  PromiseResolve,
17  Proxy,
18  RegExpPrototypeSymbolMatch,
19  RegExpPrototypeSymbolSplit,
20  RegExpPrototypeTest,
21  StringPrototypeEndsWith,
22  StringPrototypeSplit,
23} = primordials;
24
25const { spawn } = require('child_process');
26const { EventEmitter } = require('events');
27const net = require('net');
28const util = require('util');
29const {
30  AbortController,
31} = require('internal/abort_controller');
32
33const pSetTimeout = util.promisify(require('timers').setTimeout);
34async function* pSetInterval(delay) {
35  while (true) {
36    await pSetTimeout(delay);
37    yield;
38  }
39}
40
41// TODO(aduh95): remove console calls
42const console = require('internal/console/global');
43
44const { 0: InspectClient, 1: createRepl } =
45    [
46      require('internal/debugger/inspect_client'),
47      require('internal/debugger/inspect_repl'),
48    ];
49
50const debuglog = util.debuglog('inspect');
51
52const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes;
53
54async function portIsFree(host, port, timeout = 9999) {
55  if (port === 0) return; // Binding to a random port.
56
57  const retryDelay = 150;
58  const ac = new AbortController();
59  const { signal } = ac;
60
61  pSetTimeout(timeout).then(() => ac.abort());
62
63  const asyncIterator = pSetInterval(retryDelay);
64  while (true) {
65    await asyncIterator.next();
66    if (signal.aborted) {
67      throw new ERR_DEBUGGER_STARTUP_ERROR(
68        `Timeout (${timeout}) waiting for ${host}:${port} to be free`);
69    }
70    const error = await new Promise((resolve) => {
71      const socket = net.connect(port, host);
72      socket.on('error', resolve);
73      socket.on('connect', resolve);
74    });
75    if (error?.code === 'ECONNREFUSED') {
76      return;
77    }
78  }
79}
80
81const debugRegex = /Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\//;
82async function runScript(script, scriptArgs, inspectHost, inspectPort,
83                         childPrint) {
84  await portIsFree(inspectHost, inspectPort);
85  const args = ArrayPrototypeConcat(
86    [`--inspect-brk=${inspectPort}`, script],
87    scriptArgs);
88  const child = spawn(process.execPath, args);
89  child.stdout.setEncoding('utf8');
90  child.stderr.setEncoding('utf8');
91  child.stdout.on('data', (chunk) => childPrint(chunk, 'stdout'));
92  child.stderr.on('data', (chunk) => childPrint(chunk, 'stderr'));
93
94  let output = '';
95  return new Promise((resolve) => {
96    function waitForListenHint(text) {
97      output += text;
98      const debug = RegExpPrototypeSymbolMatch(debugRegex, output);
99      if (debug) {
100        const host = debug[1];
101        const port = Number(debug[2]);
102        child.stderr.removeListener('data', waitForListenHint);
103        resolve([child, port, host]);
104      }
105    }
106
107    child.stderr.on('data', waitForListenHint);
108  });
109}
110
111function createAgentProxy(domain, client) {
112  const agent = new EventEmitter();
113  agent.then = (then, _catch) => {
114    // TODO: potentially fetch the protocol and pretty-print it here.
115    const descriptor = {
116      [util.inspect.custom](depth, { stylize }) {
117        return stylize(`[Agent ${domain}]`, 'special');
118      },
119    };
120    return PromisePrototypeThen(PromiseResolve(descriptor), then, _catch);
121  };
122
123  return new Proxy(agent, {
124    get(target, name) {
125      if (name in target) return target[name];
126      return function callVirtualMethod(params) {
127        return client.callMethod(`${domain}.${name}`, params);
128      };
129    },
130  });
131}
132
133class NodeInspector {
134  constructor(options, stdin, stdout) {
135    this.options = options;
136    this.stdin = stdin;
137    this.stdout = stdout;
138
139    this.paused = true;
140    this.child = null;
141
142    if (options.script) {
143      this._runScript = FunctionPrototypeBind(
144        runScript, null,
145        options.script,
146        options.scriptArgs,
147        options.host,
148        options.port,
149        FunctionPrototypeBind(this.childPrint, this));
150    } else {
151      this._runScript =
152          () => PromiseResolve([null, options.port, options.host]);
153    }
154
155    this.client = new InspectClient();
156
157    this.domainNames = ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'];
158    ArrayPrototypeForEach(this.domainNames, (domain) => {
159      this[domain] = createAgentProxy(domain, this.client);
160    });
161    this.handleDebugEvent = (fullName, params) => {
162      const { 0: domain, 1: name } = StringPrototypeSplit(fullName, '.');
163      if (domain in this) {
164        this[domain].emit(name, params);
165      }
166    };
167    this.client.on('debugEvent', this.handleDebugEvent);
168    const startRepl = createRepl(this);
169
170    // Handle all possible exits
171    process.on('exit', () => this.killChild());
172    const exitCodeZero = () => process.exit(0);
173    process.once('SIGTERM', exitCodeZero);
174    process.once('SIGHUP', exitCodeZero);
175
176    PromisePrototypeCatch(PromisePrototypeThen(this.run(), async () => {
177      const repl = await startRepl();
178      this.repl = repl;
179      this.repl.on('exit', exitCodeZero);
180      this.paused = false;
181    }), (error) => process.nextTick(() => { throw error; }));
182  }
183
184  suspendReplWhile(fn) {
185    if (this.repl) {
186      this.repl.pause();
187    }
188    this.stdin.pause();
189    this.paused = true;
190    return PromisePrototypeCatch(PromisePrototypeThen(new Promise((resolve) => {
191      resolve(fn());
192    }), () => {
193      this.paused = false;
194      if (this.repl) {
195        this.repl.resume();
196        this.repl.displayPrompt();
197      }
198      this.stdin.resume();
199    }), (error) => process.nextTick(() => { throw error; }));
200  }
201
202  killChild() {
203    this.client.reset();
204    if (this.child) {
205      this.child.kill();
206      this.child = null;
207    }
208  }
209
210  async run() {
211    this.killChild();
212
213    const { 0: child, 1: port, 2: host } = await this._runScript();
214    this.child = child;
215
216    this.print(`connecting to ${host}:${port} ..`, false);
217    for (let attempt = 0; attempt < 5; attempt++) {
218      debuglog('connection attempt #%d', attempt);
219      this.stdout.write('.');
220      try {
221        await this.client.connect(port, host);
222        debuglog('connection established');
223        this.stdout.write(' ok\n');
224        return;
225      } catch (error) {
226        debuglog('connect failed', error);
227        await pSetTimeout(1000);
228      }
229    }
230    this.stdout.write(' failed to connect, please retry\n');
231    process.exit(1);
232  }
233
234  clearLine() {
235    if (this.stdout.isTTY) {
236      this.stdout.cursorTo(0);
237      this.stdout.clearLine(1);
238    } else {
239      this.stdout.write('\b');
240    }
241  }
242
243  print(text, appendNewline = false) {
244    this.clearLine();
245    this.stdout.write(appendNewline ? `${text}\n` : text);
246  }
247
248  #stdioBuffers = { stdout: '', stderr: '' };
249  childPrint(text, which) {
250    const lines = RegExpPrototypeSymbolSplit(
251      /\r\n|\r|\n/g,
252      this.#stdioBuffers[which] + text);
253
254    this.#stdioBuffers[which] = '';
255
256    if (lines[lines.length - 1] !== '') {
257      this.#stdioBuffers[which] = ArrayPrototypePop(lines);
258    }
259
260    const textToPrint = ArrayPrototypeJoin(
261      ArrayPrototypeMap(lines, (chunk) => `< ${chunk}`),
262      '\n');
263
264    if (lines.length) {
265      this.print(textToPrint, true);
266      if (!this.paused) {
267        this.repl.displayPrompt(true);
268      }
269    }
270
271    if (StringPrototypeEndsWith(
272      textToPrint,
273      'Waiting for the debugger to disconnect...\n'
274    )) {
275      this.killChild();
276    }
277  }
278}
279
280function parseArgv(args) {
281  const target = ArrayPrototypeShift(args);
282  let host = '127.0.0.1';
283  let port = 9229;
284  let isRemote = false;
285  let script = target;
286  let scriptArgs = args;
287
288  const hostMatch = RegExpPrototypeSymbolMatch(/^([^:]+):(\d+)$/, target);
289  const portMatch = RegExpPrototypeSymbolMatch(/^--port=(\d+)$/, target);
290
291  if (hostMatch) {
292    // Connecting to remote debugger
293    host = hostMatch[1];
294    port = Number(hostMatch[2]);
295    isRemote = true;
296    script = null;
297  } else if (portMatch) {
298    // Start on custom port
299    port = Number(portMatch[1]);
300    script = args[0];
301    scriptArgs = ArrayPrototypeSlice(args, 1);
302  } else if (args.length === 1 && RegExpPrototypeTest(/^\d+$/, args[0]) &&
303             target === '-p') {
304    // Start debugger against a given pid
305    const pid = Number(args[0]);
306    try {
307      process._debugProcess(pid);
308    } catch (e) {
309      if (e.code === 'ESRCH') {
310        console.error(`Target process: ${pid} doesn't exist.`);
311        process.exit(1);
312      }
313      throw e;
314    }
315    script = null;
316    isRemote = true;
317  }
318
319  return {
320    host, port, isRemote, script, scriptArgs,
321  };
322}
323
324function startInspect(argv = ArrayPrototypeSlice(process.argv, 2),
325                      stdin = process.stdin,
326                      stdout = process.stdout) {
327  if (argv.length < 1) {
328    const invokedAs = `${process.argv0} ${process.argv[1]}`;
329
330    console.error(`Usage: ${invokedAs} script.js`);
331    console.error(`       ${invokedAs} <host>:<port>`);
332    console.error(`       ${invokedAs} --port=<port>`);
333    console.error(`       ${invokedAs} -p <pid>`);
334    process.exit(1);
335  }
336
337  const options = parseArgv(argv);
338  const inspector = new NodeInspector(options, stdin, stdout);
339
340  stdin.resume();
341
342  function handleUnexpectedError(e) {
343    if (e.code !== 'ERR_DEBUGGER_STARTUP_ERROR') {
344      console.error('There was an internal error in Node.js. ' +
345                    'Please report this bug.');
346      console.error(e.message);
347      console.error(e.stack);
348    } else {
349      console.error(e.message);
350    }
351    if (inspector.child) inspector.child.kill();
352    process.exit(1);
353  }
354
355  process.on('uncaughtException', handleUnexpectedError);
356}
357exports.start = startInspect;
358