Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PowerShell: Runspace problem with DownloadFileAsync

I needed to download file using WebClient in PowerShell 2.0, and I wanted to show download progress, so I did it this way:

$activity = "Downloading"

$client = New-Object System.Net.WebClient
$urlAsUri = New-Object System.Uri($url)

$event = New-Object System.Threading.ManualResetEvent($false)

$downloadProgress = [System.Net.DownloadProgressChangedEventHandler] {
    $progress = [int]((100.0 * $_.BytesReceived) / $_.TotalBytesToReceive)
    Write-Progress -Activity $activity -Status "${progress}% done" -PercentComplete $progress
}

$downloadComplete = [System.ComponentModel.AsyncCompletedEventHandler] {
    Write-Progress -Activity $activity -Completed
    $event.Set()
}

$client.add_DownloadFileCompleted($downloadComplete) 
$client.add_DownloadProgressChanged($downloadProgress)

Write-Progress -Activity $activity -Status "0% done" -PercentComplete 0
$client.DownloadFileAsync($urlAsUri, $file)    

$event.WaitOne()

I am getting a error There is no Runspace available to run scripts in this thread. for the code in $downloadProgress handler, which is logical. However, how do I provide a Runspace for the thread that (probably) belongs to the ThreadPool?

UPDATE: Note that both answers to this question are worth reading, and I would accept both if I could.

like image 978
Andrey Shchekin Avatar asked Feb 07 '11 20:02

Andrey Shchekin


2 Answers

Thanks stej for the nod.

Andrey, powershell has its own threadpool and each service thread keeps a threadstatic pointer to a runspace (the System.Management.Automation.Runspaces.Runspace.DefaultRunspace static member exposes this - and would be a null ref in your callbacks.) Ultimately this means it's difficult - especially in script - to use your own threadpool (as is provided by .NET for async methods) to execute scriptblocks.

PowerShell 2.0

Regardless, there is no need to play with this as powershell v2 has full support for eventing:

$client = New-Object System.Net.WebClient
$url = [uri]"http://download.microsoft.com/download/6/2/F/" +
    "62F70029-A592-4158-BB51-E102812CBD4F/IE9-Windows7-x64-enu.exe"

try {

   Register-ObjectEvent $client DownloadProgressChanged -action {     

        Write-Progress -Activity "Downloading" -Status `
            ("{0} of {1}" -f $eventargs.BytesReceived, $eventargs.TotalBytesToReceive) `
            -PercentComplete $eventargs.ProgressPercentage    
    }

    Register-ObjectEvent $client DownloadFileCompleted -SourceIdentifier Finished

    $file = "c:\temp\ie9-beta.exe"
    $client.DownloadFileAsync($url, $file)

    # optionally wait, but you can break out and it will still write progress
    Wait-Event -SourceIdentifier Finished

} finally { 
    $client.dispose()
}

PowerShell v1.0

If you're stuck on v1 (this is not specifically for you as you mention v2 in the question) you can use my powershell 1.0 eventing snap-in at http://pseventing.codeplex.com/

Async Callbacks

Another tricky area in .NET is async callbacks. There is nothing directly in v1 or v2 of powershell that can help you here, but you can convert an async callback to an event with some simple plumbing and then deal with that event using regular eventing. I posted a script for this (New-ScriptBlockCallback) at http://poshcode.org/1382

Hope this helps,

-Oisin

like image 98
x0n Avatar answered Nov 19 '22 01:11

x0n


I see that you use async call so that you can show the progress. Then you can use BitsTransfer module for that. It shows the progress by default:

Import-Module BitsTransfer
Start-BitsTransfer -Source $url -dest d:\temp\yourfile.zip

If you would like to transfer the file in the background, you could use something like this:

Import-Module BitsTransfer

$timer = New-Object Timers.Timer
$timer.Interval = 300
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {
    if ($transfer.JobState -ne 'Transferring') { 
        $timer.Enabled = 0; 
        Write-Progress -Completed -Activity Downloading -Status done
        return 
    }
    $progress = [int](100* $transfer.BytesTransferred/$transfer.BytesTotal)
    Write-Progress -Activity Downloading -Status "$progress% done" -PercentComplete $progress
} -sourceId mytransfer
$transfer = Start-BitsTransfer -Source $url -dest d:\temp\yourfile.zip -async
$timer.Enabled = 1

# after that
Unregister-Event -SourceIdentifier mytransfer
$timer.Dispose()

The key parameter is -async. It starts the transfer in background. I haven't found any event triggered by the transfer, so I query the job each second to report the state via Timers.Timer object.

However with this solution, it is needed to unregister the event and dispose the timer. Some time ago I had problems with unregistering in the scriptblock passed as -Action (it could be in the if branch), so I unregister the event in separate command.


I think @oising (x0n) has some solution on his blog. He'll tell you that hopefully and that would be answer for your question.

like image 6
stej Avatar answered Nov 19 '22 01:11

stej