提问人:Dron 提问时间:11/8/2023 最后编辑:Santiago SquarzonDron 更新时间:11/11/2023 访问量:93
如何将具有当前值的变量传递给 runspace?
How to pass a variable with the current value to runspace?
问:
我正在编写一个脚本,该脚本将下载我需要的选定程序(感谢 Santiago Squarzon 的帮助)。当我需要下载一个程序时,该脚本运行良好,但要连续下载多个程序,我需要在下载结束时启动下一个程序。我制作了一个下载列表,我想将其值传递给运行空间,以便开始下载下一个程序(并从列表中删除已下载的程序)。依此类推,直到我浏览整个列表。
稍后会有更多的节目。目前,两个足以进行测试。$FileList
Add-Type -assembly System.Windows.Forms
$webMain = New-Object System.Net.WebClient
$FileList = @()
$MainForm = New-Object System.Windows.Forms.Form
$MainForm.Width = 420
$MainForm.Height = 200
$MainForm.FormBorderStyle = "Fixed3d"
$MainForm.MaximizeBox = $false
$MainForm.StartPosition = "CenterScreen"
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Location = New-Object System.Drawing.Size(120,50)
$Button1.Size = New-Object System.Drawing.Size(160,50)
$Button1.Text = "Download selected"
$Button1.Name = "Button1"
$Button1.Add_Click({
$Button1.Enabled = $false
if ($CheckBox1.Checked) {
$FileList += @{Link = "https://dl.google.com/chrome/install/standalonesetup64.exe"; Path = "D:\chrome.exe"}
}
if ($CheckBox2.Checked) {
$FileList += @{Link = "https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=ru"; Path = "D:\firefox.exe"}
}
$FileList = [System.Collections.ArrayList]$FileList
Wait-Event -Timeout 5
if ($FileList.count -gt 0) {
$webMain.DownloadFileAsync($FileList[0].Link, $FileList[0].Path)
$FileList.Remove($FileList[0])
}
})
$MainForm.Controls.Add($Button1)
$CheckBox1 = New-Object System.Windows.Forms.CheckBox
$CheckBox1.Location = New-Object System.Drawing.Size(10,10)
$CheckBox1.Size = New-Object System.Drawing.Size(100,20)
$CheckBox1.Text = "Chrome"
$MainForm.Controls.Add($CheckBox1)
$CheckBox2 = New-Object System.Windows.Forms.CheckBox
$CheckBox2.Location = New-Object System.Drawing.Size(10,30)
$CheckBox2.Size = New-Object System.Drawing.Size(100,20)
$CheckBox2.Text = "Firefox"
$MainForm.Controls.Add($CheckBox2)
$ProgressBar1 = New-Object System.Windows.Forms.ProgressBar
$ProgressBar1.Location = New-Object System.Drawing.Size(5,120)
$ProgressBar1.Size = New-Object System.Drawing.Size(400,40)
$ProgressBar1.Name = "ProgressBar1"
$MainForm.Controls.Add($ProgressBar1)
$rs = [runspacefactory]::CreateRunspace($Host)
$rs.Open()
$rs.SessionStateProxy.PSVariable.Set([psvariable]::new('form', $MainForm))
$rs.SessionStateProxy.PSVariable.Set([psvariable]::new('FileList', $FileList))
$ps = [powershell]::Create().AddScript({
Register-ObjectEvent -InputObject $args[0] -EventName 'DownloadProgressChanged' -SourceIdentifier 'WebMainDownloadProgressChanged' -Action {
[System.Threading.Monitor]::Enter($form)
$progress = $form.Controls.Find('ProgressBar1', $false)[0]
$progress.Value = $eventArgs.ProgressPercentage
[System.Threading.Monitor]::Exit($form)
}
Register-ObjectEvent -InputObject $args[0] -EventName 'DownloadFileCompleted' -SourceIdentifier 'WebMainDownloadFileCompleted' -Action {
[System.Threading.Monitor]::Enter($form)
$progress = $form.Controls.Find('ProgressBar1', $false)[0]
$progress.Value = 0
[System.Threading.Monitor]::Exit($form)
if ($FileList.count -gt 0) {
$webMain.DownloadFileAsync($FileList[0].Link, $FileList[0].Path)
$FileList.Remove($FileList[0])
}
if ($FileList.count -eq 0) {
$form.Controls.Find('Button1', $false)[0].Enabled = $true
}
}
}).AddArgument($webMain)
$ps.Runspace = $rs
$task = $ps.BeginInvoke()
$MainForm.ShowDialog()
据我了解,在脚本启动时传输当前值,而不是在按下按钮时传输当前值。如何正确地将变量传递给 runspace,使其包含按下按钮时的当前值?$rs.SessionStateProxy.PSVariable.Set([psvariable]::new('FileList', $FileList))
$FileList
答:
如果要处理多个异步下载的“排队”,则过去实现中的代码会变得越来越复杂。基本上,过去答案中的代码能够在不阻塞表单(主线程)的情况下处理单个下载,但是为了处理多个下载,需要重构它。
此实现设置了一个工作线程,该线程处理来自主线程(窗体)的“排队”下载,此工作线程将保持活动状态,等待新的下载排队,并且仅在父进程终止时终止。
我添加了一些指针注释,这些注释可能有助于您理解代码背后的逻辑。
演示
法典
Add-Type -Assembly System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$MainForm = New-Object System.Windows.Forms.Form
$MainForm.Width = 420
$MainForm.Height = 200
$MainForm.FormBorderStyle = "Fixed3d"
$MainForm.MaximizeBox = $false
$MainForm.StartPosition = "CenterScreen"
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Location = New-Object System.Drawing.Size(120,50)
$Button1.Size = New-Object System.Drawing.Size(160,50)
$Button1.Text = "Download selected"
$Button1.Name = "Button1"
$Button1.Add_Click({
# from this event handler we just enqueue downloads, there is no need to disable
# this button because the current implementation will allow as many downloads as you like
# and the download is limited by a `SemaphoreSlim` to handle throttling.
if ($CheckBox1.Checked) {
$queue.Enqueue(@{
Link = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
Path = Join-Path $pwd -ChildPath ('chrome' + [guid]::NewGuid() + '.exe')
})
}
if ($CheckBox2.Checked) {
$queue.Enqueue(@{
Link = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
Path = Join-Path $pwd -ChildPath ('firefox' + [guid]::NewGuid() + '.exe')
})
}
})
$MainForm.Controls.Add($Button1)
$CheckBox1 = New-Object System.Windows.Forms.CheckBox
$CheckBox1.Location = New-Object System.Drawing.Size(10,10)
$CheckBox1.Size = New-Object System.Drawing.Size(100,20)
$CheckBox1.Text = "Chrome"
$MainForm.Controls.Add($CheckBox1)
$CheckBox2 = New-Object System.Windows.Forms.CheckBox
$CheckBox2.Location = New-Object System.Drawing.Size(10,30)
$CheckBox2.Size = New-Object System.Drawing.Size(100,20)
$CheckBox2.Text = "Firefox"
$MainForm.Controls.Add($CheckBox2)
$ProgressBar1 = New-Object System.Windows.Forms.ProgressBar
$ProgressBar1.Location = New-Object System.Drawing.Size(5,120)
$ProgressBar1.Size = New-Object System.Drawing.Size(390,30)
$ProgressBar1.Name = "ProgressBar1"
$MainForm.Controls.Add($ProgressBar1)
$queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new()
$rs = [runspacefactory]::CreateRunspace($Host)
$rs.Open()
# the 2 objects that we need available in the worker thread are the queue and the progressbar,
# if you want more objects available from the form itslef it might be better to pass in `$MainForm`
# then use `.Controls.Find(...)` as shown in the previous answer
$rs.SessionStateProxy.PSVariable.Set('queue', $queue)
$rs.SessionStateProxy.PSVariable.Set('progress', $ProgressBar1)
$ps = [powershell]::Create().AddScript({
$client = [System.Net.WebClient]::new()
$task = $null
# allow only 1 download at a time
$throttleLimit = [System.Threading.SemaphoreSlim]::new(1, 1)
# NOTE: A single instance of `WebClient` can handle more than one download at the same
# but you need additional logic, i.e. check for `$client.IsBusy` before downloading async.
$registerObjectEventSplat = @{
InputObject = $client
EventName = 'DownloadProgressChanged'
Action = {
$progress.Value = $eventArgs.ProgressPercentage
Write-Progress -Activity 'Downloading...' -PercentComplete $eventArgs.ProgressPercentage
}
}
Register-ObjectEvent @registerObjectEventSplat
$registerObjectEventSplat = @{
InputObject = $client
EventName = 'DownloadFileCompleted'
Action = {
# Release the Semaphore handle here once the download is completed
$throttleLimit.Release()
"A download was completed...", "Items in Queue: $($queue.Count)" | Out-Host
Write-Progress -Activity 'Downloading...' -Completed
}
}
Register-ObjectEvent @registerObjectEventSplat
while ($true) {
# if there is nothing in queue
if (-not $queue.TryDequeue([ref] $task)) {
# sleep for a bit
Start-Sleep -Milliseconds 200
# and go to the next iteration
continue
}
# else, we know there is a download here but we want to allow only 1 at a time
# so, this inner `while` will unblock only when the SemaphoreSlim allows it
while (-not $throttleLimit.Wait(200)) { }
# once we have the handle of the Semaphore we can start the download here
$client.DownloadFileAsync($task['Link'], $task['Path'])
"A download has started...", "Items in Queue: $($queue.Count)" | Out-Host
$task | Out-Host
}
}, $false)
$ps.Runspace = $rs
$task = $ps.BeginInvoke()
$MainForm.ShowDialog()
# this should be a must in your code,
# always dispose the resources when done
$ps.Stop()
$ps.Dispose()
$rs.Dispose()
评论
虽然我还没有弄清楚为什么传输您可以使用的“当前”形式,并且在脚本启动时仅传输变量的值,并且在更改时不“更新”它,但我意识到您可以简单地将所有处理转移到那个单独的运行空间,然后没有必要传输任何内容。$rs.SessionStateProxy.PSVariable.Set([psvariable]::new('form', $MainForm))
$rs.SessionStateProxy.PSVariable.Set ([psvariable]::new('FileList', $FileList))
这就是我最终得到的:
Add-Type -assembly System.Windows.Forms
$MainForm = New-Object System.Windows.Forms.Form
$MainForm.Width = 420
$MainForm.Height = 200
$MainForm.FormBorderStyle = "Fixed3d"
$MainForm.MaximizeBox = $false
$MainForm.StartPosition = "CenterScreen"
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Location = New-Object System.Drawing.Size(120,50)
$Button1.Size = New-Object System.Drawing.Size(160,50)
$Button1.Text = "Download selected"
$Button1.Name = "Button1"
$MainForm.Controls.Add($Button1)
$CheckBox1 = New-Object System.Windows.Forms.CheckBox
$CheckBox1.Location = New-Object System.Drawing.Size(10,10)
$CheckBox1.Size = New-Object System.Drawing.Size(100,20)
$CheckBox1.Text = "Chrome"
$CheckBox1.Name = "CheckBox1"
$MainForm.Controls.Add($CheckBox1)
$CheckBox2 = New-Object System.Windows.Forms.CheckBox
$CheckBox2.Location = New-Object System.Drawing.Size(10,30)
$CheckBox2.Size = New-Object System.Drawing.Size(100,20)
$CheckBox2.Text = "Firefox"
$CheckBox2.Name = "CheckBox2"
$MainForm.Controls.Add($CheckBox2)
$ProgressBar1 = New-Object System.Windows.Forms.ProgressBar
$ProgressBar1.Location = New-Object System.Drawing.Size(5,120)
$ProgressBar1.Size = New-Object System.Drawing.Size(400,40)
$ProgressBar1.Name = "ProgressBar1"
$MainForm.Controls.Add($ProgressBar1)
$rs = [runspacefactory]::CreateRunspace($Host)
$rs.Open()
$rs.SessionStateProxy.PSVariable.Set([psvariable]::new('form', $MainForm))
$ps = [powershell]::Create().AddScript({
$FileList = @()
$webMain = New-Object System.Net.WebClient
$Button1 = $form.Controls.Find('Button1', $false)[0]
$Button1.Add_Click({
$Button1.Enabled = $false
if ($form.Controls.Find('CheckBox1', $false)[0].Checked) {
$Global:FileList += @{Link = "https://dl.google.com/chrome/install/standalonesetup64.exe"; Path = "D:\chrome.exe"}
}
if ($form.Controls.Find('CheckBox2', $false)[0].Checked) {
$Global:FileList += @{Link = "https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=ru"; Path = "D:\firefox.exe"}
}
$Global:FileList = [System.Collections.ArrayList]$Global:FileList
Wait-Event -Timeout 5
if ($Global:FileList.count -gt 0) {
$webMain.DownloadFileAsync($Global:FileList[0].Link, $Global:FileList[0].Path)
$Global:FileList.Remove($Global:FileList[0])
}
})
Register-ObjectEvent -InputObject $webMain -EventName 'DownloadProgressChanged' -SourceIdentifier 'WebMainDownloadProgressChanged' -Action {
[System.Threading.Monitor]::Enter($form)
$progress = $form.Controls.Find('ProgressBar1', $false)[0]
$progress.Value = $eventArgs.ProgressPercentage
[System.Threading.Monitor]::Exit($form)
}
Register-ObjectEvent -InputObject $webMain -EventName 'DownloadFileCompleted' -SourceIdentifier 'WebMainDownloadFileCompleted' -Action {
[System.Threading.Monitor]::Enter($form)
$progress = $form.Controls.Find('ProgressBar1', $false)[0]
$progress.Value = 0
if ($Global:FileList.count -eq 0) {
$form.Controls.Find('Button1', $false)[0].Enabled = $true
}
[System.Threading.Monitor]::Exit($form)
if ($Global:FileList.count -gt 0) {
$webMain.DownloadFileAsync($Global:FileList[0].Link, $Global:FileList[0].Path)
$Global:FileList.Remove($FileList[0])
}
}
})
$ps.Runspace = $rs
$ps.BeginInvoke()
$MainForm.ShowDialog()
评论
ConcurrentQueue<T>
0