PowerShell: проблема Runspace с DownloadFileAsync
Мне нужно было скачать файл с помощью WebClient
в PowerShell 2.0, и я хотел показать ход загрузки, поэтому я сделал это следующим образом:
$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()
Я получаю сообщение об ошибке There is no Runspace available to run scripts in this thread.
для кода в обработчике $downloadProgress
, что является логическим. Однако как я могу предоставить Runspace
для потока, который (возможно) принадлежит ThreadPool
?
UPDATE:
Обратите внимание, что оба ответа на этот вопрос заслуживают внимания, и я согласился бы, если бы мог.
Ответы
Ответ 1
Спасибо stej за кивок.
Андрей, у powershell есть свой собственный threadpool, и каждый поток обслуживания хранит указатель на threadstatic для рабочей области (статический член System.Management.Automation.Runspaces.Runspace.DefaultRunspace предоставляет это - и будет иметь нулевое значение в ваших обратных вызовах). В конечном итоге это означает, что для выполнения сценариев блокировки - особенно в script - использовать собственный поток (как это предусмотрено .NET для методов async).
PowerShell 2.0
Несмотря на это, нет необходимости играть с этим, поскольку powershell v2 имеет полную поддержку для 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
Если вы застряли в v1 (это не специально для вас, поскольку вы упоминаете v2 в вопросе), вы можете использовать мою powershell 1.0 eventing snap-in на http://pseventing.codeplex.com/
Асинхронные обратные вызовы
Еще одна сложная область в .NET - это асинхронные обратные вызовы. Нет ничего прямо в v1 или v2 powershell, которые могут вам помочь, но вы можете преобразовать обратный вызов асинхронного события в какое-то событие с помощью простой сантехники, а затем обработать это событие, используя регулярные события. Я отправил script для этого (New-ScriptBlockCallback) в http://poshcode.org/1382
Надеюсь, что это поможет,
-Oisin
Ответ 2
Я вижу, что вы используете асинхронный вызов, чтобы показать прогресс. Тогда вы можете использовать для этого модуль BitsTransfer. Он показывает прогресс по умолчанию:
Import-Module BitsTransfer
Start-BitsTransfer -Source $url -dest d:\temp\yourfile.zip
Если вы хотите перенести файл в фоновом режиме, вы можете использовать что-то вроде этого:
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()
Ключевым параметром является -async
. Он начинает передачу в фоновом режиме. Я не нашел никакого события, вызванного передачей, поэтому я запрашиваю каждое задание каждую секунду, чтобы сообщить состояние через объект Timers.Timer
.
Однако с этим решением необходимо отменить регистрацию события и установить таймер. Некоторое время назад у меня были проблемы с регистрацией в скриптблоке, прошедшем как -Action
(он мог быть в ветке if
), поэтому я отменил регистрацию события в отдельной команде.
Я думаю, что @oising (x0n) имеет некоторое решение в своем блоге. Он скажет вам, что, надеюсь, и это ответ на ваш вопрос.