Skip to content

Commit 9e3f518

Browse files
authored
Make session-state lock task-reentrant (#1217)
1 parent ca431bb commit 9e3f518

File tree

1 file changed

+90
-5
lines changed

1 file changed

+90
-5
lines changed

src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)