pf-async.ps1

# Requires -Module pf-basic

function Invoke-RunSpace {
    PAram([ScriptBlock]$script, [HashTable]$Parameters)

    try {
        $Runspace = [runspacefactory]::CreateRunspace()
        $ps = [powershell]::create()
        $ps.Runspace = $Runspace
        $Runspace.Open()

        $ps.AddScript( $script )
        if ($Parameters) {
            $ps.AddParameters($Parameters)
        }
        $async = $ps.BeginInvoke()
        $async.IsCompleted
        $result = $ps.EndInvoke($async)
        return $result
    }
    finally {
        Invoke-Dispose ([ref] $Runspace)
        Invoke-Dispose ([ref] $ps)
    }
}

function Invoke-RunSpace:::Example {
    $script = {
                PARAM($dest) 
                'START'
                ping $dest
                'END' 
            }

    Invoke-RunSpace -script $script -Parameters @{dest = $env:COMPUTERNAME}
}

function Invoke-Script_InMutexBlock( [ScriptBlock]$script, $ArgumentList, [string]$name, [TimeSpan]$TimeOut) { 
    if (-not $name) {
        #If no name is provided for the mutex, one is generated for the script
        # based on where this function was called
        $name = Get-ScriptLine -level 2
        #If this follows a file format it will try to create a lock on a file
        $name = $name | Update-Path_ReplaceFileSpecialChars
    }
    
    $mtx = New-Object System.Threading.Mutex($false, $name)
    try {
        if ( $mtx.WaitOne($TimeOut) ) {
            $result = Invoke-Command -ScriptBlock $script -ArgumentList $ArgumentList
            return $result
        }
        else {
            Write-Warning "Mutex '$name' not obtained "
        }
    }
    finally {
        $mtx.ReleaseMutex()
        Invoke-Dispose ([ref] $mtx)
    }
}
function Invoke-Script_InMutexBlock:::Test {
    $initScript = New-ScriptBlock "import-module Common -DisableNameChecking"
    $AssertFolder = "$env:temp\PSAssert"
    New-Folder_EnsureExists $AssertFolder
    $timeStamp = [DateTime]::Now.Ticks
    $testFile = "$AssertFolder\Invoke-Script_InMutexBlock_$timeStamp.test"
    
    # Every execution use a different GUID the test passes if no other instance change the GUID stored in the file
    [ScriptBlock]$script = { param ( $testFile )
            Write-Host "Start $PID $testFile"
            $mutexName = $testFile | Update-Path_ReplaceFileSpecialChars
            Invoke-Script_InMutexBlock -name $mutexName  -TimeOut '00:01:00' -script {
                $expected = [Guid]::NewGuid().ToString()
                Write-Host "Mutex Start $PID $testFile $expected"
                Set-Content $testFile $expected
                Start-Sleep -Milliseconds 1000
                $actual = ( Get-Content $testFile -Raw ).Trim()
                Write-Host "Mutex End $PID $testFile $expected"
                return ( $actual -eq $expected )
            }
        }

    $jobs = @()
    try {
        # Test Script alone
        Invoke-Command -ScriptBlock $script -ArgumentList $testFile | assert -eq $true

        # Use concurrent Jobs
        $jobs = (1..10) | ForEach-Object {
            start-job -ScriptBlock $script -InitializationScript $initScript -ArgumentList $testFile
        }
        $jobs | Wait-Job -Timeout 200 | Out-Null
        $results =  $jobs | Receive-Job
        Write-Debug $results
    }
    finally {
        $jobs | Remove-Job
        if (Test-Path $testFile) {
            Remove-Item $testFile
        }
    }
}

function wait-until {
    param (
        [scriptblock]$condition, 
        [timespan]$timeout = [TimeSpan]::FromSeconds(30), 
        [timespan]$testInterval = [TimeSpan]::FromSeconds(1),
        [string]$Activity, 
        [scriptblock]$canRetry 
    ) 
    $deadline = [DateTime]::Now + $timeout
    $tics = 0
    $lineWidth = 60 #$Host.UI.RawUI.WindowSize.Width

    if (-not $Activity) {
        $Activity = $condition.ToString()
    }
    write-host "wait-until $Activity"

    $startedAt = [DateTime]::Now
    while ($true)
    {
        if (-not $canRetry ) {
            $result = & $condition
        }
        else {
            try {
                $result = & $condition
            }
            catch {
                $retry = Invoke-Command -ScriptBlock $canRetry -ArgumentList $_
                if (-not $retry) {
                    throw $_
                }
                else {
                    Write-Warning "Retry $($_.ToString())"     
                }
            }
        }
        
        if ( $result ) {
            Write-Progress -Activity "$Activity" -Completed
            return $result
        } 

        $remaning = $deadline - [DateTime]::Now
        if ( $remaning.TotalSeconds -lt 0 ) {
            throw "Timeout. wait-until '$condition' "
        }
        Write-Progress -Activity "$Activity" -SecondsRemaining $remaning.TotalSeconds
        write-host '.' -NoNewline
        $tics++
        if ( $tics % $lineWidth -eq 0 ) {
            $duration = [DateTime]::Now - $startedAt
            write-host $duration.ToString()
        }

        Start-Sleep -Milliseconds $testInterval.TotalMilliseconds
    }
}
function wait-until:::Test {
  $d = [DateTime]::Now.AddSeconds(10); wait-until -condition { [DateTime]::Now -gt $d }  -timeout ([TimeSpan]'00:00:20') | assert $true 
  { $d = [DateTime]::Now.AddSeconds(4); wait-until -condition { [DateTime]::Now -gt $d }  -timeout ([TimeSpan]'00:00:02') } | assert -throw 'Timeout'
}