From 11589434185c887a43b3c05ee09d58b7a33f868b Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 16 Oct 2018 22:50:05 -0300 Subject: [PATCH 01/31] Add 5 InvokeAsync overloads --- .../engine/hostifaces/PowerShell.cs | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index c6839716d8f..71413c3eae0 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -14,6 +14,7 @@ using Dbg = System.Management.Automation.Diagnostics; using System.Diagnostics; using Microsoft.Management.Infrastructure; +using System.Threading.Tasks; #pragma warning disable 1634, 1691 // Stops compiler from warning about unknown warnings @@ -3010,6 +3011,199 @@ public IAsyncResult BeginInvoke(PSDataCollection input, return CoreInvokeAsync(input, output, settings, callback, state, null); } + /// + /// Invoke the asynchronously. + /// Use await to wait for the command to complete and obtain the output of the command. + /// + /// + /// Cannot perform the operation because the command is already started. + /// Stop the command and try the operation again. + /// (or) + /// No command is added. + /// + /// + /// Object is disposed. + /// + public async Task> InvokeAsync() + => await Task>.Factory.FromAsync(BeginInvoke(), pResult => EndInvoke(pResult)); + + /// + /// Invoke the asynchronously. + /// Use await to wait for the command to complete and obtain the output of the command. + /// + /// + /// When invoked using InvokeAsync, invocation doesn't + /// finish until Input is closed. Caller of InvokeAsync must + /// close the input buffer after all input has been written to + /// input buffer. Input buffer is closed by calling + /// Close() method. + /// + /// If you want this command to execute as a standalone cmdlet + /// (that is, using command-line parameters only), + /// be sure to call Close() before calling InvokeAsync(). Otherwise, + /// the command will be executed as though it had external input. + /// If you observe that the command isn't doing anything, + /// this may be the reason. + /// + /// + /// Type of the input buffer + /// + /// + /// Input to the command. See remarks for more details. + /// + /// + /// Cannot perform the operation because the command is already started. + /// Stop the command and try the operation again. + /// (or) + /// No command is added. + /// + /// + /// Object is disposed. + /// + public async Task> InvokeAsync(PSDataCollection input) + => await Task>.Factory.FromAsync(BeginInvoke(input), pResult => EndInvoke(pResult)); + + /// + /// Invoke the asynchronously. + /// Use await to wait for the command to complete and obtain the output of the command. + /// + /// + /// When invoked using InvokeAsync, invocation doesn't + /// finish until Input is closed. Caller of InvokeAsync must + /// close the input buffer after all input has been written to + /// input buffer. Input buffer is closed by calling + /// Close() method. + /// + /// If you want this command to execute as a standalone cmdlet + /// (that is, using command-line parameters only), + /// be sure to call Close() before calling InvokeAsync(). Otherwise, + /// the command will be executed as though it had external input. + /// If you observe that the command isn't doing anything, + /// this may be the reason. + /// + /// + /// Type of the input buffer + /// + /// + /// Input to the command. See remarks for more details. + /// + /// + /// Invocation Settings. + /// + /// + /// An AsyncCallback to call once the command is invoked. + /// + /// + /// A user supplied state to call the + /// with. + /// + /// + /// Cannot perform the operation because the command is already started. + /// Stop the command and try the operation again. + /// (or) + /// No command is added. + /// + /// + /// Object is disposed. + /// + public async Task> InvokeAsync(PSDataCollection input, PSInvocationSettings settings, AsyncCallback callback, object state) + => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), pResult => EndInvoke(pResult)); + + /// + /// Invoke the asynchronously. + /// Use await to wait for the command to complete and obtain the output of the command. + /// + /// + /// When invoked using InvokeAsync, invocation doesn't + /// finish until Input is closed. Caller of InvokeAsync must + /// close the input buffer after all input has been written to + /// input buffer. Input buffer is closed by calling + /// Close() method. + /// + /// If you want this command to execute as a standalone cmdlet + /// (that is, using command-line parameters only), + /// be sure to call Close() before calling InvokeAsync(). Otherwise, + /// the command will be executed as though it had external input. + /// If you observe that the command isn't doing anything, + /// this may be the reason. + /// + /// + /// Type of input object(s) for the command invocation. + /// + /// + /// Type of output object(s) expected from the command invocation. + /// + /// + /// Input to the command. See remarks for more details. + /// + /// + /// A buffer supplied by the user where output is collected. + /// + /// + /// Cannot perform the operation because the command is already started. + /// Stop the command and try the operation again. + /// (or) + /// No command is added. + /// + /// + /// Object is disposed. + /// + public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output) + => await Task>.Factory.FromAsync(BeginInvoke(input, output), pResult => EndInvoke(pResult)); + + /// + /// Invoke the asynchronously and collect + /// output data into the buffer . + /// Use await to wait for the command to complete and obtain the output of the command. + /// + /// + /// When invoked using InvokeAsync, invocation doesn't + /// finish until Input is closed. Caller of InvokeAsync must + /// close the input buffer after all input has been written to + /// input buffer. Input buffer is closed by calling + /// Close() method. + /// + /// If you want this command to execute as a standalone cmdlet + /// (that is, using command-line parameters only), + /// be sure to call Close() before calling InvokeAsync(). Otherwise, + /// the command will be executed as though it had external input. + /// If you observe that the command isn't doing anything, + /// this may be the reason. + /// + /// + /// Type of input object(s) for the command invocation. + /// + /// + /// Type of output object(s) expected from the command invocation. + /// + /// + /// Input to the command. See remarks for more details. + /// + /// + /// A buffer supplied by the user where output is collected. + /// + /// + /// Invocation Settings. + /// + /// + /// An AsyncCallback to call once the command is invoked. + /// + /// + /// A user supplied state to call the + /// with. + /// + /// + /// Cannot perform the operation because the command is already started. + /// Stop the command and try the operation again. + /// (or) + /// No command is added. + /// + /// + /// Object is disposed. + /// + public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output, PSInvocationSettings settings, AsyncCallback callback, object state) + => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), pResult => EndInvoke(pResult)); + /// /// Begins a batch execution /// From ad01380b50e5ab193c2c5bfeece262d978cfb3c9 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 16 Oct 2018 23:06:28 -0300 Subject: [PATCH 02/31] CodeFactor changes --- .../engine/hostifaces/PowerShell.cs | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index 71413c3eae0..e46ca632669 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -13,8 +13,8 @@ using System.Diagnostics.CodeAnalysis; // for fxcop. using Dbg = System.Management.Automation.Diagnostics; using System.Diagnostics; -using Microsoft.Management.Infrastructure; using System.Threading.Tasks; +using Microsoft.Management.Infrastructure; #pragma warning disable 1634, 1691 // Stops compiler from warning about unknown warnings @@ -3015,6 +3015,9 @@ public IAsyncResult BeginInvoke(PSDataCollection input, /// Invoke the asynchronously. /// Use await to wait for the command to complete and obtain the output of the command. /// + /// + /// The output buffer created to hold the results of the asynchronous invoke. + /// /// /// Cannot perform the operation because the command is already started. /// Stop the command and try the operation again. @@ -3032,25 +3035,30 @@ public async Task> InvokeAsync() /// Use await to wait for the command to complete and obtain the output of the command. /// /// + /// /// When invoked using InvokeAsync, invocation doesn't /// finish until Input is closed. Caller of InvokeAsync must /// close the input buffer after all input has been written to /// input buffer. Input buffer is closed by calling /// Close() method. - /// + /// /// If you want this command to execute as a standalone cmdlet /// (that is, using command-line parameters only), /// be sure to call Close() before calling InvokeAsync(). Otherwise, /// the command will be executed as though it had external input. /// If you observe that the command isn't doing anything, /// this may be the reason. + /// /// /// - /// Type of the input buffer + /// Type of the input buffer. /// /// /// Input to the command. See remarks for more details. /// + /// + /// The output buffer created to hold the results of the asynchronous invoke. + /// /// /// Cannot perform the operation because the command is already started. /// Stop the command and try the operation again. @@ -3068,21 +3076,23 @@ public async Task> InvokeAsync(PSDataCollection /// Use await to wait for the command to complete and obtain the output of the command. /// /// + /// /// When invoked using InvokeAsync, invocation doesn't /// finish until Input is closed. Caller of InvokeAsync must /// close the input buffer after all input has been written to /// input buffer. Input buffer is closed by calling /// Close() method. - /// + /// /// If you want this command to execute as a standalone cmdlet /// (that is, using command-line parameters only), /// be sure to call Close() before calling InvokeAsync(). Otherwise, /// the command will be executed as though it had external input. /// If you observe that the command isn't doing anything, /// this may be the reason. + /// /// /// - /// Type of the input buffer + /// Type of the input buffer. /// /// /// Input to the command. See remarks for more details. @@ -3097,6 +3107,9 @@ public async Task> InvokeAsync(PSDataCollection /// A user supplied state to call the /// with. /// + /// + /// The output buffer created to hold the results of the asynchronous invoke. + /// /// /// Cannot perform the operation because the command is already started. /// Stop the command and try the operation again. @@ -3114,18 +3127,20 @@ public async Task> InvokeAsync(PSDataCollection /// Use await to wait for the command to complete and obtain the output of the command. /// /// + /// /// When invoked using InvokeAsync, invocation doesn't /// finish until Input is closed. Caller of InvokeAsync must /// close the input buffer after all input has been written to /// input buffer. Input buffer is closed by calling /// Close() method. - /// + /// /// If you want this command to execute as a standalone cmdlet /// (that is, using command-line parameters only), /// be sure to call Close() before calling InvokeAsync(). Otherwise, /// the command will be executed as though it had external input. /// If you observe that the command isn't doing anything, /// this may be the reason. + /// /// /// /// Type of input object(s) for the command invocation. @@ -3139,6 +3154,9 @@ public async Task> InvokeAsync(PSDataCollection /// /// A buffer supplied by the user where output is collected. /// + /// + /// The output buffer created to hold the results of the asynchronous invoke, or null if the caller provided their own buffer. + /// /// /// Cannot perform the operation because the command is already started. /// Stop the command and try the operation again. @@ -3157,18 +3175,20 @@ public async Task> InvokeAsync(PSDat /// Use await to wait for the command to complete and obtain the output of the command. /// /// + /// /// When invoked using InvokeAsync, invocation doesn't /// finish until Input is closed. Caller of InvokeAsync must /// close the input buffer after all input has been written to /// input buffer. Input buffer is closed by calling /// Close() method. - /// + /// /// If you want this command to execute as a standalone cmdlet /// (that is, using command-line parameters only), /// be sure to call Close() before calling InvokeAsync(). Otherwise, /// the command will be executed as though it had external input. /// If you observe that the command isn't doing anything, /// this may be the reason. + /// /// /// /// Type of input object(s) for the command invocation. @@ -3192,6 +3212,9 @@ public async Task> InvokeAsync(PSDat /// A user supplied state to call the /// with. /// + /// + /// The output buffer created to hold the results of the asynchronous invoke, or null if the caller provided their own buffer. + /// /// /// Cannot perform the operation because the command is already started. /// Stop the command and try the operation again. From f7e1f628be7917718e2f6128de0a768b1449fdb6 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 4 Nov 2018 17:14:59 -0400 Subject: [PATCH 03/31] first crack at some C# unit tests --- test/csharp/test_PowerShell.cs | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/csharp/test_PowerShell.cs diff --git a/test/csharp/test_PowerShell.cs b/test/csharp/test_PowerShell.cs new file mode 100644 index 00000000000..7db723ab4c8 --- /dev/null +++ b/test/csharp/test_PowerShell.cs @@ -0,0 +1,56 @@ +using System.Diagnostics; +using System.Management.Automation; +using Xunit; + +namespace PSTests.Parallel +{ + public class PowerShellTests + { + [Fact] + public async System.Threading.Tasks.Task TestPowerShellInvokeAsync() + { + using (PowerShell ps = PowerShell.Create()) + { + ps.AddCommand("Get-Process") + .AddParameter("Id", Process.GetCurrentProcess().Id); + + PSDataCollection results = await ps.InvokeAsync(); + + Assert.Single(results); + Assert.IsType(results[0]?.BaseObject); + Assert.Equal(Process.GetCurrentProcess().Id, ((Process)results[0].BaseObject).Id); + } + } + + [Fact] + public async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInput() + { + using (PowerShell ps = PowerShell.Create()) + { + ps.AddCommand("Get-Command"); + + PSDataCollection results = await ps.InvokeAsync(new PSDataCollection(new string[] { "Get-Command" })); + + Assert.Single(results); + Assert.IsType(results[0]?.BaseObject); + Assert.Equal("Get-Command", ((CmdletInfo)results[0].BaseObject).Name); + } + } + + [Fact] + public async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInputAndOutput() + { + using (PowerShell ps = PowerShell.Create()) + { + ps.AddCommand("Get-Command"); + + PSDataCollection results = new PSDataCollection(); + await ps.InvokeAsync(new PSDataCollection(new string[] { "Get-Command" }), results); + + Assert.Single(results); + Assert.IsType(results[0]); + Assert.Equal("Get-Command", results[0].Name); + } + } + } +} From 1d32ec6aa235f6ea51293032da26c7c4c1c92229 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 4 Nov 2018 17:17:35 -0400 Subject: [PATCH 04/31] codefactor -- added header to file --- test/csharp/test_PowerShell.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/csharp/test_PowerShell.cs b/test/csharp/test_PowerShell.cs index 7db723ab4c8..a6ef59f3941 100644 --- a/test/csharp/test_PowerShell.cs +++ b/test/csharp/test_PowerShell.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.Diagnostics; using System.Management.Automation; using Xunit; From e42db00b753edc9f8d1c2c272d2c18f9eaf0229e Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 4 Nov 2018 20:21:54 -0400 Subject: [PATCH 05/31] updated PowerShell member count --- test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index b93891632b7..206ca7f9c4f 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -696,7 +696,7 @@ dir -Recurse ` It "Test member completion of a static method invocation" { $inputStr = '[powershell]::Create().' $res = TabExpansion2 -inputScript $inputStr -cursorColumn $inputStr.Length - $res.CompletionMatches | Should -HaveCount 31 + $res.CompletionMatches | Should -HaveCount 32 $res.CompletionMatches[0].CompletionText | Should -BeExactly "Commands" } } From 745156b887e6dad6c83f4d41d2c06024ecc8d657 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 15 Jan 2019 11:40:48 -0400 Subject: [PATCH 06/31] updated tab completion test --- test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 88a4b85dcae..dab26d597f3 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -725,7 +725,7 @@ dir -Recurse ` It "Test member completion of a static method invocation" { $inputStr = '[powershell]::Create().' $res = TabExpansion2 -inputScript $inputStr -cursorColumn $inputStr.Length - $res.CompletionMatches | Should -HaveCount 32 + $res.CompletionMatches | Should -HaveCount 33 $res.CompletionMatches[0].CompletionText | Should -BeExactly "Commands" } } From 536db9c222c3dedc1f230ce28ea2650c050a2b09 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 15 Feb 2019 09:02:31 -0400 Subject: [PATCH 07/31] Codacy changes --- .../engine/hostifaces/PowerShell.cs | 10 +++++----- test/xUnit/test_PowerShell.cs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index 4e8e42879a8..5d6d27fe4df 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -3075,7 +3075,7 @@ public IAsyncResult BeginInvoke(PSDataCollection input, /// Object is disposed. /// public async Task> InvokeAsync() - => await Task>.Factory.FromAsync(BeginInvoke(), pResult => EndInvoke(pResult)); + => await Task>.Factory.FromAsync(BeginInvoke(), pResult => EndInvoke(pResult)).ConfigureAwait(false); /// /// Invoke the asynchronously. @@ -3116,7 +3116,7 @@ public async Task> InvokeAsync() /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input) - => await Task>.Factory.FromAsync(BeginInvoke(input), pResult => EndInvoke(pResult)); + => await Task>.Factory.FromAsync(BeginInvoke(input), pResult => EndInvoke(pResult)).ConfigureAwait(false); /// /// Invoke the asynchronously. @@ -3167,7 +3167,7 @@ public async Task> InvokeAsync(PSDataCollection /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSInvocationSettings settings, AsyncCallback callback, object state) - => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), pResult => EndInvoke(pResult)); + => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), pResult => EndInvoke(pResult)).ConfigureAwait(false); /// /// Invoke the asynchronously. @@ -3214,7 +3214,7 @@ public async Task> InvokeAsync(PSDataCollection /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output) - => await Task>.Factory.FromAsync(BeginInvoke(input, output), pResult => EndInvoke(pResult)); + => await Task>.Factory.FromAsync(BeginInvoke(input, output), pResult => EndInvoke(pResult)).ConfigureAwait(false); /// /// Invoke the asynchronously and collect @@ -3272,7 +3272,7 @@ public async Task> InvokeAsync(PSDat /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output, PSInvocationSettings settings, AsyncCallback callback, object state) - => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), pResult => EndInvoke(pResult)); + => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), pResult => EndInvoke(pResult)).ConfigureAwait(false); /// /// Begins a batch execution. diff --git a/test/xUnit/test_PowerShell.cs b/test/xUnit/test_PowerShell.cs index a6ef59f3941..9604b7dc72f 100644 --- a/test/xUnit/test_PowerShell.cs +++ b/test/xUnit/test_PowerShell.cs @@ -10,7 +10,7 @@ namespace PSTests.Parallel public class PowerShellTests { [Fact] - public async System.Threading.Tasks.Task TestPowerShellInvokeAsync() + public static async System.Threading.Tasks.Task TestPowerShellInvokeAsync() { using (PowerShell ps = PowerShell.Create()) { @@ -26,13 +26,13 @@ public async System.Threading.Tasks.Task TestPowerShellInvokeAsync() } [Fact] - public async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInput() + public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInput() { using (PowerShell ps = PowerShell.Create()) { ps.AddCommand("Get-Command"); - PSDataCollection results = await ps.InvokeAsync(new PSDataCollection(new string[] { "Get-Command" })); + PSDataCollection results = await ps.InvokeAsync(new PSDataCollection(new [] { "Get-Command" })); Assert.Single(results); Assert.IsType(results[0]?.BaseObject); @@ -41,14 +41,14 @@ public async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInput() } [Fact] - public async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInputAndOutput() + public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInputAndOutput() { using (PowerShell ps = PowerShell.Create()) { ps.AddCommand("Get-Command"); PSDataCollection results = new PSDataCollection(); - await ps.InvokeAsync(new PSDataCollection(new string[] { "Get-Command" }), results); + await ps.InvokeAsync(new PSDataCollection(new [] { "Get-Command" }), results); Assert.Single(results); Assert.IsType(results[0]); From 3b051e81f33134ee782caf7eb8d69cd295ade777 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 26 Feb 2019 15:59:06 -0400 Subject: [PATCH 08/31] add default formats for Task class --- .../DotNetTypes_format_ps1xml.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs index b1472ee100a..8d44946898f 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs @@ -280,6 +280,10 @@ internal static IEnumerable GetFormatData() yield return new ExtendedTypeDefinition( "Microsoft.Management.Infrastructure.CimInstance#__PartialCIMInstance", ViewsOf_Microsoft_Management_Infrastructure_CimInstance___PartialCIMInstance()); + + yield return new ExtendedTypeDefinition( + "System.Threading.Tasks.Task", + ViewsOf_System_Threading_Tasks_Task()); } private static IEnumerable ViewsOf_System_CodeDom_Compiler_CompilerError() @@ -1709,5 +1713,43 @@ private static IEnumerable ViewsOf_Microsoft_Management_In .EndEntry() .EndControl()); } + + private static IEnumerable ViewsOf_System_Threading_Tasks_Task() + { + yield return new FormatViewDefinition("System.Threading.Tasks.Task", + TableControl.Create() + .AddHeader(label: "Id") + .AddHeader(label: "IsCompleted") + .AddHeader(label: "Status") + .StartRowDefinition() + .AddPropertyColumn("Id") + .AddPropertyColumn("IsCompleted") + .AddPropertyColumn("Status") + .EndRowDefinition() + .EndTable()); + + yield return new FormatViewDefinition("System.Threading.Tasks.Task", + ListControl.Create() + .StartEntry() + .AddItemProperty(@"AsyncState") + .AddItemProperty(@"AsyncWaitHandle") + .AddItemProperty(@"CompletedSynchronously") + .AddItemProperty(@"CreationOptions") + .AddItemProperty(@"Exception") + .AddItemProperty(@"Id") + .AddItemProperty(@"IsCanceled") + .AddItemProperty(@"IsCompleted") + .AddItemProperty(@"IsCompletedSuccessfully") + .AddItemProperty(@"IsFaulted") + .AddItemScriptBlock(@" + if ($_.IsCompleted) { + $_.Result + } + ", label: "Result") + .AddItemProperty(@"Status") + .EndEntry() + .EndList()); + } + } } From 561db67317f9765d102568632887bcf5734d319f Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 26 Feb 2019 16:00:26 -0400 Subject: [PATCH 09/31] new tests for InvokeAsync methods --- .../engine/Api/MultithreadedEngine.Tests.ps1 | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 diff --git a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 new file mode 100644 index 00000000000..b2c678b811c --- /dev/null +++ b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 @@ -0,0 +1,123 @@ +Describe 'Multithreaded engine APIs' -Tags 'CI' { + Context 'PowerShell::InvokeAsync' { + It 'can invoke a single script asynchronously' { + $r = [powershell]::Create().AddScript(@' +@(1,2,3,4,5).foreach{ + [pscustomobject]@{ + Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') + Value = $_ + ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId + } + Start-Sleep -Milliseconds 500 +} +'@).InvokeAsync() + [System.Threading.Tasks.Task]::WaitAll(@($r)) + $r.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r.IsCompletedSuccessfully | Should -Be $true + } + + It 'can invoke multiple scripts asynchronously' { + $r1 = [powershell]::Create().AddScript(@' +@(1,3,5,7,9,11,13,15,17,19).foreach{ + [pscustomobject]@{ + Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') + Value = $_ + ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId + } + Start-Sleep -Milliseconds 500 +} +'@).InvokeAsync() + $r2 = [powershell]::Create().AddScript(@' +@(2,4,6,8,10,12,14,16,18,20).foreach{ + [pscustomobject]@{ + Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') + Value = $_ + ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId + } + Start-Sleep -Milliseconds 500 +} +'@).InvokeAsync() + [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) + $r1.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r1.IsCompletedSuccessfully | Should -Be $true + $r2.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r2.IsCompletedSuccessfully | Should -Be $true + $results = @($r1.Result.foreach('Value')) + @($r2.Result.foreach('Value')) + Compare-Object -ReferenceObject @(1..20) -DifferenceObject $results -SyncWindow 20 | Should -Be $null + } + + It 'can invoke multiple scripts asynchronously with input' { + $d1 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[PSObject]' + $d2 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[PSObject]' + foreach ($i in 1..20) { + $d1.Add($foreach.Current) + $foreach.MoveNext() > $null + $d2.Add($foreach.Current) + } + $script = @' +@($input).foreach{ + [pscustomobject]@{ + Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') + Value = $_ + ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId + } + Start-Sleep -Milliseconds 500 +} +'@ + $r1 = [powershell]::Create().AddScript($script).InvokeAsync($d1) + $r2 = [powershell]::Create().AddScript($script).InvokeAsync($d2) + [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) + $r1.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r1.IsCompletedSuccessfully | Should -Be $true + $r2.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r2.IsCompletedSuccessfully | Should -Be $true + $groupedResults = @($r1.Result) + @($r2.Result) | Group-Object -Property ThreadId + Compare-Object -ReferenceObject @(1..20) -DifferenceObject $groupedResults.Group.Value -SyncWindow 20 | Should -Be $null + } + + <# + public async Task> InvokeAsync(PSDataCollection input) + => await Task>.Factory.FromAsync(BeginInvoke(input), pResult => EndInvoke(pResult)).ConfigureAwait(false); + + public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output) + => await Task>.Factory.FromAsync(BeginInvoke(input, output), pResult => EndInvoke(pResult)).ConfigureAwait(false); + + public async Task> InvokeAsync(PSDataCollection input, PSInvocationSettings settings, AsyncCallback callback, object state) + => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), pResult => EndInvoke(pResult)).ConfigureAwait(false); + + public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output, PSInvocationSettings settings, AsyncCallback callback, object state) + => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), pResult => EndInvoke(pResult)).ConfigureAwait(false); + #> + + <# + It 'can create instance with runspace' { + $rs = [runspacefactory]::CreateRunspace() + $ps = [powershell]::Create($rs) + $ps | Should -Not -BeNullOrEmpty + $ps.Runspace | Should -Be $rs + $ps.Dispose() + $rs.Dispose() + } + + It 'cannot create instance with null runspace' { + { [powershell]::Create([runspace]$null) } | Should -Throw -ErrorId 'PSArgumentNullException' + } + + It 'can load the default snapin "Microsoft.WSMan.Management"' -skip:(-not $IsWindows) { + $ps = [powershell]::Create() + $ps.AddScript('Get-Command -Name Test-WSMan') > $null + + $result = $ps.Invoke() + $result.Count | Should -Be 1 + $result[0].Source | Should -BeExactly 'Microsoft.WSMan.Management' + } + } + + Context 'executioncontext' { + It 'args are passed correctly' { + $result = $ExecutionContext.SessionState.InvokeCommand.InvokeScript('"`$args:($args); `$input:($input)"', 1, 2, 3) + $result | Should -BeExactly '$args:(1 2 3); $input:()' + } + #> + } +} From ce40d03f4ebc5d7a82abb2694b4098dbb5035211 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 26 Feb 2019 16:25:38 -0400 Subject: [PATCH 10/31] fixed multithreaded test that passes input --- test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 index b2c678b811c..b943a990b2f 100644 --- a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 +++ b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 @@ -47,13 +47,15 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { } It 'can invoke multiple scripts asynchronously with input' { - $d1 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[PSObject]' - $d2 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[PSObject]' + $d1 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[int]' + $d2 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[int]' foreach ($i in 1..20) { $d1.Add($foreach.Current) $foreach.MoveNext() > $null $d2.Add($foreach.Current) } + $d1.Complete() + $d2.Complete() $script = @' @($input).foreach{ [pscustomobject]@{ From e070175be520d0421ab7641ef250cfaaa42afbec Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 26 Feb 2019 16:30:24 -0400 Subject: [PATCH 11/31] minor update to latest test --- test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 index b943a990b2f..a1b52d389a6 100644 --- a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 +++ b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 @@ -73,8 +73,8 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { $r1.IsCompletedSuccessfully | Should -Be $true $r2.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) $r2.IsCompletedSuccessfully | Should -Be $true - $groupedResults = @($r1.Result) + @($r2.Result) | Group-Object -Property ThreadId - Compare-Object -ReferenceObject @(1..20) -DifferenceObject $groupedResults.Group.Value -SyncWindow 20 | Should -Be $null + $sortedResults = @($r1.Result) + @($r2.Result) | Sort-Object -Property Time + Compare-Object -ReferenceObject @(1..20) -DifferenceObject $sortedResults.Value -SyncWindow 20 | Should -Be $null } <# From 4b9814e96eb734d647f9b026ebff99ed455c72a1 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 26 Feb 2019 16:50:30 -0400 Subject: [PATCH 12/31] simplified multithreaded pester tests --- .../engine/Api/MultithreadedEngine.Tests.ps1 | 64 ++++++------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 index a1b52d389a6..b01f0cbf779 100644 --- a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 +++ b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 @@ -45,8 +45,10 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { $results = @($r1.Result.foreach('Value')) + @($r2.Result.foreach('Value')) Compare-Object -ReferenceObject @(1..20) -DifferenceObject $results -SyncWindow 20 | Should -Be $null } + } - It 'can invoke multiple scripts asynchronously with input' { + Context 'PowerShell::InvokeAsync with input and output' { + BeforeAll { $d1 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[int]' $d2 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[int]' foreach ($i in 1..20) { @@ -57,7 +59,7 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { $d1.Complete() $d2.Complete() $script = @' -@($input).foreach{ +$input.foreach{ [pscustomobject]@{ Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') Value = $_ @@ -66,6 +68,9 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { Start-Sleep -Milliseconds 500 } '@ + } + + It 'can invoke multiple scripts asynchronously with input' { $r1 = [powershell]::Create().AddScript($script).InvokeAsync($d1) $r2 = [powershell]::Create().AddScript($script).InvokeAsync($d2) [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) @@ -77,49 +82,18 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { Compare-Object -ReferenceObject @(1..20) -DifferenceObject $sortedResults.Value -SyncWindow 20 | Should -Be $null } - <# - public async Task> InvokeAsync(PSDataCollection input) - => await Task>.Factory.FromAsync(BeginInvoke(input), pResult => EndInvoke(pResult)).ConfigureAwait(false); - - public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output) - => await Task>.Factory.FromAsync(BeginInvoke(input, output), pResult => EndInvoke(pResult)).ConfigureAwait(false); - - public async Task> InvokeAsync(PSDataCollection input, PSInvocationSettings settings, AsyncCallback callback, object state) - => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), pResult => EndInvoke(pResult)).ConfigureAwait(false); - - public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output, PSInvocationSettings settings, AsyncCallback callback, object state) - => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), pResult => EndInvoke(pResult)).ConfigureAwait(false); - #> - - <# - It 'can create instance with runspace' { - $rs = [runspacefactory]::CreateRunspace() - $ps = [powershell]::Create($rs) - $ps | Should -Not -BeNullOrEmpty - $ps.Runspace | Should -Be $rs - $ps.Dispose() - $rs.Dispose() - } - - It 'cannot create instance with null runspace' { - { [powershell]::Create([runspace]$null) } | Should -Throw -ErrorId 'PSArgumentNullException' - } - - It 'can load the default snapin "Microsoft.WSMan.Management"' -skip:(-not $IsWindows) { - $ps = [powershell]::Create() - $ps.AddScript('Get-Command -Name Test-WSMan') > $null - - $result = $ps.Invoke() - $result.Count | Should -Be 1 - $result[0].Source | Should -BeExactly 'Microsoft.WSMan.Management' - } - } - - Context 'executioncontext' { - It 'args are passed correctly' { - $result = $ExecutionContext.SessionState.InvokeCommand.InvokeScript('"`$args:($args); `$input:($input)"', 1, 2, 3) - $result | Should -BeExactly '$args:(1 2 3); $input:()' + It 'can invoke multiple scripts asynchronously with input and capture output' { + $o = New-Object -TypeName 'System.Management.Automation.PSDataCollection[PSObject]' + $r1 = [powershell]::Create().AddScript($script).InvokeAsync($d1, $o) + $r2 = [powershell]::Create().AddScript($script).InvokeAsync($d2, $o) + [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) + $o.Complete() + $r1.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r1.IsCompletedSuccessfully | Should -Be $true + $r2.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r2.IsCompletedSuccessfully | Should -Be $true + $sortedResults = $o | Sort-Object -Property Time + Compare-Object -ReferenceObject @(1..20) -DifferenceObject $sortedResults.Value -SyncWindow 20 | Should -Be $null } - #> } } From 2611869d0ffacadc7e83be8acf8126f6d4b70bdb Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Wed, 27 Feb 2019 21:38:54 -0400 Subject: [PATCH 13/31] flushed out more PowerShell tests --- .../engine/Api/MultithreadedEngine.Tests.ps1 | 150 +++++++++++++----- 1 file changed, 111 insertions(+), 39 deletions(-) diff --git a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 index b01f0cbf779..b019f63080f 100644 --- a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 +++ b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 @@ -1,42 +1,123 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { - Context 'PowerShell::InvokeAsync' { - It 'can invoke a single script asynchronously' { - $r = [powershell]::Create().AddScript(@' -@(1,2,3,4,5).foreach{ + BeforeAll { + $sbStub = @' +.foreach{ [pscustomobject]@{ - Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') - Value = $_ - ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId + Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') + Value = $_ + ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId + RunspaceId = [runspace]::DefaultRunspace.Id } - Start-Sleep -Milliseconds 500 + Start-Sleep -Seconds 1 } -'@).InvokeAsync() - [System.Threading.Tasks.Task]::WaitAll(@($r)) +'@ + } + + Context 'PowerShell::InvokeAsync - Single script tests' { + BeforeAll { + function InvokeAsyncThenWait { + param( + [powershell]$PowerShell + ) + $sb = [scriptblock]::Create("@(1,2,3,4,5)${sbStub}") + $r = $PowerShell.AddScript($sb).InvokeAsync() + [System.Threading.Tasks.Task]::WaitAll(@($r)) + $r + } + + function GetInnerErrorId { + param( + [exception]$Exception + ) + while ($null -ne $Exception.InnerException) { + $Exception = $Exception.InnerException + if ($null -ne (Get-Member -InputObject $Exception -Name ErrorRecord)) { + $Exception.ErrorRecord.FullyQualifiedErrorId + break + } + } + } + } + + It 'can invoke a single script asynchronously, but only in a new runspace' { + $ps = [powershell]::Create() + $r = InvokeAsyncThenWait -PowerShell $ps $r.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) $r.IsCompletedSuccessfully | Should -Be $true } - It 'can invoke multiple scripts asynchronously' { - $r1 = [powershell]::Create().AddScript(@' -@(1,3,5,7,9,11,13,15,17,19).foreach{ - [pscustomobject]@{ - Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') - Value = $_ - ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId - } - Start-Sleep -Milliseconds 500 + It 'handles terminating errors properly when invoked asynchronously in a new runspace' { + $ps = [powershell]::Create() + $sb = { + $r = $ps.AddScript(@' +try { + Get-Process -InvalidParameter 42 -ErrorAction Stop +} catch { + throw } '@).InvokeAsync() - $r2 = [powershell]::Create().AddScript(@' -@(2,4,6,8,10,12,14,16,18,20).foreach{ - [pscustomobject]@{ - Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') - Value = $_ - ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId + Set-Variable -Name r -Scope 2 -Value $r + [System.Threading.Tasks.Task]::WaitAll(@($r)) + $r + } + # This test is designed to gracefully fail with an error when invoked asynchronously. + $err = { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' -PassThru + $r.IsFaulted | Should -Be $true + $err.Exception.InnerException.InnerException.ErrorRecord.FullyQualifiedErrorId | Should -BeExactly 'NamedParameterNotFound,Microsoft.PowerShell.Commands.GetProcessCommand' + } + + It 'cannot invoke a single script asynchronously in a runspace that has not been opened' { + $rs = [runspacefactory]::CreateRunspace() + $ps = [powershell]::Create($rs) + $r = $ps.AddScript('@(1..10).foreach{Start-Sleep -Seconds 1}').InvokeAsync() + # This test is designed to fail. You cannot invoke PowerShell asynchronously + # in a runspace that has not been opened. + $r.IsFaulted | Should -Be $true + $r.Exception.InnerException -is [System.Management.Automation.Runspaces.InvalidRunspaceStateException] | Should -Be $true + $r.Exception.InnerException.CurrentState | Should -Be 'BeforeOpen' + $r.Exception.InnerException.ExpectedState | Should -Be 'Opened' + } + + It 'cannot invoke a single script asynchronously in a runspace that is busy' { + $rs = [runspacefactory]::CreateRunspace() + $rs.Open() + $psBusy = [powershell]::Create($rs) + $r = $psBusy.AddScript('@(1..10).foreach{Start-Sleep -Seconds 1}').InvokeAsync() + $rs.RunspaceAvailability | Should -Be 'Busy' + $ps = [powershell]::Create($rs) + $sb = { + InvokeAsyncThenWait -PowerShell $ps + } + # This test is designed to fail. You cannot invoke PowerShell asynchronously + # in a runspace that is busy, because pipelines cannot be run concurrently. + $err = { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' -PassThru + GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' + $count = 0 + while (-not $r.IsCompleted -and $count -lt 10) { + Start-Sleep -Seconds 1 + $count++ + } + } + + It 'cannot invoke a single script asynchronously in the current runspace' { + $ps = [powershell]::Create('CurrentRunspace') + $sb = { + InvokeAsyncThenWait -PowerShell $ps + } + # This test is designed to fail. You cannot invoke PowerShell asynchronously + # in the current runspace because nested PowerShell instances cannot be + # invoked asynchronously + $err = { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' -PassThru + GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' + } } - Start-Sleep -Milliseconds 500 -} -'@).InvokeAsync() + + Context 'PowerShell::InvokeAsync - Multiple script tests' { + It 'can invoke multiple scripts asynchronously' { + $sb1 = [scriptblock]::Create("@(1,3,5,7,9,11,13,15,17,19)${sbStub}") + $sb2 = [scriptblock]::Create("@(2,4,6,8,10,12,14,16,18,20)${sbStub}") + $r1 = [powershell]::Create().AddScript($sb1).InvokeAsync() + $r2 = [powershell]::Create().AddScript($sb2).InvokeAsync() [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) $r1.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) $r1.IsCompletedSuccessfully | Should -Be $true @@ -47,7 +128,7 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { } } - Context 'PowerShell::InvokeAsync with input and output' { + Context 'PowerShell::InvokeAsync - With input and output' { BeforeAll { $d1 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[int]' $d2 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[int]' @@ -58,16 +139,7 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { } $d1.Complete() $d2.Complete() - $script = @' -$input.foreach{ - [pscustomobject]@{ - Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') - Value = $_ - ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId - } - Start-Sleep -Milliseconds 500 -} -'@ + $script = [scriptblock]::Create("`$input${sbStub}") } It 'can invoke multiple scripts asynchronously with input' { From def3d07a75015207f57a162ec1fe6b10400049bb Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Wed, 27 Feb 2019 21:42:40 -0400 Subject: [PATCH 14/31] added missing copyright notice --- test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 index b019f63080f..a8446de5e7d 100644 --- a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 +++ b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 @@ -1,3 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. Describe 'Multithreaded engine APIs' -Tags 'CI' { BeforeAll { $sbStub = @' From 57b5a7688854644d4bbfeadb9ca2f9a0804aa2c7 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Wed, 27 Feb 2019 21:52:50 -0400 Subject: [PATCH 15/31] CodeFactor changes --- .../DefaultFormatters/DotNetTypes_format_ps1xml.cs | 1 - test/xUnit/{ => csharp}/test_PowerShell.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) rename test/xUnit/{ => csharp}/test_PowerShell.cs (94%) diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs index 8d44946898f..8532b92d237 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs @@ -1750,6 +1750,5 @@ private static IEnumerable ViewsOf_System_Threading_Tasks_ .EndEntry() .EndList()); } - } } diff --git a/test/xUnit/test_PowerShell.cs b/test/xUnit/csharp/test_PowerShell.cs similarity index 94% rename from test/xUnit/test_PowerShell.cs rename to test/xUnit/csharp/test_PowerShell.cs index 9604b7dc72f..8563a524de5 100644 --- a/test/xUnit/test_PowerShell.cs +++ b/test/xUnit/csharp/test_PowerShell.cs @@ -32,7 +32,7 @@ public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInp { ps.AddCommand("Get-Command"); - PSDataCollection results = await ps.InvokeAsync(new PSDataCollection(new [] { "Get-Command" })); + PSDataCollection results = await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Command" })); Assert.Single(results); Assert.IsType(results[0]?.BaseObject); @@ -48,7 +48,7 @@ public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInp ps.AddCommand("Get-Command"); PSDataCollection results = new PSDataCollection(); - await ps.InvokeAsync(new PSDataCollection(new [] { "Get-Command" }), results); + await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Command" }), results); Assert.Single(results); Assert.IsType(results[0]); From 2347a84738c1898478fc7f00a2600693dcab0f0b Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Thu, 28 Feb 2019 10:30:17 -0400 Subject: [PATCH 16/31] More CodeFactor changes --- .../DotNetTypes_format_ps1xml.cs | 72 ++++++++++--------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs index 8532b92d237..0d385e2dabc 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs @@ -1716,39 +1716,45 @@ private static IEnumerable ViewsOf_Microsoft_Management_In private static IEnumerable ViewsOf_System_Threading_Tasks_Task() { - yield return new FormatViewDefinition("System.Threading.Tasks.Task", - TableControl.Create() - .AddHeader(label: "Id") - .AddHeader(label: "IsCompleted") - .AddHeader(label: "Status") - .StartRowDefinition() - .AddPropertyColumn("Id") - .AddPropertyColumn("IsCompleted") - .AddPropertyColumn("Status") - .EndRowDefinition() - .EndTable()); - - yield return new FormatViewDefinition("System.Threading.Tasks.Task", - ListControl.Create() - .StartEntry() - .AddItemProperty(@"AsyncState") - .AddItemProperty(@"AsyncWaitHandle") - .AddItemProperty(@"CompletedSynchronously") - .AddItemProperty(@"CreationOptions") - .AddItemProperty(@"Exception") - .AddItemProperty(@"Id") - .AddItemProperty(@"IsCanceled") - .AddItemProperty(@"IsCompleted") - .AddItemProperty(@"IsCompletedSuccessfully") - .AddItemProperty(@"IsFaulted") - .AddItemScriptBlock(@" - if ($_.IsCompleted) { - $_.Result - } - ", label: "Result") - .AddItemProperty(@"Status") - .EndEntry() - .EndList()); + yield return new FormatViewDefinition( + "System.Threading.Tasks.Task", + TableControl + .Create() + .AddHeader(label: "Id") + .AddHeader(label: "IsCompleted") + .AddHeader(label: "Status") + .StartRowDefinition() + .AddPropertyColumn("Id") + .AddPropertyColumn("IsCompleted") + .AddPropertyColumn("Status") + .EndRowDefinition() + .EndTable()); + + yield return new FormatViewDefinition( + "System.Threading.Tasks.Task", + ListControl + .Create() + .StartEntry() + .AddItemProperty(@"AsyncState") + .AddItemProperty(@"AsyncWaitHandle") + .AddItemProperty(@"CompletedSynchronously") + .AddItemProperty(@"CreationOptions") + .AddItemProperty(@"Exception") + .AddItemProperty(@"Id") + .AddItemProperty(@"IsCanceled") + .AddItemProperty(@"IsCompleted") + .AddItemProperty(@"IsCompletedSuccessfully") + .AddItemProperty(@"IsFaulted") + .AddItemScriptBlock( + @" + if ($_.IsCompleted) { + $_.Result + } + ", + label: "Result") + .AddItemProperty(@"Status") + .EndEntry() + .EndList()); } } } From c3ad8f40d0c48c4057f71b480b6204aba07b9e70 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Thu, 28 Feb 2019 22:54:46 -0400 Subject: [PATCH 17/31] refactor delegate and additional tests --- .../engine/hostifaces/PowerShell.cs | 14 ++++++---- test/xUnit/csharp/test_PowerShell.cs | 26 ++++++++++++++----- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index 4512cb22f07..67e1aaee3c7 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -3058,6 +3058,8 @@ public IAsyncResult BeginInvoke(PSDataCollection input, return CoreInvokeAsync(input, output, settings, callback, state, null); } + private Action _pEndInvokeMethod = pResult => EndInvoke(pResult); + /// /// Invoke the asynchronously. /// Use await to wait for the command to complete and obtain the output of the command. @@ -3075,7 +3077,9 @@ public IAsyncResult BeginInvoke(PSDataCollection input, /// Object is disposed. /// public async Task> InvokeAsync() - => await Task>.Factory.FromAsync(BeginInvoke(), pResult => EndInvoke(pResult)).ConfigureAwait(false); + { + return await Task>.Factory.FromAsync(BeginInvoke(), _pEndInvokeMethod).ConfigureAwait(false); + } /// /// Invoke the asynchronously. @@ -3116,7 +3120,7 @@ public async Task> InvokeAsync() /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input) - => await Task>.Factory.FromAsync(BeginInvoke(input), pResult => EndInvoke(pResult)).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input), _pEndInvokeMethod).ConfigureAwait(false); /// /// Invoke the asynchronously. @@ -3167,7 +3171,7 @@ public async Task> InvokeAsync(PSDataCollection /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSInvocationSettings settings, AsyncCallback callback, object state) - => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), pResult => EndInvoke(pResult)).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), _pEndInvokeMethod).ConfigureAwait(false); /// /// Invoke the asynchronously. @@ -3214,7 +3218,7 @@ public async Task> InvokeAsync(PSDataCollection /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output) - => await Task>.Factory.FromAsync(BeginInvoke(input, output), pResult => EndInvoke(pResult)).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input, output), _pEndInvokeMethod).ConfigureAwait(false); /// /// Invoke the asynchronously and collect @@ -3272,7 +3276,7 @@ public async Task> InvokeAsync(PSDat /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output, PSInvocationSettings settings, AsyncCallback callback, object state) - => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), pResult => EndInvoke(pResult)).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), _pEndInvokeMethod).ConfigureAwait(false); /// /// Begins a batch execution. diff --git a/test/xUnit/csharp/test_PowerShell.cs b/test/xUnit/csharp/test_PowerShell.cs index 8563a524de5..5582c2a4020 100644 --- a/test/xUnit/csharp/test_PowerShell.cs +++ b/test/xUnit/csharp/test_PowerShell.cs @@ -12,12 +12,12 @@ public class PowerShellTests [Fact] public static async System.Threading.Tasks.Task TestPowerShellInvokeAsync() { - using (PowerShell ps = PowerShell.Create()) + using (var ps = PowerShell.Create()) { ps.AddCommand("Get-Process") .AddParameter("Id", Process.GetCurrentProcess().Id); - PSDataCollection results = await ps.InvokeAsync(); + var results = await ps.InvokeAsync(); Assert.Single(results); Assert.IsType(results[0]?.BaseObject); @@ -28,11 +28,11 @@ public static async System.Threading.Tasks.Task TestPowerShellInvokeAsync() [Fact] public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInput() { - using (PowerShell ps = PowerShell.Create()) + using (var ps = PowerShell.Create()) { ps.AddCommand("Get-Command"); - PSDataCollection results = await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Command" })); + var results = await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Command" })); Assert.Single(results); Assert.IsType(results[0]?.BaseObject); @@ -43,11 +43,11 @@ public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInp [Fact] public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInputAndOutput() { - using (PowerShell ps = PowerShell.Create()) + using (var ps = PowerShell.Create()) { ps.AddCommand("Get-Command"); - PSDataCollection results = new PSDataCollection(); + var results = new PSDataCollection(); await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Command" }), results); Assert.Single(results); @@ -55,5 +55,19 @@ public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInp Assert.Equal("Get-Command", results[0].Name); } } + + [Fact] + public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithErrorInScript() + { + using (var ps = PowerShell.Create()) + { + ps.AddCommand("Get-Process") + .AddParameter("InvalidParameterName", 42) + .AddParameter("ErrorAction", ActionPreference.Stop); + + var results = await ps.InvokeAsync(); + + } + } } } From 322e6c312400d31c5f52841730f00203c7cee622 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 1 Mar 2019 12:38:02 -0400 Subject: [PATCH 18/31] Add lazy delegate initializers and StopAsync method --- .../engine/hostifaces/PowerShell.cs | 86 ++++++++++++++++--- .../engine/Api/MultithreadedEngine.Tests.ps1 | 45 +++++++--- 2 files changed, 107 insertions(+), 24 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index 67e1aaee3c7..b8c482ef303 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -599,6 +599,10 @@ public sealed class PowerShell : IDisposable private bool _isBatching = false; private bool _stopBatchExecution = false; + // Lazy delegates for asynchronous invocation/termination of PowerShell commands + private Lazy>> _endInvokeMethod; + private Lazy> _endStopMethod; + #endregion #region Internal Constructors @@ -634,6 +638,7 @@ private PowerShell(PSCommand command, Collection extraCommands, objec ErrorBufferOwner = true; InformationalBuffers = new PSInformationalBuffers(InstanceId); Streams = new PSDataStreams(this); + InitializeDelegates(); } /// @@ -696,6 +701,8 @@ internal PowerShell(ObjectStreamBase inputstream, { RemotePowerShell = new ClientRemotePowerShell(this, runspacePool.RemoteRunspacePoolInternal); } + + InitializeDelegates(); } /// @@ -728,6 +735,28 @@ internal PowerShell(ConnectCommandInfo connectCmdInfo, ObjectStreamBase inputstr RemotePowerShell = new ClientRemotePowerShell(this, runspacePool.RemoteRunspacePoolInternal); } + /// + /// Lazy initializers for the _endInvokeMethod and _endStopMethod delegates. + /// + private void InitializeDelegates() + { + _endInvokeMethod = new Lazy>> + ( + () => + { + return EndInvoke; + } + ); + + _endStopMethod = new Lazy> + ( + () => + { + return EndStop; + } + ); + } + /// /// Sets the command collection in this powershell. /// @@ -3058,10 +3087,8 @@ public IAsyncResult BeginInvoke(PSDataCollection input, return CoreInvokeAsync(input, output, settings, callback, state, null); } - private Action _pEndInvokeMethod = pResult => EndInvoke(pResult); - /// - /// Invoke the asynchronously. + /// Invoke a PowerShell command asynchronously. /// Use await to wait for the command to complete and obtain the output of the command. /// /// @@ -3078,11 +3105,11 @@ public IAsyncResult BeginInvoke(PSDataCollection input, /// public async Task> InvokeAsync() { - return await Task>.Factory.FromAsync(BeginInvoke(), _pEndInvokeMethod).ConfigureAwait(false); + return await Task>.Factory.FromAsync(BeginInvoke(), _endInvokeMethod.Value).ConfigureAwait(false); } /// - /// Invoke the asynchronously. + /// Invoke a PowerShell command asynchronously. /// Use await to wait for the command to complete and obtain the output of the command. /// /// @@ -3120,10 +3147,10 @@ public async Task> InvokeAsync() /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input) - => await Task>.Factory.FromAsync(BeginInvoke(input), _pEndInvokeMethod).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input), _endInvokeMethod.Value).ConfigureAwait(false); /// - /// Invoke the asynchronously. + /// Invoke a PowerShell command asynchronously. /// Use await to wait for the command to complete and obtain the output of the command. /// /// @@ -3171,10 +3198,10 @@ public async Task> InvokeAsync(PSDataCollection /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSInvocationSettings settings, AsyncCallback callback, object state) - => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), _pEndInvokeMethod).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), _endInvokeMethod.Value).ConfigureAwait(false); /// - /// Invoke the asynchronously. + /// Invoke a PowerShell command asynchronously. /// Use await to wait for the command to complete and obtain the output of the command. /// /// @@ -3206,7 +3233,8 @@ public async Task> InvokeAsync(PSDataCollection /// A buffer supplied by the user where output is collected. /// /// - /// The output buffer created to hold the results of the asynchronous invoke, or null if the caller provided their own buffer. + /// The output buffer created to hold the results of the asynchronous invoke, + /// or null if the caller provided their own buffer. /// /// /// Cannot perform the operation because the command is already started. @@ -3218,10 +3246,10 @@ public async Task> InvokeAsync(PSDataCollection /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output) - => await Task>.Factory.FromAsync(BeginInvoke(input, output), _pEndInvokeMethod).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input, output), _endInvokeMethod.Value).ConfigureAwait(false); /// - /// Invoke the asynchronously and collect + /// Invoke a PowerShell command asynchronously and collect /// output data into the buffer . /// Use await to wait for the command to complete and obtain the output of the command. /// @@ -3264,7 +3292,8 @@ public async Task> InvokeAsync(PSDat /// with. /// /// - /// The output buffer created to hold the results of the asynchronous invoke, or null if the caller provided their own buffer. + /// The output buffer created to hold the results of the asynchronous invoke, + /// or null if the caller provided their own buffer. /// /// /// Cannot perform the operation because the command is already started. @@ -3276,7 +3305,7 @@ public async Task> InvokeAsync(PSDat /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output, PSInvocationSettings settings, AsyncCallback callback, object state) - => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), _pEndInvokeMethod).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), _endInvokeMethod.Value).ConfigureAwait(false); /// /// Begins a batch execution. @@ -3775,6 +3804,35 @@ public void EndStop(IAsyncResult asyncResult) ResetOutputBufferAsNeeded(); } + /// + /// Stop a PowerShell command asynchronously. + /// Use await to wait for the command to stop. + /// + /// + /// + /// If the command is not started, the state of the PowerShell instance + /// is changed to Stopped and corresponding events will be raised. + /// + /// + /// + /// An AsyncCallback to call once the command is invoked. + /// + /// + /// A user supplied state to call the + /// with. + /// + /// + /// The output buffer created to hold the results of the asynchronous invoke, + /// or null if the caller provided their own buffer. + /// + /// + /// Object is disposed. + /// + public async Task StopAsync(AsyncCallback callback, object state) + { + await Task.Factory.FromAsync(BeginStop(callback, state), _endStopMethod.Value).ConfigureAwait(false); + } + #endregion #region Event Handlers diff --git a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 index a8446de5e7d..7e43a3bcd40 100644 --- a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 +++ b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 @@ -10,20 +10,23 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId RunspaceId = [runspace]::DefaultRunspace.Id } - Start-Sleep -Seconds 1 + Start-Sleep -Milliseconds 500 } '@ } Context 'PowerShell::InvokeAsync - Single script tests' { BeforeAll { - function InvokeAsyncThenWait { + function InvokeAsyncHelper { param( - [powershell]$PowerShell + [powershell]$PowerShell, + [switch]$Wait = $false ) $sb = [scriptblock]::Create("@(1,2,3,4,5)${sbStub}") $r = $PowerShell.AddScript($sb).InvokeAsync() - [System.Threading.Tasks.Task]::WaitAll(@($r)) + if ($Wait) { + [System.Threading.Tasks.Task]::WaitAll(@($r)) + } $r } @@ -43,7 +46,7 @@ Describe 'Multithreaded engine APIs' -Tags 'CI' { It 'can invoke a single script asynchronously, but only in a new runspace' { $ps = [powershell]::Create() - $r = InvokeAsyncThenWait -PowerShell $ps + $r = InvokeAsyncHelper -PowerShell $ps -Wait $r.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) $r.IsCompletedSuccessfully | Should -Be $true } @@ -71,7 +74,7 @@ try { It 'cannot invoke a single script asynchronously in a runspace that has not been opened' { $rs = [runspacefactory]::CreateRunspace() $ps = [powershell]::Create($rs) - $r = $ps.AddScript('@(1..10).foreach{Start-Sleep -Seconds 1}').InvokeAsync() + $r = $ps.AddScript('@(1..10).foreach{Start-Sleep -Milliseconds 500}').InvokeAsync() # This test is designed to fail. You cannot invoke PowerShell asynchronously # in a runspace that has not been opened. $r.IsFaulted | Should -Be $true @@ -84,11 +87,11 @@ try { $rs = [runspacefactory]::CreateRunspace() $rs.Open() $psBusy = [powershell]::Create($rs) - $r = $psBusy.AddScript('@(1..10).foreach{Start-Sleep -Seconds 1}').InvokeAsync() + $r = $psBusy.AddScript('@(1..5).foreach{Start-Sleep -Milliseconds 500}').InvokeAsync() $rs.RunspaceAvailability | Should -Be 'Busy' $ps = [powershell]::Create($rs) $sb = { - InvokeAsyncThenWait -PowerShell $ps + InvokeAsyncHelper -PowerShell $ps -Wait } # This test is designed to fail. You cannot invoke PowerShell asynchronously # in a runspace that is busy, because pipelines cannot be run concurrently. @@ -96,7 +99,7 @@ try { GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' $count = 0 while (-not $r.IsCompleted -and $count -lt 10) { - Start-Sleep -Seconds 1 + Start-Sleep -Milliseconds 500 $count++ } } @@ -104,7 +107,7 @@ try { It 'cannot invoke a single script asynchronously in the current runspace' { $ps = [powershell]::Create('CurrentRunspace') $sb = { - InvokeAsyncThenWait -PowerShell $ps + InvokeAsyncHelper -PowerShell $ps -Wait } # This test is designed to fail. You cannot invoke PowerShell asynchronously # in the current runspace because nested PowerShell instances cannot be @@ -170,4 +173,26 @@ try { Compare-Object -ReferenceObject @(1..20) -DifferenceObject $sortedResults.Value -SyncWindow 20 | Should -Be $null } } + + Context 'PowerShell::StopAsync' { + It 'can stop multiple scripts that are running asynchronously' { + $sb1 = [scriptblock]::Create("@(1,3,5,7,9,11,13,15,17,19)${sbStub}") + $sb2 = [scriptblock]::Create("@(2,4,6,8,10,12,14,16,18,20)${sbStub}") + $ps1 = [powershell]::Create().AddScript($sb1) + $ps2 = [powershell]::Create().AddScript($sb2) + $ps1.InvokeAsync() > $null + $ps2.InvokeAsync() > $null + while ($ps1.InvocationStateInfo.State -ne [System.Management.Automation.PSInvocationState]::Running -and + $ps2.InvocationStateInfo.State -ne [System.Management.Automation.PSInvocationState]::Running) { + Start-Sleep -Milliseconds 100 + } + $sr1 = $ps1.StopAsync({}, $null) + $sr2 = $ps2.StopAsync({}, $null) + [System.Threading.Tasks.Task]::WaitAll(@($sr1, $sr2)) + $sr1.IsCompletedSuccessfully | Should -Be $true + $ps1.InvocationStateInfo.State | Should -Be ([System.Management.Automation.PSInvocationState]::Stopped) + $sr2.IsCompletedSuccessfully | Should -Be $true + $ps2.InvocationStateInfo.State | Should -Be ([System.Management.Automation.PSInvocationState]::Stopped) + } + } } From 429ba369434a77da562f5865e2bf3667b0f855af Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 1 Mar 2019 14:13:47 -0400 Subject: [PATCH 19/31] CodeFactor changes --- .../engine/hostifaces/PowerShell.cs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index b8c482ef303..e2b82bf7121 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -740,21 +740,8 @@ internal PowerShell(ConnectCommandInfo connectCmdInfo, ObjectStreamBase inputstr /// private void InitializeDelegates() { - _endInvokeMethod = new Lazy>> - ( - () => - { - return EndInvoke; - } - ); - - _endStopMethod = new Lazy> - ( - () => - { - return EndStop; - } - ); + _endInvokeMethod = new Lazy>>(() => { return EndInvoke; }); + _endStopMethod = new Lazy>(() => { return EndStop; }); } /// From b4c2f59f9cf23d38763279ae3a3a582ef9a98f77 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 1 Mar 2019 14:23:04 -0400 Subject: [PATCH 20/31] CodeFactor changes --- .../engine/hostifaces/PowerShell.cs | 18 +++++++++--------- test/xUnit/csharp/test_PowerShell.cs | 1 - 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index e2b82bf7121..44a62671a7c 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -735,15 +735,6 @@ internal PowerShell(ConnectCommandInfo connectCmdInfo, ObjectStreamBase inputstr RemotePowerShell = new ClientRemotePowerShell(this, runspacePool.RemoteRunspacePoolInternal); } - /// - /// Lazy initializers for the _endInvokeMethod and _endStopMethod delegates. - /// - private void InitializeDelegates() - { - _endInvokeMethod = new Lazy>>(() => { return EndInvoke; }); - _endStopMethod = new Lazy>(() => { return EndStop; }); - } - /// /// Sets the command collection in this powershell. /// @@ -952,6 +943,15 @@ private static PowerShell Create(bool isNested, PSCommand psCommand, Collection< return powerShell; } + /// + /// Lazy initializers for the _endInvokeMethod and _endStopMethod delegates. + /// + private void InitializeDelegates() + { + _endInvokeMethod = new Lazy>>(() => { return EndInvoke; }); + _endStopMethod = new Lazy>(() => { return EndStop; }); + } + #endregion #region Command / Parameter Construction diff --git a/test/xUnit/csharp/test_PowerShell.cs b/test/xUnit/csharp/test_PowerShell.cs index 5582c2a4020..f949c57264e 100644 --- a/test/xUnit/csharp/test_PowerShell.cs +++ b/test/xUnit/csharp/test_PowerShell.cs @@ -66,7 +66,6 @@ public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithErr .AddParameter("ErrorAction", ActionPreference.Stop); var results = await ps.InvokeAsync(); - } } } From 3d3aff04f3cc1e8dc7954df8b728aa7d04928698 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 1 Mar 2019 18:42:04 -0400 Subject: [PATCH 21/31] review updates, more tests, some refactoring --- .../engine/hostifaces/PowerShell.cs | 47 ++-- .../TabCompletion/TabCompletion.Tests.ps1 | 2 +- .../engine/Api/MultithreadedEngine.Tests.ps1 | 198 -------------- .../Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 | 251 ++++++++++++++++++ test/xUnit/csharp/test_PowerShell.cs | 119 ++++++++- 5 files changed, 379 insertions(+), 238 deletions(-) delete mode 100644 test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 create mode 100644 test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index 44a62671a7c..9a45560db58 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -599,9 +599,9 @@ public sealed class PowerShell : IDisposable private bool _isBatching = false; private bool _stopBatchExecution = false; - // Lazy delegates for asynchronous invocation/termination of PowerShell commands - private Lazy>> _endInvokeMethod; - private Lazy> _endStopMethod; + // Delegates for asynchronous invocation/termination of PowerShell commands + private Func> _endInvokeMethod; + private Action _endStopMethod; #endregion @@ -638,16 +638,17 @@ private PowerShell(PSCommand command, Collection extraCommands, objec ErrorBufferOwner = true; InformationalBuffers = new PSInformationalBuffers(InstanceId); Streams = new PSDataStreams(this); - InitializeDelegates(); + _endInvokeMethod = EndInvoke; + _endStopMethod = EndStop; } - /// - /// Constructs a PowerShell instance in the disconnected start state with - /// the provided remote command connect information and runspace(pool) objects. - /// - /// Remote command connect information. - /// Remote Runspace or RunspacePool object. - internal PowerShell(ConnectCommandInfo connectCmdInfo, object rsConnection) + /// + /// Constructs a PowerShell instance in the disconnected start state with + /// the provided remote command connect information and runspace(pool) objects. + /// + /// Remote command connect information. + /// Remote Runspace or RunspacePool object. + internal PowerShell(ConnectCommandInfo connectCmdInfo, object rsConnection) : this(new PSCommand(), null, rsConnection) { ExtraCommands = new Collection(); @@ -702,7 +703,8 @@ internal PowerShell(ObjectStreamBase inputstream, RemotePowerShell = new ClientRemotePowerShell(this, runspacePool.RemoteRunspacePoolInternal); } - InitializeDelegates(); + _endInvokeMethod = EndInvoke; + _endStopMethod = EndStop; } /// @@ -943,15 +945,6 @@ private static PowerShell Create(bool isNested, PSCommand psCommand, Collection< return powerShell; } - /// - /// Lazy initializers for the _endInvokeMethod and _endStopMethod delegates. - /// - private void InitializeDelegates() - { - _endInvokeMethod = new Lazy>>(() => { return EndInvoke; }); - _endStopMethod = new Lazy>(() => { return EndStop; }); - } - #endregion #region Command / Parameter Construction @@ -3092,7 +3085,7 @@ public IAsyncResult BeginInvoke(PSDataCollection input, /// public async Task> InvokeAsync() { - return await Task>.Factory.FromAsync(BeginInvoke(), _endInvokeMethod.Value).ConfigureAwait(false); + return await Task>.Factory.FromAsync(BeginInvoke(), _endInvokeMethod).ConfigureAwait(false); } /// @@ -3134,7 +3127,7 @@ public async Task> InvokeAsync() /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input) - => await Task>.Factory.FromAsync(BeginInvoke(input), _endInvokeMethod.Value).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input), _endInvokeMethod).ConfigureAwait(false); /// /// Invoke a PowerShell command asynchronously. @@ -3185,7 +3178,7 @@ public async Task> InvokeAsync(PSDataCollection /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSInvocationSettings settings, AsyncCallback callback, object state) - => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), _endInvokeMethod.Value).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input, settings, callback, state), _endInvokeMethod).ConfigureAwait(false); /// /// Invoke a PowerShell command asynchronously. @@ -3233,7 +3226,7 @@ public async Task> InvokeAsync(PSDataCollection /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output) - => await Task>.Factory.FromAsync(BeginInvoke(input, output), _endInvokeMethod.Value).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input, output), _endInvokeMethod).ConfigureAwait(false); /// /// Invoke a PowerShell command asynchronously and collect @@ -3292,7 +3285,7 @@ public async Task> InvokeAsync(PSDat /// Object is disposed. /// public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output, PSInvocationSettings settings, AsyncCallback callback, object state) - => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), _endInvokeMethod.Value).ConfigureAwait(false); + => await Task>.Factory.FromAsync(BeginInvoke(input, output, settings, callback, state), _endInvokeMethod).ConfigureAwait(false); /// /// Begins a batch execution. @@ -3817,7 +3810,7 @@ public void EndStop(IAsyncResult asyncResult) /// public async Task StopAsync(AsyncCallback callback, object state) { - await Task.Factory.FromAsync(BeginStop(callback, state), _endStopMethod.Value).ConfigureAwait(false); + await Task.Factory.FromAsync(BeginStop(callback, state), _endStopMethod).ConfigureAwait(false); } #endregion diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index dab26d597f3..1e77bf64af6 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -725,7 +725,7 @@ dir -Recurse ` It "Test member completion of a static method invocation" { $inputStr = '[powershell]::Create().' $res = TabExpansion2 -inputScript $inputStr -cursorColumn $inputStr.Length - $res.CompletionMatches | Should -HaveCount 33 + $res.CompletionMatches | Should -HaveCount 34 $res.CompletionMatches[0].CompletionText | Should -BeExactly "Commands" } } diff --git a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 b/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 deleted file mode 100644 index 7e43a3bcd40..00000000000 --- a/test/powershell/engine/Api/MultithreadedEngine.Tests.ps1 +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -Describe 'Multithreaded engine APIs' -Tags 'CI' { - BeforeAll { - $sbStub = @' -.foreach{ - [pscustomobject]@{ - Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') - Value = $_ - ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId - RunspaceId = [runspace]::DefaultRunspace.Id - } - Start-Sleep -Milliseconds 500 -} -'@ - } - - Context 'PowerShell::InvokeAsync - Single script tests' { - BeforeAll { - function InvokeAsyncHelper { - param( - [powershell]$PowerShell, - [switch]$Wait = $false - ) - $sb = [scriptblock]::Create("@(1,2,3,4,5)${sbStub}") - $r = $PowerShell.AddScript($sb).InvokeAsync() - if ($Wait) { - [System.Threading.Tasks.Task]::WaitAll(@($r)) - } - $r - } - - function GetInnerErrorId { - param( - [exception]$Exception - ) - while ($null -ne $Exception.InnerException) { - $Exception = $Exception.InnerException - if ($null -ne (Get-Member -InputObject $Exception -Name ErrorRecord)) { - $Exception.ErrorRecord.FullyQualifiedErrorId - break - } - } - } - } - - It 'can invoke a single script asynchronously, but only in a new runspace' { - $ps = [powershell]::Create() - $r = InvokeAsyncHelper -PowerShell $ps -Wait - $r.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) - $r.IsCompletedSuccessfully | Should -Be $true - } - - It 'handles terminating errors properly when invoked asynchronously in a new runspace' { - $ps = [powershell]::Create() - $sb = { - $r = $ps.AddScript(@' -try { - Get-Process -InvalidParameter 42 -ErrorAction Stop -} catch { - throw -} -'@).InvokeAsync() - Set-Variable -Name r -Scope 2 -Value $r - [System.Threading.Tasks.Task]::WaitAll(@($r)) - $r - } - # This test is designed to gracefully fail with an error when invoked asynchronously. - $err = { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' -PassThru - $r.IsFaulted | Should -Be $true - $err.Exception.InnerException.InnerException.ErrorRecord.FullyQualifiedErrorId | Should -BeExactly 'NamedParameterNotFound,Microsoft.PowerShell.Commands.GetProcessCommand' - } - - It 'cannot invoke a single script asynchronously in a runspace that has not been opened' { - $rs = [runspacefactory]::CreateRunspace() - $ps = [powershell]::Create($rs) - $r = $ps.AddScript('@(1..10).foreach{Start-Sleep -Milliseconds 500}').InvokeAsync() - # This test is designed to fail. You cannot invoke PowerShell asynchronously - # in a runspace that has not been opened. - $r.IsFaulted | Should -Be $true - $r.Exception.InnerException -is [System.Management.Automation.Runspaces.InvalidRunspaceStateException] | Should -Be $true - $r.Exception.InnerException.CurrentState | Should -Be 'BeforeOpen' - $r.Exception.InnerException.ExpectedState | Should -Be 'Opened' - } - - It 'cannot invoke a single script asynchronously in a runspace that is busy' { - $rs = [runspacefactory]::CreateRunspace() - $rs.Open() - $psBusy = [powershell]::Create($rs) - $r = $psBusy.AddScript('@(1..5).foreach{Start-Sleep -Milliseconds 500}').InvokeAsync() - $rs.RunspaceAvailability | Should -Be 'Busy' - $ps = [powershell]::Create($rs) - $sb = { - InvokeAsyncHelper -PowerShell $ps -Wait - } - # This test is designed to fail. You cannot invoke PowerShell asynchronously - # in a runspace that is busy, because pipelines cannot be run concurrently. - $err = { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' -PassThru - GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' - $count = 0 - while (-not $r.IsCompleted -and $count -lt 10) { - Start-Sleep -Milliseconds 500 - $count++ - } - } - - It 'cannot invoke a single script asynchronously in the current runspace' { - $ps = [powershell]::Create('CurrentRunspace') - $sb = { - InvokeAsyncHelper -PowerShell $ps -Wait - } - # This test is designed to fail. You cannot invoke PowerShell asynchronously - # in the current runspace because nested PowerShell instances cannot be - # invoked asynchronously - $err = { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' -PassThru - GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' - } - } - - Context 'PowerShell::InvokeAsync - Multiple script tests' { - It 'can invoke multiple scripts asynchronously' { - $sb1 = [scriptblock]::Create("@(1,3,5,7,9,11,13,15,17,19)${sbStub}") - $sb2 = [scriptblock]::Create("@(2,4,6,8,10,12,14,16,18,20)${sbStub}") - $r1 = [powershell]::Create().AddScript($sb1).InvokeAsync() - $r2 = [powershell]::Create().AddScript($sb2).InvokeAsync() - [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) - $r1.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) - $r1.IsCompletedSuccessfully | Should -Be $true - $r2.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) - $r2.IsCompletedSuccessfully | Should -Be $true - $results = @($r1.Result.foreach('Value')) + @($r2.Result.foreach('Value')) - Compare-Object -ReferenceObject @(1..20) -DifferenceObject $results -SyncWindow 20 | Should -Be $null - } - } - - Context 'PowerShell::InvokeAsync - With input and output' { - BeforeAll { - $d1 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[int]' - $d2 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[int]' - foreach ($i in 1..20) { - $d1.Add($foreach.Current) - $foreach.MoveNext() > $null - $d2.Add($foreach.Current) - } - $d1.Complete() - $d2.Complete() - $script = [scriptblock]::Create("`$input${sbStub}") - } - - It 'can invoke multiple scripts asynchronously with input' { - $r1 = [powershell]::Create().AddScript($script).InvokeAsync($d1) - $r2 = [powershell]::Create().AddScript($script).InvokeAsync($d2) - [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) - $r1.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) - $r1.IsCompletedSuccessfully | Should -Be $true - $r2.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) - $r2.IsCompletedSuccessfully | Should -Be $true - $sortedResults = @($r1.Result) + @($r2.Result) | Sort-Object -Property Time - Compare-Object -ReferenceObject @(1..20) -DifferenceObject $sortedResults.Value -SyncWindow 20 | Should -Be $null - } - - It 'can invoke multiple scripts asynchronously with input and capture output' { - $o = New-Object -TypeName 'System.Management.Automation.PSDataCollection[PSObject]' - $r1 = [powershell]::Create().AddScript($script).InvokeAsync($d1, $o) - $r2 = [powershell]::Create().AddScript($script).InvokeAsync($d2, $o) - [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) - $o.Complete() - $r1.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) - $r1.IsCompletedSuccessfully | Should -Be $true - $r2.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) - $r2.IsCompletedSuccessfully | Should -Be $true - $sortedResults = $o | Sort-Object -Property Time - Compare-Object -ReferenceObject @(1..20) -DifferenceObject $sortedResults.Value -SyncWindow 20 | Should -Be $null - } - } - - Context 'PowerShell::StopAsync' { - It 'can stop multiple scripts that are running asynchronously' { - $sb1 = [scriptblock]::Create("@(1,3,5,7,9,11,13,15,17,19)${sbStub}") - $sb2 = [scriptblock]::Create("@(2,4,6,8,10,12,14,16,18,20)${sbStub}") - $ps1 = [powershell]::Create().AddScript($sb1) - $ps2 = [powershell]::Create().AddScript($sb2) - $ps1.InvokeAsync() > $null - $ps2.InvokeAsync() > $null - while ($ps1.InvocationStateInfo.State -ne [System.Management.Automation.PSInvocationState]::Running -and - $ps2.InvocationStateInfo.State -ne [System.Management.Automation.PSInvocationState]::Running) { - Start-Sleep -Milliseconds 100 - } - $sr1 = $ps1.StopAsync({}, $null) - $sr2 = $ps2.StopAsync({}, $null) - [System.Threading.Tasks.Task]::WaitAll(@($sr1, $sr2)) - $sr1.IsCompletedSuccessfully | Should -Be $true - $ps1.InvocationStateInfo.State | Should -Be ([System.Management.Automation.PSInvocationState]::Stopped) - $sr2.IsCompletedSuccessfully | Should -Be $true - $ps2.InvocationStateInfo.State | Should -Be ([System.Management.Automation.PSInvocationState]::Stopped) - } - } -} diff --git a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 new file mode 100644 index 00000000000..0e1b9cac2ec --- /dev/null +++ b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 @@ -0,0 +1,251 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +Describe 'Task-based PowerShell async APIs' -Tags 'CI' { + BeforeAll { + $sbStub = @' +.foreach{ + [pscustomobject]@{ + Time = [DateTime]::Now.ToString('yyyyMMddTHHmmss.fffffff') + Value = $_ + ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId + RunspaceId = [runspace]::DefaultRunspace.Id + } + Start-Sleep -Milliseconds 500 +} +'@ + } + + Context 'PowerShell::InvokeAsync - Single script tests' { + BeforeAll { + function InvokeAsyncHelper { + param( + [powershell]$PowerShell, + [switch]$Wait + ) + $r = $PowerShell.AddScript("@(1,2,3,4,5)${sbStub}").InvokeAsync() + if ($Wait) { + [System.Threading.Tasks.Task]::WaitAll(@($r)) + } + $r + } + + function GetInnerErrorId { + param( + [exception]$Exception + ) + while ($null -ne $Exception.InnerException) { + $Exception = $Exception.InnerException + if ($null -ne (Get-Member -InputObject $Exception -Name ErrorRecord)) { + $Exception.ErrorRecord.FullyQualifiedErrorId + break + } + } + } + } + + It 'can invoke a single script asynchronously, but only in a new runspace' { + $ps = [powershell]::Create() + try { + $r = InvokeAsyncHelper -PowerShell $ps -Wait + $r.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r.IsCompletedSuccessfully | Should -Be $true + } finally { + $ps.Dispose() + } + } + + It 'handles terminating errors properly when invoked asynchronously in a new runspace' { + $ps = [powershell]::Create() + try { + $sb = { + $r = $ps.AddScript(@' +try { + Get-Process -Invalid 42 -ErrorAction Stop +} catch { + throw +} +'@).InvokeAsync() + Set-Variable -Name r -Scope 2 -Value $r + [System.Threading.Tasks.Task]::WaitAll(@($r)) + $r + } + # This test is designed to gracefully fail with an error when invoked asynchronously. + { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' + $r.IsFaulted | Should -Be $true + $r.Exception.InnerException -is [System.Management.Automation.ParameterBindingException] | Should -Be $true + $r.Exception.InnerException.CommandInvocation.InvocationName | Should -BeExactly 'Get-Process' + $r.Exception.InnerException.ParameterName | Should -BeExactly 'Invalid' + $r.Exception.InnerException.ErrorId | Should -BeExactly 'NamedParameterNotFound' + } finally { + $ps.Dispose() + } + } + + It 'cannot invoke a single script asynchronously in a runspace that has not been opened' { + $rs = [runspacefactory]::CreateRunspace() + $ps = [powershell]::Create($rs) + try { + $r = $ps.AddScript('@(1..10).foreach{Start-Sleep -Milliseconds 500}').InvokeAsync() + # This test is designed to fail. You cannot invoke PowerShell asynchronously + # in a runspace that has not been opened. + $r.IsFaulted | Should -Be $true + $r.Exception -is [System.AggregateException] | Should -Be $true + $r.Exception.InnerException -is [System.Management.Automation.Runspaces.InvalidRunspaceStateException] | Should -Be $true + $r.Exception.InnerException.CurrentState | Should -Be 'BeforeOpen' + $r.Exception.InnerException.ExpectedState | Should -Be 'Opened' + } finally { + $ps.Dispose() + $rs.Dispose() + } + } + + It 'cannot invoke a single script asynchronously in a runspace that is busy' { + $rs = [runspacefactory]::CreateRunspace() + $rs.Open() + $psBusy = [powershell]::Create($rs) + try { + $psBusy.AddScript('@(1..120).foreach{Start-Sleep -Milliseconds 500}').InvokeAsync() > $null + $time = 0 + while ($rs.RunspaceAvailability -ne 'Busy') { + if (($time += 100) -ge 120000) { + break + } + Start-Sleep -Milliseconds 100 + } + $rs.RunspaceAvailability | Should -Be 'Busy' + $ps = [powershell]::Create($rs) + try { + $sb = { + InvokeAsyncHelper -PowerShell $ps -Wait + } + # This test is designed to fail. You cannot invoke PowerShell asynchronously + # in a runspace that is busy, because pipelines cannot be run concurrently. + $err = { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' -PassThru + GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' + $psBusy.Stop({}, $null); + } finally { + $ps.Dispose() + } + } finally { + $psBusy.Dispose() + $rs.Close() + $rs.Dispose() + } + } + + It 'cannot invoke a single script asynchronously in the current runspace' { + $ps = [powershell]::Create('CurrentRunspace') + try { + $sb = { + InvokeAsyncHelper -PowerShell $ps -Wait + } + # This test is designed to fail. You cannot invoke PowerShell asynchronously + # in the current runspace because nested PowerShell instances cannot be + # invoked asynchronously + $err = { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' -PassThru + GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' + } finally { + $ps.Dispose() + } + } + } + + Context 'PowerShell::InvokeAsync - Multiple script tests' { + It 'can invoke multiple scripts asynchronously' { + $ps1 = [powershell]::Create() + $ps2 = [powershell]::Create() + try { + $r1 = $ps1.AddScript("@(1,3,5,7,9,11,13,15,17,19)${sbStub}").InvokeAsync() + $r2 = $ps2.AddScript("@(2,4,6,8,10,12,14,16,18,20)${sbStub}").InvokeAsync() + [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) + $r1.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r1.IsCompletedSuccessfully | Should -Be $true + $r2.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r2.IsCompletedSuccessfully | Should -Be $true + $results = @($r1.Result.foreach('Value')) + @($r2.Result.foreach('Value')) + Compare-Object -ReferenceObject @(1..20) -DifferenceObject $results -SyncWindow 20 | Should -Be $null + } finally { + $ps1.Dispose() + $ps2.Dispose() + } + } + } + + Context 'PowerShell::InvokeAsync - With input and output' { + BeforeAll { + $d1 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[int]' + $d2 = New-Object -TypeName 'System.Management.Automation.PSDataCollection[int]' + foreach ($i in 1..20) { + $d1.Add($foreach.Current) + $foreach.MoveNext() > $null + $d2.Add($foreach.Current) + } + $d1.Complete() + $d2.Complete() + $script = "`$input${sbStub}" + } + + It 'can invoke multiple scripts asynchronously with input' { + $ps1 = [powershell]::Create() + $ps2 = [powershell]::Create() + try { + $r1 = $ps1.AddScript($script).InvokeAsync($d1) + $r2 = $ps2.AddScript($script).InvokeAsync($d2) + [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) + $r1.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r1.IsCompletedSuccessfully | Should -Be $true + $r2.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r2.IsCompletedSuccessfully | Should -Be $true + $allResults = @($r1.Result) + @($r2.Result) + Compare-Object -ReferenceObject @(1..20) -DifferenceObject $allResults.Value -SyncWindow 20 | Should -Be $null + } finally { + $ps1.Dispose() + $ps2.Dispose() + } + } + + It 'can invoke multiple scripts asynchronously with input and capture output' { + $ps1 = [powershell]::Create() + $ps2 = [powershell]::Create() + try { + $o = New-Object -TypeName 'System.Management.Automation.PSDataCollection[PSObject]' + $r1 = $ps1.AddScript($script).InvokeAsync($d1, $o) + $r2 = $ps2.AddScript($script).InvokeAsync($d2, $o) + [System.Threading.Tasks.Task]::WaitAll(@($r1, $r2)) + $o.Complete() + $r1.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r1.IsCompletedSuccessfully | Should -Be $true + $r2.Status | Should -Be ([System.Threading.Tasks.TaskStatus]::RanToCompletion) + $r2.IsCompletedSuccessfully | Should -Be $true + Compare-Object -ReferenceObject @(1..20) -DifferenceObject $o.Value -SyncWindow 20 | Should -Be $null + } finally { + $ps1.Dispose() + $ps2.Dispose() + } + } + } + + Context 'PowerShell::StopAsync' { + It 'can stop multiple scripts that are running asynchronously' { + $ps = @([powershell]::Create(),[powershell]::Create()) + try { + $ir = @($ps[0].AddScript("@(1,3,5,7,9,11,13,15,17,19)${sbStub}").InvokeAsync(), + $ps[1].AddScript("@(2,4,6,8,10,12,14,16,18,20)${sbStub}").InvokeAsync()) + while ($ps.InvocationStateInfo.State -ne [System.Management.Automation.PSInvocationState]::Running) { + Start-Sleep -Milliseconds 100 + } + $sr = @($ps.foreach{$_.StopAsync({}, $null)}) + [System.Threading.Tasks.Task]::WaitAll($sr) + @(0..1).foreach{ + $sr[$_].IsCompletedSuccessfully | Should -Be $true + $ir[$_].IsFaulted | Should -Be $true + $ir[$_].Exception -is [System.AggregateException] | Should -Be $true + $ir[$_].Exception.InnerException -is [System.Management.Automation.PipelineStoppedException] | Should -Be $true + $ps[$_].InvocationStateInfo.State | Should -Be ([System.Management.Automation.PSInvocationState]::Stopped) + } + } finally { + $ps.foreach{$_.Dispose()} + } + } + } +} diff --git a/test/xUnit/csharp/test_PowerShell.cs b/test/xUnit/csharp/test_PowerShell.cs index f949c57264e..654dac8738a 100644 --- a/test/xUnit/csharp/test_PowerShell.cs +++ b/test/xUnit/csharp/test_PowerShell.cs @@ -1,8 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; using System.Diagnostics; using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace PSTests.Parallel @@ -10,7 +14,7 @@ namespace PSTests.Parallel public class PowerShellTests { [Fact] - public static async System.Threading.Tasks.Task TestPowerShellInvokeAsync() + public static async Task TestPowerShellInvokeAsync() { using (var ps = PowerShell.Create()) { @@ -26,47 +30,138 @@ public static async System.Threading.Tasks.Task TestPowerShellInvokeAsync() } [Fact] - public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInput() + public static async Task TestPowerShellInvokeAsyncWithInput() { using (var ps = PowerShell.Create()) { ps.AddCommand("Get-Command"); - var results = await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Command" })); + var results = await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Process" })); Assert.Single(results); Assert.IsType(results[0]?.BaseObject); - Assert.Equal("Get-Command", ((CmdletInfo)results[0].BaseObject).Name); + Assert.Equal("Get-Process", ((CmdletInfo)results[0].BaseObject).Name); } } [Fact] - public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithInputAndOutput() + public static async Task TestPowerShellInvokeAsyncWithInputAndOutput() { using (var ps = PowerShell.Create()) { ps.AddCommand("Get-Command"); var results = new PSDataCollection(); - await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Command" }), results); + await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Process" }), results); Assert.Single(results); Assert.IsType(results[0]); - Assert.Equal("Get-Command", results[0].Name); + Assert.Equal("Get-Process", results[0].Name); } } [Fact] - public static async System.Threading.Tasks.Task TestPowerShellInvokeAsyncWithErrorInScript() + public static async Task TestPowerShellInvokeAsyncWithErrorInScript() { using (var ps = PowerShell.Create()) { - ps.AddCommand("Get-Process") - .AddParameter("InvalidParameterName", 42) - .AddParameter("ErrorAction", ActionPreference.Stop); - + ps.AddScript(@" +try { + Get-Process -Invalid 42 -ErrorAction Stop +} catch { + throw +} +"); var results = await ps.InvokeAsync(); + + Assert.True(results.IsFaulted); + Assert.IsType(results.Exception); + Assert.IsType(results.Exception.InnerException); + Assert.Equal(results.Exception.InnerException.CommandInvocation.InvocationName, "Get-Process"); + Assert.Equal(results.Exception.InnerException.ParameterName, "Invalid"); + Assert.Equal(results.Exception.InnerException.ErrorId, "NamedParameterNotFound"); + } + } + + [Fact] + public static async Task TestPowerShellInvokeAsyncInUnopenedRunspace() + { + using (var rs = RunspaceFactory.CreateRunspace()) + using (var ps = PowerShell.Create(rs)) + { + var results = await ps.AddScript(@"@(1..10).foreach{Start-Sleep -Milliseconds 500}") + .InvokeAsync(); + + Assert.True(results.IsFaulted); + Assert.IsType(results.Exception); + Assert.IsType(results.Exception.InnerException); + Assert.Equal(results.Exception.InnerException.CurrentState, RunspaceState.BeforeOpen); + Assert.Equal(results.Exception.InnerException.ExpectedState, RunspaceState.Opened); + } + } + + [Fact] + public static async Task TestPowerShellInvokeAsyncInBusyRunspace() + { + using (var rs = RunspaceFactory.CreateRunspace()) + { + rs.Open(); + using (var ps = PowerShell.Create(rs)) + { + await ps.AddScript(@"@(1..120).foreach{Start-Sleep -Milliseconds 500}") + .InvokeAsync(); + + int time = 0; + while (rs.RunspaceAvailability != RunspaceAvailability.Busy) + { + if ((time += 100) >= 120000) + { + break + } + Thread.Sleep(100); + } + + Assert.Equal(rs.RunspaceAvailability, RunspaceAvailability.Busy); + + using (var ps2 = PowerShell.Create(rs)) + { + var results = await ps2.AddScript(@"@(6..10).foreach{Start-Sleep -Milliseconds 500}") + .InvokeAsync(); + + Assert.True(results.IsFaulted); + Assert.IsType(results.Exception); + Assert.IsType(results.Exception.InnerException); + } + + ps.Stop(() => { }, null); + } } } + + [Fact] + public static async Task TestPowerShellInvokeAsyncMultiple() + { + var tasks = new List(); + + using (var ps1 = PowerShell.Create()) + using (var ps2 = PowerShell.Create()) + { + tasks.Add(await ps1.AddScript(@"@(1,3,5,7,9,11,13,15,17,19).foreach{Start-Sleep -Milliseconds 500}") + .InvokeAsync()); + tasks.Add(await ps2.AddScript(@"@(2,4,6,8,10,12,14,16,18,20).foreach{Start-Sleep -Milliseconds 500}") + .InvokeAsync()); + } + + foreach (var task in tasks) + { + Assert.Equal(task.Status, TaskStatus.RanToCompletion); + Assert.True(task.IsCompletedSuccessfully); + } + + var results = tasks.Select(x => x.Result).ToList(); + Assert.Equal(results.Count, 20); + } + + // More testing with these tests, plus new tests in progress for next commit } } From 006e56b4131320c15a8fad09e59b49d694b73948 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 3 Mar 2019 13:06:45 -0400 Subject: [PATCH 22/31] Codacy changes --- .../engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 | 3 ++- test/xUnit/csharp/test_PowerShell.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 index 0e1b9cac2ec..40d4a769f0b 100644 --- a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 +++ b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 @@ -107,7 +107,8 @@ try { $psBusy.AddScript('@(1..120).foreach{Start-Sleep -Milliseconds 500}').InvokeAsync() > $null $time = 0 while ($rs.RunspaceAvailability -ne 'Busy') { - if (($time += 100) -ge 120000) { + $time += 100 + if ($time -ge 120000) { break } Start-Sleep -Milliseconds 100 diff --git a/test/xUnit/csharp/test_PowerShell.cs b/test/xUnit/csharp/test_PowerShell.cs index 654dac8738a..323503fb3c9 100644 --- a/test/xUnit/csharp/test_PowerShell.cs +++ b/test/xUnit/csharp/test_PowerShell.cs @@ -11,7 +11,7 @@ namespace PSTests.Parallel { - public class PowerShellTests + public static class PowerShellTests { [Fact] public static async Task TestPowerShellInvokeAsync() From 3ad47f39f1bad073428fa0fdb4bd8de1784df4b2 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 3 Mar 2019 13:53:46 -0400 Subject: [PATCH 23/31] Updates based on @daxian-dbw's feedback --- .../engine/hostifaces/PowerShell.cs | 4 +- .../Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 | 37 +++++++------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index 9a45560db58..765b345cc61 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -600,8 +600,8 @@ public sealed class PowerShell : IDisposable private bool _stopBatchExecution = false; // Delegates for asynchronous invocation/termination of PowerShell commands - private Func> _endInvokeMethod; - private Action _endStopMethod; + private readonly Func> _endInvokeMethod; + private readonly Action _endStopMethod; #endregion diff --git a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 index 40d4a769f0b..dea0533b9c0 100644 --- a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 +++ b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 @@ -116,14 +116,11 @@ try { $rs.RunspaceAvailability | Should -Be 'Busy' $ps = [powershell]::Create($rs) try { - $sb = { - InvokeAsyncHelper -PowerShell $ps -Wait - } # This test is designed to fail. You cannot invoke PowerShell asynchronously # in a runspace that is busy, because pipelines cannot be run concurrently. - $err = { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' -PassThru + $err = { InvokeAsyncHelper -PowerShell $ps -Wait } | Should -Throw -ErrorId 'AggregateException' -PassThru GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' - $psBusy.Stop({}, $null); + $psBusy.Stop(); } finally { $ps.Dispose() } @@ -137,13 +134,10 @@ try { It 'cannot invoke a single script asynchronously in the current runspace' { $ps = [powershell]::Create('CurrentRunspace') try { - $sb = { - InvokeAsyncHelper -PowerShell $ps -Wait - } # This test is designed to fail. You cannot invoke PowerShell asynchronously # in the current runspace because nested PowerShell instances cannot be # invoked asynchronously - $err = { $sb.Invoke() } | Should -Throw -ErrorId 'AggregateException' -PassThru + $err = { InvokeAsyncHelper -PowerShell $ps -Wait } | Should -Throw -ErrorId 'AggregateException' -PassThru GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' } finally { $ps.Dispose() @@ -227,25 +221,22 @@ try { } Context 'PowerShell::StopAsync' { - It 'can stop multiple scripts that are running asynchronously' { - $ps = @([powershell]::Create(),[powershell]::Create()) + It 'can stop a script that is running asynchronously' { + $ps = [powershell]::Create() try { - $ir = @($ps[0].AddScript("@(1,3,5,7,9,11,13,15,17,19)${sbStub}").InvokeAsync(), - $ps[1].AddScript("@(2,4,6,8,10,12,14,16,18,20)${sbStub}").InvokeAsync()) + $ir = $ps.AddScript("@(1..240)${sbStub}").InvokeAsync() while ($ps.InvocationStateInfo.State -ne [System.Management.Automation.PSInvocationState]::Running) { Start-Sleep -Milliseconds 100 } - $sr = @($ps.foreach{$_.StopAsync({}, $null)}) - [System.Threading.Tasks.Task]::WaitAll($sr) - @(0..1).foreach{ - $sr[$_].IsCompletedSuccessfully | Should -Be $true - $ir[$_].IsFaulted | Should -Be $true - $ir[$_].Exception -is [System.AggregateException] | Should -Be $true - $ir[$_].Exception.InnerException -is [System.Management.Automation.PipelineStoppedException] | Should -Be $true - $ps[$_].InvocationStateInfo.State | Should -Be ([System.Management.Automation.PSInvocationState]::Stopped) - } + $sr = $ps.StopAsync({}, $null) + [System.Threading.Tasks.Task]::WaitAll(@($sr)) + $sr.IsCompletedSuccessfully | Should -Be $true + $ir.IsFaulted | Should -Be $true + $ir.Exception -is [System.AggregateException] | Should -Be $true + $ir.Exception.InnerException -is [System.Management.Automation.PipelineStoppedException] | Should -Be $true + $ps.InvocationStateInfo.State | Should -Be ([System.Management.Automation.PSInvocationState]::Stopped) } finally { - $ps.foreach{$_.Dispose()} + $ps.Dispose() } } } From af6db95dd7d347523cf0c8352f1f7e16998cc991 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 3 Mar 2019 13:59:39 -0400 Subject: [PATCH 24/31] replace code with Wait-UntilTrue invocation --- .../engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 index dea0533b9c0..07bbfe14606 100644 --- a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 +++ b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 @@ -225,9 +225,7 @@ try { $ps = [powershell]::Create() try { $ir = $ps.AddScript("@(1..240)${sbStub}").InvokeAsync() - while ($ps.InvocationStateInfo.State -ne [System.Management.Automation.PSInvocationState]::Running) { - Start-Sleep -Milliseconds 100 - } + Wait-UntilTrue { $ps.InvocationStateInfo.State -ne [System.Management.Automation.PSInvocationState]::Running } $sr = $ps.StopAsync({}, $null) [System.Threading.Tasks.Task]::WaitAll(@($sr)) $sr.IsCompletedSuccessfully | Should -Be $true From 297439797c4bdfa2dcf1b497895c74daa1a41d9a Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 3 Mar 2019 14:11:58 -0400 Subject: [PATCH 25/31] refactor code (replace with Wait-UntilTrue) --- .../engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 index 07bbfe14606..3db58d32c52 100644 --- a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 +++ b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 @@ -104,15 +104,8 @@ try { $rs.Open() $psBusy = [powershell]::Create($rs) try { - $psBusy.AddScript('@(1..120).foreach{Start-Sleep -Milliseconds 500}').InvokeAsync() > $null - $time = 0 - while ($rs.RunspaceAvailability -ne 'Busy') { - $time += 100 - if ($time -ge 120000) { - break - } - Start-Sleep -Milliseconds 100 - } + $psBusy.AddScript('@(1..240).foreach{Start-Sleep -Milliseconds 500}').InvokeAsync() > $null + Wait-UntilTrue { $rs.RunspaceAvailability -eq 'Busy' } -Timeout 120000 $rs.RunspaceAvailability | Should -Be 'Busy' $ps = [powershell]::Create($rs) try { From 12645db3f9d5a10d54ed53acf38ff3ec2b91f5f3 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 3 Mar 2019 14:15:26 -0400 Subject: [PATCH 26/31] correction to earlier refactoring of test --- .../powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 index 3db58d32c52..c9c349205f4 100644 --- a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 +++ b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 @@ -218,7 +218,7 @@ try { $ps = [powershell]::Create() try { $ir = $ps.AddScript("@(1..240)${sbStub}").InvokeAsync() - Wait-UntilTrue { $ps.InvocationStateInfo.State -ne [System.Management.Automation.PSInvocationState]::Running } + Wait-UntilTrue { $ps.InvocationStateInfo.State -eq [System.Management.Automation.PSInvocationState]::Running } $sr = $ps.StopAsync({}, $null) [System.Threading.Tasks.Task]::WaitAll(@($sr)) $sr.IsCompletedSuccessfully | Should -Be $true From 34d4b82c4aa1aa8a68b6a9c4b73714e2ee786889 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 3 Mar 2019 14:23:08 -0400 Subject: [PATCH 27/31] fixes to PowerShell engine async tests --- test/xUnit/csharp/test_PowerShell.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/xUnit/csharp/test_PowerShell.cs b/test/xUnit/csharp/test_PowerShell.cs index 323503fb3c9..77b1c236a5c 100644 --- a/test/xUnit/csharp/test_PowerShell.cs +++ b/test/xUnit/csharp/test_PowerShell.cs @@ -89,7 +89,7 @@ public static async Task TestPowerShellInvokeAsyncInUnopenedRunspace() using (var rs = RunspaceFactory.CreateRunspace()) using (var ps = PowerShell.Create(rs)) { - var results = await ps.AddScript(@"@(1..10).foreach{Start-Sleep -Milliseconds 500}") + var results = await ps.AddScript(@"'This will not run'") .InvokeAsync(); Assert.True(results.IsFaulted); @@ -116,7 +116,7 @@ await ps.AddScript(@"@(1..120).foreach{Start-Sleep -Milliseconds 500}") { if ((time += 100) >= 120000) { - break + break; } Thread.Sleep(100); } @@ -125,7 +125,7 @@ await ps.AddScript(@"@(1..120).foreach{Start-Sleep -Milliseconds 500}") using (var ps2 = PowerShell.Create(rs)) { - var results = await ps2.AddScript(@"@(6..10).foreach{Start-Sleep -Milliseconds 500}") + var results = await ps2.AddScript(@"'This will not run'") .InvokeAsync(); Assert.True(results.IsFaulted); @@ -146,9 +146,9 @@ public static async Task TestPowerShellInvokeAsyncMultiple() using (var ps1 = PowerShell.Create()) using (var ps2 = PowerShell.Create()) { - tasks.Add(await ps1.AddScript(@"@(1,3,5,7,9,11,13,15,17,19).foreach{Start-Sleep -Milliseconds 500}") + tasks.Add(await ps1.AddScript(@"@(1..5).foreach{Start-Sleep -Milliseconds 500; $_}") .InvokeAsync()); - tasks.Add(await ps2.AddScript(@"@(2,4,6,8,10,12,14,16,18,20).foreach{Start-Sleep -Milliseconds 500}") + tasks.Add(await ps2.AddScript(@"@(6..10).foreach{Start-Sleep -Milliseconds 500; $_}") .InvokeAsync()); } @@ -159,7 +159,7 @@ public static async Task TestPowerShellInvokeAsyncMultiple() } var results = tasks.Select(x => x.Result).ToList(); - Assert.Equal(results.Count, 20); + Assert.Equal(results.Count, 10); } // More testing with these tests, plus new tests in progress for next commit From 54cfc7ad5f32055e584c55d49e52f1d9521c42c4 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 3 Mar 2019 14:30:54 -0400 Subject: [PATCH 28/31] fixed incorrect waiting logic in C# tests --- test/xUnit/csharp/test_PowerShell.cs | 29 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/test/xUnit/csharp/test_PowerShell.cs b/test/xUnit/csharp/test_PowerShell.cs index 77b1c236a5c..2f8cc81c546 100644 --- a/test/xUnit/csharp/test_PowerShell.cs +++ b/test/xUnit/csharp/test_PowerShell.cs @@ -108,8 +108,8 @@ public static async Task TestPowerShellInvokeAsyncInBusyRunspace() rs.Open(); using (var ps = PowerShell.Create(rs)) { - await ps.AddScript(@"@(1..120).foreach{Start-Sleep -Milliseconds 500}") - .InvokeAsync(); + ps.AddScript(@"@(1..120).foreach{Start-Sleep -Milliseconds 500}") + .InvokeAsync(); int time = 0; while (rs.RunspaceAvailability != RunspaceAvailability.Busy) @@ -146,20 +146,21 @@ public static async Task TestPowerShellInvokeAsyncMultiple() using (var ps1 = PowerShell.Create()) using (var ps2 = PowerShell.Create()) { - tasks.Add(await ps1.AddScript(@"@(1..5).foreach{Start-Sleep -Milliseconds 500; $_}") - .InvokeAsync()); - tasks.Add(await ps2.AddScript(@"@(6..10).foreach{Start-Sleep -Milliseconds 500; $_}") - .InvokeAsync()); - } + tasks.Add(ps1.AddScript(@"@(1..5).foreach{Start-Sleep -Milliseconds 500; $_}") + .InvokeAsync()); + tasks.Add(ps2.AddScript(@"@(6..10).foreach{Start-Sleep -Milliseconds 500; $_}") + .InvokeAsync()); + Task.WaitAll(tasks); - foreach (var task in tasks) - { - Assert.Equal(task.Status, TaskStatus.RanToCompletion); - Assert.True(task.IsCompletedSuccessfully); - } + foreach (var task in tasks) + { + Assert.Equal(task.Status, TaskStatus.RanToCompletion); + Assert.True(task.IsCompletedSuccessfully); + } - var results = tasks.Select(x => x.Result).ToList(); - Assert.Equal(results.Count, 10); + var results = tasks.Select(x => x.Result).ToList(); + Assert.Equal(results.Count, 10); + } } // More testing with these tests, plus new tests in progress for next commit From 957ec770df87ea58f30c0cc6661571f3369c3b79 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 3 Mar 2019 20:39:09 -0400 Subject: [PATCH 29/31] more changes in response to PR feedback --- .../engine/hostifaces/PowerShell.cs | 14 +- .../Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 | 4 +- test/xUnit/csharp/test_PowerShell.cs | 168 ------------------ 3 files changed, 9 insertions(+), 177 deletions(-) delete mode 100644 test/xUnit/csharp/test_PowerShell.cs diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index 765b345cc61..4fea236d9b7 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -642,13 +642,13 @@ private PowerShell(PSCommand command, Collection extraCommands, objec _endStopMethod = EndStop; } - /// - /// Constructs a PowerShell instance in the disconnected start state with - /// the provided remote command connect information and runspace(pool) objects. - /// - /// Remote command connect information. - /// Remote Runspace or RunspacePool object. - internal PowerShell(ConnectCommandInfo connectCmdInfo, object rsConnection) + /// + /// Constructs a PowerShell instance in the disconnected start state with + /// the provided remote command connect information and runspace(pool) objects. + /// + /// Remote command connect information. + /// Remote Runspace or RunspacePool object. + internal PowerShell(ConnectCommandInfo connectCmdInfo, object rsConnection) : this(new PSCommand(), null, rsConnection) { ExtraCommands = new Collection(); diff --git a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 index c9c349205f4..8c0fd8c5870 100644 --- a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 +++ b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 @@ -60,7 +60,7 @@ Describe 'Task-based PowerShell async APIs' -Tags 'CI' { $sb = { $r = $ps.AddScript(@' try { - Get-Process -Invalid 42 -ErrorAction Stop + Get-Process -Invalid 42 } catch { throw } @@ -217,7 +217,7 @@ try { It 'can stop a script that is running asynchronously' { $ps = [powershell]::Create() try { - $ir = $ps.AddScript("@(1..240)${sbStub}").InvokeAsync() + $ir = $ps.AddScript("Start-Sleep -Seconds 60").InvokeAsync() Wait-UntilTrue { $ps.InvocationStateInfo.State -eq [System.Management.Automation.PSInvocationState]::Running } $sr = $ps.StopAsync({}, $null) [System.Threading.Tasks.Task]::WaitAll(@($sr)) diff --git a/test/xUnit/csharp/test_PowerShell.cs b/test/xUnit/csharp/test_PowerShell.cs deleted file mode 100644 index 2f8cc81c546..00000000000 --- a/test/xUnit/csharp/test_PowerShell.cs +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Diagnostics; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace PSTests.Parallel -{ - public static class PowerShellTests - { - [Fact] - public static async Task TestPowerShellInvokeAsync() - { - using (var ps = PowerShell.Create()) - { - ps.AddCommand("Get-Process") - .AddParameter("Id", Process.GetCurrentProcess().Id); - - var results = await ps.InvokeAsync(); - - Assert.Single(results); - Assert.IsType(results[0]?.BaseObject); - Assert.Equal(Process.GetCurrentProcess().Id, ((Process)results[0].BaseObject).Id); - } - } - - [Fact] - public static async Task TestPowerShellInvokeAsyncWithInput() - { - using (var ps = PowerShell.Create()) - { - ps.AddCommand("Get-Command"); - - var results = await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Process" })); - - Assert.Single(results); - Assert.IsType(results[0]?.BaseObject); - Assert.Equal("Get-Process", ((CmdletInfo)results[0].BaseObject).Name); - } - } - - [Fact] - public static async Task TestPowerShellInvokeAsyncWithInputAndOutput() - { - using (var ps = PowerShell.Create()) - { - ps.AddCommand("Get-Command"); - - var results = new PSDataCollection(); - await ps.InvokeAsync(new PSDataCollection(new[] { "Get-Process" }), results); - - Assert.Single(results); - Assert.IsType(results[0]); - Assert.Equal("Get-Process", results[0].Name); - } - } - - [Fact] - public static async Task TestPowerShellInvokeAsyncWithErrorInScript() - { - using (var ps = PowerShell.Create()) - { - ps.AddScript(@" -try { - Get-Process -Invalid 42 -ErrorAction Stop -} catch { - throw -} -"); - var results = await ps.InvokeAsync(); - - Assert.True(results.IsFaulted); - Assert.IsType(results.Exception); - Assert.IsType(results.Exception.InnerException); - Assert.Equal(results.Exception.InnerException.CommandInvocation.InvocationName, "Get-Process"); - Assert.Equal(results.Exception.InnerException.ParameterName, "Invalid"); - Assert.Equal(results.Exception.InnerException.ErrorId, "NamedParameterNotFound"); - } - } - - [Fact] - public static async Task TestPowerShellInvokeAsyncInUnopenedRunspace() - { - using (var rs = RunspaceFactory.CreateRunspace()) - using (var ps = PowerShell.Create(rs)) - { - var results = await ps.AddScript(@"'This will not run'") - .InvokeAsync(); - - Assert.True(results.IsFaulted); - Assert.IsType(results.Exception); - Assert.IsType(results.Exception.InnerException); - Assert.Equal(results.Exception.InnerException.CurrentState, RunspaceState.BeforeOpen); - Assert.Equal(results.Exception.InnerException.ExpectedState, RunspaceState.Opened); - } - } - - [Fact] - public static async Task TestPowerShellInvokeAsyncInBusyRunspace() - { - using (var rs = RunspaceFactory.CreateRunspace()) - { - rs.Open(); - using (var ps = PowerShell.Create(rs)) - { - ps.AddScript(@"@(1..120).foreach{Start-Sleep -Milliseconds 500}") - .InvokeAsync(); - - int time = 0; - while (rs.RunspaceAvailability != RunspaceAvailability.Busy) - { - if ((time += 100) >= 120000) - { - break; - } - Thread.Sleep(100); - } - - Assert.Equal(rs.RunspaceAvailability, RunspaceAvailability.Busy); - - using (var ps2 = PowerShell.Create(rs)) - { - var results = await ps2.AddScript(@"'This will not run'") - .InvokeAsync(); - - Assert.True(results.IsFaulted); - Assert.IsType(results.Exception); - Assert.IsType(results.Exception.InnerException); - } - - ps.Stop(() => { }, null); - } - } - } - - [Fact] - public static async Task TestPowerShellInvokeAsyncMultiple() - { - var tasks = new List(); - - using (var ps1 = PowerShell.Create()) - using (var ps2 = PowerShell.Create()) - { - tasks.Add(ps1.AddScript(@"@(1..5).foreach{Start-Sleep -Milliseconds 500; $_}") - .InvokeAsync()); - tasks.Add(ps2.AddScript(@"@(6..10).foreach{Start-Sleep -Milliseconds 500; $_}") - .InvokeAsync()); - Task.WaitAll(tasks); - - foreach (var task in tasks) - { - Assert.Equal(task.Status, TaskStatus.RanToCompletion); - Assert.True(task.IsCompletedSuccessfully); - } - - var results = tasks.Select(x => x.Result).ToList(); - Assert.Equal(results.Count, 10); - } - } - - // More testing with these tests, plus new tests in progress for next commit - } -} From c224fac7918269840313ffa25e26657ff24ea8ab Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 3 Mar 2019 20:41:46 -0400 Subject: [PATCH 30/31] added comment to new format views --- .../DefaultFormatters/DotNetTypes_format_ps1xml.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs index 0d385e2dabc..977c9649e05 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/DotNetTypes_format_ps1xml.cs @@ -1716,6 +1716,9 @@ private static IEnumerable ViewsOf_Microsoft_Management_In private static IEnumerable ViewsOf_System_Threading_Tasks_Task() { + // Avoid referencing the Result property in these views to avoid potential + // deadlocks that may occur. Result should only be referenced once the task + // is actually completed. yield return new FormatViewDefinition( "System.Threading.Tasks.Task", TableControl From 298cfe0c9dc0d3e9f03a1324e5daa9210125f16b Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 3 Mar 2019 20:50:55 -0400 Subject: [PATCH 31/31] PR review change --- .../Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 index 8c0fd8c5870..1d3d62cc399 100644 --- a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 +++ b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 @@ -100,27 +100,14 @@ try { } It 'cannot invoke a single script asynchronously in a runspace that is busy' { - $rs = [runspacefactory]::CreateRunspace() - $rs.Open() - $psBusy = [powershell]::Create($rs) + $ps = [powershell]::Create($Host.Runspace) try { - $psBusy.AddScript('@(1..240).foreach{Start-Sleep -Milliseconds 500}').InvokeAsync() > $null - Wait-UntilTrue { $rs.RunspaceAvailability -eq 'Busy' } -Timeout 120000 - $rs.RunspaceAvailability | Should -Be 'Busy' - $ps = [powershell]::Create($rs) - try { - # This test is designed to fail. You cannot invoke PowerShell asynchronously - # in a runspace that is busy, because pipelines cannot be run concurrently. - $err = { InvokeAsyncHelper -PowerShell $ps -Wait } | Should -Throw -ErrorId 'AggregateException' -PassThru - GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' - $psBusy.Stop(); - } finally { - $ps.Dispose() - } + # This test is designed to fail. You cannot invoke PowerShell asynchronously + # in a runspace that is busy, because pipelines cannot be run concurrently. + $err = { InvokeAsyncHelper -PowerShell $ps -Wait } | Should -Throw -ErrorId 'AggregateException' -PassThru + GetInnerErrorId -Exception $err.Exception | Should -Be 'InvalidOperation' } finally { - $psBusy.Dispose() - $rs.Close() - $rs.Dispose() + $ps.Dispose() } }