应用场景:
C# WPF程序通过System.Diagnostics.Process启动一个控制台程序,该子进程需要接收键盘输入Ctrl-C退出。需要在C#程序中模拟键盘输入Ctrl-C传递给子程序,然后通过Process.StandardOutput获取子程序输出。
在微软文档中提供了以下函数操作Console:
- AttachConsole 将父进程attach到指定进程的控制台
- FreeConsole 父进程detach控制台
- GenerateConsoleCtrlEvent 将指定的信号发送到共享控制台的进程组
- SetConsoleCtrlHandler 从父进程的处理函数列表中添加或删除HandlerRoutine函数
以下是具体代码
// 导入Win32 Console函数
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AttachConsole(uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern bool FreeConsole();
[DllImport("kernel32.dll")]
static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate handler, bool add);
[DllImport("kernel32.dll")]
static extern bool AllocConsole();
delegate Boolean ConsoleCtrlDelegate(CtrlTypes type);
// 控制消息
enum CtrlTypes : uint
{
CTRL_C_EVENT = 0,
CTRL_BREAK_EVENT,
CTRL_CLOSE_EVENT,
CTRL_LOGOFF_EVENT = 5,
CTRL_SHUTDOWN_EVENT
}
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);
public static void StopProcess(Process proc)
{
// 以防父进程已经attach到另一个Console,先调一次FreeConsole
FreeConsole();
// 一个进程最多只能attach到一个Console,否则失败,返回0
if (AttachConsole((uint)proc.Id))
{
// 设置父进程属性,忽略Ctrl-C信号
SetConsoleCtrlHandler(null, true);
// 发出一个Ctrl-C到共享该控制台的所有进程中
GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0);
// 父进程与控制台分离,此时子进程控制台收到Ctrl-C关闭
FreeConsole();
// 现在父进程没有Console,为它新建一个
AllocConsole();
// 等待子进程退出
proc.WaitForExit(2000);
// 恢复父进程处理Ctrl-C信号
SetConsoleCtrlHandler(null, false);
// C#版的GetLastError()
var lastError = Marshal.GetLastWin32Error();
}
}
原理:
- 控制台应用程序可以看成由两部分组成:主程序+控制台窗口,主程序可以调用函数对控制台进行各种操作,上面列出的4个函数也包含在其中Console Functions。
- 由于主程序只能attach到最多一个Console,这里先调用一次FreeConsole,然后将父进程attach到子进程控制台。如果父进程也是一个控制台程序,当启动子控制台程序时,子进程会attach到父进程的控制台,这里就不需要FreeConsole,下面的判断语句也需要修改。
- 接下来调用SetConsoleCtrlHandler(null, true)设置父进程的属性忽略Ctrl-C信号,注意这个属性是可以继承的,所以子进程控制台也会忽略Ctrl-C信号。
- GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0),“0”表示在共享主进程控制台的所有进程中生成Ctrl-C信号。由于上一步设置了属性,所以到这里子进程还不会响应Ctrl-C信号。
- 第二个FreeConsole让父进程脱离子进程的控制台,子程序不再继承父进程设置的属性,子程序响应Ctrl-C信号退出。(猜测:这里控制台接收到了Ctrl-C信号,保存起来没有处理,当父进程设置的属性消失后,控制台才响应Ctrl-C信号)
- 到此,父进程没有控制台窗口,但是可以继续运行。如果父进程需要,我们可以给他新建一个控制台,这个新建的控制台没有Ctrl-C信号,所以当执行SetConsoleCtrlHandler(null, false)删除属性恢复处理Ctrl-C信号时,父进程不会退出。
仅供参考,若有错误,请不吝赐教!