From 0fe1a8448b3a467d687c942a1c4e7e157de13577 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 24 Jan 2016 17:42:08 -0800 Subject: [PATCH] Remove Nito.AsyncEx dependency, add replacements This change removes all uses of the Nito.AsyncEx NuGet package from the solution. This change was made to remove the use of this third-party dependency so that consumers don't need to get extra open-source approval when using PowerShell Editor Services. Removing this dependency required the creation of our own custom implementations of the classes we were using from Nito.AsyncEx. These new classes were written from scratch. Resolves #115 --- .../PowerShellEditorServices.Host.x86.csproj | 13 -- .../packages.config | 4 - .../PowerShellEditorServices.Host.csproj | 13 -- .../packages.config | 4 - .../DebugAdapter/NextRequest.cs | 2 - .../SetExceptionBreakpointsRequest.cs | 2 - .../MessageProtocol/MessageDispatcher.cs | 22 +-- .../MessageProtocol/MessageWriter.cs | 1 - .../PowerShellEditorServices.Protocol.csproj | 12 -- .../Server/LanguageServer.cs | 9 +- .../packages.config | 1 - .../PowerShellEditorServices.csproj | 20 +-- .../Session/PowerShellContext.cs | 66 ++++---- .../Utility/AsyncContext.cs | 52 ++++++ .../Utility/AsyncContextThread.cs | 85 ++++++++++ .../Utility/AsyncLock.cs | 103 ++++++++++++ .../Utility/AsyncQueue.cs | 149 ++++++++++++++++++ .../Utility/ThreadSynchronizationContext.cs | 77 +++++++++ src/PowerShellEditorServices/packages.config | 4 - .../PowerShellEditorServices.Test.Host.csproj | 12 -- .../ServerTestsBase.cs | 18 ++- .../packages.config | 1 - .../Debugging/DebugServiceTests.cs | 18 +-- .../PowerShellEditorServices.Test.csproj | 14 +- .../Session/PowerShellContextTests.cs | 8 +- .../Utility/AsyncLockTests.cs | 50 ++++++ .../Utility/AsyncQueueTests.cs | 72 +++++++++ .../packages.config | 1 - 28 files changed, 665 insertions(+), 168 deletions(-) delete mode 100644 src/PowerShellEditorServices.Host.x86/packages.config delete mode 100644 src/PowerShellEditorServices.Host/packages.config create mode 100644 src/PowerShellEditorServices/Utility/AsyncContext.cs create mode 100644 src/PowerShellEditorServices/Utility/AsyncContextThread.cs create mode 100644 src/PowerShellEditorServices/Utility/AsyncLock.cs create mode 100644 src/PowerShellEditorServices/Utility/AsyncQueue.cs create mode 100644 src/PowerShellEditorServices/Utility/ThreadSynchronizationContext.cs delete mode 100644 src/PowerShellEditorServices/packages.config create mode 100644 test/PowerShellEditorServices.Test/Utility/AsyncLockTests.cs create mode 100644 test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs diff --git a/src/PowerShellEditorServices.Host.x86/PowerShellEditorServices.Host.x86.csproj b/src/PowerShellEditorServices.Host.x86/PowerShellEditorServices.Host.x86.csproj index e82969ef8..580b738c5 100644 --- a/src/PowerShellEditorServices.Host.x86/PowerShellEditorServices.Host.x86.csproj +++ b/src/PowerShellEditorServices.Host.x86/PowerShellEditorServices.Host.x86.csproj @@ -37,18 +37,6 @@ - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll - True - @@ -71,7 +59,6 @@ App.config - diff --git a/src/PowerShellEditorServices.Host.x86/packages.config b/src/PowerShellEditorServices.Host.x86/packages.config deleted file mode 100644 index d27aecea3..000000000 --- a/src/PowerShellEditorServices.Host.x86/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj index 74c21b635..6032de252 100644 --- a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj +++ b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj @@ -39,18 +39,6 @@ - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll - True - @@ -69,7 +57,6 @@ PreserveNewest - diff --git a/src/PowerShellEditorServices.Host/packages.config b/src/PowerShellEditorServices.Host/packages.config deleted file mode 100644 index d27aecea3..000000000 --- a/src/PowerShellEditorServices.Host/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs index 42dd7ba60..5cb2e6acd 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs @@ -4,8 +4,6 @@ // using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Nito.AsyncEx; -using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter { diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs index c86d33b09..3e6109fe5 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs @@ -4,8 +4,6 @@ // using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Nito.AsyncEx; -using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter { diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs index df28bd2a5..e28822653 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs @@ -4,7 +4,6 @@ // using Microsoft.PowerShell.EditorServices.Utility; -using Nito.AsyncEx; using System; using System.Collections.Generic; using System.Threading; @@ -26,6 +25,9 @@ public class MessageDispatcher private Action responseHandler; + private CancellationTokenSource messageLoopCancellationToken = + new CancellationTokenSource(); + #endregion #region Properties @@ -65,22 +67,24 @@ public MessageDispatcher( public void Start() { + // Start the main message loop thread. The Task is // not explicitly awaited because it is running on // an independent background thread. - this.messageLoopThread = new AsyncContextThread(true); + this.messageLoopThread = new AsyncContextThread("Message Dispatcher"); this.messageLoopThread - .Factory - .Run(this.ListenForMessages) + .Run(() => this.ListenForMessages(this.messageLoopCancellationToken.Token)) .ContinueWith(this.OnListenTaskCompleted); } public void Stop() { - // By disposing the thread we cancel all existing work - // and cause the thread to be destroyed. + // Stop the message loop thread if (this.messageLoopThread != null) - this.messageLoopThread.Dispose(); + { + this.messageLoopCancellationToken.Cancel(); + this.messageLoopThread.Stop(); + } } public void SetRequestHandler( @@ -181,13 +185,13 @@ protected void OnUnhandledException(Exception unhandledException) #region Private Methods - private async Task ListenForMessages() + private async Task ListenForMessages(CancellationToken cancellationToken) { this.SynchronizationContext = SynchronizationContext.Current; // Run the message loop bool isRunning = true; - while (isRunning) + while (isRunning && !cancellationToken.IsCancellationRequested) { Message newMessage = null; diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs index 3e6c0c784..a2c1db19e 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs @@ -6,7 +6,6 @@ using Microsoft.PowerShell.EditorServices.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Nito.AsyncEx; using System.IO; using System.Text; using System.Threading.Tasks; diff --git a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj index 2a8885546..cab4d9c6a 100644 --- a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj +++ b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj @@ -38,18 +38,6 @@ ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll True - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll - True - diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 0ed6be271..030f879dc 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -8,7 +8,6 @@ using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; using Microsoft.PowerShell.EditorServices.Protocol.Messages; using Microsoft.PowerShell.EditorServices.Utility; -using Nito.AsyncEx; using System; using System.Collections.Generic; using System.IO; @@ -780,7 +779,7 @@ private Task RunScriptDiagnostics( if (!this.currentSettings.ScriptAnalysis.Enable.Value) { // If the user has disabled script analysis, skip it entirely - return TaskConstants.Completed; + return Task.FromResult(true); } // If there's an existing task, attempt to cancel it @@ -806,7 +805,9 @@ private Task RunScriptDiagnostics( "Exception while cancelling analysis task:\n\n{0}", e.ToString())); - return TaskConstants.Canceled; + TaskCompletionSource cancelTask = new TaskCompletionSource(); + cancelTask.SetCanceled(); + return cancelTask.Task; } // Create a fresh cancellation token and then start the task. @@ -826,7 +827,7 @@ private Task RunScriptDiagnostics( TaskCreationOptions.None, TaskScheduler.Default); - return TaskConstants.Completed; + return Task.FromResult(true); } private static async Task DelayThenInvokeDiagnostics( diff --git a/src/PowerShellEditorServices.Protocol/packages.config b/src/PowerShellEditorServices.Protocol/packages.config index 5853f7f1e..505e58836 100644 --- a/src/PowerShellEditorServices.Protocol/packages.config +++ b/src/PowerShellEditorServices.Protocol/packages.config @@ -1,5 +1,4 @@  - \ No newline at end of file diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 4c5a278f4..c78b3f7bc 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -38,18 +38,6 @@ bin\Release\Microsoft.PowerShell.EditorServices.XML - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll - True - @@ -109,7 +97,12 @@ + + + + + @@ -119,9 +112,6 @@ - - - {f4bde3d0-3eef-4157-8a3e-722df7adef60} diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index aab33fe39..4690e585a 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -5,7 +5,6 @@ using Microsoft.PowerShell.EditorServices.Console; using Microsoft.PowerShell.EditorServices.Utility; -using Nito.AsyncEx; using System; using System.Collections; using System.Globalization; @@ -44,8 +43,7 @@ public class PowerShellContext : IDisposable private TaskCompletionSource pipelineResultTask; private object runspaceMutex = new object(); - private RunspaceHandle currentRunspaceHandle; - private IAsyncWaitQueue runspaceWaitQueue = new DefaultAsyncWaitQueue(); + private AsyncQueue runspaceWaitQueue = new AsyncQueue(); #endregion @@ -115,6 +113,7 @@ public PowerShellContext() this.ownsInitialRunspace = true; this.Initialize(runspace); + } /// @@ -164,6 +163,10 @@ private void Initialize(Runspace initialRunspace) #endif this.SessionState = PowerShellContextState.Ready; + + // Now that the runspace is ready, enqueue it for first use + RunspaceHandle runspaceHandle = new RunspaceHandle(this.currentRunspace, this); + this.runspaceWaitQueue.EnqueueAsync(runspaceHandle).Wait(); } private Version GetPowerShellVersion() @@ -198,21 +201,19 @@ private Version GetPowerShellVersion() /// A RunspaceHandle instance that gives access to the session's runspace. public Task GetRunspaceHandle() { - lock (this.runspaceMutex) - { - if (this.currentRunspaceHandle == null) - { - this.currentRunspaceHandle = new RunspaceHandle(this.currentRunspace, this); - TaskCompletionSource tcs = new TaskCompletionSource(); - tcs.SetResult(this.currentRunspaceHandle); - return tcs.Task; - } - else - { - // TODO: Use CancellationToken? - return this.runspaceWaitQueue.Enqueue(); - } - } + return this.GetRunspaceHandle(CancellationToken.None); + } + + /// + /// Gets a RunspaceHandle for the session's runspace. This + /// handle is used to gain temporary ownership of the runspace + /// so that commands can be executed against it directly. + /// + /// A CancellationToken that can be used to cancel the request. + /// A RunspaceHandle instance that gives access to the session's runspace. + public Task GetRunspaceHandle(CancellationToken cancellationToken) + { + return this.runspaceWaitQueue.DequeueAsync(cancellationToken); } /// @@ -532,30 +533,17 @@ internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) { Validate.IsNotNull("runspaceHandle", runspaceHandle); - IDisposable dequeuedTask = null; - - lock (this.runspaceMutex) + if (this.runspaceWaitQueue.IsEmpty) { - if (runspaceHandle != this.currentRunspaceHandle) - { - throw new InvalidOperationException("Released runspace handle was not the current handle."); - } - - this.currentRunspaceHandle = null; - - if (!this.runspaceWaitQueue.IsEmpty) - { - this.currentRunspaceHandle = new RunspaceHandle(this.currentRunspace, this); - dequeuedTask = - this.runspaceWaitQueue.Dequeue( - this.currentRunspaceHandle); - } + var newRunspaceHandle = new RunspaceHandle(this.currentRunspace, this); + this.runspaceWaitQueue.EnqueueAsync(newRunspaceHandle).Wait(); } - - // If a queued task was dequeued, call Dispose to cause it to be executed. - if (dequeuedTask != null) + else { - dequeuedTask.Dispose(); + // Write the situation to the log since this shouldn't happen + Logger.Write( + LogLevel.Error, + "The PowerShellContext.runspaceWaitQueue has more than one item"); } } diff --git a/src/PowerShellEditorServices/Utility/AsyncContext.cs b/src/PowerShellEditorServices/Utility/AsyncContext.cs new file mode 100644 index 000000000..421ca3d96 --- /dev/null +++ b/src/PowerShellEditorServices/Utility/AsyncContext.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Simplifies the setup of a SynchronizationContext for the use + /// of async calls in the current thread. + /// + public static class AsyncContext + { + /// + /// Starts a new ThreadSynchronizationContext, attaches it to + /// the thread, and then runs the given async main function. + /// + /// + /// The Task-returning Func which represents the "main" function + /// for the thread. + /// + public static void Start(Func asyncMainFunc) + { + // Is there already a synchronization context? + if (SynchronizationContext.Current != null) + { + throw new InvalidOperationException( + "A SynchronizationContext is already assigned on this thread."); + } + + // Create and register a synchronization context for this thread + var threadSyncContext = new ThreadSynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(threadSyncContext); + + // Get the main task and act on its completion + Task asyncMainTask = asyncMainFunc(); + asyncMainTask.ContinueWith( + t => threadSyncContext.EndLoop(), + TaskScheduler.Default); + + // Start the synchronization context's request loop and + // wait for the main task to complete + threadSyncContext.RunLoopOnCurrentThread(); + asyncMainTask.GetAwaiter().GetResult(); + } + } +} + diff --git a/src/PowerShellEditorServices/Utility/AsyncContextThread.cs b/src/PowerShellEditorServices/Utility/AsyncContextThread.cs new file mode 100644 index 000000000..92629437a --- /dev/null +++ b/src/PowerShellEditorServices/Utility/AsyncContextThread.cs @@ -0,0 +1,85 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides a simplified interface for creating a new thread + /// and establishing an AsyncContext in it. + /// + public class AsyncContextThread + { + #region Private Fields + + private Task threadTask; + private string threadName; + private CancellationTokenSource threadCancellationToken = + new CancellationTokenSource(); + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the AsyncContextThread class. + /// + /// + /// The name of the thread for debugging purposes. + /// + public AsyncContextThread(string threadName) + { + this.threadName = threadName; + } + + #endregion + + #region Public Methods + + /// + /// Runs a task on the AsyncContextThread. + /// + /// + /// A Func which returns the task to be run on the thread. + /// + /// + /// A Task which can be used to monitor the thread for completion. + /// + public Task Run(Func taskReturningFunc) + { + // Start up a long-running task with the action as the + // main entry point for the thread + this.threadTask = + Task.Factory.StartNew( + () => + { + // Set the thread's name to help with debugging + Thread.CurrentThread.Name = "AsyncContextThread: " + this.threadName; + + // Set up an AsyncContext to run the task + AsyncContext.Start(taskReturningFunc); + }, + this.threadCancellationToken.Token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + + return this.threadTask; + } + + /// + /// Stops the thread task. + /// + public void Stop() + { + this.threadCancellationToken.Cancel(); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices/Utility/AsyncLock.cs b/src/PowerShellEditorServices/Utility/AsyncLock.cs new file mode 100644 index 000000000..eee894d9c --- /dev/null +++ b/src/PowerShellEditorServices/Utility/AsyncLock.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides a simple wrapper over a SemaphoreSlim to allow + /// synchronization locking inside of async calls. Cannot be + /// used recursively. + /// + public class AsyncLock + { + #region Fields + + private Task lockReleaseTask; + private SemaphoreSlim lockSemaphore = new SemaphoreSlim(1, 1); + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the AsyncLock class. + /// + public AsyncLock() + { + this.lockReleaseTask = + Task.FromResult( + (IDisposable)new LockReleaser(this)); + } + + #endregion + + #region Public Methods + + /// + /// Locks + /// + /// A task which has an IDisposable + public Task LockAsync() + { + return this.LockAsync(CancellationToken.None); + } + + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. The wait may be cancelled with the + /// given CancellationToken. + /// + /// + /// A CancellationToken which can be used to cancel the lock. + /// + /// + public Task LockAsync(CancellationToken cancellationToken) + { + Task waitTask = lockSemaphore.WaitAsync(cancellationToken); + + return waitTask.IsCompleted ? + this.lockReleaseTask : + waitTask.ContinueWith( + (t, releaser) => + { + return (IDisposable)releaser; + }, + this.lockReleaseTask.Result, + cancellationToken, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + #endregion + + #region Private Classes + + /// + /// Provides an IDisposable wrapper around an AsyncLock so + /// that it can easily be used inside of a 'using' block. + /// + private class LockReleaser : IDisposable + { + private AsyncLock lockToRelease; + + internal LockReleaser(AsyncLock lockToRelease) + { + this.lockToRelease = lockToRelease; + } + + public void Dispose() + { + this.lockToRelease.lockSemaphore.Release(); + } + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices/Utility/AsyncQueue.cs b/src/PowerShellEditorServices/Utility/AsyncQueue.cs new file mode 100644 index 000000000..c26265b52 --- /dev/null +++ b/src/PowerShellEditorServices/Utility/AsyncQueue.cs @@ -0,0 +1,149 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides a synchronized queue which can be used from within async + /// operations. This is primarily used for producer/consumer scenarios. + /// + /// The type of item contained in the queue. + public class AsyncQueue + { + #region Private Fields + + private AsyncLock queueLock = new AsyncLock(); + private Queue itemQueue; + private Queue> requestQueue; + + #endregion + + #region Properties + + /// + /// Returns true if the queue is currently empty. + /// + public bool IsEmpty { get; private set; } + + #endregion + + #region Constructors + + /// + /// Initializes an empty instance of the AsyncQueue class. + /// + public AsyncQueue() : this(Enumerable.Empty()) + { + } + + /// + /// Initializes an instance of the AsyncQueue class, pre-populated + /// with the given collection of items. + /// + /// + /// An IEnumerable containing the initial items with which the queue will + /// be populated. + /// + public AsyncQueue(IEnumerable initialItems) + { + this.itemQueue = new Queue(initialItems); + this.requestQueue = new Queue>(); + } + + #endregion + + #region Public Methods + + /// + /// Enqueues an item onto the end of the queue. + /// + /// The item to be added to the queue. + /// + /// A Task which can be awaited until the synchronized enqueue + /// operation completes. + /// + public async Task EnqueueAsync(T item) + { + using (await queueLock.LockAsync()) + { + if (this.requestQueue.Count > 0) + { + // There are requests waiting, immediately dispatch the item + TaskCompletionSource requestTaskSource = this.requestQueue.Dequeue(); + requestTaskSource.SetResult(item); + } + else + { + // No requests waiting, queue the item for a later request + this.itemQueue.Enqueue(item); + this.IsEmpty = false; + } + } + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. + /// + /// + /// A Task which can be awaited until a value can be dequeued. + /// + public Task DequeueAsync() + { + return this.DequeueAsync(CancellationToken.None); + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. The wait can be cancelled + /// using the given CancellationToken. + /// + /// + /// A CancellationToken with which a dequeue wait can be cancelled. + /// + /// + /// A Task which can be awaited until a value can be dequeued. + /// + public async Task DequeueAsync(CancellationToken cancellationToken) + { + Task requestTask; + + using (await queueLock.LockAsync(cancellationToken)) + { + if (this.itemQueue.Count > 0) + { + // Items are waiting to be taken so take one immediately + T item = this.itemQueue.Dequeue(); + this.IsEmpty = this.itemQueue.Count == 0; + + return item; + } + else + { + // Queue the request for the next item + var requestTaskSource = new TaskCompletionSource(); + this.requestQueue.Enqueue(requestTaskSource); + + // Register the wait task for cancel notifications + cancellationToken.Register( + () => requestTaskSource.TrySetCanceled()); + + requestTask = requestTaskSource.Task; + } + } + + // Wait for the request task to complete outside of the lock + return await requestTask; + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices/Utility/ThreadSynchronizationContext.cs b/src/PowerShellEditorServices/Utility/ThreadSynchronizationContext.cs new file mode 100644 index 000000000..03b57bee3 --- /dev/null +++ b/src/PowerShellEditorServices/Utility/ThreadSynchronizationContext.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides a SynchronizationContext implementation that can be used + /// in console applications or any thread which doesn't have its + /// own SynchronizationContext. + /// + public class ThreadSynchronizationContext : SynchronizationContext + { + #region Private Fields + + private BlockingCollection> requestQueue = + new BlockingCollection>(); + + #endregion + + #region Constructors + + /// + /// Posts a request for execution to the SynchronizationContext. + /// This will be executed on the SynchronizationContext's thread. + /// + /// + /// The callback to be invoked on the SynchronizationContext's thread. + /// + /// + /// A state object to pass along to the callback when executed through + /// the SynchronizationContext. + /// + public override void Post(SendOrPostCallback callback, object state) + { + // Add the request to the queue + this.requestQueue.Add( + new Tuple( + callback, state)); + } + + #endregion + + #region Public Methods + + /// + /// Starts the SynchronizationContext message loop on the current thread. + /// + public void RunLoopOnCurrentThread() + { + Tuple request; + + while (this.requestQueue.TryTake(out request, Timeout.Infinite)) + { + // Invoke the request's callback + request.Item1(request.Item2); + } + } + + /// + /// Ends the SynchronizationContext message loop. + /// + public void EndLoop() + { + // Tell the blocking queue that we're done + this.requestQueue.CompleteAdding(); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices/packages.config b/src/PowerShellEditorServices/packages.config deleted file mode 100644 index d27aecea3..000000000 --- a/src/PowerShellEditorServices/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj b/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj index 0a600ce67..373881fec 100644 --- a/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj +++ b/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj @@ -39,18 +39,6 @@ ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll True - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll - True - diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index 4ec18efc6..c54ca5886 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -1,6 +1,11 @@ -using Microsoft.PowerShell.EditorServices.Protocol.Client; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.Client; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Nito.AsyncEx; +using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Concurrent; using System.Threading.Tasks; @@ -11,8 +16,8 @@ public class ServerTestsBase { protected ProtocolClient protocolClient; - private ConcurrentDictionary> eventQueuePerType = - new ConcurrentDictionary>(); + private ConcurrentDictionary> eventQueuePerType = + new ConcurrentDictionary>(); protected Task SendRequest( RequestType requestType, @@ -37,7 +42,7 @@ protected void QueueEventsForType(EventType eventType) var eventQueue = this.eventQueuePerType.AddOrUpdate( eventType.MethodName, - new AsyncProducerConsumerQueue(), + new AsyncQueue(), (key, queue) => queue); this.protocolClient.SetEventHandler( @@ -55,7 +60,7 @@ protected async Task WaitForEvent( Task eventTask = null; // Use the event queue if one has been registered - AsyncProducerConsumerQueue eventQueue = null; + AsyncQueue eventQueue = null; if (this.eventQueuePerType.TryGetValue(eventType.MethodName, out eventQueue)) { eventTask = @@ -101,3 +106,4 @@ protected async Task WaitForEvent( } } } + diff --git a/test/PowerShellEditorServices.Test.Host/packages.config b/test/PowerShellEditorServices.Test.Host/packages.config index 372977744..b656fafe2 100644 --- a/test/PowerShellEditorServices.Test.Host/packages.config +++ b/test/PowerShellEditorServices.Test.Host/packages.config @@ -1,7 +1,6 @@  - diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index df38d42e8..ef29e8c2f 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Nito.AsyncEx; +using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Linq; using System.Management.Automation; @@ -22,10 +22,10 @@ public class DebugServiceTests : IDisposable private PowerShellContext powerShellContext; private SynchronizationContext runnerContext; - private AsyncProducerConsumerQueue debuggerStoppedQueue = - new AsyncProducerConsumerQueue(); - private AsyncProducerConsumerQueue sessionStateQueue = - new AsyncProducerConsumerQueue(); + private AsyncQueue debuggerStoppedQueue = + new AsyncQueue(); + private AsyncQueue sessionStateQueue = + new AsyncQueue(); public DebugServiceTests() { @@ -45,12 +45,12 @@ public DebugServiceTests() this.runnerContext = SynchronizationContext.Current; } - void powerShellContext_SessionStateChanged(object sender, SessionStateChangedEventArgs e) + async void powerShellContext_SessionStateChanged(object sender, SessionStateChangedEventArgs e) { // Skip all transitions except those back to 'Ready' if (e.NewSessionState == PowerShellContextState.Ready) { - this.sessionStateQueue.Enqueue(e); + await this.sessionStateQueue.EnqueueAsync(e); } } @@ -59,9 +59,9 @@ void debugService_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) // TODO: Needed? } - void debugService_DebuggerStopped(object sender, DebuggerStopEventArgs e) + async void debugService_DebuggerStopped(object sender, DebuggerStopEventArgs e) { - this.debuggerStoppedQueue.Enqueue(e); + await this.debuggerStoppedQueue.EnqueueAsync(e); } public void Dispose() diff --git a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj index af58edc48..261e4fd1b 100644 --- a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj +++ b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj @@ -37,18 +37,6 @@ 4 - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll - True - - - ..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll - True - @@ -83,6 +71,8 @@ + + diff --git a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs index 57f265beb..d00e5f94e 100644 --- a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Nito.AsyncEx; +using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; using System.Linq; @@ -16,7 +16,7 @@ namespace Microsoft.PowerShell.EditorServices.Test.Console public class PowerShellContextTests : IDisposable { private PowerShellContext powerShellContext; - private AsyncProducerConsumerQueue stateChangeQueue; + private AsyncQueue stateChangeQueue; private const string DebugTestFilePath = @"..\..\..\PowerShellEditorServices.Test.Shared\Debugging\DebugTest.ps1"; @@ -25,7 +25,7 @@ public PowerShellContextTests() { this.powerShellContext = new PowerShellContext(); this.powerShellContext.SessionStateChanged += OnSessionStateChanged; - this.stateChangeQueue = new AsyncProducerConsumerQueue(); + this.stateChangeQueue = new AsyncQueue(); } public void Dispose() @@ -106,7 +106,7 @@ private async Task AssertStateChange(PowerShellContextState expectedState) private void OnSessionStateChanged(object sender, SessionStateChangedEventArgs e) { - this.stateChangeQueue.Enqueue(e); + this.stateChangeQueue.EnqueueAsync(e).Wait(); } #endregion diff --git a/test/PowerShellEditorServices.Test/Utility/AsyncLockTests.cs b/test/PowerShellEditorServices.Test/Utility/AsyncLockTests.cs new file mode 100644 index 000000000..5b6a02e63 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Utility/AsyncLockTests.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.PowerShell.EditorServices.Test.Utility +{ + public class AsyncLockTests + { + [Fact] + public async Task AsyncLockSynchronizesAccess() + { + AsyncLock asyncLock = new AsyncLock(); + + Task lockOne = asyncLock.LockAsync(); + Task lockTwo = asyncLock.LockAsync(); + + Assert.Equal(TaskStatus.RanToCompletion, lockOne.Status); + Assert.Equal(TaskStatus.WaitingForActivation, lockTwo.Status); + lockOne.Result.Dispose(); + + await lockTwo; + Assert.Equal(TaskStatus.RanToCompletion, lockTwo.Status); + } + + [Fact] + public void AsyncLockCancelsWhenRequested() + { + CancellationTokenSource cts = new CancellationTokenSource(); + AsyncLock asyncLock = new AsyncLock(); + + Task lockOne = asyncLock.LockAsync(); + Task lockTwo = asyncLock.LockAsync(cts.Token); + + // Cancel the second lock before the first is released + cts.Cancel(); + lockOne.Result.Dispose(); + + Assert.Equal(TaskStatus.RanToCompletion, lockOne.Status); + Assert.Equal(TaskStatus.Canceled, lockTwo.Status); + } + } +} + diff --git a/test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs b/test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs new file mode 100644 index 000000000..19e4106d2 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.PowerShell.EditorServices.Test.Utility +{ + public class AsyncQueueTests + { + [Fact] + public async Task AsyncQueueSynchronizesAccess() + { + ConcurrentBag outputItems = new ConcurrentBag(); + AsyncQueue inputQueue = new AsyncQueue(Enumerable.Range(0, 100)); + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + try + { + // Start 5 consumers + await Task.WhenAll( + Task.Run(() => ConsumeItems(inputQueue, outputItems, cancellationTokenSource.Token)), + Task.Run(() => ConsumeItems(inputQueue, outputItems, cancellationTokenSource.Token)), + Task.Run(() => ConsumeItems(inputQueue, outputItems, cancellationTokenSource.Token)), + Task.Run(() => ConsumeItems(inputQueue, outputItems, cancellationTokenSource.Token)), + Task.Run(() => ConsumeItems(inputQueue, outputItems, cancellationTokenSource.Token)), + Task.Run( + async () => + { + // Wait for a bit and then add more items to the queue + await Task.Delay(250); + + foreach (var i in Enumerable.Range(100, 200)) + { + await inputQueue.EnqueueAsync(i); + } + + // Cancel the waiters + cancellationTokenSource.Cancel(); + })); + } + catch (TaskCanceledException) + { + // Do nothing, this is expected. + } + + // At this point, numbers 0 through 299 should be in the outputItems + IEnumerable expectedItems = Enumerable.Range(0, 300); + Assert.Equal(0, expectedItems.Except(outputItems).Count()); + } + + private async Task ConsumeItems( + AsyncQueue inputQueue, + ConcurrentBag outputItems, + CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + int consumedItem = await inputQueue.DequeueAsync(cancellationToken); + outputItems.Add(consumedItem); + } + } + } +} + diff --git a/test/PowerShellEditorServices.Test/packages.config b/test/PowerShellEditorServices.Test/packages.config index e28241059..3fe1237f0 100644 --- a/test/PowerShellEditorServices.Test/packages.config +++ b/test/PowerShellEditorServices.Test/packages.config @@ -3,7 +3,6 @@ -