等待来自 PowerShell 的异步 C# 方法



我想使用静态成员访问器从 PowerShell 调用静态异步 C# 方法,例如:

PowerShell

function CallMyStaticMethod([parameter(Mandatory=$true)][string]$myParam)
{
...
[MyNamespace.MyClass]::MyStaticMethod($myParam)
...
}

C#

public static async Task MyStaticMethod(string myParam)
{
...
await ...
...
}

由于我的 C# 方法是异步的,因此我的 C# 方法是否可以在没有来自 PowerShell 的某种"等待"调用的情况下正常运行?

它自己运行良好,但如果你想等待它完成,你可以使用它

$null = [MyNamespace.MyClass]::MyStaticMethod($myParam).GetAwaiter().GetResult()

这将解开如果您改用类似$task.Result之类的东西将抛出的AggregateException

但是,这将阻止,直到完成,这将阻止CTRL + C正确停止管道。 您可以等待它完成,同时仍然遵守这样的管道停止

$task = [MyNamespace.MyClass]::MyStaticMethod($myParam)
while (-not $task.AsyncWaitHandle.WaitOne(200)) { }
$null = $task.GetAwaiter().GetResult()

如果异步方法实际上返回某些内容,请删除$null =

借用Patrick Meinecke的答案,可以创建一个可管道的函数来为您解析任务(或任务列表(:

function Await-Task {
param (
[Parameter(ValueFromPipeline=$true, Mandatory=$true)]
$task
)
process {
while (-not $task.AsyncWaitHandle.WaitOne(200)) { }
$task.GetAwaiter().GetResult()
}
}

用法:

$results = Get-SomeTasks $paramA $paramB | Await-Task

我最近遇到了这个问题,发现创建一个PowerShell作业似乎也可以很好地解决问题。这为您提供了标准作业功能(等待作业、接收作业和删除作业(。 工作可能令人生畏,但这个很简单。它是用 C# 编写的,因此您可能需要使用 Add-Type 添加它(需要对其编写方式进行一些调整,Add-Type -TypeDefintition '...'当我使用 lambda 时似乎失败了,所以它们需要替换为适当的 Get 访问器(或编译它。

using System;
using System.Management.Automation;
using System.Threading;
using System.Threading.Tasks;
namespace MyNamespace
{
public class TaskJob : Job
{
private readonly Task _task;
private readonly CancellationTokenSource? _cts;
public override bool HasMoreData => Error.Count > 0 || Output.Count > 0;
public sealed override string Location => Environment.MachineName;
public override string StatusMessage => _task.Status.ToString();
public override void StopJob()
{
// to prevent the job from hanging, we'll say the job is stopped
// if we can't stop it. Otherwise, we'll cancel _cts and let the
// .ContinueWith() invocation set the job's state.
if (_cts is null)
{
SetJobState(JobState.Stopped);
}
else
{
_cts.Cancel();
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_task.Dispose();
_cts?.Dispose();
}
base.Dispose(disposing);
}
public TaskJob(string? name, string? command, Task task, CancellationTokenSource? cancellationTokenSource)
: base(command, name)
{
PSJobTypeName = nameof(TaskJob);
if (task is null)
{
throw new ArgumentNullException(nameof(task));
}
_task = task;
task.ContinueWith(OnTaskCompleted);
_cts = cancellationTokenSource;
}
public virtual void OnTaskCompleted(Task task)
{
if (task.IsCanceled)
{
SetJobState(JobState.Stopped);
}
else if (task.Exception != null)
{
Error.Add(new ErrorRecord(
task.Exception,
"TaskException",
ErrorCategory.NotSpecified,
task)
{
ErrorDetails = new ErrorDetails($"An exception occurred in the task. {task.Exception}"),
}
);
SetJobState(JobState.Failed);
}
else
{
SetJobState(JobState.Completed);
}
}
}
public class TaskJob<T> : TaskJob
{
public TaskJob(string? name, string? command, Task<T> task, CancellationTokenSource? cancellationTokenSource)
: base(name, command, task, cancellationTokenSource)
{
}
public override void OnTaskCompleted(Task task)
{
if (task is Task<T> taskT)
{
try
{
Output.Add(PSObject.AsPSObject(taskT.GetAwaiter().GetResult()));
}
// error handling dealt with in base.OnTaskCompleted
catch { }
}
base.OnTaskCompleted(task);
}
}
}

将此类添加到 PowerShell 会话后,可以轻松地将任务转换为任务:

$task = [MyNamespace.MyClass]::MyStaticMethod($myParam)
$job = ([MyNamespace.TaskJob]::new('MyTaskJob', $MyInvocation.Line, $task, $null))
# Add the job to the repository so that it can be retrieved later. This requires that you're using an advanced script or function (has an attribute declaration, particularly [CmldetBinding()] before the param() block). If not, you can always make a Register-Job function to just take an unregistered job and add it to the job repository.
$PSCmdlet.JobRepository.Add($job)
# now you can do all this with your task
Get-Job 'MyTaskJob' | Wait-Job
Get-Job 'MyTaskJob' | Receive-Job
Get-Job 'MyTaskJob' | Remove-Job

我会指出我对任务不是很熟悉,所以如果有人看到一些看起来很糟糕的东西,请告诉我,我一直在寻找改进的方法。 :)

在这个任务工作要点中可以找到一个更发达的概念。

最新更新