Commit d69bed2

Browse files
panvaArchkon
authored andcommitted
debugger: wait for target startup
The inspector can accept a connection before an --inspect-brk target enters its frontend wait. Runtime.runIfWaitingForDebugger can then be handled too early, allowing the target to subsequently block forever. Wait for NodeRuntime.waitingForDebugger before initializing and releasing launched targets. Race the handshake against disconnects and apply it to both interactive and probe startup. Refs: #64116 Assisted-by: codex:gpt-5.6-sol Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65194 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5219e53 commit d69bed2

7 files changed

Lines changed: 629 additions & 3 deletions

‎lib/internal/debugger/inspect_helpers.js‎

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
'use strict';
22

33
const{
4+
ArrayPrototypePop,
5+
ArrayPrototypePush,
46
ArrayPrototypePushApply,
7+
MapPrototypeGet,
58
Number,
69
Promise,
10+
PromiseWithResolvers,
711
RegExpPrototypeExec,
12+
RegExpPrototypeSymbolReplace,
13+
SafePromiseRace,
814
StringPrototypeEndsWith,
15+
StringPrototypeIndexOf,
16+
StringPrototypeSlice,
17+
StringPrototypeStartsWith,
918
}=primordials;
1019

1120
const{ spawn }=require('child_process');
@@ -18,12 +27,24 @@ const {
1827
AbortController,
1928
}=require('internal/abort_controller');
2029

21-
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
30+
const{
31+
ERR_DEBUGGER_ERROR,
32+
ERR_DEBUGGER_STARTUP_ERROR,
33+
}=require('internal/errors').codes;
2234
const{
2335
exitCodes: {
2436
kInvalidCommandLineArgument,
2537
},
2638
}=internalBinding('errors');
39+
const{
40+
types: {
41+
kBoolean,
42+
kNoOp,
43+
kV8Option,
44+
},
45+
}=internalBinding('options');
46+
47+
const{ getCLIOptionsInfo }=require('internal/options');
2748

2849
constdebugRegex=/Debuggerlisteningonws:\/\/\[?(.+?)\]?:(\d+)\//;
2950

@@ -61,6 +82,176 @@ function ensureTrailingNewline(text) {
6182
returnStringPrototypeEndsWith(text,'\n') ? text : `${text}\n`;
6283
}
6384

