Powershell GUI 冻结,即使有运行空间 [英] Powershell GUI Freezing, even with runspace

查看:72
本文介绍了Powershell GUI 冻结,即使有运行空间的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在创建一个带有 GUI 的 powershell 脚本,它将用户配置文件从选定的源磁盘复制到目标磁盘.我已经使用 VS Community 2019 在 XAML 中创建了 GUI.该脚本的工作方式如下:您选择源磁盘、目标磁盘、用户配置文件和要复制的文件夹.当您按下开始"按钮时,它会调用一个名为 Backup_data 的函数,在其中创建一个运行空间.在此运行空间中,只有一小部分Copy-Item, 将您选择的内容作为参数.

脚本工作正常,所有想要的项目都被正确复制.问题是 GUI 在复制期间冻结(没有无响应"消息或其他任何消息,它只是完全冻结;无法点击任何地方,无法移动窗口).我已经看到使用运行空间可以解决这个问题,但对我来说不是.我错过了什么吗?

这是Backup_Data函数:

函数BackupData {##创建运行空间$PowerShell = [powershell]::Create()[无效]$PowerShell.AddScript( {参数 ($global:ReturnedDiskSource, $global:SelectedUser, $global:SelectedFolders, $global:ReturnedDiskDestination)##脚本块foreach ($global:SelectedFolders 中的 $item) {Copy-Item -Path$global:ReturnedDiskSource\Users\$global:SelectedUser\$item"-目的地$global:ReturnedDiskDestination\Users\$global:SelectedUser\$item"-Force -Recurse}}).AddArgument($global:ReturnedDiskSource).AddArgument($global:SelectedUser).AddArgument($global:SelectedFolders).AddArgument($global:ReturnedDiskDestination)#调用命令$PowerShell.Invoke()$PowerShell.Dispose()}

解决方案

PowerShell SDK 的

源代码:

使用命名空间 System.Windows使用命名空间 System.Windows.Threading# 加载 WPF 程序集.添加类型 -AssemblyName PresentationCore、PresentationFramework# 定义XAML文件,包含一对background-operation-launching# 按钮加上相关的状态文本框.[xml] $xaml = @";<窗口xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d=http://schemas.microsoft.com/expression/blend/2008"xmlns:mc=http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:Test";标题=主窗口"高度=220"宽度=600"><网格><TextBox x:Name="Status1";高度=140"宽度=280"保证金=10,10"TextWrapping="Wrap"垂直对齐=顶部"水平对齐=左"AcceptsReturn="True"AcceptsTab="True"填充=4"VerticalScrollBarVisibility=自动"/><TextBox x:Name="Status2";高度=140"宽度=280"保证金=10,10"TextWrapping="Wrap"垂直对齐=顶部"水平对齐=右"AcceptsReturn="True"AcceptsTab="True"填充=4"VerticalScrollBarVisibility=自动"/><Button x:Name="DoThing1";内容=做事 1"水平对齐=左"VerticalAlignment=底部"宽度=100"高度=22"保证金=10,5"IsDefault=真"/><Button x:Name="DoThing2";内容=做事 2"水平对齐=右"VerticalAlignment=底部"宽度=100"高度=22"保证金=10,5"/></网格></窗口>"@# 解析 XAML,它返回一个 [System.Windows.Window] 实例.$Window = [Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))# 将窗口的相关控件保存在 PowerShell 变量中.# 后台操作启动按钮.$btns = $Window.FindName('DoThing1'), $Window.FindName('DoThing2')# 使用 [hashtable] 将按钮映射到关联的状态文本框.$txtBoxes = @{$btns[0] = $Window.FindName('Status1')$btns[1] = $Window.FindName('Status2')}# 使用 [hashtable] 将按钮映射到关联的背景# 操作,定义为稍后传递给 Start-ThreadJob 的脚本块.# 这里的示例操作运行了几秒钟,# 发出 '.'每秒和完成时的消息.$scriptBlocks = @{$btns[0] ={1..3 |ForEach-Object { '.';开始睡眠 1 }第一件事完成了."}$btns[1] ={1..2 |ForEach-Object { '.';开始睡眠 1 }事情 2 已经完成了."}}# 附加按钮单击事件处理程序# 启动后台操作(线程作业).foreach($btn 中的 $btn){$btn.Add_Click({# 暂时禁用此按钮以防止重新输入.$this.IsEnabled = $false# 在关联的文本框中显示状态消息.$txtBoxes[$this].Text = "在 $(Get-Date -Format T) 处开始 $($this.Name -replace '\D').";# 异步启动以该按钮命名的后台线程作业.# 注意:也可以与 Start-Job 一起使用,但它会在 *子进程* 中运行代码,# 这要慢得多,而且还有其他含义.$null = Start-ThreadJob -Name $this.Name $scriptBlocks[$this]})}# 定义一个自定义的类似 DoEvents() 的函数来处理 GUI WPF 事件并且可以# 在前台线程的自定义事件循环中调用.# 改编自:https://docs.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcherframe函数 DoWpfEvents {[DispatcherFrame] $frame = [DispatcherFrame]::new($True)$null = [Dispatcher]::CurrentDispatcher.BeginInvoke('背景',[DispatcherOperationCallback] {参数([对象] $f)($f -as [DispatcherFrame]).Continue = $false返回 $null},$frame)[调度器]::PushFrame($frame)}# 最后,非模态地显示窗口...$Window.Show()$null = $Windows.Activate() # 确保窗口获得焦点.# ... 并基于调用自定义 .DoEvents() 方法进入自定义事件循环而 ($Window.IsVisible) {# 处理 GUI 事件.DoWpfEvents# 处理挂起的后台(线程)作业,如果有的话.找工作|ForEach-Object {# 通过作业名称获取原始按钮.$btn = $Window.FindName($_.Name)# 获取对应的状态文本框.$txtBox = $txtBoxes[$btn]# 测试作业是否已终止.$completed = $_.State -in 'Completed', 'Failed', 'Stopped'# 将任何新结果附加到相应的状态文本框.# 注意使用重定向 *>&1 来捕获所有流,特别是包括错误流.if ($data = Receive-Job $_ *>&1) {$txtBox.Text += "`n";+ ($data -join "`n")}# 清理,如果作业完成.如果($完成){删除作业 $_$btn.IsEnabled = $true # 重新启用按钮.$txtBox.Text += "`nJob 终止于: $(Get-Date -Format T);状态:$($_.State)."}}# 注意:如果没有挂起的 GUI 事件,此循环将非常快速地循环.# 为了减轻这种情况,我们*也*睡了一点,但足够短以保持# GUI 响应.Start-Sleep -Milliseconds 50}# 窗口已关闭;清理:# 如果窗口在所有作业完成之前关闭,# 获取未完成作业的剩余输出,等待它们完成,然后删除它们.找工作|Receive-Job -Wait -AutoRemoveJob

I am creating a powershell script with a GUI, that copies user profiles from a selected source disk to a destination disk. I've created the GUI in XAML, with VS Community 2019. The script works like this : you select the source disk, the destination disk, the user profile and the folders you want to copy. When you press the button "Start", it calls a function called Backup_data, where a runspace is created. In this runspace, there's just a litte Copy-Item, with as arguments what you've selected.

The script works fine, all the wanted items are correctly copied. The problem is that the GUI is freezing during the copy (no "not responding" message or whatever, it's just completly freezed ; can't click anywhere, can't move the window). I've seen that using runspaces would fix this problem, but it doesn't to me. Am I missing something ?

Here's the function Backup_Data:

Function BackupData {  
  ##CREATE RUNSPACE
  $PowerShell = [powershell]::Create()
  [void]$PowerShell.AddScript( {
      Param ($global:ReturnedDiskSource, $global:SelectedUser, $global:SelectedFolders, $global:ReturnedDiskDestination)
      ##SCRIPT BLOCK
      foreach ($item in $global:SelectedFolders) {
        Copy-Item -Path "$global:ReturnedDiskSource\Users\$global:SelectedUser\$item" -Destination "$global:ReturnedDiskDestination\Users\$global:SelectedUser\$item" -Force -Recurse
      }
    }).AddArgument($global:ReturnedDiskSource).AddArgument($global:SelectedUser).AddArgument($global:SelectedFolders).AddArgument($global:ReturnedDiskDestination)
  #Invoke the command
  $PowerShell.Invoke()
  $PowerShell.Dispose()
}

解决方案

The PowerShell SDK's PowerShell.Invoke() method is synchronous and therefore by design blocks while the script in the other runspace (thread) runs.

You must use the asynchronous PowerShell.BeginInvoke() method instead.

Simple example without WPF in the picture (see the bottom section for a WPF solution):

$ps = [powershell]::Create()

# Add the script and invoke it *asynchronously*
$asyncResult = $ps.AddScript({ Start-Sleep 3; 'done' }).BeginInvoke()

# Wait in a loop and check periodically if the script has completed.
Write-Host -NoNewline 'Doing other things..'
while (-not $asyncResult.IsCompleted) {
  Write-Host -NoNewline .
  Start-Sleep 1
}
Write-Host

# Get the script's success output.
"result: " + $ps.EndInvoke($asyncResult)

$ps.Dispose()

Note that there's a simpler alternative to using the PowerShell SDK: the ThreadJob module's Start-ThreadJob cmdlet, a thread-based alternative to the child-process-based regular background jobs started with Start-Job, that is compatible with all the other *-Job cmdlets.

Start-ThreadJob comes with PowerShell [Core] 7+, and can be installed from the PowerShell Gallery in Windows PowerShell (Install-Module ThreadJob).

# Requires module ThreadJob (preinstalled in v6+)

# Start the thread job, always asynchronously.
$threadJob = Start-ThreadJob { Start-Sleep 3; 'done' }

# Wait in a loop and check periodically if the job has terminated.
Write-Host -NoNewline 'Doing other things..'
while ($threadJob.State -notin 'Completed', 'Failed') {
  Write-Host -NoNewline .
  Start-Sleep 1
}
Write-Host

# Get the job's success output.
"result: " + ($threadJob | Receive-Job -Wait -AutoRemoveJob)


Complete example with WPF:

If, as in your case, the code needs to run from an event handler attached to a control in a WPF window, more work is needed, because Start-Sleep can not be used, since it blocks processing of GUI events and therefore freezes the window.

Unlike WinForms, which has a built-in method for processing pending GUI events on demand ([System.Windows.Forms.Application]::DoEvents(), WPF has no equivalent method, but it can be added manually, as shown in the DispatcherFrame documentation.

The following example:

  • Creates a window with two background-operation-launching buttons and corresponding status text boxes.

  • Uses the button-click event handlers to launch the background operations via Start-ThreadJob:

    • Note: Start-Job would work too, but that would run the code in a child process rather than a thread, which is much slower and has other important ramifications.

    • It also wouldn't be hard to adapt the example to use of the PowerShell SDK ([powershell]), but thread jobs are more PowerShell-idiomatic and are easier to manage, via the regular *-Job cmdlets.

  • Displays the WPF window non-modally and enters a custom event loop:

    • A custom DoEvents()-like function, DoWpfEvents, adapted from the DispatcherFrame documentation is called in each loop operation for GUI event processing.

      • Note: For WinForms code, you could simply call [System.Windows.Forms.Application]::DoEvents().
    • Additionally, the progress of the background thread jobs is monitored and output received is appended to the job-specific status text box. Completed jobs are cleaned up.

Note: Just as it would if you invoked the window modally (with .ShowModal()), the foreground thread and therefore the console session is blocked while the window is being displayed. The simplest way to avoid this is to run the code in a hidden child process instead; assuming that the code is in script wpfDemo.ps1:

# In PowerShell [Core] 7+, use `pwsh` instead of `powershell`
Start-Process -WindowStyle Hidden powershell '-noprofile -file wpfDemo.ps1'

You could also do this via the SDK, which would be faster, but it's much more verbose and cumbersome:
$runspace = [runspacefactory]::CreateRunspace() $runspace.ApartmentState = 'STA'; $runspace.Open(); $ps = [powershell]::Create(); $ps.Runspace = $runspace; $null = $ps.AddScript((Get-Content -Raw wpfDemo.ps1)).BeginInvoke()

Screenshot:

This sample screen shot shows one completed background operation, and one ongoing one (running them in parallel is supported); note how the button that launched the ongoing operation is disabled for the duration of the operation, to prevent re-entry:

Source code:

using namespace System.Windows
using namespace System.Windows.Threading

# Load WPF assemblies.
Add-Type -AssemblyName PresentationCore, PresentationFramework

# Define the XAML document, containing a pair of background-operation-launching
# buttons plus associated status text boxes.
[xml] $xaml = @"
<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Test"
        Title="MainWindow" Height="220" Width="600">
    <Grid>
        <TextBox x:Name="Status1" Height="140" Width="280" Margin="10,10" TextWrapping="Wrap" VerticalAlignment="Top" HorizontalAlignment="Left" AcceptsReturn="True" AcceptsTab="True" Padding="4" VerticalScrollBarVisibility="Auto" />
        <TextBox x:Name="Status2" Height="140" Width="280" Margin="10,10" TextWrapping="Wrap" VerticalAlignment="Top" HorizontalAlignment="Right" AcceptsReturn="True" AcceptsTab="True" Padding="4" VerticalScrollBarVisibility="Auto" />
        <Button x:Name="DoThing1" Content="Do Thing 1" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="100" Height="22" Margin="10,5" IsDefault="True" />
        <Button x:Name="DoThing2" Content="Do Thing 2" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="100" Height="22" Margin="10,5" />
    </Grid>
</Window>
"@

# Parse the XAML, which returns a [System.Windows.Window] instance.
$Window = [Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))

# Save the window's relevant controls in PowerShell variables.
# Background-operation-launching buttons.
$btns = $Window.FindName('DoThing1'), $Window.FindName('DoThing2')

# Use a [hashtable] to map the buttons to the associated status text boxes.
$txtBoxes = @{
  $btns[0] = $Window.FindName('Status1')
  $btns[1] = $Window.FindName('Status2')
}
# Use a [hashtable] to map the buttons to the associated background
# operations, defined as script blocks to be passed to Start-ThreadJob later.
# The sample operations here run for a few seconds, 
# emitting '.' every second and a message on completion.
$scriptBlocks = @{
  $btns[0] = 
    {
      1..3 | ForEach-Object { '.'; Start-Sleep 1 }
      'Thing 1 is done.'
    }
  $btns[1] = 
    {
      1..2 | ForEach-Object { '.'; Start-Sleep 1 }
      'Thing 2 is done.'
    }
}

# Attach the button-click event handlers that
# launch the background operations (thread jobs).
foreach ($btn in $btns) {

  $btn.Add_Click({

    # Temporarily disable this button to prevent re-entry.
    $this.IsEnabled = $false

    # Show a status message in the associated text box.
    $txtBoxes[$this].Text = "Started thing $($this.Name -replace '\D') at $(Get-Date -Format T)."

    # Asynchronously start a background thread job named for this button.
    # Note: Would work with Start-Job too, but that runs the code in *child process*, 
    #       which is much slower and has other implications.
    $null = Start-ThreadJob -Name $this.Name $scriptBlocks[$this]

  })

}

# Define a custom DoEvents()-like function that processes GUI WPF events and can be 
# called in a custom event loop in the foreground thread.
# Adapted from: https://docs.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcherframe
function DoWpfEvents {
  [DispatcherFrame] $frame = [DispatcherFrame]::new($True)
  $null = [Dispatcher]::CurrentDispatcher.BeginInvoke(
    'Background', 
    [DispatcherOperationCallback] {
      param([object] $f)
      ($f -as [DispatcherFrame]).Continue = $false
      return $null
    }, 
    $frame)
  [Dispatcher]::PushFrame($frame)
}


# Finally, display the window NON-modally...
$Window.Show() 
$null = $Windows.Activate() # Ensures that the window gets the focus.
# ... and enter a custom event loop based on calling the custom .DoEvents() method
while ($Window.IsVisible) {

  # Process GUI events.
  DoWpfEvents

  # Process pending background (thread) jobs, if any.
  Get-Job | ForEach-Object {
    
    # Get the originating button via the job name.
    $btn = $Window.FindName($_.Name)
    # Get the corresponding status text box.
    $txtBox = $txtBoxes[$btn]

    # Test if the job has terminated.
    $completed = $_.State -in 'Completed', 'Failed', 'Stopped'

    # Append any new results to the respective status text boxes.
    # Note the use of redirection *>&1 to capture ALL streams, notably including the error stream.
    if ($data = Receive-Job $_ *>&1) {
      $txtBox.Text += "`n" + ($data -join "`n")
    }

    # Clean up, if the job is completed.
    if ($completed) {
      Remove-Job $_
      $btn.IsEnabled = $true # re-enable the button.
      $txtBox.Text += "`nJob terminated on: $(Get-Date -Format T); status: $($_.State)."
    }

  }

  # Note: If there are no GUI events pending, this loop will cycle very rapidly.
  #       To mitigate this, we *also* sleep a little, but short enough to still keep
  #       the GUI responsive.
  Start-Sleep -Milliseconds 50

}

# Window was closed; clean up:
# If the window was closed before all jobs completed, 
# get the incomplete jobs' remaining output, wait for them to finish, and delete them.
Get-Job | Receive-Job -Wait -AutoRemoveJob


这篇关于Powershell GUI 冻结,即使有运行空间的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