如何将 PowerShell 会话配置为在请求从另一个进程终止后优雅且可靠地结束?

问题描述 投票:0回答:1

考虑以下背景:

  1. 以编程方式从另一个进程停止 PowerShell 的明显方法都不会导致
    clean{}
    运行。
  2. Stop-Process
    “直接跳杀”甚至没有尝试标准化但不可靠的请求终止方法,如
    WM_CLOSE
    GenerateConsoleCtrlEvent
    ,如这里所述。
  3. 有无数线索表明,在其他平台上成功使用 POSIX 信号(如
    SIGTERM
    )的移植程序在 Windows 上却无法成功执行相同操作。 也许这条评论最简洁地总结了这种情况。

现在考虑运行我的一些代码的 PowerShell 会话。 我可以在控制台上按 CTRL+C,执行

clean{}
块,通过任何活动的
Dispose()
和其他对象树进行级联
Cmdlet
调用,并且该过程最终会优雅地停止清理工作已完成。 我想从另一个进程中调用类似程度的有序停止。

我怎样才能以可靠的方式实现这一目标?

windows powershell terminate sigint
1个回答
0
投票

说明

此技术导致

pwsh.exe
的目标实例使用
EventWaitHandle
监听特定的命名事件。 当引发该事件时,处理程序通过
CTRL_C_EVENT
 引发 
GenerateConsoleCtrlEvent
。 这似乎与在控制台按 CTRL+C 具有相同的效果。 优点是另一个进程可以获取并引发该事件,除了将目标的进程 ID 插入事件名称模式之外,无需任何先决条件。

原则上,可以在

Stop-Process
之前有条件地调用
pwsh
实例,方法是测试
TryOpenExisting()
来查看目标是否已注册句柄,如果已注册,则引发事件并允许目标有一些时间来注册句柄。优雅地关闭。 如果目标进程在超时后没有关闭,则可以调用通常的
Stop-Process
来强制终止。

要使用此技术,需要在

<registration>
的实例中调用
pwsh.exe
标签中的部分,以便该实例能够优雅地响应信号。 然后可以调用
<signal>
标签中的部分(插入了正确的进程 ID)来指示实例正常停止。

演示代码

下面的代码执行以下操作:

  • 创建名为

    remote.ps1
    的脚本,并启动运行该脚本的
    pwsh.exe
    的独立实例。 然后,该“远程”PowerShell 实例执行以下操作:

    • 将其进度记录到文件中
    • 注册一个名为
      pwsh_$PID_CTRL_C_EVENT_78008808-fb42-4ce6-b8bb-cfe8135a0914
      的事件,通过
      CTRL_C_EVENT
       引发 
      GenerateConsoleCtrlEvent
    • 睡觉
  • 原始脚本在创建“远程”PowerShell 实例后继续并执行以下操作:

    • 引发“远程”进程订阅的
      pwsh_$PID_CTRL_C_EVENT_78008808-fb42-4ce6-b8bb-cfe8135a0914
      事件
    • 等待“远程”进程结束
    • 输出“远程”进程写入的日志

请注意,运行演示代码会输出

begin
clean

表示

end{}
块被信号中断并且没有完成,但是
clean{}
块确实完成了。

{
    begin { 'begin' | Set-Content .\log.txt }
    end {
        # <registration>
        $remote_ctrl_c_event =
            [System.Threading.EventWaitHandle]::new(
                $false,
                'ManualReset',
                "pwsh_$PID`_CTRL_C_EVENT_78008808-fb42-4ce6-b8bb-cfe8135a0914")

        Add-Type @'
namespace n { public static class c {
    [System.Runtime.InteropServices.DllImport("KERNEL32.dll",ExactSpelling=true,SetLastError=true)]
    public static extern bool GenerateConsoleCtrlEvent(int dwCtrlEvent,int dwProcessGroupId);
    public static void RegisterCtrlC(
        System.Threading.WaitHandle handle
    ) {
        System.Threading.ThreadPool.RegisterWaitForSingleObject(
            handle,
            (s,to) => GenerateConsoleCtrlEvent(0,0),
            null,-1,true);
    }
}}
'@
        [n.c]::RegisterCtrlC($remote_ctrl_c_event)
        # </registration>
        Start-Sleep -Seconds 1000
        'end' | Add-Content .\log.txt
    }
    clean { 'clean' | Add-Content .\log.txt }
} |
    Set-Content .\remote.ps1

$remote_process = Start-Process -FilePath pwsh.exe -ArgumentList .\remote.ps1 -PassThru
Start-Sleep -Seconds 2
# <signal>
$remote_ctrl_c_event =
    [System.Threading.EventWaitHandle]::new(
        $false,
        'ManualReset',
        "pwsh_$($remote_process.Id)_CTRL_C_EVENT_78008808-fb42-4ce6-b8bb-cfe8135a0914"
    )
$remote_ctrl_c_event.Set() | Out-Null
# </signal>
while (Get-Process -Id $remote_process.Id -ea si) {
    Start-Sleep -Milliseconds 10
}
Get-Content .\log.txt
© www.soinside.com 2019 - 2024. All rights reserved.