• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2const common = require('../common');
3
4common.skipIfInspectorDisabled();
5
6const assert = require('assert');
7const EventEmitter = require('events');
8const { Session } = require('inspector');
9const { pathToFileURL } = require('url');
10const {
11  Worker, isMainThread, parentPort, workerData
12} = require('worker_threads');
13
14
15const workerMessage = 'This is a message from a worker';
16
17function waitForMessage() {
18  return new Promise((resolve) => {
19    parentPort.once('message', resolve);
20  });
21}
22
23// This is at the top so line numbers change less often
24if (!isMainThread) {
25  if (workerData === 1) {
26    console.log(workerMessage);
27    debugger;  // eslint-disable-line no-debugger
28  } else if (workerData === 2) {
29    parentPort.postMessage('running');
30    waitForMessage();
31  }
32  return;
33}
34
35function doPost(session, method, params) {
36  return new Promise((resolve, reject) => {
37    session.post(method, params, (error, result) => {
38      if (error)
39        reject(JSON.stringify(error));
40      else
41        resolve(result);
42    });
43  });
44}
45
46function waitForEvent(emitter, event) {
47  return new Promise((resolve) => emitter.once(event, resolve));
48}
49
50function waitForWorkerAttach(session) {
51  return waitForEvent(session, 'NodeWorker.attachedToWorker')
52      .then(({ params }) => params);
53}
54
55async function waitForWorkerDetach(session, id) {
56  let sessionId;
57  do {
58    const { params } =
59        await waitForEvent(session, 'NodeWorker.detachedFromWorker');
60    sessionId = params.sessionId;
61  } while (sessionId !== id);
62}
63
64function runWorker(id, workerCallback = () => {}) {
65  return new Promise((resolve, reject) => {
66    const worker = new Worker(__filename, { workerData: id });
67    workerCallback(worker);
68    worker.on('error', reject);
69    worker.on('exit', resolve);
70  });
71}
72
73class WorkerSession extends EventEmitter {
74  constructor(parentSession, id) {
75    super();
76    this._parentSession = parentSession;
77    this._id = id;
78    this._requestCallbacks = new Map();
79    this._nextCommandId = 1;
80    this._parentSession.on('NodeWorker.receivedMessageFromWorker',
81                           ({ params }) => {
82                             if (params.sessionId === this._id)
83                               this._processMessage(JSON.parse(params.message));
84                           });
85  }
86
87  _processMessage(message) {
88    if (message.id === undefined) {
89      // console.log(JSON.stringify(message));
90      this.emit('inspectorNotification', message);
91      this.emit(message.method, message);
92      return;
93    }
94    if (!this._requestCallbacks.has(message.id))
95      return;
96    const [ resolve, reject ] = this._requestCallbacks.get(message.id);
97    this._requestCallbacks.delete(message.id);
98    if (message.error)
99      reject(new Error(message.error.message));
100    else
101      resolve(message.result);
102  }
103
104  async waitForBreakAfterCommand(command, script, line) {
105    const notificationPromise = waitForEvent(this, 'Debugger.paused');
106    this.post(command);
107    const notification = await notificationPromise;
108    const callFrame = notification.params.callFrames[0];
109    assert.strictEqual(callFrame.url, pathToFileURL(script).toString());
110    assert.strictEqual(callFrame.location.lineNumber, line);
111  }
112
113  post(method, parameters) {
114    const msg = {
115      id: this._nextCommandId++,
116      method
117    };
118    if (parameters)
119      msg.params = parameters;
120
121    return new Promise((resolve, reject) => {
122      this._requestCallbacks.set(msg.id, [resolve, reject]);
123      this._parentSession.post('NodeWorker.sendMessageToWorker', {
124        sessionId: this._id, message: JSON.stringify(msg)
125      });
126    });
127  }
128}
129
130async function testBasicWorkerDebug(session, post) {
131  // 1. Do 'enable' with waitForDebuggerOnStart = true
132  // 2. Run worker. It should break on start.
133  // 3. Enable Runtime (to get console message) and Debugger. Resume.
134  // 4. Breaks on the 'debugger' statement. Resume.
135  // 5. Console message received, worker runs to a completion.
136  // 6. contextCreated/contextDestroyed had been properly dispatched
137  console.log('Test basic debug scenario');
138  await post('NodeWorker.enable', { waitForDebuggerOnStart: true });
139  const attached = waitForWorkerAttach(session);
140  const worker = runWorker(1);
141  const { sessionId, waitingForDebugger } = await attached;
142  assert.strictEqual(waitingForDebugger, true);
143  const detached = waitForWorkerDetach(session, sessionId);
144  const workerSession = new WorkerSession(session, sessionId);
145  const contextEventPromises = Promise.all([
146    waitForEvent(workerSession, 'Runtime.executionContextCreated'),
147    waitForEvent(workerSession, 'Runtime.executionContextDestroyed'),
148  ]);
149  const consolePromise = waitForEvent(workerSession, 'Runtime.consoleAPICalled')
150      .then((notification) => notification.params.args[0].value);
151  await workerSession.post('Debugger.enable');
152  await workerSession.post('Runtime.enable');
153  await workerSession.waitForBreakAfterCommand(
154    'Runtime.runIfWaitingForDebugger', __filename, 1);
155  await workerSession.waitForBreakAfterCommand(
156    'Debugger.resume', __filename, 26);  // V8 line number is zero-based
157  const msg = await consolePromise;
158  assert.strictEqual(msg, workerMessage);
159  workerSession.post('Debugger.resume');
160  await Promise.all([worker, detached, contextEventPromises]);
161}
162
163async function testNoWaitOnStart(session, post) {
164  console.log('Test disabled waitForDebuggerOnStart');
165  await post('NodeWorker.enable', { waitForDebuggerOnStart: false });
166  let worker;
167  const promise = waitForWorkerAttach(session);
168  const exitPromise = runWorker(2, (w) => { worker = w; });
169  const { waitingForDebugger } = await promise;
170  assert.strictEqual(waitingForDebugger, false);
171  worker.postMessage('resume');
172  await exitPromise;
173}
174
175async function testTwoWorkers(session, post) {
176  console.log('Test attach to a running worker and then start a new one');
177  await post('NodeWorker.disable');
178  let okToAttach = false;
179  const worker1attached = waitForWorkerAttach(session).then((notification) => {
180    assert.strictEqual(okToAttach, true);
181    return notification;
182  });
183
184  let worker1Exited;
185  const worker = await new Promise((resolve, reject) => {
186    worker1Exited = runWorker(2, resolve);
187  }).then((worker) => new Promise(
188    (resolve) => worker.once('message', () => resolve(worker))));
189  okToAttach = true;
190  await post('NodeWorker.enable', { waitForDebuggerOnStart: true });
191  const { waitingForDebugger: worker1Waiting } = await worker1attached;
192  assert.strictEqual(worker1Waiting, false);
193
194  const worker2Attached = waitForWorkerAttach(session);
195  let worker2Done = false;
196  const worker2Exited = runWorker(1)
197      .then(() => assert.strictEqual(worker2Done, true));
198  const worker2AttachInfo = await worker2Attached;
199  assert.strictEqual(worker2AttachInfo.waitingForDebugger, true);
200  worker2Done = true;
201
202  const workerSession = new WorkerSession(session, worker2AttachInfo.sessionId);
203  workerSession.post('Runtime.runIfWaitingForDebugger');
204  worker.postMessage('resume');
205  await Promise.all([worker1Exited, worker2Exited]);
206}
207
208async function testWaitForDisconnectInWorker(session, post) {
209  console.log('Test NodeRuntime.waitForDisconnect in worker');
210
211  const sessionWithoutWaiting = new Session();
212  sessionWithoutWaiting.connect();
213  const sessionWithoutWaitingPost = doPost.bind(null, sessionWithoutWaiting);
214
215  await sessionWithoutWaitingPost('NodeWorker.enable', {
216    waitForDebuggerOnStart: true
217  });
218  await post('NodeWorker.enable', { waitForDebuggerOnStart: true });
219
220  const attached = [
221    waitForWorkerAttach(session),
222    waitForWorkerAttach(sessionWithoutWaiting),
223  ];
224
225  let worker = null;
226  const exitPromise = runWorker(2, (w) => worker = w);
227
228  const [{ sessionId: sessionId1 }, { sessionId: sessionId2 }] =
229      await Promise.all(attached);
230
231  const workerSession1 = new WorkerSession(session, sessionId1);
232  const workerSession2 = new WorkerSession(sessionWithoutWaiting, sessionId2);
233
234  await workerSession2.post('Runtime.enable');
235  await workerSession1.post('Runtime.enable');
236  await workerSession1.post('NodeRuntime.notifyWhenWaitingForDisconnect', {
237    enabled: true
238  });
239  await workerSession1.post('Runtime.runIfWaitingForDebugger');
240
241  // Create the promises before sending the exit message to the Worker in order
242  // to avoid race conditions.
243  const disconnectPromise =
244    waitForEvent(workerSession1, 'NodeRuntime.waitingForDisconnect');
245  const executionContextDestroyedPromise =
246    waitForEvent(workerSession2, 'Runtime.executionContextDestroyed');
247  worker.postMessage('resume');
248
249  await disconnectPromise;
250  post('NodeWorker.detach', { sessionId: sessionId1 });
251  await executionContextDestroyedPromise;
252
253  await exitPromise;
254
255  await post('NodeWorker.disable');
256  await sessionWithoutWaitingPost('NodeWorker.disable');
257  sessionWithoutWaiting.disconnect();
258}
259
260(async function test() {
261  const session = new Session();
262  session.connect();
263  const post = doPost.bind(null, session);
264
265  await testBasicWorkerDebug(session, post);
266
267  console.log('Test disabling attach to workers');
268  await post('NodeWorker.disable');
269  await runWorker(1);
270
271  await testNoWaitOnStart(session, post);
272
273  await testTwoWorkers(session, post);
274
275  await testWaitForDisconnectInWorker(session, post);
276
277  session.disconnect();
278  console.log('Test done');
279})().then(common.mustCall()).catch((err) => {
280  console.error(err);
281  process.exitCode = 1;
282});
283