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 @@ -