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