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 b1472ee100..977c9649e0 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 @@ namespace System.Management.Automation.Runspaces 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,51 @@ namespace System.Management.Automation.Runspaces .EndEntry() .EndControl()); } + + 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 + .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()); + } } } diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs index e7e4893a20..4fea236d9b 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShell.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShell.cs @@ -13,6 +13,7 @@ using System.Management.Automation.Runspaces; using System.Management.Automation.Runspaces.Internal; using System.Runtime.Serialization; using System.Threading; +using System.Threading.Tasks; using Microsoft.Management.Infrastructure; using Dbg = System.Management.Automation.Diagnostics; @@ -598,6 +599,10 @@ namespace System.Management.Automation private bool _isBatching = false; private bool _stopBatchExecution = false; + // Delegates for asynchronous invocation/termination of PowerShell commands + private readonly Func> _endInvokeMethod; + private readonly Action _endStopMethod; + #endregion #region Internal Constructors @@ -633,6 +638,8 @@ namespace System.Management.Automation ErrorBufferOwner = true; InformationalBuffers = new PSInformationalBuffers(InstanceId); Streams = new PSDataStreams(this); + _endInvokeMethod = EndInvoke; + _endStopMethod = EndStop; } /// @@ -695,6 +702,9 @@ namespace System.Management.Automation { RemotePowerShell = new ClientRemotePowerShell(this, runspacePool.RemoteRunspacePoolInternal); } + + _endInvokeMethod = EndInvoke; + _endStopMethod = EndStop; } /// @@ -3057,6 +3067,226 @@ namespace System.Management.Automation return CoreInvokeAsync(input, output, settings, callback, state, null); } + /// + /// Invoke a PowerShell command 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. + /// (or) + /// No command is added. + /// + /// + /// Object is disposed. + /// + public async Task> InvokeAsync() + { + return await Task>.Factory.FromAsync(BeginInvoke(), _endInvokeMethod).ConfigureAwait(false); + } + + /// + /// Invoke a PowerShell command 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. + /// + /// + /// 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. + /// (or) + /// No command is added. + /// + /// + /// Object is disposed. + /// + public async Task> InvokeAsync(PSDataCollection input) + => await Task>.Factory.FromAsync(BeginInvoke(input), _endInvokeMethod).ConfigureAwait(false); + + /// + /// Invoke a PowerShell command 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. + /// + /// + /// 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. + /// (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), _endInvokeMethod).ConfigureAwait(false); + + /// + /// Invoke a PowerShell command 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. + /// + /// + /// 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. + /// (or) + /// No command is added. + /// + /// + /// Object is disposed. + /// + public async Task> InvokeAsync(PSDataCollection input, PSDataCollection output) + => await Task>.Factory.FromAsync(BeginInvoke(input, output), _endInvokeMethod).ConfigureAwait(false); + + /// + /// 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. + /// + /// + /// + /// 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. + /// + /// + /// 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. + /// (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), _endInvokeMethod).ConfigureAwait(false); + /// /// Begins a batch execution. /// @@ -3554,6 +3784,35 @@ namespace System.Management.Automation 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).ConfigureAwait(false); + } + #endregion #region Event Handlers diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 88a4b85dca..1e77bf64af 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 34 $res.CompletionMatches[0].CompletionText | Should -BeExactly "Commands" } } diff --git a/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 new file mode 100644 index 0000000000..1d3d62cc39 --- /dev/null +++ b/test/powershell/engine/Api/TaskBasedAsyncPowerShellAPI.Tests.ps1 @@ -0,0 +1,221 @@ +# 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 +} 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' { + $ps = [powershell]::Create($Host.Runspace) + 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' + } finally { + $ps.Dispose() + } + } + + It 'cannot invoke a single script asynchronously in the current runspace' { + $ps = [powershell]::Create('CurrentRunspace') + try { + # This test is designed to fail. You cannot invoke PowerShell asynchronously + # in the current runspace because nested PowerShell instances cannot be + # invoked asynchronously + $err = { InvokeAsyncHelper -PowerShell $ps -Wait } | 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 a script that is running asynchronously' { + $ps = [powershell]::Create() + try { + $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)) + $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.Dispose() + } + } + } +}