Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Await async C# method from PowerShell

I want to call a static async C# method from PowerShell by using the static member accessor, such as:

PowerShell

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

C#

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

Will my C# method run properly without some sort of "await" call from PowerShell since my C# method is async?

like image 357
BlueTriangles Avatar asked Jul 06 '18 22:07

BlueTriangles


3 Answers

It'll run fine on its own, but if you want to wait for it to finish you can use this

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

This will unwrap the AggregateException that would be thrown if you used something like $task.Result instead.

However that will block until it's complete, which will prevent CTRL + C from properly stopping the pipeline. You can wait for it to finish while still obeying pipeline stops like this

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

If the async method actually returns something, remove $null =

like image 72
Patrick Meinecke Avatar answered Oct 17 '22 11:10

Patrick Meinecke


Borrowing from Patrick Meinecke's answer, it's possible to make a pipeline-able function that will resolve a task (or list of tasks) for you:

function Await-Task {
    param (
        [Parameter(ValueFromPipeline=$true, Mandatory=$true)]
        $task
    )

    process {
        while (-not $task.AsyncWaitHandle.WaitOne(200)) { }
        $task.GetAwaiter().GetResult()
    }
}

Usage:

$results = Get-SomeTasks $paramA $paramB | Await-Task
like image 22
JLRishe Avatar answered Oct 17 '22 09:10

JLRishe


I recently ran into this and found that creating a PowerShell job seems to do the trick pretty nicely as well. This gives you the standard job capabilities (Wait-Job, Receive-Job, and Remove-Job). Jobs can be daunting, but this one's pretty simple. It's written in C# so you may need to add it with Add-Type (will require some tweaks to how it's written, Add-Type -TypeDefintition '...' seems to fail when I use lambdas, so they'd need replaced with proper Get accessors) or compile it.

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);
        }
    }
}

After adding this class to your PowerShell session, you can turn a task into a task pretty easily:

$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

I will point out I'm not incredibly familiar with tasks, so if anyone sees something that looks bad up there let me know, I'm always looking for ways to improve. :)

A more developed concept can be found in this TaskJob gist.

like image 33
Stroniax Avatar answered Oct 17 '22 09:10

Stroniax