如何将具有当前值的变量传递给 runspace?

How to pass a variable with the current value to runspace?

提问人:Dron 提问时间:11/8/2023 最后编辑:Santiago SquarzonDron 更新时间:11/11/2023 访问量:93

问:

我正在编写一个脚本,该脚本将下载我需要的选定程序(感谢 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

PowerShell WinForms 异步 事件 运行空间

评论

0赞 Santiago Squarzon 11/9/2023
对于这个问题,您目前的期望是什么?要处理的代码太多,使其适用于超过 1 个异步下载将增加复杂性。PowerShell 确实不是一种理想的语言(如我之前的回答所述)。此外,您还希望同时下载 2 次,但只有 1 个进度条,那么应该如何处理?
1赞 Dron 11/9/2023
我不需要一次两次异步下载。我想在完成第一个后开始第二个。因此,我将在一次下载结束时运行并删除数组元素(表示下载信息)。依此类推,直到我遍历所有元素,数组变为空。我每次下载都使用相同的指标。
0赞 Santiago Squarzon 11/9/2023
好吧,这更容易处理。如果没有其他人这样做,我会稍后在我有空闲时间时尝试发布答案,但这里的关键是使用最有可能的进度条,但我仍然不清楚您想如何处理进度条,它应该重置并从第一次下载完成后开始吗?ConcurrentQueue<T>0
0赞 Dron 11/9/2023
所有处理都已经在代码中实现,并且可以正常工作。如果在初始化期间设置了 $FileList 变量的值,则通过两次下载,一切正常。但是,如果在按钮单击处理中已经设置了值,则该变量在逻辑上将为空。唯一的问题是在正确的时间传递变量。代码按应有的方式工作(尽管您可以提出自己的建议)。

答:

2赞 Santiago Squarzon 11/9/2023 #1

如果要处理多个异步下载的“排队”则过去实现中的代码会变得越来越复杂。基本上,过去答案中的代码能够在不阻塞表单(主线程)的情况下处理单个下载,但是为了处理多个下载,需要重构它。

此实现设置了一个工作线程,该线程处理来自主线程(窗体)的“排队”下载,此工作线程将保持活动状态,等待新的下载排队,并且仅在父进程终止时终止。

我添加了一些指针注释,这些注释可能有助于您理解代码背后的逻辑。

演示

demo

法典

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()

评论

0赞 Dron 11/11/2023
您的解决方案当然很漂亮。但对我来说太麻烦了。我只需要以某种方式传达变量的实际值。但是你的代码给了我一个想法......
0赞 Santiago Squarzon 11/11/2023
@Dron我在之前的回答中已经解释过,powershell 不是为此:P设计的语言
0赞 Dron 11/11/2023 #2

虽然我还没有弄清楚为什么传输您可以使用的“当前”形式,并且在脚本启动时仅传输变量的值,并且在更改时不“更新”它,但我意识到您可以简单地将所有处理转移到那个单独的运行空间,然后没有必要传输任何内容。$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()