• 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  /*
132    1. Do 'enable' with waitForDebuggerOnStart = true
133    2. Run worker. It should break on start.
134    3. Enable Runtime (to get console message) and Debugger. Resume.
135    4. Breaks on the 'debugger' statement. Resume.
136    5. Console message received, worker runs to a completion.
137    6. contextCreated/contextDestroyed had been properly dispatched
138  */
139  console.log('Test basic debug scenario');
140  await post('NodeWorker.enable', { waitForDebuggerOnStart: true });
141  const attached = waitForWorkerAttach(session);
142  const worker = runWorker(1);
143  const { sessionId, waitingForDebugger } = await attached;
144  assert.strictEqual(waitingForDebugger, true);
145  const detached = waitForWorkerDetach(session, sessionId);
146  const workerSession = new WorkerSession(session, sessionId);
147  const contextEventPromises = Promise.all([
148    waitForEvent(workerSession, 'Runtime.executionContextCreated'),
149    waitForEvent(workerSession, 'Runtime.executionContextDestroyed')
150  ]);
151  const consolePromise = waitForEvent(workerSession, 'Runtime.consoleAPICalled')
152      .then((notification) => notification.params.args[0].value);
153  await workerSession.post('Debugger.enable');
154  await workerSession.post('Runtime.enable');
155  await workerSession.waitForBreakAfterCommand(
156    'Runtime.runIfWaitingForDebugger', __filename, 1);
157  await workerSession.waitForBreakAfterCommand(
158    'Debugger.resume', __filename, 26);  // V8 line number is zero-based
159  const msg = await consolePromise;
160  assert.strictEqual(msg, workerMessage);
161  workerSession.post('Debugger.resume');
162  await Promise.all([worker, detached, contextEventPromises]);
163}
164
165async function testNoWaitOnStart(session, post) {
166  console.log('Test disabled waitForDebuggerOnStart');
167  await post('NodeWorker.enable', { waitForDebuggerOnStart: false });
168  let worker;
169  const promise = waitForWorkerAttach(session);
170  const exitPromise = runWorker(2, (w) => { worker = w; });
171  const { waitingForDebugger } = await promise;
172  assert.strictEqual(waitingForDebugger, false);
173  worker.postMessage('resume');
174  await exitPromise;
175}
176
177async function testTwoWorkers(session, post) {
178  console.log('Test attach to a running worker and then start a new one');
179  await post('NodeWorker.disable');
180  let okToAttach = false;
181  const worker1attached = waitForWorkerAttach(session).then((notification) => {
182    assert.strictEqual(okToAttach, true);
183    return notification;
184  });
185
186  let worker1Exited;
187  const worker = await new Promise((resolve, reject) => {
188    worker1Exited = runWorker(2, resolve);
189  }).then((worker) => new Promise(
190    (resolve) => worker.once('message', () => resolve(worker))));
191  okToAttach = true;
192  await post('NodeWorker.enable', { waitForDebuggerOnStart: true });
193  const { waitingForDebugger: worker1Waiting } = await worker1attached;
194  assert.strictEqual(worker1Waiting, false);
195
196  const worker2Attached = waitForWorkerAttach(session);
197  let worker2Done = false;
198  const worker2Exited = runWorker(1)
199      .then(() => assert.strictEqual(worker2Done, true));
200  const worker2AttachInfo = await worker2Attached;
201  assert.strictEqual(worker2AttachInfo.waitingForDebugger, true);
202  worker2Done = true;
203
204  const workerSession = new WorkerSession(session, worker2AttachInfo.sessionId);
205  workerSession.post('Runtime.runIfWaitingForDebugger');
206  worker.postMessage('resume');
207  await Promise.all([worker1Exited, worker2Exited]);
208}
209
210async function testWaitForDisconnectInWorker(session, post) {
211  console.log('Test NodeRuntime.waitForDisconnect in worker');
212
213  const sessionWithoutWaiting = new Session();
214  sessionWithoutWaiting.connect();
215  const sessionWithoutWaitingPost = doPost.bind(null, sessionWithoutWaiting);
216
217  await sessionWithoutWaitingPost('NodeWorker.enable', {
218    waitForDebuggerOnStart: true
219  });
220  await post('NodeWorker.enable', { waitForDebuggerOnStart: true });
221
222  const attached = [
223    waitForWorkerAttach(session),
224    waitForWorkerAttach(sessionWithoutWaiting)
225  ];
226
227  let worker = null;
228  const exitPromise = runWorker(2, (w) => worker = w);
229
230  const [{ sessionId: sessionId1 }, { sessionId: sessionId2 }] =
231      await Promise.all(attached);
232
233  const workerSession1 = new WorkerSession(session, sessionId1);
234  const workerSession2 = new WorkerSession(sessionWithoutWaiting, sessionId2);
235
236  await workerSession2.post('Runtime.enable');
237  await workerSession1.post('Runtime.enable');
238  await workerSession1.post('NodeRuntime.notifyWhenWaitingForDisconnect', {
239    enabled: true
240  });
241  await workerSession1.post('Runtime.runIfWaitingForDebugger');
242
243  // Create the promises before sending the exit message to the Worker in order
244  // to avoid race conditions.
245  const disconnectPromise =
246    waitForEvent(workerSession1, 'NodeRuntime.waitingForDisconnect');
247  const executionContextDestroyedPromise =
248    waitForEvent(workerSession2, 'Runtime.executionContextDestroyed');
249  worker.postMessage('resume');
250
251  await disconnectPromise;
252  post('NodeWorker.detach', { sessionId: sessionId1 });
253  await executionContextDestroyedPromise;
254
255  await exitPromise;
256
257  await post('NodeWorker.disable');
258  await sessionWithoutWaitingPost('NodeWorker.disable');
259  sessionWithoutWaiting.disconnect();
260}
261
262(async function test() {
263  const session = new Session();
264  session.connect();
265  const post = doPost.bind(null, session);
266
267  await testBasicWorkerDebug(session, post);
268
269  console.log('Test disabling attach to workers');
270  await post('NodeWorker.disable');
271  await runWorker(1);
272
273  await testNoWaitOnStart(session, post);
274
275  await testTwoWorkers(session, post);
276
277  await testWaitForDisconnectInWorker(session, post);
278
279  session.disconnect();
280  console.log('Test done');
281})().then(common.mustCall()).catch((err) => {
282  console.error(err);
283  process.exitCode = 1;
284});
285