@@ -66,7 +66,7 @@ static PowerShellContextService()
66
66
#region Fields
67
67
68
68
private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils . CreateSimpleLockingSemaphore ( ) ;
69
- private readonly SemaphoreSlim sessionStateLock = AsyncUtils . CreateSimpleLockingSemaphore ( ) ;
69
+ private readonly SessionStateLock sessionStateLock = new SessionStateLock ( ) ;
70
70
71
71
private readonly OmniSharp . Extensions . LanguageServer . Protocol . Server . ILanguageServer _languageServer ;
72
72
private readonly bool isPSReadLineEnabled ;
@@ -744,7 +744,7 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
744
744
// Don't change our SessionState for ReadLine.
745
745
if ( ! executionOptions . IsReadLine )
746
746
{
747
- await this . sessionStateLock . WaitAsync ( ) . ConfigureAwait ( false ) ;
747
+ await this . sessionStateLock . AcquireForExecuteCommandAsync ( ) . ConfigureAwait ( false ) ;
748
748
shell . InvocationStateChanged += PowerShell_InvocationStateChanged ;
749
749
}
750
750
@@ -768,7 +768,7 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
768
768
if ( ! executionOptions . IsReadLine )
769
769
{
770
770
shell . InvocationStateChanged -= PowerShell_InvocationStateChanged ;
771
- this . sessionStateLock . Release ( ) ;
771
+ await this . sessionStateLock . ReleaseForExecuteCommand ( ) . ConfigureAwait ( false ) ;
772
772
}
773
773
774
774
if ( shell . HadErrors )
@@ -1242,7 +1242,7 @@ public void AbortExecution(bool shouldAbortDebugSession)
1242
1242
// Currently we try to acquire a lock on the execution status changed event.
1243
1243
// If we can't, it's because a command is executing, so we shouldn't change the status.
1244
1244
// If we can, we own the status and should fire the event.
1245
- if ( this . sessionStateLock . Wait ( 0 ) )
1245
+ if ( this . sessionStateLock . TryAcquireForDebuggerAbort ( ) )
1246
1246
{
1247
1247
try
1248
1248
{
@@ -1255,7 +1255,7 @@ public void AbortExecution(bool shouldAbortDebugSession)
1255
1255
finally
1256
1256
{
1257
1257
this . SessionState = PowerShellContextState . Ready ;
1258
- this . sessionStateLock . Release ( ) ;
1258
+ this . sessionStateLock . ReleaseForDebuggerAbort ( ) ;
1259
1259
}
1260
1260
}
1261
1261
}
@@ -2700,5 +2700,90 @@ void IHostSupportsInteractiveSession.PopRunspace()
2700
2700
}
2701
2701
2702
2702
#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
+ }
2703
2788
}
2704
2789
}
0 commit comments