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