From 4114c5b46eaddafbf1bf1f13042ce0ac04410529 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Fri, 26 Jun 2026 12:52:05 -0700 Subject: [PATCH] Fix UI-thread hang when resuming the target during launch AD7Engine.ContinueFromSynchronousEvent (the AD7ProgramCreateEvent path) dispatched ResumeFromLaunch through the blocking WorkerThread.RunOperation, coupling the VS UI/SDM thread to debugger I/O. Over a slow or remote (SSH) transport, AD7Engine.ContinueFromSynchronousEvent + ResumeFromLaunch could stall for a long time or indefinitely, producing a Watson hang. - Add WorkerThread.PostAsyncOperation, a non-blocking dispatch that claims the running-op slot (so later AD7 operations still serialize behind it) but returns without waiting for completion. Faults from the operation are routed to an onError callback from both the synchronous and async completion paths. - ContinueFromSynchronousEvent now posts ResumeFromLaunch via PostAsyncOperation and returns S_OK immediately, so the UI thread is no longer blocked. Resume faults report through SendStartDebuggingError + Terminate via onError, and synchronous failures (OnLoadComplete or the dispatch itself) are still handled locally, since the SDM drops errors returned from this method. - [x] Built and tested locally - [x] Debugged through WSL Linux gdb and the async resume completed. --- src/MIDebugEngine/AD7.Impl/AD7Engine.cs | 28 +++--- .../Engine.Impl/OperationThread.cs | 96 +++++++++++++++++++ 2 files changed, 108 insertions(+), 16 deletions(-) diff --git a/src/MIDebugEngine/AD7.Impl/AD7Engine.cs b/src/MIDebugEngine/AD7.Impl/AD7Engine.cs index ffb8146b8..65dcf5681 100755 --- a/src/MIDebugEngine/AD7.Impl/AD7Engine.cs +++ b/src/MIDebugEngine/AD7.Impl/AD7Engine.cs @@ -319,28 +319,24 @@ public int ContinueFromSynchronousEvent(IDebugEvent2 eventObject) { if (eventObject is AD7ProgramCreateEvent) { - Exception exception = null; - try { _engineCallback.OnLoadComplete(); - // At this point breakpoints and exception settings have been sent down, so we can resume the target - _pollThread.RunOperation(() => - { - return _debuggedProcess.ResumeFromLaunch(); - }); + + // Resume the target on the worker thread without blocking the UI thread this runs on. + // Resume faults are reported via onError, since the SDM drops errors returned from here. + _pollThread.PostAsyncOperation( + () => _debuggedProcess.ResumeFromLaunch(), + (exception) => + { + SendStartDebuggingError(exception); + _debuggedProcess.Terminate(); + }); } catch (Exception e) { - exception = e; - // Return from the catch block so that we can let the exception unwind - the stack can get kind of big - } - - if (exception != null) - { - // If something goes wrong, report the error and then stop debugging. The SDM will drop errors - // from ContinueFromSynchronousEvent, so we want to deal with them ourself. - SendStartDebuggingError(exception); + // Report synchronous failures ourselves, since the SDM drops errors returned from here. + SendStartDebuggingError(e); _debuggedProcess.Terminate(); } diff --git a/src/MIDebugEngine/Engine.Impl/OperationThread.cs b/src/MIDebugEngine/Engine.Impl/OperationThread.cs index 2c5d71a24..b59843c20 100755 --- a/src/MIDebugEngine/Engine.Impl/OperationThread.cs +++ b/src/MIDebugEngine/Engine.Impl/OperationThread.cs @@ -37,6 +37,12 @@ private class OperationDescriptor /// Delegate that was added via 'RunOperation'. Is of type 'Operation' or 'AsyncOperation' /// public readonly Delegate Target; + + /// + /// Handler invoked on the worker thread if the operation faults. Only set for PostAsyncOperation. + /// + public Action ErrorHandler; + public ExceptionDispatchInfo ExceptionDispatchInfo; public Task Task; private bool _isStarted; @@ -121,6 +127,20 @@ public void RunOperation(string text, CancellationTokenSource canTokenSource, As SetOperationInternalWithProgress(op, text, canTokenSource); } + /// + /// Send an async operation to the worker thread, claiming the running-op slot but returning without + /// waiting for it to complete. Later operations still serialize behind it. Faults are reported to + /// . + /// + public void PostAsyncOperation(AsyncOperation op, Action onError) + { + if (op == null) + throw new ArgumentNullException(nameof(op)); + if (onError == null) + throw new ArgumentNullException(nameof(onError)); + + PostAsyncOperationInternal(op, onError); + } public void Close() { @@ -189,6 +209,27 @@ internal void SetOperationInternalWithProgress(AsyncProgressOperation op, string } } } + + internal void PostAsyncOperationInternal(AsyncOperation op, Action onError) + { + // If this is called on the Worker thread it will deadlock + Debug.Assert(!IsPollThread()); + + while (true) + { + if (_isClosed) + throw new ObjectDisposedException("WorkerThread"); + + // Wait for the slot so this serializes behind any in-flight operation. + _runningOpCompleteEvent.WaitOne(); + + if (TryPostAsyncOperationInternal(op, onError)) + { + return; + } + } + } + public void PostOperation(Operation op) { if (op == null) @@ -276,7 +317,28 @@ private bool TrySetOperationInternalWithProgress(AsyncProgressOperation op, stri return false; } + private bool TryPostAsyncOperationInternal(AsyncOperation op, Action onError) + { + lock (_eventLock) + { + if (_isClosed) + throw new ObjectDisposedException("WorkerThread"); + + if (_runningOp == null) + { + _runningOpCompleteEvent.Reset(); + + _runningOp = new OperationDescriptor(op) { ErrorHandler = onError }; + + _opSet.Set(); + // Unlike TrySetOperationInternal, do not wait for completion; faults are routed to onError. + return true; + } + } + + return false; + } // Thread routine for the poll loop. It handles calls coming in from the debug engine as well as polling for debug events. private void ThreadFunc() @@ -333,11 +395,20 @@ private void ThreadFunc() if (!completeAsync) { + // Capture the fault before clearing the slot so a synchronous throw is still reported. + Action errorHandler = runningOp.ErrorHandler; + ExceptionDispatchInfo exceptionDispatchInfo = runningOp.ExceptionDispatchInfo; + runningOp.MarkComplete(); Debug.Assert(_runningOp == runningOp, "How did m_runningOp change?"); _runningOp = null; _runningOpCompleteEvent.Set(); + + if (errorHandler != null && exceptionDispatchInfo != null) + { + InvokeErrorHandler(errorHandler, exceptionDispatchInfo.SourceException); + } } } @@ -389,8 +460,33 @@ internal void OnAsyncRunningOpComplete(Task t) } } _runningOp.MarkComplete(); + + // Capture the fault before clearing the slot so it is routed to the handler, not discarded. + Action errorHandler = _runningOp.ErrorHandler; + ExceptionDispatchInfo exceptionDispatchInfo = _runningOp.ExceptionDispatchInfo; + _runningOp = null; _runningOpCompleteEvent.Set(); + + if (errorHandler != null && exceptionDispatchInfo != null) + { + InvokeErrorHandler(errorHandler, exceptionDispatchInfo.SourceException); + } + } + + private void InvokeErrorHandler(Action errorHandler, Exception exception) + { + try + { + errorHandler(exception); + } + catch (Exception e) when (ExceptionHelper.BeforeCatch(e, Logger, reportOnlyCorrupting: false)) + { + if (PostedOperationErrorEvent != null) + { + PostedOperationErrorEvent(this, e); + } + } } internal bool IsPollThread()