@@ -66,7 +66,7 @@ static PowerShellContextService()
6666 #region Fields
6767
6868 private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils . CreateSimpleLockingSemaphore ( ) ;
69- private readonly SemaphoreSlim sessionStateLock = AsyncUtils . CreateSimpleLockingSemaphore ( ) ;
69+ private readonly SessionStateLock sessionStateLock = new SessionStateLock ( ) ;
7070
7171 private readonly OmniSharp . Extensions . LanguageServer . Protocol . Server . ILanguageServer _languageServer ;
7272 private readonly bool isPSReadLineEnabled ;
@@ -744,7 +744,7 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
744744 // Don't change our SessionState for ReadLine.
745745 if ( ! executionOptions . IsReadLine )
746746 {
747- await this . sessionStateLock . WaitAsync ( ) . ConfigureAwait ( false ) ;
747+ await this . sessionStateLock . AcquireForExecuteCommandAsync ( ) . ConfigureAwait ( false ) ;
748748 shell . InvocationStateChanged += PowerShell_InvocationStateChanged ;
749749 }
750750
@@ -768,7 +768,7 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
768768 if ( ! executionOptions . IsReadLine )
769769 {
770770 shell . InvocationStateChanged -= PowerShell_InvocationStateChanged ;
771- this . sessionStateLock . Release ( ) ;
771+ await this . sessionStateLock . ReleaseForExecuteCommand ( ) . ConfigureAwait ( false ) ;
772772 }
773773
774774 if ( shell . HadErrors )
@@ -1242,7 +1242,7 @@ public void AbortExecution(bool shouldAbortDebugSession)
12421242 // Currently we try to acquire a lock on the execution status changed event.
12431243 // If we can't, it's because a command is executing, so we shouldn't change the status.
12441244 // If we can, we own the status and should fire the event.
1245- if ( this . sessionStateLock . Wait ( 0 ) )
1245+ if ( this . sessionStateLock . TryAcquireForDebuggerAbort ( ) )
12461246 {
12471247 try
12481248 {
@@ -1255,7 +1255,7 @@ public void AbortExecution(bool shouldAbortDebugSession)
12551255 finally
12561256 {
12571257 this . SessionState = PowerShellContextState . Ready ;
1258- this . sessionStateLock . Release ( ) ;
1258+ this . sessionStateLock . ReleaseForDebuggerAbort ( ) ;
12591259 }
12601260 }
12611261 }
@@ -2700,5 +2700,90 @@ void IHostSupportsInteractiveSession.PopRunspace()
27002700 }
27012701
27022702 #endregion
2703+
2704+ /// <summary>
2705+ /// Encapsulates the locking semantics hacked together for debugging to work.
2706+ /// This allows ExecuteCommandAsync locking to work "re-entrantly",
2707+ /// while making sure that a debug abort won't corrupt state.
2708+ /// </summary>
2709+ private class SessionStateLock
2710+ {
2711+ /// <summary>
2712+ /// The actual lock to acquire to modify the session state of the PowerShellContextService.
2713+ /// </summary>
2714+ private readonly SemaphoreSlim _sessionStateLock ;
2715+
2716+ /// <summary>
2717+ /// A lock used by this class to ensure that count incrementing and session state locking happens atomically.
2718+ /// </summary>
2719+ private readonly SemaphoreSlim _internalLock ;
2720+
2721+ /// <summary>
2722+ /// A count of how re-entrant the current execute command lock call is,
2723+ /// so we can effectively use it as a two-way semaphore.
2724+ /// </summary>
2725+ private int _executeCommandLockCount ;
2726+
2727+ public SessionStateLock ( )
2728+ {
2729+ _sessionStateLock = AsyncUtils . CreateSimpleLockingSemaphore ( ) ;
2730+ _internalLock = AsyncUtils . CreateSimpleLockingSemaphore ( ) ;
2731+ _executeCommandLockCount = 0 ;
2732+ }
2733+
2734+ public async Task AcquireForExecuteCommandAsync ( )
2735+ {
2736+ // Algorithm here is:
2737+ // - Acquire the internal lock to keep operations atomic
2738+ // - Increment the number of lock holders
2739+ // - If we're the only one, acquire the lock
2740+ // - Release the internal lock
2741+
2742+ await _internalLock . WaitAsync ( ) . ConfigureAwait ( false ) ;
2743+ try
2744+ {
2745+ if ( _executeCommandLockCount ++ == 0 )
2746+ {
2747+ await _sessionStateLock . WaitAsync ( ) . ConfigureAwait ( false ) ;
2748+ }
2749+ }
2750+ finally
2751+ {
2752+ _internalLock . Release ( ) ;
2753+ }
2754+ }
2755+
2756+ public bool TryAcquireForDebuggerAbort ( )
2757+ {
2758+ return _sessionStateLock . Wait ( 0 ) ;
2759+ }
2760+
2761+ public async Task ReleaseForExecuteCommand ( )
2762+ {
2763+ // Algorithm here is the opposite of the acquisition algorithm:
2764+ // - Acquire the internal lock to ensure the operation is atomic
2765+ // - Decrement the lock holder count
2766+ // - If we were the last ones, release the lock
2767+ // - Release the internal lock
2768+
2769+ await _internalLock . WaitAsync ( ) . ConfigureAwait ( false ) ;
2770+ try
2771+ {
2772+ if ( -- _executeCommandLockCount == 0 )
2773+ {
2774+ _sessionStateLock . Release ( ) ;
2775+ }
2776+ }
2777+ finally
2778+ {
2779+ _internalLock . Release ( ) ;
2780+ }
2781+ }
2782+
2783+ public void ReleaseForDebuggerAbort ( )
2784+ {
2785+ _sessionStateLock . Release ( ) ;
2786+ }
2787+ }
27032788 }
27042789}
0 commit comments