Blocking While Waiting For C# Event In Powershell Causes Deadlock
在PowerShell中,我试图启动一个进程,然后等待直到该进程退出或脚本从命名管道中获取信号。虽然命名管道组件可以正常工作,但是当脚本被阻止时,代码无法处理事件。这只是事件相关代码的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | $ErrorActionPreference ="Stop" # launch process with the appropriate args. $p = [System.Diagnostics.Process]::new() $p.StartInfo.FileName ="notepad.exe" $p.StartInfo.Arguments = $null $p.EnableRaisingEvents = $true $p.Start() # create a task completion source $tcs = [System.Threading.Tasks.TaskCompletionSource[Boolean]]::new() Register-ObjectEvent -InputObject $p -EventName Exited ` -MessageData $tcs ` -Action { $Event.MessageData.SetResult($true) } # wait for the program to exit $processTask = $tcs.Task # this deadlocks $processTask.Wait() |
是否存在通过PowerShell的作业系统解决该问题的正确方法,还是通过手动旋转另一个等待该过程退出的C#任务来解决该问题,从而使脚本能够在发生任何一个事件后恢复运行?
作为参考,这是更完整的脚本(包括对流程事件或命名管道的等待):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | $ErrorActionPreference ="Stop" #inbound pipe $pipeListener = [System.IO.Pipes.NamedPipeServerStream]::new("testPipe", 1) # launch process with the appropriate args. $p = [System.Diagnostics.Process]::new() $p.StartInfo.FileName ="notepad.exe" $p.StartInfo.Arguments = $null $p.EnableRaisingEvents = $true $p.Start() # I would normally solve this using a TaskCompletionSource # in C#, and have it fire off the process's exited event. # create a task completion source $tcs = [System.Threading.Tasks.TaskCompletionSource[Boolean]]::new() Register-ObjectEvent -InputObject $p -EventName Exited ` -MessageData $tcs ` -Action { $Event.MessageData.SetResult($true) } # wait for the program to exit, or for pipe signal try { $pipeTask = $pipeListener.WaitForConnectionAsync() $processTask = $tcs.Task while ($true) { echo"Launched Process, waiting for sequence to complete..." # this works, but not instantly - the timeout appears to allow the # event to process. #$res = [System.Threading.Tasks.Task]::WaitAny(@($processTask, $pipeTask), 10000) # this does not. $res = [System.Threading.Tasks.Task]::WaitAny(@($processTask, $pipeTask)) echo"Got wait result: $res" if ($pipeTask.IsCompleted) { echo"Got pipe connection" $sr = [System.IO.StreamReader]::new($pipeListener) $msg = $sr.ReadToEnd() echo"Pipe sent: $msg" break } if ($processTask.IsCompleted) { echo"Process completed." break } } } finally { $pipeListener.Dispose() } |
经过更多研究,这似乎不可能-PowerShell仅在主线程中处理C#事件,即使事件的源是后台线程也是如此。
此外,PowerShell作业系统为每个作业使用进程而不是线程,因此您不能从后台作业设置"任务完成源"。
但是,从PowerShell调用的C#代码可以正常工作,并且可以生成线程。因此,为解决此问题,我编写了一个C#函数,该函数在进程终止时将消息发送到命名管道:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public static void NotifyPipeOnProcessExit( Process processToMonitor, String pipeName ) { // Create a pipe client. var client = new NamedPipeClientStream(".", pipeName, PipeDirection.Out); // create a process task var processTask = Task.Run(() => { processToMonitor.WaitForExit(); using (client) { client.Connect(0); var sw = new StreamWriter(client); sw.Write("Process exitted."); sw.Close(); } }); return; } |
然后我在PowerShell中简单地调用了此函数以将过程对象附加到管道:
1 2 3 | [ThreadTool.ThreadHelper]::NotifyPipeOnProcessExit($p, $pipeName) $pipeListener.WaitForConnection() |
这也简化了代码,因为PowerShell现在只需要监视一个事件。
也就是说,C#代码可以像设置命名管道一样轻松地设置TaskCompletionSource而不是发送消息。