current commit
260817.9 · debugger: wait for target startup
// Flags: --expose-internals
'use strict';

const common = require('../common');

common.skipIfInspectorDisabled();

const assert = require('assert');
const { EventEmitter } = require('events');
const { PassThrough } = require('stream');
const createRepl = require('internal/debugger/inspect_repl');

function createGate() {
  let resolve;
  const promise = new Promise((res) => {
    resolve = res;
  });
  return { promise, resolve };
}

function createAgent(domain, calls, gates) {
  const agent = new EventEmitter();
  const method = (name) => async () => {
    calls.push(`${domain}.${name}`);
    if (domain === 'Runtime' && name === 'runIfWaitingForDebugger') {
      const gate = gates.shift();
      if (gate) {
        await gate.promise;
      }
    }
  };

  agent.enable = method('enable');
  agent.setSamplingInterval = method('setSamplingInterval');
  agent.setAsyncCallStackDepth = method('setAsyncCallStackDepth');
  agent.setBlackboxPatterns = method('setBlackboxPatterns');
  agent.setPauseOnExceptions = method('setPauseOnExceptions');
  agent.runIfWaitingForDebugger = method('runIfWaitingForDebugger');
  return agent;
}

function evalCommand(repl, command) {
  return new Promise((resolve, reject) => {
    repl.eval(
      command,
      repl.context,
      'debugger-repl-test',
      (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      },
    );
  });
}

async function assertCommandWaitsForInit(repl, command, gate, calls) {
  let settled = false;
  const promise = evalCommand(repl, command).then(() => {
    settled = true;
  });

  await new Promise(setImmediate);
  assert.strictEqual(
    settled,
    false,
    `${command} resolved before post-connect initialization completed: ${calls}`,
  );

  gate.resolve();
  await promise;
  assert.strictEqual(settled, true);
}

(async () => {
  const calls = [];
  const runGate = createGate();
  const restartGate = createGate();
  const gates = [null, runGate, restartGate];
  const client = new EventEmitter();
  let nodeRuntimeEnableCount = 0;
  client.callMethod = common.mustCall(async (method) => {
    calls.push(method);
    if (method === 'NodeRuntime.enable') {
      const emitWaiting = () => {
        calls.push('NodeRuntime.waitingForDebugger');
        client.emit('NodeRuntime.waitingForDebugger');
      };
      // Cover notifications arriving both before and after the enable reply.
      if (nodeRuntimeEnableCount++ % 2 === 0) {
        emitWaiting();
      } else {
        setImmediate(emitWaiting);
      }
    } else {
      assert.strictEqual(method, 'NodeRuntime.disable');
    }
  }, 6);
  const inspector = {
    client,
    domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'],
    options: { script: 'debugger-target.js' },
    stdin: new PassThrough(),
    stdout: new PassThrough(),
    run: common.mustCall(async () => {
      calls.push('inspector.run');
    }, 2),
    suspendReplWhile(fn) {
      return fn();
    },
  };

  for (const domain of inspector.domainNames) {
    inspector[domain] = createAgent(domain, calls, gates);
  }

  const repl = await createRepl(inspector)();

  await assertCommandWaitsForInit(repl, 'run', runGate, calls);
  await assertCommandWaitsForInit(repl, 'restart', restartGate, calls);

  assert.deepStrictEqual(
    calls.filter((call) => (
      call === 'NodeRuntime.enable' ||
      call === 'NodeRuntime.waitingForDebugger' ||
      call === 'NodeRuntime.disable' ||
      call === 'Runtime.runIfWaitingForDebugger'
    )),
    [
      'NodeRuntime.enable',
      'NodeRuntime.waitingForDebugger',
      'NodeRuntime.disable',
      'Runtime.runIfWaitingForDebugger',
      'NodeRuntime.enable',
      'NodeRuntime.waitingForDebugger',
      'NodeRuntime.disable',
      'Runtime.runIfWaitingForDebugger',
      'NodeRuntime.enable',
      'NodeRuntime.waitingForDebugger',
      'NodeRuntime.disable',
      'Runtime.runIfWaitingForDebugger',
    ],
  );

  assert.deepStrictEqual(
    calls.filter((call) => (
      call === 'inspector.run' ||
      call === 'Runtime.runIfWaitingForDebugger'
    )),
    [
      'Runtime.runIfWaitingForDebugger',
      'inspector.run',
      'Runtime.runIfWaitingForDebugger',
      'inspector.run',
      'Runtime.runIfWaitingForDebugger',
    ],
  );

  repl.close();

  const attachCalls = [];
  const attachClient = new EventEmitter();
  attachClient.callMethod = common.mustNotCall();
  const attachInspector = {
    client: attachClient,
    domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'],
    options: {},
    stdin: new PassThrough(),
    stdout: new PassThrough(),
    suspendReplWhile(fn) {
      return fn();
    },
  };

  for (const domain of attachInspector.domainNames) {
    attachInspector[domain] = createAgent(domain, attachCalls, []);
  }

  const attachRepl = await createRepl(attachInspector)();
  attachRepl.close();
})().then(common.mustCall());