From 9968df6330d2fb846381fbfd9f9ee3632c35bd17 Mon Sep 17 00:00:00 2001 From: inoway46 Date: Sun, 28 Jun 2026 19:30:05 +0900 Subject: [PATCH] debugger: wait for initial break render during startup Signed-off-by: inoway46 --- lib/internal/debugger/inspect_repl.js | 73 ++++++++++++--- .../test-debugger-run-restart-init.js | 91 ++++++++++++++++++- 2 files changed, 148 insertions(+), 16 deletions(-) diff --git a/lib/internal/debugger/inspect_repl.js b/lib/internal/debugger/inspect_repl.js index 87388b41a47b2c..c3af545242f99b 100644 --- a/lib/internal/debugger/inspect_repl.js +++ b/lib/internal/debugger/inspect_repl.js @@ -893,10 +893,32 @@ function createRepl(inspector) { }); } - function createInitialBreakRenderPromise(pauseRender) { + function finishInitialBreakRenderWait(wait, error) { + if (initialBreakRender !== wait) return; + + wait.cleanup(); + initialBreakRender = null; + waitForInitialBreakRender = false; + if (error) { + wait.reject(error); + } else { + wait.resolve(); + } + } + + function createInitialBreakRenderWait(child) { const { promise, resolve, reject } = PromiseWithResolvers(); - initialBreakRender = promise; - PromisePrototypeThen(pauseRender, resolve, reject); + const wait = { promise, resolve, reject, cleanup: null }; + const onEnd = () => finishInitialBreakRenderWait(wait); + wait.cleanup = () => { + inspector.client.removeListener('close', onEnd); + child.removeListener('exit', onEnd); + }; + + initialBreakRender = wait; + waitForInitialBreakRender = true; + inspector.client.once('close', onEnd); + child.once('exit', onEnd); return promise; } @@ -930,8 +952,14 @@ function createRepl(inspector) { print(`${header}\n${breakContext}`); })); - if (waitForInitialBreakRender && !initialBreakRender) { - createInitialBreakRenderPromise(pauseRender); + if (waitForInitialBreakRender && initialBreakRender) { + const wait = initialBreakRender; + waitForInitialBreakRender = false; + PromisePrototypeThen( + pauseRender, + () => finishInitialBreakRenderWait(wait), + (error) => finishInitialBreakRenderWait(wait, error), + ); } }); @@ -944,6 +972,14 @@ function createRepl(inspector) { Debugger.on('breakpointResolved', handleBreakpointResolved); + // Startup can fail before the initial pause is reported, so release the + // startup wait instead of blocking the REPL prompt indefinitely. + Runtime.on('exceptionThrown', () => { + if (waitForInitialBreakRender && initialBreakRender) { + finishInitialBreakRenderWait(initialBreakRender); + } + }); + Debugger.on('scriptParsed', (script) => { const { scriptId, url } = script; if (url) { @@ -1199,10 +1235,15 @@ function createRepl(inspector) { aliasProperties(context, SHORTCUTS); } - async function initAfterStart() { - waitForInitialBreakRender = + async function initAfterStart(waitForInitialBreak = false) { + const child = inspector.child; + const initialBreakRenderPromise = + waitForInitialBreak && !!inspector.options?.script && - process.env.NODE_INSPECT_RESUME_ON_START !== '1'; + process.env.NODE_INSPECT_RESUME_ON_START !== '1' && + child?.exitCode === null && + child?.signalCode === null ? + createInitialBreakRenderWait(child) : null; await Runtime.enable(); await Profiler.enable(); await Profiler.setSamplingInterval({ interval: 100 }); @@ -1211,12 +1252,14 @@ function createRepl(inspector) { await Debugger.setBlackboxPatterns({ patterns: [] }); await Debugger.setPauseOnExceptions({ state: pauseOnExceptionState }); await restoreBreakpoints(); - await Runtime.runIfWaitingForDebugger(); - await PromiseResolve(); - waitForInitialBreakRender = false; - const initialBreakRenderPromise = initialBreakRender; - initialBreakRender = null; - await initialBreakRenderPromise; + try { + await Runtime.runIfWaitingForDebugger(); + await initialBreakRenderPromise; + } finally { + if (initialBreakRender) { + finishInitialBreakRenderWait(initialBreakRender); + } + } } return async function startRepl() { @@ -1225,7 +1268,7 @@ function createRepl(inspector) { }); // Init once for the initial connection - await initAfterStart(); + await initAfterStart(true); const replOptions = { prompt: 'debug> ', diff --git a/test/parallel/test-debugger-run-restart-init.js b/test/parallel/test-debugger-run-restart-init.js index 78f237353baf8c..e92832ea64d3c7 100644 --- a/test/parallel/test-debugger-run-restart-init.js +++ b/test/parallel/test-debugger-run-restart-init.js @@ -36,6 +36,10 @@ function createAgent(domain, calls, gates) { agent.setBlackboxPatterns = method('setBlackboxPatterns'); agent.setPauseOnExceptions = method('setPauseOnExceptions'); agent.runIfWaitingForDebugger = method('runIfWaitingForDebugger'); + agent.getScriptSource = async () => { + calls.push(`${domain}.getScriptSource`); + return { scriptSource: 'let x = 1;\nx = x + 1;\n' }; + }; return agent; } @@ -56,6 +60,29 @@ function evalCommand(repl, command) { }); } +function createChild() { + const child = new EventEmitter(); + child.exitCode = null; + child.signalCode = null; + return child; +} + +function emitInitialPause(inspector) { + inspector.Debugger.emit('paused', { + reason: 'Break on start', + callFrames: [{ + callFrameId: 'call-frame-id', + functionName: '', + location: { + scriptId: '1', + lineNumber: 0, + columnNumber: 0, + }, + scopeChain: [], + }], + }); +} + async function assertCommandWaitsForInit(repl, command, gate, calls) { let settled = false; const promise = evalCommand(repl, command).then(() => { @@ -74,18 +101,74 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) { assert.strictEqual(settled, true); } -(async () => { +async function assertStartupWaitsForInitialPauseRender() { + const calls = []; + const runtimeGate = createGate(); + const pauseRenderGate = createGate(); + const gates = [runtimeGate]; + const inspector = { + child: createChild(), + client: new EventEmitter(), + domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'], + options: { script: 'script.js' }, + stdin: new PassThrough(), + stdout: new PassThrough(), + print(text, addNewline = true) { + inspector.stdout.write(addNewline ? `${text}\n` : text); + }, + suspendReplWhile(fn) { + return Promise.resolve(fn()).then(() => pauseRenderGate.promise); + }, + }; + + for (const domain of inspector.domainNames) { + inspector[domain] = createAgent(domain, calls, gates); + } + + let repl; + let settled = false; + const promise = createRepl(inspector)().then((createdRepl) => { + repl = createdRepl; + settled = true; + }); + + runtimeGate.resolve(); + await new Promise(setImmediate); + assert.strictEqual( + settled, + false, + `startup resolved before initial pause was observed: ${calls}`, + ); + + emitInitialPause(inspector); + await new Promise(setImmediate); + assert.strictEqual( + settled, + false, + `startup resolved before initial pause render completed: ${calls}`, + ); + + pauseRenderGate.resolve(); + await promise; + assert.strictEqual(settled, true); + repl.close(); +} + +async function assertRunAndRestartWaitForInit() { const calls = []; const runGate = createGate(); const restartGate = createGate(); const gates = [null, runGate, restartGate]; const inspector = { + child: null, client: new EventEmitter(), domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'], + options: {}, stdin: new PassThrough(), stdout: new PassThrough(), run: common.mustCall(async () => { calls.push('inspector.run'); + inspector.child = createChild(); }, 2), suspendReplWhile(fn) { return fn(); @@ -97,6 +180,7 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) { } const repl = await createRepl(inspector)(); + inspector.options = { script: 'script.js' }; await assertCommandWaitsForInit(repl, 'run', runGate, calls); await assertCommandWaitsForInit(repl, 'restart', restartGate, calls); @@ -116,4 +200,9 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) { ); repl.close(); +} + +(async () => { + await assertRunAndRestartWaitForInit(); + await assertStartupWaitsForInitialPauseRender(); })().then(common.mustCall());