From 99278d4b16f85583b7781416cf46974b2fe6ff5f Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Fri, 30 Oct 2026 23:15:08 +0100 Subject: [PATCH] fix(inspector): pause debug-brk apps before they run The app was released about a second after a frontend connected, whatever it had enabled by then, and the pause relied on a require call, which an ES module app doesn't make, while schedulePauseOnNextStatement didn't stop on the main module. The app now waits for Runtime.runIfWaitingForDebugger, or 3 seconds for frontends that don't send it, then stops on a debugger statement before any app code runs. --- NativeScript/inspector/JsV8InspectorClient.h | 4 +- NativeScript/inspector/JsV8InspectorClient.mm | 44 ++++++++++--------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/NativeScript/inspector/JsV8InspectorClient.h b/NativeScript/inspector/JsV8InspectorClient.h index 9be37467..5ea63485 100644 --- a/NativeScript/inspector/JsV8InspectorClient.h +++ b/NativeScript/inspector/JsV8InspectorClient.h @@ -61,7 +61,7 @@ class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel { void runMessageLoopOnPause(int contextGroupId) override; void quitMessageLoopOnPause() override; - void scheduleBreak(); + void pauseBeforeApp(); void registerModules(); void consoleLog(v8::Isolate* isolate, ConsoleAPIType method, @@ -106,7 +106,7 @@ class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel { uint64_t sessionConnectionId_ = 0; uint64_t frontendGeneration_ = 0; // guarded by senderMutex_ bool isWaitingForDebugger_ = false; - bool hasScheduledDebugBreak_ = false; + bool debuggerAttachedWhileWaiting_ = false; std::unique_ptr tracing_agent_; diff --git a/NativeScript/inspector/JsV8InspectorClient.mm b/NativeScript/inspector/JsV8InspectorClient.mm index 6d7fe775..5688d126 100644 --- a/NativeScript/inspector/JsV8InspectorClient.mm +++ b/NativeScript/inspector/JsV8InspectorClient.mm @@ -161,6 +161,9 @@ bool ShouldRewriteSourceMapURLs() { do { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false); } while (this->isWaitingForDebugger_); + if (this->debuggerAttachedWhileWaiting_) { + this->pauseBeforeApp(); + } }); CFRunLoopWakeUp(CFRunLoopGetMain()); }); @@ -216,14 +219,13 @@ bool ShouldRewriteSourceMapURLs() { std::function sender) { std::lock_guard lifecycle(this->frontendLifecycleMutex_); - if (this->isWaitingForDebugger_) { - this->isWaitingForDebugger_ = NO; - CFRunLoopRef runloop = CFRunLoopGetMain(); - CFRunLoopPerformBlock(runloop, (__bridge CFTypeRef)(NSRunLoopCommonModes), ^{ - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, false); - this->scheduleBreak(); + if (this->isWaitingForDebugger_ && !this->debuggerAttachedWhileWaiting_) { + this->debuggerAttachedWhileWaiting_ = true; + // Frontends that don't send Runtime.runIfWaitingForDebugger still get the app + // going, once they've had time to enable the debugger. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 3), dispatch_get_main_queue(), ^{ + this->isWaitingForDebugger_ = NO; }); - CFRunLoopWakeUp(runloop); } bool replacesFrontend; @@ -748,6 +750,13 @@ bool ShouldRewriteSourceMapURLs() { auto json_message = json::parse(message); std::string method = json_message["method"]; + // The frontend's last message before it expects the app to run, so it arrives + // after the Debugger.enable that the pause depends on. The Runtime domain still + // answers it below. + if (method == "Runtime.runIfWaitingForDebugger") { + this->isWaitingForDebugger_ = NO; + } + // Note: the Tracing domain is handled in onFrontendMessageReceived, so it // never waits on the message queue or takes the Locker below. @@ -1097,26 +1106,21 @@ json WithSessionId(json message, const std::string& sessionId) { return result; } -void JsV8InspectorClient::scheduleBreak() { +void JsV8InspectorClient::pauseBeforeApp() { Isolate* isolate = isolate_; v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); - auto context = isolate->GetCurrentContext(); + Local context = tns::Caches::Get(isolate)->GetContext(); Context::Scope context_scope(context); - if (!this->hasScheduledDebugBreak_) { - this->hasScheduledDebugBreak_ = true; - // hack: force a debugger; statement in ModuleInternal to actually break before loading the next - // (main) script... - // FIXME: find a proper fix to not need to resort to this hack. - context->Global() - ->Set(context, tns::ToV8String(isolate, "__pauseOnNextRequire"), - v8::Boolean::New(isolate, true)) - .ToChecked(); + // schedulePauseOnNextStatement doesn't break on the evaluation of the main + // module, and an ES module has no require call to stop at either. This stops + // before any app code has run, so its breakpoints can still be set. + Local