Add 5 'InvokeAsync' overloads and 'StopAsync' to the 'PowerShell' type (#8056)

This commit is contained in:
Kirk Munro
2019-03-06 10:06:11 -08:00
committed by Dongbo Wang
parent 7caf59be3e
commit 45aba2acb5
4 changed files with 531 additions and 1 deletions
@@ -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<FormatViewDefinition> ViewsOf_System_CodeDom_Compiler_CompilerError()
@@ -1709,5 +1713,51 @@ namespace System.Management.Automation.Runspaces
.EndEntry()
.EndControl());
}
private static IEnumerable<FormatViewDefinition> 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());
}
}
}
@@ -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<IAsyncResult, PSDataCollection<PSObject>> _endInvokeMethod;
private readonly Action<IAsyncResult> _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;
}
/// <summary>
@@ -695,6 +702,9 @@ namespace System.Management.Automation
{
RemotePowerShell = new ClientRemotePowerShell(this, runspacePool.RemoteRunspacePoolInternal);
}
_endInvokeMethod = EndInvoke;
_endStopMethod = EndStop;
}
/// <summary>
@@ -3057,6 +3067,226 @@ namespace System.Management.Automation
return CoreInvokeAsync<TInput, TOutput>(input, output, settings, callback, state, null);
}
/// <summary>
/// Invoke a PowerShell command asynchronously.
/// Use await to wait for the command to complete and obtain the output of the command.
/// </summary>
/// <returns>
/// The output buffer created to hold the results of the asynchronous invoke.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Cannot perform the operation because the command is already started.
/// Stop the command and try the operation again.
/// (or)
/// No command is added.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
public async Task<PSDataCollection<PSObject>> InvokeAsync()
{
return await Task<PSDataCollection<PSObject>>.Factory.FromAsync(BeginInvoke(), _endInvokeMethod).ConfigureAwait(false);
}
/// <summary>
/// Invoke a PowerShell command asynchronously.
/// Use await to wait for the command to complete and obtain the output of the command.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para><para>
/// 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.
/// </para>
/// </remarks>
/// <typeparam name="T">
/// Type of the input buffer.
/// </typeparam>
/// <param name="input">
/// Input to the command. See remarks for more details.
/// </param>
/// <returns>
/// The output buffer created to hold the results of the asynchronous invoke.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Cannot perform the operation because the command is already started.
/// Stop the command and try the operation again.
/// (or)
/// No command is added.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
public async Task<PSDataCollection<PSObject>> InvokeAsync<T>(PSDataCollection<T> input)
=> await Task<PSDataCollection<PSObject>>.Factory.FromAsync(BeginInvoke<T>(input), _endInvokeMethod).ConfigureAwait(false);
/// <summary>
/// Invoke a PowerShell command asynchronously.
/// Use await to wait for the command to complete and obtain the output of the command.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para><para>
/// 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.
/// </para>
/// </remarks>
/// <typeparam name="T">
/// Type of the input buffer.
/// </typeparam>
/// <param name="input">
/// Input to the command. See remarks for more details.
/// </param>
/// <param name="settings">
/// Invocation Settings.
/// </param>
/// <param name="callback">
/// An AsyncCallback to call once the command is invoked.
/// </param>
/// <param name="state">
/// A user supplied state to call the <paramref name="callback"/>
/// with.
/// </param>
/// <returns>
/// The output buffer created to hold the results of the asynchronous invoke.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Cannot perform the operation because the command is already started.
/// Stop the command and try the operation again.
/// (or)
/// No command is added.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
public async Task<PSDataCollection<PSObject>> InvokeAsync<T>(PSDataCollection<T> input, PSInvocationSettings settings, AsyncCallback callback, object state)
=> await Task<PSDataCollection<PSObject>>.Factory.FromAsync(BeginInvoke<T>(input, settings, callback, state), _endInvokeMethod).ConfigureAwait(false);
/// <summary>
/// Invoke a PowerShell command asynchronously.
/// Use await to wait for the command to complete and obtain the output of the command.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para><para>
/// 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.
/// </para>
/// </remarks>
/// <typeparam name="TInput">
/// Type of input object(s) for the command invocation.
/// </typeparam>
/// <typeparam name="TOutput">
/// Type of output object(s) expected from the command invocation.
/// </typeparam>
/// <param name="input">
/// Input to the command. See remarks for more details.
/// </param>
/// <param name="output">
/// A buffer supplied by the user where output is collected.
/// </param>
/// <returns>
/// The output buffer created to hold the results of the asynchronous invoke,
/// or null if the caller provided their own buffer.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Cannot perform the operation because the command is already started.
/// Stop the command and try the operation again.
/// (or)
/// No command is added.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
public async Task<PSDataCollection<PSObject>> InvokeAsync<TInput, TOutput>(PSDataCollection<TInput> input, PSDataCollection<TOutput> output)
=> await Task<PSDataCollection<PSObject>>.Factory.FromAsync(BeginInvoke<TInput, TOutput>(input, output), _endInvokeMethod).ConfigureAwait(false);
/// <summary>
/// Invoke a PowerShell command asynchronously and collect
/// output data into the buffer <paramref name="output"/>.
/// Use await to wait for the command to complete and obtain the output of the command.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para><para>
/// 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.
/// </para>
/// </remarks>
/// <typeparam name="TInput">
/// Type of input object(s) for the command invocation.
/// </typeparam>
/// <typeparam name="TOutput">
/// Type of output object(s) expected from the command invocation.
/// </typeparam>
/// <param name="input">
/// Input to the command. See remarks for more details.
/// </param>
/// <param name="output">
/// A buffer supplied by the user where output is collected.
/// </param>
/// <param name="settings">
/// Invocation Settings.
/// </param>
/// <param name="callback">
/// An AsyncCallback to call once the command is invoked.
/// </param>
/// <param name="state">
/// A user supplied state to call the <paramref name="callback"/>
/// with.
/// </param>
/// <returns>
/// The output buffer created to hold the results of the asynchronous invoke,
/// or null if the caller provided their own buffer.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Cannot perform the operation because the command is already started.
/// Stop the command and try the operation again.
/// (or)
/// No command is added.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
public async Task<PSDataCollection<PSObject>> InvokeAsync<TInput, TOutput>(PSDataCollection<TInput> input, PSDataCollection<TOutput> output, PSInvocationSettings settings, AsyncCallback callback, object state)
=> await Task<PSDataCollection<PSObject>>.Factory.FromAsync(BeginInvoke<TInput, TOutput>(input, output, settings, callback, state), _endInvokeMethod).ConfigureAwait(false);
/// <summary>
/// Begins a batch execution.
/// </summary>
@@ -3554,6 +3784,35 @@ namespace System.Management.Automation
ResetOutputBufferAsNeeded();
}
/// <summary>
/// Stop a PowerShell command asynchronously.
/// Use await to wait for the command to stop.
/// </summary>
/// <remarks>
/// <para>
/// If the command is not started, the state of the PowerShell instance
/// is changed to Stopped and corresponding events will be raised.
/// </para>
/// </remarks>
/// <param name="callback">
/// An AsyncCallback to call once the command is invoked.
/// </param>
/// <param name="state">
/// A user supplied state to call the <paramref name="callback"/>
/// with.
/// </param>
/// <returns>
/// The output buffer created to hold the results of the asynchronous invoke,
/// or null if the caller provided their own buffer.
/// </returns>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
public async Task StopAsync(AsyncCallback callback, object state)
{
await Task.Factory.FromAsync(BeginStop(callback, state), _endStopMethod).ConfigureAwait(false);
}
#endregion
#region Event Handlers
@@ -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"
}
}
@@ -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()
}
}
}
}