需要从主 C# WPF 应用程序将 CTRL+C (SIGINT) 发送到 Process 对象
Need to send CTRL+C (SIGINT) to Process object from main C# WPF app
我有一个应用程序,我在其中启动了一些 Process 对象,将输出重定向到触发事件。这些进程应该能够 运行 无限期地运行,但我也希望能够向它们发出信号以优雅地终止(例如,完成它们所有的业务然后结束)。出于测试目的,我正在使用 tracert。以下是我如何创建和启动流程:
//Create
this.process = new Process();
this.process.StartInfo.FileName = "tracert.exe";
this.process.StartInfo.Arguments = "google.com";
this.process.StartInfo.UseShellExecute = false;
this.process.StartInfo.CreateNoWindow = true;
this.process.StartInfo.RedirectStandardOutput = true;
this.process.StartInfo.RedirectStandardError = true;
this.process.StartInfo.RedirectStandardInput = true;
this.process.EnableRaisingEvents = true;
this.process.OutputDataReceived += Process_OutputDataReceived;
this.process.Exited += Process_Exited;
...
//Start
new Thread(() =>
{
Thread.CurrentThread.IsBackground = true;
this.process.Refresh();
this.process.Start();
this.process.BeginOutputReadLine();
this.process.WaitForExit();
}).Start();
根据我的阅读,我的理解是发送 ctrl+c 信号的方式如下:
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GenerateConsoleCtrlEvent(ConsoleCtrlEvent sigevent, int dwProcessGroupId);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AttachConsole(uint dwProcessId);
[DllImport("kernel32.dll")]
static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern bool FreeConsole();
delegate Boolean ConsoleCtrlDelegate(uint CtrlType);
public enum ConsoleCtrlEvent
{
CTRL_C = 0,
CTRL_BREAK = 1,
CTRL_CLOSE = 2,
CTRL_LOGOFF = 5,
CTRL_SHUTDOWN = 6
}
private void StopProcess {
if (AttachConsole((uint)this.process.Id))
{
SetConsoleCtrlHandler(null, true);
GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
FreeConsole();
SetConsoleCtrlHandler(null, false);
}
}
但这似乎不起作用。它似乎立即触发进程的 Exited 事件(tracert 的退出代码为负),但 tracert 的输出继续显示完成,然后第二次触发进程的 Exited 事件,这次的退出代码为零。如果我调用 StopProcess 函数一次,然后在 tracert 继续执行其业务时调用它第二次,整个应用程序将关闭。
我正在使用针对 .NET 5.0 框架的 WPF 构建主应用程序。任何帮助将不胜感激!
我能够重现您报告的第二个问题,即当您尝试将信号发送到控制台进程时 WPF 进程退出。 (第一个问题,您在 中解释说,它原来是您的看门狗代码中的一个错误,即使它被明确指示要退出,它也会重新启动该过程。)
经过调查,在我看来,这是由对 GenerateConsoleCtrlEvent()
的调用和对 SetConsoleCtrlHandler()
的后续调用之间的竞争条件引起的。似乎如果这些调用发生得太快,GenerateConsoleCtrlEvent()
发送的 Ctrl+C 对默认处理仍然可见WPF 应用程序,导致进程以 STATUS_CONTROL_C_EXIT
代码退出(即按 Ctrl+C 的正常结果,但对于错误的过程)。
有趣的是,关于您用来发送信号的代码,我最感兴趣的一件事是它恢复了控制台的进程状态,并按照修改这些状态的相同顺序处理信号.这对我来说似乎很不寻常,因为通常以相反的顺序恢复状态,可以说是“退出”状态。
如果更改代码以便在释放附加控制台之前恢复信号处理(即通常编写代码的方式),那么主机进程接收信号并退出的问题会重现 第一次 方法被调用的时间。 IE。似乎它甚至第一次工作的唯一原因是主机进程第一次调用 FreeConsole()
函数时有一些延迟,这足以让信号不被注意。第二次通过,延迟不再存在(可能在 p/invoke 层中缓存了一些东西......我没有费心去调查那部分)。
不过在那之后,它的工作方式就像按预期顺序恢复状态一样。
总之……
通过在目标进程实际退出之前不恢复当前进程的状态,我能够可靠地解决问题。在我必须构建以重现问题的概念验证应用程序中,这相对简单,因为我已经实现了 TaskCompletionSource
,它是在引发 Exited
事件时设置的,因此我能够将该源的 Task
传递给 StopProcess()
方法,这样它就可以在恢复状态之前 await
Task
。
我建议您以类似的方式修复您的代码。请注意,您不能在 Process
本身上调用 WaitForExit()
,除非您从 UI 线程以外的某个线程这样做,因为 Process
class 使用UI 线程引发 Exited
事件,因此通过调用 WaitForExit()
阻塞 UI 线程将导致死锁。您可以通过将对 StopProcess()
的整个调用放在不同的线程中来避免这种情况,但这对我来说似乎有点过分,尤其是当有更优雅的方式来实现整个事情时。
您可以使用任何您喜欢的机制来等待进程终止,只要您注意不要让 UI 线程死锁。但这是我写的代码,如果你想参考它......
在 window class 中(请注意,WPF 完全损坏,因为这里根本没有 MVVM……这只是为了让基本的最小完整示例工作):
private Process _process;
private TaskCompletionSource _processTask;
private async void startButton_Click(object sender, RoutedEventArgs e)
{
startButton.IsEnabled = false;
stopButton.IsEnabled = true;
try
{
_process = new Process();
_processTask = new TaskCompletionSource();
_process.StartInfo.FileName = "tracert.exe";
_process.StartInfo.Arguments = "google.com";
_process.StartInfo.UseShellExecute = false;
_process.StartInfo.CreateNoWindow = true;
_process.StartInfo.RedirectStandardOutput = true;
_process.StartInfo.RedirectStandardError = true;
_process.StartInfo.RedirectStandardInput = true;
_process.EnableRaisingEvents = true;
_process.OutputDataReceived += (_, e) => _WriteLine($"stdout: \"{e.Data}\"");
_process.ErrorDataReceived += (_, e) => _WriteLine($"stderr: \"{e.Data}\"");
_process.Exited += (_, _) =>
{
_WriteLine($"Process exited. Exit code: {_process.ExitCode}");
_processTask.SetResult();
};
_process.Start();
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
await _processTask.Task;
}
finally
{
_process?.Dispose();
_process = null;
_processTask = null;
startButton.IsEnabled = true;
stopButton.IsEnabled = false;
}
}
private async void stopButton_Click(object sender, RoutedEventArgs e)
{
try
{
await Win32Process.StopProcess(_process, _processTask.Task);
}
catch (InvalidOperationException exception)
{
_WriteLine(exception.Message);
}
}
private void _WriteLine(string text)
{
Dispatcher.Invoke(() => consoleOutput.Text += $"{text}{Environment.NewLine}");
}
这是 StopProcess()
方法的更新版本(我将其放入自己的助手 class 中):
public static async Task StopProcess(Process process, Task processTask)
{
if (AttachConsole((uint)process.Id))
{
// NOTE: each of these functions could fail. Error-handling omitted
// for clarity. A real-world program should check the result of each
// call and handle errors appropriately.
SetConsoleCtrlHandler(null, true);
GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
await processTask;
SetConsoleCtrlHandler(null, false);
FreeConsole();
}
else
{
int hresult = Marshal.GetLastWin32Error();
Exception e = Marshal.GetExceptionForHR(hresult);
throw new InvalidOperationException(
$"ERROR: failed to attach console to process {process.Id}: {e?.Message ?? hresult.ToString()}");
}
}
您可能可以推断出 XAML 是什么——只是几个按钮和一个 TextBlock
来显示消息——但为了完整起见,无论如何,它就在这里:
<Window x:Class="WpfApp1.MainWindow"
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"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button x:Name="startButton" Grid.Row="0" Grid.Column="0" Content="Start" Click="startButton_Click"/>
<Button x:Name="stopButton" Grid.Row="0" Grid.Column="1" Content="Ctrl-C" Click="stopButton_Click" IsEnabled="False"/>
<ScrollViewer Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3">
<TextBlock x:Name="consoleOutput"/>
</ScrollViewer>
</Grid>
</Window>
我有一个应用程序,我在其中启动了一些 Process 对象,将输出重定向到触发事件。这些进程应该能够 运行 无限期地运行,但我也希望能够向它们发出信号以优雅地终止(例如,完成它们所有的业务然后结束)。出于测试目的,我正在使用 tracert。以下是我如何创建和启动流程:
//Create
this.process = new Process();
this.process.StartInfo.FileName = "tracert.exe";
this.process.StartInfo.Arguments = "google.com";
this.process.StartInfo.UseShellExecute = false;
this.process.StartInfo.CreateNoWindow = true;
this.process.StartInfo.RedirectStandardOutput = true;
this.process.StartInfo.RedirectStandardError = true;
this.process.StartInfo.RedirectStandardInput = true;
this.process.EnableRaisingEvents = true;
this.process.OutputDataReceived += Process_OutputDataReceived;
this.process.Exited += Process_Exited;
...
//Start
new Thread(() =>
{
Thread.CurrentThread.IsBackground = true;
this.process.Refresh();
this.process.Start();
this.process.BeginOutputReadLine();
this.process.WaitForExit();
}).Start();
根据我的阅读,我的理解是发送 ctrl+c 信号的方式如下:
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GenerateConsoleCtrlEvent(ConsoleCtrlEvent sigevent, int dwProcessGroupId);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AttachConsole(uint dwProcessId);
[DllImport("kernel32.dll")]
static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern bool FreeConsole();
delegate Boolean ConsoleCtrlDelegate(uint CtrlType);
public enum ConsoleCtrlEvent
{
CTRL_C = 0,
CTRL_BREAK = 1,
CTRL_CLOSE = 2,
CTRL_LOGOFF = 5,
CTRL_SHUTDOWN = 6
}
private void StopProcess {
if (AttachConsole((uint)this.process.Id))
{
SetConsoleCtrlHandler(null, true);
GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
FreeConsole();
SetConsoleCtrlHandler(null, false);
}
}
但这似乎不起作用。它似乎立即触发进程的 Exited 事件(tracert 的退出代码为负),但 tracert 的输出继续显示完成,然后第二次触发进程的 Exited 事件,这次的退出代码为零。如果我调用 StopProcess 函数一次,然后在 tracert 继续执行其业务时调用它第二次,整个应用程序将关闭。
我正在使用针对 .NET 5.0 框架的 WPF 构建主应用程序。任何帮助将不胜感激!
我能够重现您报告的第二个问题,即当您尝试将信号发送到控制台进程时 WPF 进程退出。 (第一个问题,您在
经过调查,在我看来,这是由对 GenerateConsoleCtrlEvent()
的调用和对 SetConsoleCtrlHandler()
的后续调用之间的竞争条件引起的。似乎如果这些调用发生得太快,GenerateConsoleCtrlEvent()
发送的 Ctrl+C 对默认处理仍然可见WPF 应用程序,导致进程以 STATUS_CONTROL_C_EXIT
代码退出(即按 Ctrl+C 的正常结果,但对于错误的过程)。
有趣的是,关于您用来发送信号的代码,我最感兴趣的一件事是它恢复了控制台的进程状态,并按照修改这些状态的相同顺序处理信号.这对我来说似乎很不寻常,因为通常以相反的顺序恢复状态,可以说是“退出”状态。
如果更改代码以便在释放附加控制台之前恢复信号处理(即通常编写代码的方式),那么主机进程接收信号并退出的问题会重现 第一次 方法被调用的时间。 IE。似乎它甚至第一次工作的唯一原因是主机进程第一次调用 FreeConsole()
函数时有一些延迟,这足以让信号不被注意。第二次通过,延迟不再存在(可能在 p/invoke 层中缓存了一些东西......我没有费心去调查那部分)。
不过在那之后,它的工作方式就像按预期顺序恢复状态一样。
总之……
通过在目标进程实际退出之前不恢复当前进程的状态,我能够可靠地解决问题。在我必须构建以重现问题的概念验证应用程序中,这相对简单,因为我已经实现了 TaskCompletionSource
,它是在引发 Exited
事件时设置的,因此我能够将该源的 Task
传递给 StopProcess()
方法,这样它就可以在恢复状态之前 await
Task
。
我建议您以类似的方式修复您的代码。请注意,您不能在 Process
本身上调用 WaitForExit()
,除非您从 UI 线程以外的某个线程这样做,因为 Process
class 使用UI 线程引发 Exited
事件,因此通过调用 WaitForExit()
阻塞 UI 线程将导致死锁。您可以通过将对 StopProcess()
的整个调用放在不同的线程中来避免这种情况,但这对我来说似乎有点过分,尤其是当有更优雅的方式来实现整个事情时。
您可以使用任何您喜欢的机制来等待进程终止,只要您注意不要让 UI 线程死锁。但这是我写的代码,如果你想参考它......
在 window class 中(请注意,WPF 完全损坏,因为这里根本没有 MVVM……这只是为了让基本的最小完整示例工作):
private Process _process;
private TaskCompletionSource _processTask;
private async void startButton_Click(object sender, RoutedEventArgs e)
{
startButton.IsEnabled = false;
stopButton.IsEnabled = true;
try
{
_process = new Process();
_processTask = new TaskCompletionSource();
_process.StartInfo.FileName = "tracert.exe";
_process.StartInfo.Arguments = "google.com";
_process.StartInfo.UseShellExecute = false;
_process.StartInfo.CreateNoWindow = true;
_process.StartInfo.RedirectStandardOutput = true;
_process.StartInfo.RedirectStandardError = true;
_process.StartInfo.RedirectStandardInput = true;
_process.EnableRaisingEvents = true;
_process.OutputDataReceived += (_, e) => _WriteLine($"stdout: \"{e.Data}\"");
_process.ErrorDataReceived += (_, e) => _WriteLine($"stderr: \"{e.Data}\"");
_process.Exited += (_, _) =>
{
_WriteLine($"Process exited. Exit code: {_process.ExitCode}");
_processTask.SetResult();
};
_process.Start();
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
await _processTask.Task;
}
finally
{
_process?.Dispose();
_process = null;
_processTask = null;
startButton.IsEnabled = true;
stopButton.IsEnabled = false;
}
}
private async void stopButton_Click(object sender, RoutedEventArgs e)
{
try
{
await Win32Process.StopProcess(_process, _processTask.Task);
}
catch (InvalidOperationException exception)
{
_WriteLine(exception.Message);
}
}
private void _WriteLine(string text)
{
Dispatcher.Invoke(() => consoleOutput.Text += $"{text}{Environment.NewLine}");
}
这是 StopProcess()
方法的更新版本(我将其放入自己的助手 class 中):
public static async Task StopProcess(Process process, Task processTask)
{
if (AttachConsole((uint)process.Id))
{
// NOTE: each of these functions could fail. Error-handling omitted
// for clarity. A real-world program should check the result of each
// call and handle errors appropriately.
SetConsoleCtrlHandler(null, true);
GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
await processTask;
SetConsoleCtrlHandler(null, false);
FreeConsole();
}
else
{
int hresult = Marshal.GetLastWin32Error();
Exception e = Marshal.GetExceptionForHR(hresult);
throw new InvalidOperationException(
$"ERROR: failed to attach console to process {process.Id}: {e?.Message ?? hresult.ToString()}");
}
}
您可能可以推断出 XAML 是什么——只是几个按钮和一个 TextBlock
来显示消息——但为了完整起见,无论如何,它就在这里:
<Window x:Class="WpfApp1.MainWindow"
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"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button x:Name="startButton" Grid.Row="0" Grid.Column="0" Content="Start" Click="startButton_Click"/>
<Button x:Name="stopButton" Grid.Row="0" Grid.Column="1" Content="Ctrl-C" Click="stopButton_Click" IsEnabled="False"/>
<ScrollViewer Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3">
<TextBlock x:Name="consoleOutput"/>
</ScrollViewer>
</Grid>
</Window>