85+
// Mirror OptionsParser::Parse() far enough to find the child script. Options
86+
// before it must not undo the inspector setup added by launchChildProcess().
87+
functionvalidateChildArgs(childArgs){
88+
const{ options, aliases }=getCLIOptionsInfo();
89+
constsyntheticArgs=[];
90+
letbreakFirstLine=true;
91+
letchildArgIndex=0;
92+
letinspectorEnabled=true;
93+
94+
functionpeekArg(){
95+
returnsyntheticArgs.length>0 ?
96+
syntheticArgs[syntheticArgs.length-1] :
97+
childArgs[childArgIndex];
98+
}
99+
100+
functionshiftArg(){
101+
returnsyntheticArgs.length>0 ?
102+
ArrayPrototypePop(syntheticArgs) :
103+
childArgs[childArgIndex++];
104+
}
105+
106+
while(true){
107+
constnextArg=peekArg();
108+
if(nextArg===undefined||nextArg.length<=1||nextArg[0]!=='-'){
109+
break;
110+
}
111+
112+
constisSynthetic=syntheticArgs.length>0;
113+
constarg=shiftArg();
114+
if(arg==='--'){break;}
115+
if(!isSynthetic&&
116+
(arg==='--experimental-config-file'||
117+
arg==='--experimental-default-config-file')){
118+
// ConfigReader rewrites these to an inline default path before parsing.
119+
continue;
120+
}
121+
if(!isSynthetic&&
122+
StringPrototypeStartsWith(
123+
arg,'--experimental-default-config-file=')){
124+
// ConfigReader rejects this form before parsing the remaining options.
125+
return;
126+
}
127+
128+
constequalsIndex=arg[1]==='-' ? StringPrototypeIndexOf(arg,'=') : -1;
129+
letname=equalsIndex===-1 ? arg : StringPrototypeSlice(arg,0,equalsIndex);
130+
if(name.length>2){
131+
name=`${StringPrototypeSlice(name,0,2)}${
132+
RegExpPrototypeSymbolReplace(/_/g,StringPrototypeSlice(name,2),'-')}`;
133+
}
134+
135+
letisNegation=false;
136+
if(StringPrototypeStartsWith(name,'--no-')){
137+
name=`--${StringPrototypeSlice(name,5)}`;
138+
isNegation=true;
139+
}
140+
141+
while(true){
142+
letexpansion=MapPrototypeGet(aliases,name);
143+
if(expansion===undefined&&equalsIndex!==-1){
144+
expansion=MapPrototypeGet(aliases,`${name}=`);
145+
}
146+
constaliasArg=peekArg();
147+
if(expansion===undefined&&
148+
aliasArg!==undefined&&
149+
aliasArg.length>0&&
150+
aliasArg[0]!=='-'){
151+
expansion=MapPrototypeGet(aliases,`${name} <arg>`);
152+
}
153+
if(expansion===undefined){break;}
154+
155+
constpreviousName=name;
156+
// process.allowedNodeEnvironmentFlags may remove a self-recursive
157+
// first entry from the cached alias metadata. Preserve the native
158+
// parser's synthetic option terminator in that case.
159+
if(expansion[0]==='--'){
160+
for(leti=expansion.length-1;i>=0;i--){
161+
ArrayPrototypePush(syntheticArgs,expansion[i]);
162+
}
163+
break;
164+
}
165+
name=expansion[0];
166+
for(leti=expansion.length-1;i>0;i--){
167+
ArrayPrototypePush(syntheticArgs,expansion[i]);
168+
}
169+
if(name===previousName){break;}
170+
}
171+
172+
constinfo=MapPrototypeGet(options,name);
173+
if(info===undefined){continue;}
174+
if(isNegation&&info.type!==kBoolean&&info.type!==kV8Option){
175+
return;
176+
}
177+
if(info.type===kBoolean||info.type===kNoOp||info.type===kV8Option){
178+
if(name==='--inspect'){
179+
inspectorEnabled=!isNegation;
180+
}elseif(name==='--inspect-brk'){
181+
breakFirstLine=!isNegation;
182+
if(!isNegation){inspectorEnabled=true;}
183+
}elseif(!isNegation&&
184+
(name==='--inspect-wait'||
185+
name==='--inspect-brk-node')){
186+
inspectorEnabled=true;
187+
}
188+
continue;
189+
}
190+
191+
if(equalsIndex!==-1){
192+
if(equalsIndex===arg.length-1){return;}
193+
continue;
194+
}
195+
196+
constvalue=peekArg();
197+
if(value===undefined||(value.length>0&&value[0]==='-')){
198+
return;
199+
}
200+
shiftArg();
201+
}
202+
203+
if(!inspectorEnabled){
204+
thrownewERR_DEBUGGER_STARTUP_ERROR(
205+
'--no-inspect is incompatible with node inspect before the child script');
206+
}
207+
if(!breakFirstLine){
208+
thrownewERR_DEBUGGER_STARTUP_ERROR(
209+
'--no-inspect-brk is incompatible with node inspect before the child script');
210+
}
211+
}
212+
213+
asyncfunctionwaitForDebugger(
214+
client,
215+
callMethod=(method)=>client.callMethod(method),
216+
){
217+
const{
218+
promise: waitingPromise,
219+
resolve: resolveWaiting,
220+
}=PromiseWithResolvers();
221+
const{
222+
promise: closedPromise,
223+
reject: rejectClosed,
224+
}=PromiseWithResolvers();
225+
constonWaiting=()=>resolveWaiting();
226+
constonClose=()=>{
227+
rejectClosed(newERR_DEBUGGER_ERROR(
228+
'Debugger session ended while waiting for target startup'));
229+
};
230+
231+
// The inspector can accept a connection before the target reaches its
232+
// startup wait. Enabling NodeRuntime makes that state observable whether
233+
// the target was already waiting or starts waiting later.
234+
client.once('NodeRuntime.waitingForDebugger',onWaiting);
235+
client.once('close',onClose);
236+
try{
237+
awaitSafePromiseRace([
238+
callMethod('NodeRuntime.enable'),
239+
closedPromise,
240+
]);
241+
awaitSafePromiseRace([
242+
waitingPromise,
243+
closedPromise,
244+
]);
245+
awaitSafePromiseRace([
246+
callMethod('NodeRuntime.disable'),
247+
closedPromise,
248+
]);
249+
}finally{
250+
client.removeListener('NodeRuntime.waitingForDebugger',onWaiting);
251+
client.removeListener('close',onClose);
252+
}
253+
}
254+
64255
functionwriteInspectUsageAndExit(invokedAs,message,exitCode){
65256
constcode=exitCode??(message ? kInvalidCommandLineArgument : 0);
66257
constout=code===0 ? process.stdout : process.stderr;
@@ -141,6 +332,8 @@ probe output schema.
141332

142333
asyncfunctionlaunchChildProcess(childArgs,inspectHost,inspectPort,
143334
childOutput,options={__proto__: null}){
335+
validateChildArgs(childArgs);
336+
144337
if(!options.skipPortPreflight){
145338
awaitportIsFree(inspectHost,inspectPort);
146339
}
@@ -189,5 +382,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
189382
module.exports={
190383
ensureTrailingNewline,
191384
launchChildProcess,
385+
waitForDebugger,
192386
writeInspectUsageAndExit,
193387
};

‎lib/internal/debugger/inspect_probe.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client');
3333
const{
3434
ensureTrailingNewline,
3535
launchChildProcess,
36+
waitForDebugger,
3637
}=require('internal/debugger/inspect_helpers');
3738

3839
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
@@ -1044,6 +1045,17 @@ class ProbeInspectorSession {
10441045
this.connected=true;
10451046

10461047
try{
1048+
try{
1049+
awaitwaitForDebugger(
1050+
this.client,
1051+
(method)=>this.callCdp(method),
1052+
);
1053+
}catch(err){
1054+
// A close event may have completed the structured report while the
1055+
// readiness helper was rejecting its disconnect race.
1056+
if(this.finished){throwkInspectorFailedSentinel;}
1057+
throwerr;
1058+
}
10471059
awaitthis.callCdp('Runtime.enable');
10481060
awaitthis.callCdp('Debugger.enable');
10491061
awaitthis.bindBreakpoints();

‎lib/internal/debugger/inspect_repl.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url');
6060
const{ customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace }=require('internal/util');
6161
const{inspect: utilInspect}=require('internal/util/inspect');
6262
const{ isObjectLiteral }=require('internal/repl/utils');
63+
const{ waitForDebugger }=require('internal/debugger/inspect_helpers');
6364
constdebuglog=require('internal/util/debuglog').debuglog('inspect');
6465

6566
constSHORTCUTS={
@@ -1204,9 +1205,13 @@ function createRepl(inspector) {
12041205
}
12051206

12061207
asyncfunctioninitAfterStart(){
1208+
constwaitForDebuggerOnStart=!!inspector.options?.script;
12071209
waitForInitialBreakRender=
1208-
!!inspector.options?.script&&
1210+
waitForDebuggerOnStart&&
12091211
process.env.NODE_INSPECT_RESUME_ON_START!=='1';
1212+
if(waitForDebuggerOnStart){
1213+
awaitwaitForDebugger(inspector.client);
1214+
}
12101215
awaitRuntime.enable();
12111216
awaitProfiler.enable();
12121217
awaitProfiler.setSamplingInterval({interval: 100});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit d69bed2

Browse files
panvaArchkon
authored andcommitted
debugger: wait for target startup
The inspector can accept a connection before an --inspect-brk target enters its frontend wait. Runtime.runIfWaitingForDebugger can then be handled too early, allowing the target to subsequently block forever. Wait for NodeRuntime.waitingForDebugger before initializing and releasing launched targets. Race the handshake against disconnects and apply it to both interactive and probe startup. Refs: #64116 Assisted-by: codex:gpt-5.6-sol Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65194 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5219e53 commit d69bed2

7 files changed

Lines changed: 629 additions & 3 deletions

‎lib/internal/debugger/inspect_helpers.js‎

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
'use strict';
22

33
const{
4+
ArrayPrototypePop,
5+
ArrayPrototypePush,
46
ArrayPrototypePushApply,
7+
MapPrototypeGet,
58
Number,
69
Promise,
10+
PromiseWithResolvers,
711
RegExpPrototypeExec,
12+
RegExpPrototypeSymbolReplace,
13+
SafePromiseRace,
814
StringPrototypeEndsWith,
15+
StringPrototypeIndexOf,
16+
StringPrototypeSlice,
17+
StringPrototypeStartsWith,
918
}=primordials;
1019

1120
const{ spawn }=require('child_process');
@@ -18,12 +27,24 @@ const {
1827
AbortController,
1928
}=require('internal/abort_controller');
2029

21-
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
30+
const{
31+
ERR_DEBUGGER_ERROR,
32+
ERR_DEBUGGER_STARTUP_ERROR,
33+
}=require('internal/errors').codes;
2234
const{
2335
exitCodes: {
2436
kInvalidCommandLineArgument,
2537
},
2638
}=internalBinding('errors');
39+
const{
40+
types: {
41+
kBoolean,
42+
kNoOp,
43+
kV8Option,
44+
},
45+
}=internalBinding('options');
46+
47+
const{ getCLIOptionsInfo }=require('internal/options');
2748

2849
constdebugRegex=/Debuggerlisteningonws:\/\/\[?(.+?)\]?:(\d+)\//;
2950

@@ -61,6 +82,176 @@ function ensureTrailingNewline(text) {
6182
returnStringPrototypeEndsWith(text,'\n') ? text : `${text}\n`;
6283
}
6384

85+
// Mirror OptionsParser::Parse() far enough to find the child script. Options
86+
// before it must not undo the inspector setup added by launchChildProcess().
87+
functionvalidateChildArgs(childArgs){
88+
const{ options, aliases }=getCLIOptionsInfo();
89+
constsyntheticArgs=[];
90+
letbreakFirstLine=true;
91+
letchildArgIndex=0;
92+
letinspectorEnabled=true;
93+
94+
functionpeekArg(){
95+
returnsyntheticArgs.length>0 ?
96+
syntheticArgs[syntheticArgs.length-1] :
97+
childArgs[childArgIndex];
98+
}
99+
100+
functionshiftArg(){
101+
returnsyntheticArgs.length>0 ?
102+
ArrayPrototypePop(syntheticArgs) :
103+
childArgs[childArgIndex++];
104+
}
105+
106+
while(true){
107+
constnextArg=peekArg();
108+
if(nextArg===undefined||nextArg.length<=1||nextArg[0]!=='-'){
109+
break;
110+
}
111+
112+
constisSynthetic=syntheticArgs.length>0;
113+
constarg=shiftArg();
114+
if(arg==='--'){break;}
115+
if(!isSynthetic&&
116+
(arg==='--experimental-config-file'||
117+
arg==='--experimental-default-config-file')){
118+
// ConfigReader rewrites these to an inline default path before parsing.
119+
continue;
120+
}
121+
if(!isSynthetic&&
122+
StringPrototypeStartsWith(
123+
arg,'--experimental-default-config-file=')){
124+
// ConfigReader rejects this form before parsing the remaining options.
125+
return;
126+
}
127+
128+
constequalsIndex=arg[1]==='-' ? StringPrototypeIndexOf(arg,'=') : -1;
129+
letname=equalsIndex===-1 ? arg : StringPrototypeSlice(arg,0,equalsIndex);
130+
if(name.length>2){
131+
name=`${StringPrototypeSlice(name,0,2)}${
132+
RegExpPrototypeSymbolReplace(/_/g,StringPrototypeSlice(name,2),'-')}`;
133+
}
134+
135+
letisNegation=false;
136+
if(StringPrototypeStartsWith(name,'--no-')){
137+
name=`--${StringPrototypeSlice(name,5)}`;
138+
isNegation=true;
139+
}
140+
141+
while(true){
142+
letexpansion=MapPrototypeGet(aliases,name);
143+
if(expansion===undefined&&equalsIndex!==-1){
144+
expansion=MapPrototypeGet(aliases,`${name}=`);
145+
}
146+
constaliasArg=peekArg();
147+
if(expansion===undefined&&
148+
aliasArg!==undefined&&
149+
aliasArg.length>0&&
150+
aliasArg[0]!=='-'){
151+
expansion=MapPrototypeGet(aliases,`${name} <arg>`);
152+
}
153+
if(expansion===undefined){break;}
154+
155+
constpreviousName=name;
156+
// process.allowedNodeEnvironmentFlags may remove a self-recursive
157+
// first entry from the cached alias metadata. Preserve the native
158+
// parser's synthetic option terminator in that case.
159+
if(expansion[0]==='--'){
160+
for(leti=expansion.length-1;i>=0;i--){
161+
ArrayPrototypePush(syntheticArgs,expansion[i]);
162+
}
163+
break;
164+
}
165+
name=expansion[0];
166+
for(leti=expansion.length-1;i>0;i--){
167+
ArrayPrototypePush(syntheticArgs,expansion[i]);
168+
}
169+
if(name===previousName){break;}
170+
}
171+
172+
constinfo=MapPrototypeGet(options,name);
173+
if(info===undefined){continue;}
174+
if(isNegation&&info.type!==kBoolean&&info.type!==kV8Option){
175+
return;
176+
}
177+
if(info.type===kBoolean||info.type===kNoOp||info.type===kV8Option){
178+
if(name==='--inspect'){
179+
inspectorEnabled=!isNegation;
180+
}elseif(name==='--inspect-brk'){
181+
breakFirstLine=!isNegation;
182+
if(!isNegation){inspectorEnabled=true;}
183+
}elseif(!isNegation&&
184+
(name==='--inspect-wait'||
185+
name==='--inspect-brk-node')){
186+
inspectorEnabled=true;
187+
}
188+
continue;
189+
}
190+
191+
if(equalsIndex!==-1){
192+
if(equalsIndex===arg.length-1){return;}
193+
continue;
194+
}
195+
196+
constvalue=peekArg();
197+
if(value===undefined||(value.length>0&&value[0]==='-')){
198+
return;
199+
}
200+
shiftArg();
201+
}
202+
203+
if(!inspectorEnabled){
204+
thrownewERR_DEBUGGER_STARTUP_ERROR(
205+
'--no-inspect is incompatible with node inspect before the child script');
206+
}
207+
if(!breakFirstLine){
208+
thrownewERR_DEBUGGER_STARTUP_ERROR(
209+
'--no-inspect-brk is incompatible with node inspect before the child script');
210+
}
211+
}
212+
213+
asyncfunctionwaitForDebugger(
214+
client,
215+
callMethod=(method)=>client.callMethod(method),
216+
){
217+
const{
218+
promise: waitingPromise,
219+
resolve: resolveWaiting,
220+
}=PromiseWithResolvers();
221+
const{
222+
promise: closedPromise,
223+
reject: rejectClosed,
224+
}=PromiseWithResolvers();
225+
constonWaiting=()=>resolveWaiting();
226+
constonClose=()=>{
227+
rejectClosed(newERR_DEBUGGER_ERROR(
228+
'Debugger session ended while waiting for target startup'));
229+
};
230+
231+
// The inspector can accept a connection before the target reaches its
232+
// startup wait. Enabling NodeRuntime makes that state observable whether
233+
// the target was already waiting or starts waiting later.
234+
client.once('NodeRuntime.waitingForDebugger',onWaiting);
235+
client.once('close',onClose);
236+
try{
237+
awaitSafePromiseRace([
238+
callMethod('NodeRuntime.enable'),
239+
closedPromise,
240+
]);
241+
awaitSafePromiseRace([
242+
waitingPromise,
243+
closedPromise,
244+
]);
245+
awaitSafePromiseRace([
246+
callMethod('NodeRuntime.disable'),
247+
closedPromise,
248+
]);
249+
}finally{
250+
client.removeListener('NodeRuntime.waitingForDebugger',onWaiting);
251+
client.removeListener('close',onClose);
252+
}
253+
}
254+
64255
functionwriteInspectUsageAndExit(invokedAs,message,exitCode){
65256
constcode=exitCode??(message ? kInvalidCommandLineArgument : 0);
66257
constout=code===0 ? process.stdout : process.stderr;
@@ -141,6 +332,8 @@ probe output schema.
141332

142333
asyncfunctionlaunchChildProcess(childArgs,inspectHost,inspectPort,
143334
childOutput,options={__proto__: null}){
335+
validateChildArgs(childArgs);
336+
144337
if(!options.skipPortPreflight){
145338
awaitportIsFree(inspectHost,inspectPort);
146339
}
@@ -189,5 +382,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
189382
module.exports={
190383
ensureTrailingNewline,
191384
launchChildProcess,
385+
waitForDebugger,
192386
writeInspectUsageAndExit,
193387
};

‎lib/internal/debugger/inspect_probe.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client');
3333
const{
3434
ensureTrailingNewline,
3535
launchChildProcess,
36+
waitForDebugger,
3637
}=require('internal/debugger/inspect_helpers');
3738

3839
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
@@ -1044,6 +1045,17 @@ class ProbeInspectorSession {
10441045
this.connected=true;
10451046

10461047
try{
1048+
try{
1049+
awaitwaitForDebugger(
1050+
this.client,
1051+
(method)=>this.callCdp(method),
1052+
);
1053+
}catch(err){
1054+
// A close event may have completed the structured report while the
1055+
// readiness helper was rejecting its disconnect race.
1056+
if(this.finished){throwkInspectorFailedSentinel;}
1057+
throwerr;
1058+
}
10471059
awaitthis.callCdp('Runtime.enable');
10481060
awaitthis.callCdp('Debugger.enable');
10491061
awaitthis.bindBreakpoints();

‎lib/internal/debugger/inspect_repl.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url');
6060
const{ customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace }=require('internal/util');
6161
const{inspect: utilInspect}=require('internal/util/inspect');
6262
const{ isObjectLiteral }=require('internal/repl/utils');
63+
const{ waitForDebugger }=require('internal/debugger/inspect_helpers');
6364
constdebuglog=require('internal/util/debuglog').debuglog('inspect');
6465

6566
constSHORTCUTS={
@@ -1204,9 +1205,13 @@ function createRepl(inspector) {
12041205
}
12051206

12061207
asyncfunctioninitAfterStart(){
1208+
constwaitForDebuggerOnStart=!!inspector.options?.script;
12071209
waitForInitialBreakRender=
1208-
!!inspector.options?.script&&
1210+
waitForDebuggerOnStart&&
12091211
process.env.NODE_INSPECT_RESUME_ON_START!=='1';
1212+
if(waitForDebuggerOnStart){
1213+
awaitwaitForDebugger(inspector.client);
1214+
}
12101215
awaitRuntime.enable();
12111216
awaitProfiler.enable();
12121217
awaitProfiler.setSamplingInterval({interval: 100});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit d69bed2

Browse files
panvaArchkon
authored andcommitted
debugger: wait for target startup
The inspector can accept a connection before an --inspect-brk target enters its frontend wait. Runtime.runIfWaitingForDebugger can then be handled too early, allowing the target to subsequently block forever. Wait for NodeRuntime.waitingForDebugger before initializing and releasing launched targets. Race the handshake against disconnects and apply it to both interactive and probe startup. Refs: #64116 Assisted-by: codex:gpt-5.6-sol Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65194 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5219e53 commit d69bed2

7 files changed

Lines changed: 629 additions & 3 deletions

‎lib/internal/debugger/inspect_helpers.js‎

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
'use strict';
22

33
const{
4+
ArrayPrototypePop,
5+
ArrayPrototypePush,
46
ArrayPrototypePushApply,
7+
MapPrototypeGet,
58
Number,
69
Promise,
10+
PromiseWithResolvers,
711
RegExpPrototypeExec,
12+
RegExpPrototypeSymbolReplace,
13+
SafePromiseRace,
814
StringPrototypeEndsWith,
15+
StringPrototypeIndexOf,
16+
StringPrototypeSlice,
17+
StringPrototypeStartsWith,
918
}=primordials;
1019

1120
const{ spawn }=require('child_process');
@@ -18,12 +27,24 @@ const {
1827
AbortController,
1928
}=require('internal/abort_controller');
2029

21-
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
30+
const{
31+
ERR_DEBUGGER_ERROR,
32+
ERR_DEBUGGER_STARTUP_ERROR,
33+
}=require('internal/errors').codes;
2234
const{
2335
exitCodes: {
2436
kInvalidCommandLineArgument,
2537
},
2638
}=internalBinding('errors');
39+
const{
40+
types: {
41+
kBoolean,
42+
kNoOp,
43+
kV8Option,
44+
},
45+
}=internalBinding('options');
46+
47+
const{ getCLIOptionsInfo }=require('internal/options');
2748

2849
constdebugRegex=/Debuggerlisteningonws:\/\/\[?(.+?)\]?:(\d+)\//;
2950

@@ -61,6 +82,176 @@ function ensureTrailingNewline(text) {
6182
returnStringPrototypeEndsWith(text,'\n') ? text : `${text}\n`;
6283
}
6384

85+
// Mirror OptionsParser::Parse() far enough to find the child script. Options
86+
// before it must not undo the inspector setup added by launchChildProcess().
87+
functionvalidateChildArgs(childArgs){
88+
const{ options, aliases }=getCLIOptionsInfo();
89+
constsyntheticArgs=[];
90+
letbreakFirstLine=true;
91+
letchildArgIndex=0;
92+
letinspectorEnabled=true;
93+
94+
functionpeekArg(){
95+
returnsyntheticArgs.length>0 ?
96+
syntheticArgs[syntheticArgs.length-1] :
97+
childArgs[childArgIndex];
98+
}
99+
100+
functionshiftArg(){
101+
returnsyntheticArgs.length>0 ?
102+
ArrayPrototypePop(syntheticArgs) :
103+
childArgs[childArgIndex++];
104+
}
105+
106+
while(true){
107+
constnextArg=peekArg();
108+
if(nextArg===undefined||nextArg.length<=1||nextArg[0]!=='-'){
109+
break;
110+
}
111+
112+
constisSynthetic=syntheticArgs.length>0;
113+
constarg=shiftArg();
114+
if(arg==='--'){break;}
115+
if(!isSynthetic&&
116+
(arg==='--experimental-config-file'||
117+
arg==='--experimental-default-config-file')){
118+
// ConfigReader rewrites these to an inline default path before parsing.
119+
continue;
120+
}
121+
if(!isSynthetic&&
122+
StringPrototypeStartsWith(
123+
arg,'--experimental-default-config-file=')){
124+
// ConfigReader rejects this form before parsing the remaining options.
125+
return;
126+
}
127+
128+
constequalsIndex=arg[1]==='-' ? StringPrototypeIndexOf(arg,'=') : -1;
129+
letname=equalsIndex===-1 ? arg : StringPrototypeSlice(arg,0,equalsIndex);
130+
if(name.length>2){
131+
name=`${StringPrototypeSlice(name,0,2)}${
132+
RegExpPrototypeSymbolReplace(/_/g,StringPrototypeSlice(name,2),'-')}`;
133+
}
134+
135+
letisNegation=false;
136+
if(StringPrototypeStartsWith(name,'--no-')){
137+
name=`--${StringPrototypeSlice(name,5)}`;
138+
isNegation=true;
139+
}
140+
141+
while(true){
142+
letexpansion=MapPrototypeGet(aliases,name);
143+
if(expansion===undefined&&equalsIndex!==-1){
144+
expansion=MapPrototypeGet(aliases,`${name}=`);
145+
}
146+
constaliasArg=peekArg();
147+
if(expansion===undefined&&
148+
aliasArg!==undefined&&
149+
aliasArg.length>0&&
150+
aliasArg[0]!=='-'){
151+
expansion=MapPrototypeGet(aliases,`${name} <arg>`);
152+
}
153+
if(expansion===undefined){break;}
154+
155+
constpreviousName=name;
156+
// process.allowedNodeEnvironmentFlags may remove a self-recursive
157+
// first entry from the cached alias metadata. Preserve the native
158+
// parser's synthetic option terminator in that case.
159+
if(expansion[0]==='--'){
160+
for(leti=expansion.length-1;i>=0;i--){
161+
ArrayPrototypePush(syntheticArgs,expansion[i]);
162+
}
163+
break;
164+
}
165+
name=expansion[0];
166+
for(leti=expansion.length-1;i>0;i--){
167+
ArrayPrototypePush(syntheticArgs,expansion[i]);
168+
}
169+
if(name===previousName){break;}
170+
}
171+
172+
constinfo=MapPrototypeGet(options,name);
173+
if(info===undefined){continue;}
174+
if(isNegation&&info.type!==kBoolean&&info.type!==kV8Option){
175+
return;
176+
}
177+
if(info.type===kBoolean||info.type===kNoOp||info.type===kV8Option){
178+
if(name==='--inspect'){
179+
inspectorEnabled=!isNegation;
180+
}elseif(name==='--inspect-brk'){
181+
breakFirstLine=!isNegation;
182+
if(!isNegation){inspectorEnabled=true;}
183+
}elseif(!isNegation&&
184+
(name==='--inspect-wait'||
185+
name==='--inspect-brk-node')){
186+
inspectorEnabled=true;
187+
}
188+
continue;
189+
}
190+
191+
if(equalsIndex!==-1){
192+
if(equalsIndex===arg.length-1){return;}
193+
continue;
194+
}
195+
196+
constvalue=peekArg();
197+
if(value===undefined||(value.length>0&&value[0]==='-')){
198+
return;
199+
}
200+
shiftArg();
201+
}
202+
203+
if(!inspectorEnabled){
204+
thrownewERR_DEBUGGER_STARTUP_ERROR(
205+
'--no-inspect is incompatible with node inspect before the child script');
206+
}
207+
if(!breakFirstLine){
208+
thrownewERR_DEBUGGER_STARTUP_ERROR(
209+
'--no-inspect-brk is incompatible with node inspect before the child script');
210+
}
211+
}
212+
213+
asyncfunctionwaitForDebugger(
214+
client,
215+
callMethod=(method)=>client.callMethod(method),
216+
){
217+
const{
218+
promise: waitingPromise,
219+
resolve: resolveWaiting,
220+
}=PromiseWithResolvers();
221+
const{
222+
promise: closedPromise,
223+
reject: rejectClosed,
224+
}=PromiseWithResolvers();
225+
constonWaiting=()=>resolveWaiting();
226+
constonClose=()=>{
227+
rejectClosed(newERR_DEBUGGER_ERROR(
228+
'Debugger session ended while waiting for target startup'));
229+
};
230+
231+
// The inspector can accept a connection before the target reaches its
232+
// startup wait. Enabling NodeRuntime makes that state observable whether
233+
// the target was already waiting or starts waiting later.
234+
client.once('NodeRuntime.waitingForDebugger',onWaiting);
235+
client.once('close',onClose);
236+
try{
237+
awaitSafePromiseRace([
238+
callMethod('NodeRuntime.enable'),
239+
closedPromise,
240+
]);
241+
awaitSafePromiseRace([
242+
waitingPromise,
243+
closedPromise,
244+
]);
245+
awaitSafePromiseRace([
246+
callMethod('NodeRuntime.disable'),
247+
closedPromise,
248+
]);
249+
}finally{
250+
client.removeListener('NodeRuntime.waitingForDebugger',onWaiting);
251+
client.removeListener('close',onClose);
252+
}
253+
}
254+
64255
functionwriteInspectUsageAndExit(invokedAs,message,exitCode){
65256
constcode=exitCode??(message ? kInvalidCommandLineArgument : 0);
66257
constout=code===0 ? process.stdout : process.stderr;
@@ -141,6 +332,8 @@ probe output schema.
141332

142333
asyncfunctionlaunchChildProcess(childArgs,inspectHost,inspectPort,
143334
childOutput,options={__proto__: null}){
335+
validateChildArgs(childArgs);
336+
144337
if(!options.skipPortPreflight){
145338
awaitportIsFree(inspectHost,inspectPort);
146339
}
@@ -189,5 +382,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
189382
module.exports={
190383
ensureTrailingNewline,
191384
launchChildProcess,
385+
waitForDebugger,
192386
writeInspectUsageAndExit,
193387
};

‎lib/internal/debugger/inspect_probe.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client');
3333
const{
3434
ensureTrailingNewline,
3535
launchChildProcess,
36+
waitForDebugger,
3637
}=require('internal/debugger/inspect_helpers');
3738

3839
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
@@ -1044,6 +1045,17 @@ class ProbeInspectorSession {
10441045
this.connected=true;
10451046

10461047
try{
1048+
try{
1049+
awaitwaitForDebugger(
1050+
this.client,
1051+
(method)=>this.callCdp(method),
1052+
);
1053+
}catch(err){
1054+
// A close event may have completed the structured report while the
1055+
// readiness helper was rejecting its disconnect race.
1056+
if(this.finished){throwkInspectorFailedSentinel;}
1057+
throwerr;
1058+
}
10471059
awaitthis.callCdp('Runtime.enable');
10481060
awaitthis.callCdp('Debugger.enable');
10491061
awaitthis.bindBreakpoints();

‎lib/internal/debugger/inspect_repl.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url');
6060
const{ customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace }=require('internal/util');
6161
const{inspect: utilInspect}=require('internal/util/inspect');
6262
const{ isObjectLiteral }=require('internal/repl/utils');
63+
const{ waitForDebugger }=require('internal/debugger/inspect_helpers');
6364
constdebuglog=require('internal/util/debuglog').debuglog('inspect');
6465

6566
constSHORTCUTS={
@@ -1204,9 +1205,13 @@ function createRepl(inspector) {
12041205
}
12051206

12061207
asyncfunctioninitAfterStart(){
1208+
constwaitForDebuggerOnStart=!!inspector.options?.script;
12071209
waitForInitialBreakRender=
1208-
!!inspector.options?.script&&
1210+
waitForDebuggerOnStart&&
12091211
process.env.NODE_INSPECT_RESUME_ON_START!=='1';
1212+
if(waitForDebuggerOnStart){
1213+
awaitwaitForDebugger(inspector.client);
1214+
}
12101215
awaitRuntime.enable();
12111216
awaitProfiler.enable();
12121217
awaitProfiler.setSamplingInterval({interval: 100});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit d69bed2

Browse files
panvaArchkon
authored andcommitted
debugger: wait for target startup
The inspector can accept a connection before an --inspect-brk target enters its frontend wait. Runtime.runIfWaitingForDebugger can then be handled too early, allowing the target to subsequently block forever. Wait for NodeRuntime.waitingForDebugger before initializing and releasing launched targets. Race the handshake against disconnects and apply it to both interactive and probe startup. Refs: #64116 Assisted-by: codex:gpt-5.6-sol Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65194 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5219e53 commit d69bed2

7 files changed

Lines changed: 629 additions & 3 deletions

‎lib/internal/debugger/inspect_helpers.js‎

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
'use strict';
22

33
const{
4+
ArrayPrototypePop,
5+
ArrayPrototypePush,
46
ArrayPrototypePushApply,
7+
MapPrototypeGet,
58
Number,
69
Promise,
10+
PromiseWithResolvers,
711
RegExpPrototypeExec,
12+
RegExpPrototypeSymbolReplace,
13+
SafePromiseRace,
814
StringPrototypeEndsWith,
15+
StringPrototypeIndexOf,
16+
StringPrototypeSlice,
17+
StringPrototypeStartsWith,
918
}=primordials;
1019

1120
const{ spawn }=require('child_process');
@@ -18,12 +27,24 @@ const {
1827
AbortController,
1928
}=require('internal/abort_controller');
2029

21-
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
30+
const{
31+
ERR_DEBUGGER_ERROR,
32+
ERR_DEBUGGER_STARTUP_ERROR,
33+
}=require('internal/errors').codes;
2234
const{
2335
exitCodes: {
2436
kInvalidCommandLineArgument,
2537
},
2638
}=internalBinding('errors');
39+
const{
40+
types: {
41+
kBoolean,
42+
kNoOp,
43+
kV8Option,
44+
},
45+
}=internalBinding('options');
46+
47+
const{ getCLIOptionsInfo }=require('internal/options');
2748

2849
constdebugRegex=/Debuggerlisteningonws:\/\/\[?(.+?)\]?:(\d+)\//;
2950

@@ -61,6 +82,176 @@ function ensureTrailingNewline(text) {
6182
returnStringPrototypeEndsWith(text,'\n') ? text : `${text}\n`;
6283
}
6384

85+
// Mirror OptionsParser::Parse() far enough to find the child script. Options
86+
// before it must not undo the inspector setup added by launchChildProcess().
87+
functionvalidateChildArgs(childArgs){
88+
const{ options, aliases }=getCLIOptionsInfo();
89+
constsyntheticArgs=[];
90+
letbreakFirstLine=true;
91+
letchildArgIndex=0;
92+
letinspectorEnabled=true;
93+
94+
functionpeekArg(){
95+
returnsyntheticArgs.length>0 ?
96+
syntheticArgs[syntheticArgs.length-1] :
97+
childArgs[childArgIndex];
98+
}
99+
100+
functionshiftArg(){
101+
returnsyntheticArgs.length>0 ?
102+
ArrayPrototypePop(syntheticArgs) :
103+
childArgs[childArgIndex++];
104+
}
105+
106+
while(true){
107+
constnextArg=peekArg();
108+
if(nextArg===undefined||nextArg.length<=1||nextArg[0]!=='-'){
109+
break;
110+
}
111+
112+
constisSynthetic=syntheticArgs.length>0;
113+
constarg=shiftArg();
114+
if(arg==='--'){break;}
115+
if(!isSynthetic&&
116+
(arg==='--experimental-config-file'||
117+
arg==='--experimental-default-config-file')){
118+
// ConfigReader rewrites these to an inline default path before parsing.
119+
continue;
120+
}
121+
if(!isSynthetic&&
122+
StringPrototypeStartsWith(
123+
arg,'--experimental-default-config-file=')){
124+
// ConfigReader rejects this form before parsing the remaining options.
125+
return;
126+
}
127+
128+
constequalsIndex=arg[1]==='-' ? StringPrototypeIndexOf(arg,'=') : -1;
129+
letname=equalsIndex===-1 ? arg : StringPrototypeSlice(arg,0,equalsIndex);
130+
if(name.length>2){
131+
name=`${StringPrototypeSlice(name,0,2)}${
132+
RegExpPrototypeSymbolReplace(/_/g,StringPrototypeSlice(name,2),'-')}`;
133+
}
134+
135+
letisNegation=false;
136+
if(StringPrototypeStartsWith(name,'--no-')){
137+
name=`--${StringPrototypeSlice(name,5)}`;
138+
isNegation=true;
139+
}
140+
141+
while(true){
142+
letexpansion=MapPrototypeGet(aliases,name);
143+
if(expansion===undefined&&equalsIndex!==-1){
144+
expansion=MapPrototypeGet(aliases,`${name}=`);
145+
}
146+
constaliasArg=peekArg();
147+
if(expansion===undefined&&
148+
aliasArg!==undefined&&
149+
aliasArg.length>0&&
150+
aliasArg[0]!=='-'){
151+
expansion=MapPrototypeGet(aliases,`${name} <arg>`);
152+
}
153+
if(expansion===undefined){break;}
154+
155+
constpreviousName=name;
156+
// process.allowedNodeEnvironmentFlags may remove a self-recursive
157+
// first entry from the cached alias metadata. Preserve the native
158+
// parser's synthetic option terminator in that case.
159+
if(expansion[0]==='--'){
160+
for(leti=expansion.length-1;i>=0;i--){
161+
ArrayPrototypePush(syntheticArgs,expansion[i]);
162+
}
163+
break;
164+
}
165+
name=expansion[0];
166+
for(leti=expansion.length-1;i>0;i--){
167+
ArrayPrototypePush(syntheticArgs,expansion[i]);
168+
}
169+
if(name===previousName){break;}
170+
}
171+
172+
constinfo=MapPrototypeGet(options,name);
173+
if(info===undefined){continue;}
174+
if(isNegation&&info.type!==kBoolean&&info.type!==kV8Option){
175+
return;
176+
}
177+
if(info.type===kBoolean||info.type===kNoOp||info.type===kV8Option){
178+
if(name==='--inspect'){
179+
inspectorEnabled=!isNegation;
180+
}elseif(name==='--inspect-brk'){
181+
breakFirstLine=!isNegation;
182+
if(!isNegation){inspectorEnabled=true;}
183+
}elseif(!isNegation&&
184+
(name==='--inspect-wait'||
185+
name==='--inspect-brk-node')){
186+
inspectorEnabled=true;
187+
}
188+
continue;
189+
}
190+
191+
if(equalsIndex!==-1){
192+
if(equalsIndex===arg.length-1){return;}
193+
continue;
194+
}
195+
196+
constvalue=peekArg();
197+
if(value===undefined||(value.length>0&&value[0]==='-')){
198+
return;
199+
}
200+
shiftArg();
201+
}
202+
203+
if(!inspectorEnabled){
204+
thrownewERR_DEBUGGER_STARTUP_ERROR(
205+
'--no-inspect is incompatible with node inspect before the child script');
206+
}
207+
if(!breakFirstLine){
208+
thrownewERR_DEBUGGER_STARTUP_ERROR(
209+
'--no-inspect-brk is incompatible with node inspect before the child script');
210+
}
211+
}
212+
213+
asyncfunctionwaitForDebugger(
214+
client,
215+
callMethod=(method)=>client.callMethod(method),
216+
){
217+
const{
218+
promise: waitingPromise,
219+
resolve: resolveWaiting,
220+
}=PromiseWithResolvers();
221+
const{
222+
promise: closedPromise,
223+
reject: rejectClosed,
224+
}=PromiseWithResolvers();
225+
constonWaiting=()=>resolveWaiting();
226+
constonClose=()=>{
227+
rejectClosed(newERR_DEBUGGER_ERROR(
228+
'Debugger session ended while waiting for target startup'));
229+
};
230+
231+
// The inspector can accept a connection before the target reaches its
232+
// startup wait. Enabling NodeRuntime makes that state observable whether
233+
// the target was already waiting or starts waiting later.
234+
client.once('NodeRuntime.waitingForDebugger',onWaiting);
235+
client.once('close',onClose);
236+
try{
237+
awaitSafePromiseRace([
238+
callMethod('NodeRuntime.enable'),
239+
closedPromise,
240+
]);
241+
awaitSafePromiseRace([
242+
waitingPromise,
243+
closedPromise,
244+
]);
245+
awaitSafePromiseRace([
246+
callMethod('NodeRuntime.disable'),
247+
closedPromise,
248+
]);
249+
}finally{
250+
client.removeListener('NodeRuntime.waitingForDebugger',onWaiting);
251+
client.removeListener('close',onClose);
252+
}
253+
}
254+
64255
functionwriteInspectUsageAndExit(invokedAs,message,exitCode){
65256
constcode=exitCode??(message ? kInvalidCommandLineArgument : 0);
66257
constout=code===0 ? process.stdout : process.stderr;
@@ -141,6 +332,8 @@ probe output schema.
141332

142333
asyncfunctionlaunchChildProcess(childArgs,inspectHost,inspectPort,
143334
childOutput,options={__proto__: null}){
335+
validateChildArgs(childArgs);
336+
144337
if(!options.skipPortPreflight){
145338
awaitportIsFree(inspectHost,inspectPort);
146339
}
@@ -189,5 +382,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
189382
module.exports={
190383
ensureTrailingNewline,
191384
launchChildProcess,
385+
waitForDebugger,
192386
writeInspectUsageAndExit,
193387
};

‎lib/internal/debugger/inspect_probe.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client');
3333
const{
3434
ensureTrailingNewline,
3535
launchChildProcess,
36+
waitForDebugger,
3637
}=require('internal/debugger/inspect_helpers');
3738

3839
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
@@ -1044,6 +1045,17 @@ class ProbeInspectorSession {
10441045
this.connected=true;
10451046

10461047
try{
1048+
try{
1049+
awaitwaitForDebugger(
1050+
this.client,
1051+
(method)=>this.callCdp(method),
1052+
);
1053+
}catch(err){
1054+
// A close event may have completed the structured report while the
1055+
// readiness helper was rejecting its disconnect race.
1056+
if(this.finished){throwkInspectorFailedSentinel;}
1057+
throwerr;
1058+
}
10471059
awaitthis.callCdp('Runtime.enable');
10481060
awaitthis.callCdp('Debugger.enable');
10491061
awaitthis.bindBreakpoints();

‎lib/internal/debugger/inspect_repl.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url');
6060
const{ customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace }=require('internal/util');
6161
const{inspect: utilInspect}=require('internal/util/inspect');
6262
const{ isObjectLiteral }=require('internal/repl/utils');
63+
const{ waitForDebugger }=require('internal/debugger/inspect_helpers');
6364
constdebuglog=require('internal/util/debuglog').debuglog('inspect');
6465

6566
constSHORTCUTS={
@@ -1204,9 +1205,13 @@ function createRepl(inspector) {
12041205
}
12051206

12061207
asyncfunctioninitAfterStart(){
1208+
constwaitForDebuggerOnStart=!!inspector.options?.script;
12071209
waitForInitialBreakRender=
1208-
!!inspector.options?.script&&
1210+
waitForDebuggerOnStart&&
12091211
process.env.NODE_INSPECT_RESUME_ON_START!=='1';
1212+
if(waitForDebuggerOnStart){
1213+
awaitwaitForDebugger(inspector.client);
1214+
}
12101215
awaitRuntime.enable();
12111216
awaitProfiler.enable();
12121217
awaitProfiler.setSamplingInterval({interval: 100});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit d69bed2

Browse files
panvaArchkon
authored andcommitted
debugger: wait for target startup
The inspector can accept a connection before an --inspect-brk target enters its frontend wait. Runtime.runIfWaitingForDebugger can then be handled too early, allowing the target to subsequently block forever. Wait for NodeRuntime.waitingForDebugger before initializing and releasing launched targets. Race the handshake against disconnects and apply it to both interactive and probe startup. Refs: #64116 Assisted-by: codex:gpt-5.6-sol Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65194 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5219e53 commit d69bed2

7 files changed

Lines changed: 629 additions & 3 deletions

‎lib/internal/debugger/inspect_helpers.js‎

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
'use strict';
22

33
const{
4+
ArrayPrototypePop,
5+
ArrayPrototypePush,
46
ArrayPrototypePushApply,
7+
MapPrototypeGet,
58
Number,
69
Promise,
10+
PromiseWithResolvers,
711
RegExpPrototypeExec,
12+
RegExpPrototypeSymbolReplace,
13+
SafePromiseRace,
814
StringPrototypeEndsWith,
15+
StringPrototypeIndexOf,
16+
StringPrototypeSlice,
17+
StringPrototypeStartsWith,
918
}=primordials;
1019

1120
const{ spawn }=require('child_process');
@@ -18,12 +27,24 @@ const {
1827
AbortController,
1928
}=require('internal/abort_controller');
2029

21-
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
30+
const{
31+
ERR_DEBUGGER_ERROR,
32+
ERR_DEBUGGER_STARTUP_ERROR,
33+
}=require('internal/errors').codes;
2234
const{
2335
exitCodes: {
2436
kInvalidCommandLineArgument,
2537
},
2638
}=internalBinding('errors');
39+
const{
40+
types: {
41+
kBoolean,
42+
kNoOp,
43+
kV8Option,
44+
},
45+
}=internalBinding('options');
46+
47+
const{ getCLIOptionsInfo }=require('internal/options');
2748

2849
constdebugRegex=/Debuggerlisteningonws:\/\/\[?(.+?)\]?:(\d+)\//;
2950

@@ -61,6 +82,176 @@ function ensureTrailingNewline(text) {
6182
returnStringPrototypeEndsWith(text,'\n') ? text : `${text}\n`;
6283
}
6384

85+
// Mirror OptionsParser::Parse() far enough to find the child script. Options
86+
// before it must not undo the inspector setup added by launchChildProcess().
87+
functionvalidateChildArgs(childArgs){
88+
const{ options, aliases }=getCLIOptionsInfo();
89+
constsyntheticArgs=[];
90+
letbreakFirstLine=true;
91+
letchildArgIndex=0;
92+
letinspectorEnabled=true;
93+
94+
functionpeekArg(){
95+
returnsyntheticArgs.length>0 ?
96+
syntheticArgs[syntheticArgs.length-1] :
97+
childArgs[childArgIndex];
98+
}
99+
100+
functionshiftArg(){
101+
returnsyntheticArgs.length>0 ?
102+
ArrayPrototypePop(syntheticArgs) :
103+
childArgs[childArgIndex++];
104+
}
105+
106+
while(true){
107+
constnextArg=peekArg();
108+
if(nextArg===undefined||nextArg.length<=1||nextArg[0]!=='-'){
109+
break;
110+
}
111+
112+
constisSynthetic=syntheticArgs.length>0;
113+
constarg=shiftArg();
114+
if(arg==='--'){break;}
115+
if(!isSynthetic&&
116+
(arg==='--experimental-config-file'||
117+
arg==='--experimental-default-config-file')){
118+
// ConfigReader rewrites these to an inline default path before parsing.
119+
continue;
120+
}
121+
if(!isSynthetic&&
122+
StringPrototypeStartsWith(
123+
arg,'--experimental-default-config-file=')){
124+
// ConfigReader rejects this form before parsing the remaining options.
125+
return;
126+
}
127+
128+
constequalsIndex=arg[1]==='-' ? StringPrototypeIndexOf(arg,'=') : -1;
129+
letname=equalsIndex===-1 ? arg : StringPrototypeSlice(arg,0,equalsIndex);
130+
if(name.length>2){
131+
name=`${StringPrototypeSlice(name,0,2)}${
132+
RegExpPrototypeSymbolReplace(/_/g,StringPrototypeSlice(name,2),'-')}`;
133+
}
134+
135+
letisNegation=false;
136+
if(StringPrototypeStartsWith(name,'--no-')){
137+
name=`--${StringPrototypeSlice(name,5)}`;
138+
isNegation=true;
139+
}
140+
141+
while(true){
142+
letexpansion=MapPrototypeGet(aliases,name);
143+
if(expansion===undefined&&equalsIndex!==-1){
144+
expansion=MapPrototypeGet(aliases,`${name}=`);
145+
}
146+
constaliasArg=peekArg();
147+
if(expansion===undefined&&
148+
aliasArg!==undefined&&
149+
aliasArg.length>0&&
150+
aliasArg[0]!=='-'){
151+
expansion=MapPrototypeGet(aliases,`${name} <arg>`);
152+
}
153+
if(expansion===undefined){break;}
154+
155+
constpreviousName=name;
156+
// process.allowedNodeEnvironmentFlags may remove a self-recursive
157+
// first entry from the cached alias metadata. Preserve the native
158+
// parser's synthetic option terminator in that case.
159+
if(expansion[0]==='--'){
160+
for(leti=expansion.length-1;i>=0;i--){
161+
ArrayPrototypePush(syntheticArgs,expansion[i]);
162+
}
163+
break;
164+
}
165+
name=expansion[0];
166+
for(leti=expansion.length-1;i>0;i--){
167+
ArrayPrototypePush(syntheticArgs,expansion[i]);
168+
}
169+
if(name===previousName){break;}
170+
}
171+
172+
constinfo=MapPrototypeGet(options,name);
173+
if(info===undefined){continue;}
174+
if(isNegation&&info.type!==kBoolean&&info.type!==kV8Option){
175+
return;
176+
}
177+
if(info.type===kBoolean||info.type===kNoOp||info.type===kV8Option){
178+
if(name==='--inspect'){
179+
inspectorEnabled=!isNegation;
180+
}elseif(name==='--inspect-brk'){
181+
breakFirstLine=!isNegation;
182+
if(!isNegation){inspectorEnabled=true;}
183+
}elseif(!isNegation&&
184+
(name==='--inspect-wait'||
185+
name==='--inspect-brk-node')){
186+
inspectorEnabled=true;
187+
}
188+
continue;
189+
}
190+
191+
if(equalsIndex!==-1){
192+
if(equalsIndex===arg.length-1){return;}
193+
continue;
194+
}
195+
196+
constvalue=peekArg();
197+
if(value===undefined||(value.length>0&&value[0]==='-')){
198+
return;
199+
}
200+
shiftArg();
201+
}
202+
203+
if(!inspectorEnabled){
204+
thrownewERR_DEBUGGER_STARTUP_ERROR(
205+
'--no-inspect is incompatible with node inspect before the child script');
206+
}
207+
if(!breakFirstLine){
208+
thrownewERR_DEBUGGER_STARTUP_ERROR(
209+
'--no-inspect-brk is incompatible with node inspect before the child script');
210+
}
211+
}
212+
213+
asyncfunctionwaitForDebugger(
214+
client,
215+
callMethod=(method)=>client.callMethod(method),
216+
){
217+
const{
218+
promise: waitingPromise,
219+
resolve: resolveWaiting,
220+
}=PromiseWithResolvers();
221+
const{
222+
promise: closedPromise,
223+
reject: rejectClosed,
224+
}=PromiseWithResolvers();
225+
constonWaiting=()=>resolveWaiting();
226+
constonClose=()=>{
227+
rejectClosed(newERR_DEBUGGER_ERROR(
228+
'Debugger session ended while waiting for target startup'));
229+
};
230+
231+
// The inspector can accept a connection before the target reaches its
232+
// startup wait. Enabling NodeRuntime makes that state observable whether
233+
// the target was already waiting or starts waiting later.
234+
client.once('NodeRuntime.waitingForDebugger',onWaiting);
235+
client.once('close',onClose);
236+
try{
237+
awaitSafePromiseRace([
238+
callMethod('NodeRuntime.enable'),
239+
closedPromise,
240+
]);
241+
awaitSafePromiseRace([
242+
waitingPromise,
243+
closedPromise,
244+
]);
245+
awaitSafePromiseRace([
246+
callMethod('NodeRuntime.disable'),
247+
closedPromise,
248+
]);
249+
}finally{
250+
client.removeListener('NodeRuntime.waitingForDebugger',onWaiting);
251+
client.removeListener('close',onClose);
252+
}
253+
}
254+
64255
functionwriteInspectUsageAndExit(invokedAs,message,exitCode){
65256
constcode=exitCode??(message ? kInvalidCommandLineArgument : 0);
66257
constout=code===0 ? process.stdout : process.stderr;
@@ -141,6 +332,8 @@ probe output schema.
141332

142333
asyncfunctionlaunchChildProcess(childArgs,inspectHost,inspectPort,
143334
childOutput,options={__proto__: null}){
335+
validateChildArgs(childArgs);
336+
144337
if(!options.skipPortPreflight){
145338
awaitportIsFree(inspectHost,inspectPort);
146339
}
@@ -189,5 +382,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
189382
module.exports={
190383
ensureTrailingNewline,
191384
launchChildProcess,
385+
waitForDebugger,
192386
writeInspectUsageAndExit,
193387
};

‎lib/internal/debugger/inspect_probe.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client');
3333
const{
3434
ensureTrailingNewline,
3535
launchChildProcess,
36+
waitForDebugger,
3637
}=require('internal/debugger/inspect_helpers');
3738

3839
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
@@ -1044,6 +1045,17 @@ class ProbeInspectorSession {
10441045
this.connected=true;
10451046

10461047
try{
1048+
try{
1049+
awaitwaitForDebugger(
1050+
this.client,
1051+
(method)=>this.callCdp(method),
1052+
);
1053+
}catch(err){
1054+
// A close event may have completed the structured report while the
1055+
// readiness helper was rejecting its disconnect race.
1056+
if(this.finished){throwkInspectorFailedSentinel;}
1057+
throwerr;
1058+
}
10471059
awaitthis.callCdp('Runtime.enable');
10481060
awaitthis.callCdp('Debugger.enable');
10491061
awaitthis.bindBreakpoints();

‎lib/internal/debugger/inspect_repl.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url');
6060
const{ customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace }=require('internal/util');
6161
const{inspect: utilInspect}=require('internal/util/inspect');
6262
const{ isObjectLiteral }=require('internal/repl/utils');
63+
const{ waitForDebugger }=require('internal/debugger/inspect_helpers');
6364
constdebuglog=require('internal/util/debuglog').debuglog('inspect');
6465

6566
constSHORTCUTS={
@@ -1204,9 +1205,13 @@ function createRepl(inspector) {
12041205
}
12051206

12061207
asyncfunctioninitAfterStart(){
1208+
constwaitForDebuggerOnStart=!!inspector.options?.script;
12071209
waitForInitialBreakRender=
1208-
!!inspector.options?.script&&
1210+
waitForDebuggerOnStart&&
12091211
process.env.NODE_INSPECT_RESUME_ON_START!=='1';
1212+
if(waitForDebuggerOnStart){
1213+
awaitwaitForDebugger(inspector.client);
1214+
}
12101215
awaitRuntime.enable();
12111216
awaitProfiler.enable();
12121217
awaitProfiler.setSamplingInterval({interval: 100});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit d69bed2

Browse files
panvaArchkon
authored andcommitted
debugger: wait for target startup
The inspector can accept a connection before an --inspect-brk target enters its frontend wait. Runtime.runIfWaitingForDebugger can then be handled too early, allowing the target to subsequently block forever. Wait for NodeRuntime.waitingForDebugger before initializing and releasing launched targets. Race the handshake against disconnects and apply it to both interactive and probe startup. Refs: #64116 Assisted-by: codex:gpt-5.6-sol Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65194 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5219e53 commit d69bed2

7 files changed

Lines changed: 629 additions & 3 deletions

‎lib/internal/debugger/inspect_helpers.js‎

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
'use strict';
22

33
const{
4+
ArrayPrototypePop,
5+
ArrayPrototypePush,
46
ArrayPrototypePushApply,
7+
MapPrototypeGet,
58
Number,
69
Promise,
10+
PromiseWithResolvers,
711
RegExpPrototypeExec,
12+
RegExpPrototypeSymbolReplace,
13+
SafePromiseRace,
814
StringPrototypeEndsWith,
15+
StringPrototypeIndexOf,
16+
StringPrototypeSlice,
17+
StringPrototypeStartsWith,
918
}=primordials;
1019

1120
const{ spawn }=require('child_process');
@@ -18,12 +27,24 @@ const {
1827
AbortController,
1928
}=require('internal/abort_controller');
2029

21-
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
30+
const{
31+
ERR_DEBUGGER_ERROR,
32+
ERR_DEBUGGER_STARTUP_ERROR,
33+
}=require('internal/errors').codes;
2234
const{
2335
exitCodes: {
2436
kInvalidCommandLineArgument,
2537
},
2638
}=internalBinding('errors');
39+
const{
40+
types: {
41+
kBoolean,
42+
kNoOp,
43+
kV8Option,
44+
},
45+
}=internalBinding('options');
46+
47+
const{ getCLIOptionsInfo }=require('internal/options');
2748

2849
constdebugRegex=/Debuggerlisteningonws:\/\/\[?(.+?)\]?:(\d+)\//;
2950

@@ -61,6 +82,176 @@ function ensureTrailingNewline(text) {
6182
returnStringPrototypeEndsWith(text,'\n') ? text : `${text}\n`;
6283
}
6384

85+
// Mirror OptionsParser::Parse() far enough to find the child script. Options
86+
// before it must not undo the inspector setup added by launchChildProcess().
87+
functionvalidateChildArgs(childArgs){
88+
const{ options, aliases }=getCLIOptionsInfo();
89+
constsyntheticArgs=[];
90+
letbreakFirstLine=true;
91+
letchildArgIndex=0;
92+
letinspectorEnabled=true;
93+
94+
functionpeekArg(){
95+
returnsyntheticArgs.length>0 ?
96+
syntheticArgs[syntheticArgs.length-1] :
97+
childArgs[childArgIndex];
98+
}
99+
100+
functionshiftArg(){
101+
returnsyntheticArgs.length>0 ?
102+
ArrayPrototypePop(syntheticArgs) :
103+
childArgs[childArgIndex++];
104+
}
105+
106+
while(true){
107+
constnextArg=peekArg();
108+
if(nextArg===undefined||nextArg.length<=1||nextArg[0]!=='-'){
109+
break;
110+
}
111+
112+
constisSynthetic=syntheticArgs.length>0;
113+
constarg=shiftArg();
114+
if(arg==='--'){break;}
115+
if(!isSynthetic&&
116+
(arg==='--experimental-config-file'||
117+
arg==='--experimental-default-config-file')){
118+
// ConfigReader rewrites these to an inline default path before parsing.
119+
continue;
120+
}
121+
if(!isSynthetic&&
122+
StringPrototypeStartsWith(
123+
arg,'--experimental-default-config-file=')){
124+
// ConfigReader rejects this form before parsing the remaining options.
125+
return;
126+
}
127+
128+
constequalsIndex=arg[1]==='-' ? StringPrototypeIndexOf(arg,'=') : -1;
129+
letname=equalsIndex===-1 ? arg : StringPrototypeSlice(arg,0,equalsIndex);
130+
if(name.length>2){
131+
name=`${StringPrototypeSlice(name,0,2)}${
132+
RegExpPrototypeSymbolReplace(/_/g,StringPrototypeSlice(name,2),'-')}`;
133+
}
134+
135+
letisNegation=false;
136+
if(StringPrototypeStartsWith(name,'--no-')){
137+
name=`--${StringPrototypeSlice(name,5)}`;
138+
isNegation=true;
139+
}
140+
141+
while(true){
142+
letexpansion=MapPrototypeGet(aliases,name);
143+
if(expansion===undefined&&equalsIndex!==-1){
144+
expansion=MapPrototypeGet(aliases,`${name}=`);
145+
}
146+
constaliasArg=peekArg();
147+
if(expansion===undefined&&
148+
aliasArg!==undefined&&
149+
aliasArg.length>0&&
150+
aliasArg[0]!=='-'){
151+
expansion=MapPrototypeGet(aliases,`${name} <arg>`);
152+
}
153+
if(expansion===undefined){break;}
154+
155+
constpreviousName=name;
156+
// process.allowedNodeEnvironmentFlags may remove a self-recursive
157+
// first entry from the cached alias metadata. Preserve the native
158+
// parser's synthetic option terminator in that case.
159+
if(expansion[0]==='--'){
160+
for(leti=expansion.length-1;i>=0;i--){
161+
ArrayPrototypePush(syntheticArgs,expansion[i]);
162+
}
163+
break;
164+
}
165+
name=expansion[0];
166+
for(leti=expansion.length-1;i>0;i--){
167+
ArrayPrototypePush(syntheticArgs,expansion[i]);
168+
}
169+
if(name===previousName){break;}
170+
}
171+
172+
constinfo=MapPrototypeGet(options,name);
173+
if(info===undefined){continue;}
174+
if(isNegation&&info.type!==kBoolean&&info.type!==kV8Option){
175+
return;
176+
}
177+
if(info.type===kBoolean||info.type===kNoOp||info.type===kV8Option){
178+
if(name==='--inspect'){
179+
inspectorEnabled=!isNegation;
180+
}elseif(name==='--inspect-brk'){
181+
breakFirstLine=!isNegation;
182+
if(!isNegation){inspectorEnabled=true;}
183+
}elseif(!isNegation&&
184+
(name==='--inspect-wait'||
185+
name==='--inspect-brk-node')){
186+
inspectorEnabled=true;
187+
}
188+
continue;
189+
}
190+
191+
if(equalsIndex!==-1){
192+
if(equalsIndex===arg.length-1){return;}
193+
continue;
194+
}
195+
196+
constvalue=peekArg();
197+
if(value===undefined||(value.length>0&&value[0]==='-')){
198+
return;
199+
}
200+
shiftArg();
201+
}
202+
203+
if(!inspectorEnabled){
204+
thrownewERR_DEBUGGER_STARTUP_ERROR(
205+
'--no-inspect is incompatible with node inspect before the child script');
206+
}
207+
if(!breakFirstLine){
208+
thrownewERR_DEBUGGER_STARTUP_ERROR(
209+
'--no-inspect-brk is incompatible with node inspect before the child script');
210+
}
211+
}
212+
213+
asyncfunctionwaitForDebugger(
214+
client,
215+
callMethod=(method)=>client.callMethod(method),
216+
){
217+
const{
218+
promise: waitingPromise,
219+
resolve: resolveWaiting,
220+
}=PromiseWithResolvers();
221+
const{
222+
promise: closedPromise,
223+
reject: rejectClosed,
224+
}=PromiseWithResolvers();
225+
constonWaiting=()=>resolveWaiting();
226+
constonClose=()=>{
227+
rejectClosed(newERR_DEBUGGER_ERROR(
228+
'Debugger session ended while waiting for target startup'));
229+
};
230+
231+
// The inspector can accept a connection before the target reaches its
232+
// startup wait. Enabling NodeRuntime makes that state observable whether
233+
// the target was already waiting or starts waiting later.
234+
client.once('NodeRuntime.waitingForDebugger',onWaiting);
235+
client.once('close',onClose);
236+
try{
237+
awaitSafePromiseRace([
238+
callMethod('NodeRuntime.enable'),
239+
closedPromise,
240+
]);
241+
awaitSafePromiseRace([
242+
waitingPromise,
243+
closedPromise,
244+
]);
245+
awaitSafePromiseRace([
246+
callMethod('NodeRuntime.disable'),
247+
closedPromise,
248+
]);
249+
}finally{
250+
client.removeListener('NodeRuntime.waitingForDebugger',onWaiting);
251+
client.removeListener('close',onClose);
252+
}
253+
}
254+
64255
functionwriteInspectUsageAndExit(invokedAs,message,exitCode){
65256
constcode=exitCode??(message ? kInvalidCommandLineArgument : 0);
66257
constout=code===0 ? process.stdout : process.stderr;
@@ -141,6 +332,8 @@ probe output schema.
141332

142333
asyncfunctionlaunchChildProcess(childArgs,inspectHost,inspectPort,
143334
childOutput,options={__proto__: null}){
335+
validateChildArgs(childArgs);
336+
144337
if(!options.skipPortPreflight){
145338
awaitportIsFree(inspectHost,inspectPort);
146339
}
@@ -189,5 +382,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
189382
module.exports={
190383
ensureTrailingNewline,
191384
launchChildProcess,
385+
waitForDebugger,
192386
writeInspectUsageAndExit,
193387
};

‎lib/internal/debugger/inspect_probe.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client');
3333
const{
3434
ensureTrailingNewline,
3535
launchChildProcess,
36+
waitForDebugger,
3637
}=require('internal/debugger/inspect_helpers');
3738

3839
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
@@ -1044,6 +1045,17 @@ class ProbeInspectorSession {
10441045
this.connected=true;
10451046

10461047
try{
1048+
try{
1049+
awaitwaitForDebugger(
1050+
this.client,
1051+
(method)=>this.callCdp(method),
1052+
);
1053+
}catch(err){
1054+
// A close event may have completed the structured report while the
1055+
// readiness helper was rejecting its disconnect race.
1056+
if(this.finished){throwkInspectorFailedSentinel;}
1057+
throwerr;
1058+
}
10471059
awaitthis.callCdp('Runtime.enable');
10481060
awaitthis.callCdp('Debugger.enable');
10491061
awaitthis.bindBreakpoints();

‎lib/internal/debugger/inspect_repl.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url');
6060
const{ customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace }=require('internal/util');
6161
const{inspect: utilInspect}=require('internal/util/inspect');
6262
const{ isObjectLiteral }=require('internal/repl/utils');
63+
const{ waitForDebugger }=require('internal/debugger/inspect_helpers');
6364
constdebuglog=require('internal/util/debuglog').debuglog('inspect');
6465

6566
constSHORTCUTS={
@@ -1204,9 +1205,13 @@ function createRepl(inspector) {
12041205
}
12051206

12061207
asyncfunctioninitAfterStart(){
1208+
constwaitForDebuggerOnStart=!!inspector.options?.script;
12071209
waitForInitialBreakRender=
1208-
!!inspector.options?.script&&
1210+
waitForDebuggerOnStart&&
12091211
process.env.NODE_INSPECT_RESUME_ON_START!=='1';
1212+
if(waitForDebuggerOnStart){
1213+
awaitwaitForDebugger(inspector.client);
1214+
}
12101215
awaitRuntime.enable();
12111216
awaitProfiler.enable();
12121217
awaitProfiler.setSamplingInterval({interval: 100});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit d69bed2

Browse files
panvaArchkon
authored andcommitted
debugger: wait for target startup
The inspector can accept a connection before an --inspect-brk target enters its frontend wait. Runtime.runIfWaitingForDebugger can then be handled too early, allowing the target to subsequently block forever. Wait for NodeRuntime.waitingForDebugger before initializing and releasing launched targets. Race the handshake against disconnects and apply it to both interactive and probe startup. Refs: #64116 Assisted-by: codex:gpt-5.6-sol Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65194 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5219e53 commit d69bed2

7 files changed

Lines changed: 629 additions & 3 deletions

‎lib/internal/debugger/inspect_helpers.js‎

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
'use strict';
22

33
const{
4+
ArrayPrototypePop,
5+
ArrayPrototypePush,
46
ArrayPrototypePushApply,
7+
MapPrototypeGet,
58
Number,
69
Promise,
10+
PromiseWithResolvers,
711
RegExpPrototypeExec,
12+
RegExpPrototypeSymbolReplace,
13+
SafePromiseRace,
814
StringPrototypeEndsWith,
15+
StringPrototypeIndexOf,
16+
StringPrototypeSlice,
17+
StringPrototypeStartsWith,
918
}=primordials;
1019

1120
const{ spawn }=require('child_process');
@@ -18,12 +27,24 @@ const {
1827
AbortController,
1928
}=require('internal/abort_controller');
2029

21-
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
30+
const{
31+
ERR_DEBUGGER_ERROR,
32+
ERR_DEBUGGER_STARTUP_ERROR,
33+
}=require('internal/errors').codes;
2234
const{
2335
exitCodes: {
2436
kInvalidCommandLineArgument,
2537
},
2638
}=internalBinding('errors');
39+
const{
40+
types: {
41+
kBoolean,
42+
kNoOp,
43+
kV8Option,
44+
},
45+
}=internalBinding('options');
46+
47+
const{ getCLIOptionsInfo }=require('internal/options');
2748

2849
constdebugRegex=/Debuggerlisteningonws:\/\/\[?(.+?)\]?:(\d+)\//;
2950

@@ -61,6 +82,176 @@ function ensureTrailingNewline(text) {
6182
returnStringPrototypeEndsWith(text,'\n') ? text : `${text}\n`;
6283
}
6384

85+
// Mirror OptionsParser::Parse() far enough to find the child script. Options
86+
// before it must not undo the inspector setup added by launchChildProcess().
87+
functionvalidateChildArgs(childArgs){
88+
const{ options, aliases }=getCLIOptionsInfo();
89+
constsyntheticArgs=[];
90+
letbreakFirstLine=true;
91+
letchildArgIndex=0;
92+
letinspectorEnabled=true;
93+
94+
functionpeekArg(){
95+
returnsyntheticArgs.length>0 ?
96+
syntheticArgs[syntheticArgs.length-1] :
97+
childArgs[childArgIndex];
98+
}
99+
100+
functionshiftArg(){
101+
returnsyntheticArgs.length>0 ?
102+
ArrayPrototypePop(syntheticArgs) :
103+
childArgs[childArgIndex++];
104+
}
105+
106+
while(true){
107+
constnextArg=peekArg();
108+
if(nextArg===undefined||nextArg.length<=1||nextArg[0]!=='-'){
109+
break;
110+
}
111+
112+
constisSynthetic=syntheticArgs.length>0;
113+
constarg=shiftArg();
114+
if(arg==='--'){break;}
115+
if(!isSynthetic&&
116+
(arg==='--experimental-config-file'||
117+
arg==='--experimental-default-config-file')){
118+
// ConfigReader rewrites these to an inline default path before parsing.
119+
continue;
120+
}
121+
if(!isSynthetic&&
122+
StringPrototypeStartsWith(
123+
arg,'--experimental-default-config-file=')){
124+
// ConfigReader rejects this form before parsing the remaining options.
125+
return;
126+
}
127+
128+
constequalsIndex=arg[1]==='-' ? StringPrototypeIndexOf(arg,'=') : -1;
129+
letname=equalsIndex===-1 ? arg : StringPrototypeSlice(arg,0,equalsIndex);
130+
if(name.length>2){
131+
name=`${StringPrototypeSlice(name,0,2)}${
132+
RegExpPrototypeSymbolReplace(/_/g,StringPrototypeSlice(name,2),'-')}`;
133+
}
134+
135+
letisNegation=false;
136+
if(StringPrototypeStartsWith(name,'--no-')){
137+
name=`--${StringPrototypeSlice(name,5)}`;
138+
isNegation=true;
139+
}
140+
141+
while(true){
142+
letexpansion=MapPrototypeGet(aliases,name);
143+
if(expansion===undefined&&equalsIndex!==-1){
144+
expansion=MapPrototypeGet(aliases,`${name}=`);
145+
}
146+
constaliasArg=peekArg();
147+
if(expansion===undefined&&
148+
aliasArg!==undefined&&
149+
aliasArg.length>0&&
150+
aliasArg[0]!=='-'){
151+
expansion=MapPrototypeGet(aliases,`${name} <arg>`);
152+
}
153+
if(expansion===undefined){break;}
154+
155+
constpreviousName=name;
156+
// process.allowedNodeEnvironmentFlags may remove a self-recursive
157+
// first entry from the cached alias metadata. Preserve the native
158+
// parser's synthetic option terminator in that case.
159+
if(expansion[0]==='--'){
160+
for(leti=expansion.length-1;i>=0;i--){
161+
ArrayPrototypePush(syntheticArgs,expansion[i]);
162+
}
163+
break;
164+
}
165+
name=expansion[0];
166+
for(leti=expansion.length-1;i>0;i--){
167+
ArrayPrototypePush(syntheticArgs,expansion[i]);
168+
}
169+
if(name===previousName){break;}
170+
}
171+
172+
constinfo=MapPrototypeGet(options,name);
173+
if(info===undefined){continue;}
174+
if(isNegation&&info.type!==kBoolean&&info.type!==kV8Option){
175+
return;
176+
}
177+
if(info.type===kBoolean||info.type===kNoOp||info.type===kV8Option){
178+
if(name==='--inspect'){
179+
inspectorEnabled=!isNegation;
180+
}elseif(name==='--inspect-brk'){
181+
breakFirstLine=!isNegation;
182+
if(!isNegation){inspectorEnabled=true;}
183+
}elseif(!isNegation&&
184+
(name==='--inspect-wait'||
185+
name==='--inspect-brk-node')){
186+
inspectorEnabled=true;
187+
}
188+
continue;
189+
}
190+
191+
if(equalsIndex!==-1){
192+
if(equalsIndex===arg.length-1){return;}
193+
continue;
194+
}
195+
196+
constvalue=peekArg();
197+
if(value===undefined||(value.length>0&&value[0]==='-')){
198+
return;
199+
}
200+
shiftArg();
201+
}
202+
203+
if(!inspectorEnabled){
204+
thrownewERR_DEBUGGER_STARTUP_ERROR(
205+
'--no-inspect is incompatible with node inspect before the child script');
206+
}
207+
if(!breakFirstLine){
208+
thrownewERR_DEBUGGER_STARTUP_ERROR(
209+
'--no-inspect-brk is incompatible with node inspect before the child script');
210+
}
211+
}
212+
213+
asyncfunctionwaitForDebugger(
214+
client,
215+
callMethod=(method)=>client.callMethod(method),
216+
){
217+
const{
218+
promise: waitingPromise,
219+
resolve: resolveWaiting,
220+
}=PromiseWithResolvers();
221+
const{
222+
promise: closedPromise,
223+
reject: rejectClosed,
224+
}=PromiseWithResolvers();
225+
constonWaiting=()=>resolveWaiting();
226+
constonClose=()=>{
227+
rejectClosed(newERR_DEBUGGER_ERROR(
228+
'Debugger session ended while waiting for target startup'));
229+
};
230+
231+
// The inspector can accept a connection before the target reaches its
232+
// startup wait. Enabling NodeRuntime makes that state observable whether
233+
// the target was already waiting or starts waiting later.
234+
client.once('NodeRuntime.waitingForDebugger',onWaiting);
235+
client.once('close',onClose);
236+
try{
237+
awaitSafePromiseRace([
238+
callMethod('NodeRuntime.enable'),
239+
closedPromise,
240+
]);
241+
awaitSafePromiseRace([
242+
waitingPromise,
243+
closedPromise,
244+
]);
245+
awaitSafePromiseRace([
246+
callMethod('NodeRuntime.disable'),
247+
closedPromise,
248+
]);
249+
}finally{
250+
client.removeListener('NodeRuntime.waitingForDebugger',onWaiting);
251+
client.removeListener('close',onClose);
252+
}
253+
}
254+
64255
functionwriteInspectUsageAndExit(invokedAs,message,exitCode){
65256
constcode=exitCode??(message ? kInvalidCommandLineArgument : 0);
66257
constout=code===0 ? process.stdout : process.stderr;
@@ -141,6 +332,8 @@ probe output schema.
141332

142333
asyncfunctionlaunchChildProcess(childArgs,inspectHost,inspectPort,
143334
childOutput,options={__proto__: null}){
335+
validateChildArgs(childArgs);
336+
144337
if(!options.skipPortPreflight){
145338
awaitportIsFree(inspectHost,inspectPort);
146339
}
@@ -189,5 +382,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
189382
module.exports={
190383
ensureTrailingNewline,
191384
launchChildProcess,
385+
waitForDebugger,
192386
writeInspectUsageAndExit,
193387
};

‎lib/internal/debugger/inspect_probe.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client');
3333
const{
3434
ensureTrailingNewline,
3535
launchChildProcess,
36+
waitForDebugger,
3637
}=require('internal/debugger/inspect_helpers');
3738

3839
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
@@ -1044,6 +1045,17 @@ class ProbeInspectorSession {
10441045
this.connected=true;
10451046

10461047
try{
1048+
try{
1049+
awaitwaitForDebugger(
1050+
this.client,
1051+
(method)=>this.callCdp(method),
1052+
);
1053+
}catch(err){
1054+
// A close event may have completed the structured report while the
1055+
// readiness helper was rejecting its disconnect race.
1056+
if(this.finished){throwkInspectorFailedSentinel;}
1057+
throwerr;
1058+
}
10471059
awaitthis.callCdp('Runtime.enable');
10481060
awaitthis.callCdp('Debugger.enable');
10491061
awaitthis.bindBreakpoints();

‎lib/internal/debugger/inspect_repl.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url');
6060
const{ customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace }=require('internal/util');
6161
const{inspect: utilInspect}=require('internal/util/inspect');
6262
const{ isObjectLiteral }=require('internal/repl/utils');
63+
const{ waitForDebugger }=require('internal/debugger/inspect_helpers');
6364
constdebuglog=require('internal/util/debuglog').debuglog('inspect');
6465

6566
constSHORTCUTS={
@@ -1204,9 +1205,13 @@ function createRepl(inspector) {
12041205
}
12051206

12061207
asyncfunctioninitAfterStart(){
1208+
constwaitForDebuggerOnStart=!!inspector.options?.script;
12071209
waitForInitialBreakRender=
1208-
!!inspector.options?.script&&
1210+
waitForDebuggerOnStart&&
12091211
process.env.NODE_INSPECT_RESUME_ON_START!=='1';
1212+
if(waitForDebuggerOnStart){
1213+
awaitwaitForDebugger(inspector.client);
1214+
}
12101215
awaitRuntime.enable();
12111216
awaitProfiler.enable();
12121217
awaitProfiler.setSamplingInterval({interval: 100});

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit d69bed2

Browse files
panvaArchkon
authored andcommitted
debugger: wait for target startup
The inspector can accept a connection before an --inspect-brk target enters its frontend wait. Runtime.runIfWaitingForDebugger can then be handled too early, allowing the target to subsequently block forever. Wait for NodeRuntime.waitingForDebugger before initializing and releasing launched targets. Race the handshake against disconnects and apply it to both interactive and probe startup. Refs: #64116 Assisted-by: codex:gpt-5.6-sol Co-authored-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65194 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5219e53 commit d69bed2

7 files changed

Lines changed: 629 additions & 3 deletions

‎lib/internal/debugger/inspect_helpers.js‎

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
'use strict';
22

33
const{
4+
ArrayPrototypePop,
5+
ArrayPrototypePush,
46
ArrayPrototypePushApply,
7+
MapPrototypeGet,
58
Number,
69
Promise,
10+
PromiseWithResolvers,
711
RegExpPrototypeExec,
12+
RegExpPrototypeSymbolReplace,
13+
SafePromiseRace,
814
StringPrototypeEndsWith,
15+
StringPrototypeIndexOf,
16+
StringPrototypeSlice,
17+
StringPrototypeStartsWith,
918
}=primordials;
1019

1120
const{ spawn }=require('child_process');
@@ -18,12 +27,24 @@ const {
1827
AbortController,
1928
}=require('internal/abort_controller');
2029

21-
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
30+
const{
31+
ERR_DEBUGGER_ERROR,
32+
ERR_DEBUGGER_STARTUP_ERROR,
33+
}=require('internal/errors').codes;
2234
const{
2335
exitCodes: {
2436
kInvalidCommandLineArgument,
2537
},
2638
}=internalBinding('errors');
39+
const{
40+
types: {
41+
kBoolean,
42+
kNoOp,
43+
kV8Option,
44+
},
45+
}=internalBinding('options');
46+
47+
const{ getCLIOptionsInfo }=require('internal/options');
2748

2849
constdebugRegex=/Debuggerlisteningonws:\/\/\[?(.+?)\]?:(\d+)\//;
2950

@@ -61,6 +82,176 @@ function ensureTrailingNewline(text) {
6182
returnStringPrototypeEndsWith(text,'\n') ? text : `${text}\n`;
6283
}
6384

85+
// Mirror OptionsParser::Parse() far enough to find the child script. Options
86+
// before it must not undo the inspector setup added by launchChildProcess().
87+
functionvalidateChildArgs(childArgs){
88+
const{ options, aliases }=getCLIOptionsInfo();
89+
constsyntheticArgs=[];
90+
letbreakFirstLine=true;
91+
letchildArgIndex=0;
92+
letinspectorEnabled=true;
93+
94+
functionpeekArg(){
95+
returnsyntheticArgs.length>0 ?
96+
syntheticArgs[syntheticArgs.length-1] :
97+
childArgs[childArgIndex];
98+
}
99+
100+
functionshiftArg(){
101+
returnsyntheticArgs.length>0 ?
102+
ArrayPrototypePop(syntheticArgs) :
103+
childArgs[childArgIndex++];
104+
}
105+
106+
while(true){
107+
constnextArg=peekArg();
108+
if(nextArg===undefined||nextArg.length<=1||nextArg[0]!=='-'){
109+
break;
110+
}
111+
112+
constisSynthetic=syntheticArgs.length>0;
113+
constarg=shiftArg();
114+
if(arg==='--'){break;}
115+
if(!isSynthetic&&
116+
(arg==='--experimental-config-file'||
117+
arg==='--experimental-default-config-file')){
118+
// ConfigReader rewrites these to an inline default path before parsing.
119+
continue;
120+
}
121+
if(!isSynthetic&&
122+
StringPrototypeStartsWith(
123+
arg,'--experimental-default-config-file=')){
124+
// ConfigReader rejects this form before parsing the remaining options.
125+
return;
126+
}
127+
128+
constequalsIndex=arg[1]==='-' ? StringPrototypeIndexOf(arg,'=') : -1;
129+
letname=equalsIndex===-1 ? arg : StringPrototypeSlice(arg,0,equalsIndex);
130+
if(name.length>2){
131+
name=`${StringPrototypeSlice(name,0,2)}${
132+
RegExpPrototypeSymbolReplace(/_/g,StringPrototypeSlice(name,2),'-')}`;
133+
}
134+
135+
letisNegation=false;
136+
if(StringPrototypeStartsWith(name,'--no-')){
137+
name=`--${StringPrototypeSlice(name,5)}`;
138+
isNegation=true;
139+
}
140+
141+
while(true){
142+
letexpansion=MapPrototypeGet(aliases,name);
143+
if(expansion===undefined&&equalsIndex!==-1){
144+
expansion=MapPrototypeGet(aliases,`${name}=`);
145+
}
146+
constaliasArg=peekArg();
147+
if(expansion===undefined&&
148+
aliasArg!==undefined&&
149+
aliasArg.length>0&&
150+
aliasArg[0]!=='-'){
151+
expansion=MapPrototypeGet(aliases,`${name} <arg>`);
152+
}
153+
if(expansion===undefined){break;}
154+
155+
constpreviousName=name;
156+
// process.allowedNodeEnvironmentFlags may remove a self-recursive
157+
// first entry from the cached alias metadata. Preserve the native
158+
// parser's synthetic option terminator in that case.
159+
if(expansion[0]==='--'){
160+
for(leti=expansion.length-1;i>=0;i--){
161+
ArrayPrototypePush(syntheticArgs,expansion[i]);
162+
}
163+
break;
164+
}
165+
name=expansion[0];
166+
for(leti=expansion.length-1;i>0;i--){
167+
ArrayPrototypePush(syntheticArgs,expansion[i]);
168+
}
169+
if(name===previousName){break;}
170+
}
171+
172+
constinfo=MapPrototypeGet(options,name);
173+
if(info===undefined){continue;}
174+
if(isNegation&&info.type!==kBoolean&&info.type!==kV8Option){
175+
return;
176+
}
177+
if(info.type===kBoolean||info.type===kNoOp||info.type===kV8Option){
178+
if(name==='--inspect'){
179+
inspectorEnabled=!isNegation;
180+
}elseif(name==='--inspect-brk'){
181+
breakFirstLine=!isNegation;
182+
if(!isNegation){inspectorEnabled=true;}
183+
}elseif(!isNegation&&
184+
(name==='--inspect-wait'||
185+
name==='--inspect-brk-node')){
186+
inspectorEnabled=true;
187+
}
188+
continue;
189+
}
190+
191+
if(equalsIndex!==-1){
192+
if(equalsIndex===arg.length-1){return;}
193+
continue;
194+
}
195+
196+
constvalue=peekArg();
197+
if(value===undefined||(value.length>0&&value[0]==='-')){
198+
return;
199+
}
200+
shiftArg();
201+
}
202+
203+
if(!inspectorEnabled){
204+
thrownewERR_DEBUGGER_STARTUP_ERROR(
205+
'--no-inspect is incompatible with node inspect before the child script');
206+
}
207+
if(!breakFirstLine){
208+
thrownewERR_DEBUGGER_STARTUP_ERROR(
209+
'--no-inspect-brk is incompatible with node inspect before the child script');
210+
}
211+
}
212+
213+
asyncfunctionwaitForDebugger(
214+
client,
215+
callMethod=(method)=>client.callMethod(method),
216+
){
217+
const{
218+
promise: waitingPromise,
219+
resolve: resolveWaiting,
220+
}=PromiseWithResolvers();
221+
const{
222+
promise: closedPromise,
223+
reject: rejectClosed,
224+
}=PromiseWithResolvers();
225+
constonWaiting=()=>resolveWaiting();
226+
constonClose=()=>{
227+
rejectClosed(newERR_DEBUGGER_ERROR(
228+
'Debugger session ended while waiting for target startup'));
229+
};
230+
231+
// The inspector can accept a connection before the target reaches its
232+
// startup wait. Enabling NodeRuntime makes that state observable whether
233+
// the target was already waiting or starts waiting later.
234+
client.once('NodeRuntime.waitingForDebugger',onWaiting);
235+
client.once('close',onClose);
236+
try{
237+
awaitSafePromiseRace([
238+
callMethod('NodeRuntime.enable'),
239+
closedPromise,
240+
]);
241+
awaitSafePromiseRace([
242+
waitingPromise,
243+
closedPromise,
244+
]);
245+
awaitSafePromiseRace([
246+
callMethod('NodeRuntime.disable'),
247+
closedPromise,
248+
]);
249+
}finally{
250+
client.removeListener('NodeRuntime.waitingForDebugger',onWaiting);
251+
client.removeListener('close',onClose);
252+
}
253+
}
254+
64255
functionwriteInspectUsageAndExit(invokedAs,message,exitCode){
65256
constcode=exitCode??(message ? kInvalidCommandLineArgument : 0);
66257
constout=code===0 ? process.stdout : process.stderr;
@@ -141,6 +332,8 @@ probe output schema.
141332

142333
asyncfunctionlaunchChildProcess(childArgs,inspectHost,inspectPort,
143334
childOutput,options={__proto__: null}){
335+
validateChildArgs(childArgs);
336+
144337
if(!options.skipPortPreflight){
145338
awaitportIsFree(inspectHost,inspectPort);
146339
}
@@ -189,5 +382,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort,
189382
module.exports={
190383
ensureTrailingNewline,
191384
launchChildProcess,
385+
waitForDebugger,
192386
writeInspectUsageAndExit,
193387
};

‎lib/internal/debugger/inspect_probe.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client');
3333
const{
3434
ensureTrailingNewline,
3535
launchChildProcess,
36+
waitForDebugger,
3637
}=require('internal/debugger/inspect_helpers');
3738

3839
const{ERR_DEBUGGER_STARTUP_ERROR}=require('internal/errors').codes;
@@ -1044,6 +1045,17 @@ class ProbeInspectorSession {
10441045
this.connected=true;
10451046

10461047
try{
1048+
try{
1049+
awaitwaitForDebugger(
1050+
this.client,
1051+
(method)=>this.callCdp(method),
1052+
);
1053+
}catch(err){
1054+
// A close event may have completed the structured report while the
1055+
// readiness helper was rejecting its disconnect race.
1056+
if(this.finished){throwkInspectorFailedSentinel;}
1057+
throwerr;
1058+
}
10471059
awaitthis.callCdp('Runtime.enable');
10481060
awaitthis.callCdp('Debugger.enable');
10491061
awaitthis.bindBreakpoints();

‎lib/internal/debugger/inspect_repl.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url');
6060
const{ customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace }=require('internal/util');
6161
const{inspect: utilInspect}=require('internal/util/inspect');
6262
const{ isObjectLiteral }=require('internal/repl/utils');
63+
const{ waitForDebugger }=require('internal/debugger/inspect_helpers');
6364
constdebuglog=require('internal/util/debuglog').debuglog('inspect');
6465

6566
constSHORTCUTS={
@@ -1204,9 +1205,13 @@ function createRepl(inspector) {
12041205
}
12051206

12061207
asyncfunctioninitAfterStart(){
1208+
constwaitForDebuggerOnStart=!!inspector.options?.script;
12071209
waitForInitialBreakRender=
1208-
!!inspector.options?.script&&
1210+
waitForDebuggerOnStart&&
12091211
process.env.NODE_INSPECT_RESUME_ON_START!=='1';
1212+
if(waitForDebuggerOnStart){
1213+
awaitwaitForDebugger(inspector.client);
1214+
}
12101215
awaitRuntime.enable();
12111216
awaitProfiler.enable();
12121217
awaitProfiler.setSamplingInterval({interval: 100});

0 commit comments

Comments
 (0)