PowerShell - 100% ensure IDisposable is disposed with async operations

Viewed 35

I've been wracking my brains (such as they are) for a solution and can't suss it.

Let's say we have some code that starts a download with a [HttpResponseMessage] object. You get a task from $response.Content.CopyToAsync($downloadFileStream). You save that [Task] and you return, giving control back to the caller.

For various reasons the caller may choose to cancel the download. For that reason we use a cancellation token as well.

# '...' is unimportant detail

# HttpResponseMessage is IDisposable, as is the FileStream

$response = $httpClient.Send(...)
$downloadFileStream = [FileStream]::new(...)
$this._downloadTask = $response.Content.CopyToAsync($downloadFileStream, $cancellationToken)
return

Let's assume that after the download task is started some unhandled exception occurs, and the download class clean-up methods are never called. In that case the IDisposables (if) saved as class members are never explicitly disposed by the clean-up routines.

How then, do you ensure that both $response and $downloadFileStream are always disposed? No matter what happens? Ideally you want that to happen as soon as the download task completes.

$downloadTask.ContinueWith(...) appears to be a non-starter. [SynchronisationContext]::Current is null.

ThreadJob and multiple runspaces appear at first glance to be an option, and I've tried some of those approaches but ran into other issues, such as thread safety and passing .NET objects between runspaces.

Also I'm not sure if disposing an object in a thread other than the thread which instantiated it is considered bad form.

Ideally I'm looking for something like

$this._downloadTask = $response.Content.CopyToAsync($downloadFileStream, $cancellationToken)
.ContinueWith({$fileStream.Dispose(); $response.Dispose()}
return

But with the disposal happening on the main thread not in any worker thread? I think the /shrug emoji is appropriate right now. Spent far too long already thinking about this, and there's got to be some elegant and obvious solution. Please.

e: As requested, the basic downloader class outline.

class basicHttpDownloader
{
  [Task] $_downloadTask

  [void] NavigateAndBeginDownload($navigationOptions)
  {
    ...
    $response = $httpClient.Send(...)
    $this.SetDownloadFilename($response)
    $this._downloadTask = $response.Content.CopyToAsync($downloadFileStream, 
    $cancellationToken)

    # looking for something like..
    #.ContinueWith({$fileStream.Dispose(); $response.Dispose()}
    return

    # the caller is expected to call GetDownloadFilename(), verify
    # that it wants to complete the download, then call either
    # CancelDownload or FinishDownloadSaveTo
  }

  [string] GetDownloadFilename()

  [void] CancelDownload()

  [void] FinishDownloadSaveTo($folderPath)
}

e2: Tried this which also doesn't work :p

using namespace System.Threading.Tasks

$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest

$csharp = @"
using System;
using System.Threading;
using System.Management.Automation.Runspaces;

public class yoyo_sync : SynchronizationContext
{
    public yoyo_sync() : base()
    {
        // parameterless
        Console.WriteLine("yoyo_sync.ctor");
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        Console.WriteLine("yoyo_sync.Send");
        if(null == Runspace.DefaultRunspace) {
            Runspace.DefaultRunspace = RunspaceFactory.CreateRunspace();
        }
        Console.WriteLine(Runspace.DefaultRunspace.Name);

        base.Send(d, state);
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        Console.WriteLine("yoyo_sync.Post");
        if(null == Runspace.DefaultRunspace) {
            Runspace.DefaultRunspace = RunspaceFactory.CreateRunspace();
        }
        Console.WriteLine(Runspace.DefaultRunspace.Name);

        base.Post(d, state);
    }
}
"@

$yoyo = Add-Type -PassThru -TypeDefinition $csharp

[runspace]::DefaultRunspace
[System.Threading.SynchronizationContext]::SetSynchronizationContext($yoyo::new())
$task = [Task]::FromResult(1).ContinueWith([System.Action[Task[int]]] {"blah" | Out-Host},
    [TaskScheduler]::FromCurrentSynchronizationContext())
$task | Format-List
0 Answers
Related