Public/Wait-PVETask.ps1

function Wait-PVETask {
  <#
  .SYNOPSIS
  Waits for Proxmox VE tasks to finish.
  .DESCRIPTION
  Polls each task's status until it stops, starting at 0.5 s between polls and backing off to 5 s. Every
  task piped in is waited on at the same time.

  - A task that ends with OK is written to the output.
  - A task that ends with WARNINGS is written to the output, with a warning.
  - A task that ends with any other exit status is a non-terminating error. The task object is the
    error's TargetObject and is not written to the output, so a following pipeline stage only receives
    tasks that succeeded.
  - A task still running when -TimeoutSec runs out is a non-terminating error. The task itself is not
    cancelled.
  .PARAMETER Upid
  Task UPID(s). Binds from piped task objects or plain UPID strings.
  .PARAMETER Server
  Connection to use for plain UPID strings. Task objects carry their own connection. Defaults to the
  default connection.
  .PARAMETER TimeoutSec
  Maximum seconds to wait. 0 (the default) waits without limit.
  .EXAMPLE
  Get-PVETask -Upid $upid | Wait-PVETask -TimeoutSec 300
  #>

  [CmdletBinding()]
  [OutputType('PVEPS.Task')]
  param(
    [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [ValidatePattern('^UPID:[^:]+:')]
    [string[]]$Upid,

    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('Connection')]
    [object[]]$Server,

    [ValidateRange(0, [int]::MaxValue)]
    [int]$TimeoutSec = 0
  )

  begin {
    $pending = [System.Collections.Generic.List[psobject]]::new()
  }

  process {
    try {
      $connection = Resolve-PVESingleConnection -Server $Server -Reason 'a task belongs to exactly one'
    }
    catch {
      $PSCmdlet.WriteError($_)
      return
    }
    foreach ($id in $Upid) {
      $pending.Add([pscustomobject]@{ Upid = $id; Connection = $connection })
    }
  }

  end {
    if ($pending.Count -eq 0) {
      return
    }

    $total = $pending.Count
    $deadline = if ($TimeoutSec -gt 0) { (Get-Date).AddSeconds($TimeoutSec) } else { $null }
    $delayMs = 500

    while ($pending.Count -gt 0) {
      foreach ($item in @($pending)) {
        try {
          $task = Get-PVETask -Upid $item.Upid -Server $item.Connection -ErrorAction Stop
        }
        catch {
          $PSCmdlet.WriteError($_)
          $null = $pending.Remove($item)
          continue
        }
        if ($task.State -ne 'Stopped') {
          continue
        }

        $null = $pending.Remove($item)
        if ($task.Succeeded) {
          if ($task.ExitStatus -ne 'OK') {
            Write-Warning "Task $($task.Type) $($task.Id) on $($task.Node) finished with $($task.ExitStatus)."
          }
          $task
        }
        else {
          $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
              [System.InvalidOperationException]::new("Task $($task.Type) $($task.Id) on $($task.Node) failed: $($task.ExitStatus)"),
              'PVETaskFailed',
              [System.Management.Automation.ErrorCategory]::OperationStopped,
              $task))
        }
      }

      if ($pending.Count -eq 0) {
        break
      }
      if ($deadline -and (Get-Date) -ge $deadline) {
        foreach ($item in $pending) {
          $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
              [System.TimeoutException]::new("Task $($item.Upid) was still running after $TimeoutSec seconds."),
              'PVETaskTimeout',
              [System.Management.Automation.ErrorCategory]::OperationTimeout,
              $item.Upid))
        }
        break
      }

      Write-Progress -Activity 'Waiting for Proxmox VE tasks' -Status "$($total - $pending.Count) of $total finished" -PercentComplete ((($total - $pending.Count) / $total) * 100)
      Start-Sleep -Milliseconds $delayMs
      $delayMs = [math]::Min($delayMs * 2, 5000)
    }

    Write-Progress -Activity 'Waiting for Proxmox VE tasks' -Completed
  }
}