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