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