如何将 ctrl+c 发送到 c# 中的进程?
Posted
技术标签:
【中文标题】如何将 ctrl+c 发送到 c# 中的进程?【英文标题】:How do I send ctrl+c to a process in c#? 【发布时间】:2010-09-21 22:32:41 【问题描述】:我正在为命令行可执行文件编写一个包装类。这个 exe 接受来自 stdin
的输入,直到我在命令提示符 shell 中点击 Ctrl+C
,在这种情况下,它会根据输入将输出打印到 stdout
。我想在 C# 代码中模拟 Ctrl+C
按下,将 kill 命令发送到 .NET Process
对象。我试过打电话给Process.Kill()
,但这似乎并没有给我在这个过程中的任何东西StandardOutput
StreamReader
。可能有什么我做的不对吗?这是我正在尝试使用的代码:
ProcessStartInfo info = new ProcessStartInfo(exe, args);
info.RedirectStandardError = true;
info.RedirectStandardInput = true;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
Process p = Process.Start(info);
p.StandardInput.AutoFlush = true;
p.StandardInput.WriteLine(scriptcode);
p.Kill();
string error = p.StandardError.ReadToEnd();
if (!String.IsNullOrEmpty(error))
throw new Exception(error);
string output = p.StandardOutput.ReadToEnd();
输出始终为空,即使我在手动运行 exe 时从 stdout
获取数据。
编辑:顺便说一句,这是 C# 2.0。
【问题讨论】:
对于任何寻找此问题答案的人,MedallionShell NuGet package 包含此问题的跨平台实现。在 Windows 上,它使用下面描述的控制台附加方法,但以一种更安全的方式解决了一些缺陷。 【参考方案1】:尽管使用GenerateConsoleCtrlEvent()
发送 Ctrl+C 信号是正确的答案,但需要大量澄清才能使其在不同的 .NET 应用程序中工作类型。
如果您的 .NET 应用程序不使用自己的控制台(Windows Forms/WPF/Windows Service/ASP.NET),则基本流程是:
-
将 .NET 主进程附加到要使用 Ctrl+C 发出信号的进程的控制台。
通过使用
SetConsoleCtrlHandler()
禁用信号处理,防止主 .NET 进程因 Ctrl+C 事件而停止。
使用GenerateConsoleCtrlEvent()
为当前 控制台生成控制台事件(processGroupId
应该为零!发送p.SessionId
的代码的答案将不起作用并且不正确)。
等待发出信号的进程响应(例如,等待它退出)
恢复 Ctrl+C 由主进程处理并断开与控制台的连接。
以下代码 sn-p 说明了如何做到这一点:
Process p;
if (AttachConsole((uint)p.Id))
SetConsoleCtrlHandler(null, true);
try
if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT,p.SessionId))
return false;
p.WaitForExit();
finally
SetConsoleCtrlHandler(null, false);
FreeConsole();
return true;
其中SetConsoleCtrlHandler()
、FreeConsole()
、AttachConsole()
和 GenerateConsoleCtrlEvent()
是本机 WinAPI 方法:
internal const int CTRL_C_EVENT = 0;
[DllImport("kernel32.dll")]
internal static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool AttachConsole(uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
internal static extern bool FreeConsole();
[DllImport("kernel32.dll")]
static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add);
// Delegate type to be used as the Handler Routine for SCCH
delegate Boolean ConsoleCtrlDelegate(uint CtrlType);
请注意,等待目标进程响应(通常是等待进程退出)至关重要。否则,Ctrl+C 信号将保留在当前进程的输入队列中,当第二次调用 SetConsoleCtrlHandler()
恢复处理时,该信号将终止 当前进程,而不是目标进程。
如果您需要从 .NET 控制台应用程序发送 Ctrl+C,事情会变得更加复杂。上述方法将不起作用,因为在这种情况下AttachConsole()
返回false
(主控制台应用程序已经有一个控制台)。可以在调用AttachConsole()
之前调用FreeConsole()
,但这样做会导致原始.NET 应用程序控制台丢失,这在大多数情况下是不可接受的。
这是我对这种情况的解决方案;它对 .NET 主进程控制台有效且没有副作用:
-
创建支持 .NET 控制台的小型程序,该程序接受来自命令行参数的进程 ID,在调用
AttachConsole()
之前丢失自己的控制台并使用 FreeConsole()
并发送 Ctrl+C kbd> 使用上面提到的代码到目标进程。
.NET 主控制台进程仅在需要将 Ctrl+C 发送到另一个控制台进程时在新进程中调用此实用程序。
【讨论】:
这适用于 .NET Framework,但我无法使用 .NET 5.0 运行它。 AttachConsole 调用失败,错误代码为 5(拒绝访问)。 @candritzky 看看***.com/questions/68700848/attach-console-to-process 感谢您的链接,但我看不到它应该告诉我什么。它是否应该告诉我我不应该调用 AttachConsole(如 @Shadow4571 所述)?我已经尝试过了,但它不起作用。还是应该告诉我应该从 .NET 5.0 迁移到 .NET Framework 4.8?对于我们手头的问题,这不是我们的选择(就像“将 Ctrl+C 信号从父进程发送到 Windows 上的 .NET Core 子进程”一样简单)。 这是一个很好的回应 - 我有一个 .NET 5 服务附加到控制台并按照这些步骤成功发送 Ctrl + C,但您确实必须按顺序执行所有操作。我试着跳过一些,没有骰子。【参考方案2】:我实际上刚刚找到了答案。谢谢你们的回答,但事实证明我所要做的就是:
p.StandardInput.Close()
这会导致我生成的程序完成从标准输入读取并输出我需要的内容。
【讨论】:
请注意,它仅在进程尝试从标准输入读取时才有效。在程序尝试从中读取内容之前,关闭 stdin 什么都不做。 为什么会显示此异常“StandardIn 尚未重定向”。 ?我正在使用 ffmpeg 进行屏幕捕获 关闭标准输入与按 Control-C 不同。如果你有一个循环读取输入然后关闭它,因为外部应用程序仍在运行,你会得到一个错误。【参考方案3】:@alonl:用户正在尝试包装命令行程序。命令行程序没有消息泵,除非它们是专门创建的,即使是这种情况,Ctrl+C 在Windows 环境应用程序(默认情况下是复制),就像它在命令行环境中所做的一样(中断)。
我把它放在一起。 CtrlCClient.exe 只是调用Console.ReadLine()
并等待:
static void Main(string[] args)
ProcessStartInfo psi = new ProcessStartInfo("CtrlCClient.exe");
psi.RedirectStandardInput = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.UseShellExecute = false;
Process proc = Process.Start(psi);
Console.WriteLine("0 is active: 1", proc.Id, !proc.HasExited);
proc.StandardInput.WriteLine("\x3");
Console.WriteLine(proc.StandardOutput.ReadToEnd());
Console.WriteLine("0 is active: 1", proc.Id, !proc.HasExited);
Console.ReadLine();
我的输出似乎符合您的要求:
4080 is active: True 4080 is active: False
希望有帮助!
(澄清一下:\x3
是十六进制字符 3 的十六进制转义序列,即 Ctrl+C。这不仅仅是一个幻数。;) )
【讨论】:
使用既分配 Console.CancelKeyPress 委托又执行 Console.ReadLine(); 的测试程序StandardInput.WriteLine("\x3"); 的建议解决方案;确实完成了 ReadLine 调用,但没有(对我而言)触发 CancelKeyPress 委托。错误/不正确的正确性演示,因为任何输入,而不仅仅是 ctrl+c,都会触发进程退出? (在键盘上按 ctrl+c 会触发我的委托) @David Burg:可能是框架代码中的错误修复?这篇文章是在三个版本和 > 4 年前撰写的。 澄清一下(对不起,gravedig),如果你不是从标准输入读取,这不起作用,因为它实际上并没有发送信号【参考方案4】:好的,这是一个解决方案。
发送 Ctrl-C 信号的方式是使用 GenerateConsoleCtrlEvent。但是,此调用采用 processGroupdID 参数,并向组中的所有进程发送 Ctrl-C 信号。如果不是因为没有办法在 .net 中生成与您(父级)不同的进程组中的子进程,这将很好。因此,当您发送 GenerateConsoleCtrlEvent 时,两个子进程你(家长)明白了。所以,你需要在父节点中也捕获Ctrl-C事件,然后判断是否需要忽略。
就我而言,我希望父级也能够处理 Ctrl-C 事件,所以我需要区分 Ctrl -C 用户在控制台发送的事件,以及父进程发送给子进程的事件。我只是通过在将 Ctrl-C 发送给孩子时粗暴地设置/取消设置布尔标志来做到这一点,然后在父母的 Ctrl-C 事件处理程序(即,如果将 Ctrl-C 发送给孩子,则忽略。)
所以,代码看起来像这样:
//import in the declaration for GenerateConsoleCtrlEvent
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool GenerateConsoleCtrlEvent(ConsoleCtrlEvent sigevent, int dwProcessGroupId);
public enum ConsoleCtrlEvent
CTRL_C = 0,
CTRL_BREAK = 1,
CTRL_CLOSE = 2,
CTRL_LOGOFF = 5,
CTRL_SHUTDOWN = 6
//set up the parents CtrlC event handler, so we can ignore the event while sending to the child
public static volatile bool SENDING_CTRL_C_TO_CHILD = false;
static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
e.Cancel = SENDING_CTRL_C_TO_CHILD;
//the main method..
static int Main(string[] args)
//hook up the event handler in the parent
Console.CancelKeyPress += new ConsoleCancelEventHandler(Console_CancelKeyPress);
//spawn some child process
System.Diagnostics.ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo();
psi.Arguments = "childProcess.exe";
Process p = new Process();
p.StartInfo = psi;
p.Start();
//sned the ctrl-c to the process group (the parent will get it too!)
SENDING_CTRL_C_TO_CHILD = true;
GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, p.SessionId);
p.WaitForExit();
SENDING_CTRL_C_TO_CHILD = false;
//note that the ctrl-c event will get called on the parent on background thread
//so you need to be sure the parent has handled and checked SENDING_CTRL_C_TO_CHILD
already before setting it to false. 1000 ways to do this, obviously.
//get out....
return 0;
【讨论】:
根据 StyleCop 和 FxCop 规则对导入进行了几项改进。评论太长,所以将尝试内联...p.SessionId
不是 GenerateConsoleCtrlEvent
的正确参数。
此代码无效。 GenerateConsoleCtrlEvent 需要进程组 ID,而不是终端会话 ID。
遇到同样的问题,这个答案对我没有用,总是提供错误 87(无效参数)。相反,我使用了 p.Id,尽管它仍然没有将 Ctrl-C 发送到我的进程,GenerateConsoleCtrlEvent
现在至少没有给出任何错误..
您无需等待进程退出。您可以休眠足够长的时间以确保所有受信号影响的线程都已调度【参考方案5】:
FWIW,就我而言,我想要的是,从控制台进程创建一个子控制台进程(ffmpeg.exe)并支持干净的 CTRL-C kbd> 在我的进程和子进程中处理(当 CTRL-C 被按下时,ffmpreg 正常退出,这是我想继续工作的一个很好的功能)
我在这里找到的解决方案都不起作用,所以我只是互操作了 Windows'CreateProcess function,它无需任何努力即可工作,CTRL-C 由子应用程序和父应用程序自动接收,输入和输出流是共享的,等等。我无法使用标准的 .NET Process 类重现那种代码:
static void RunFFMpeg(string arguments)
var startup = new STARTUPINFO();
startup.cb = Marshal.SizeOf<STARTUPINFO>();
if (!CreateProcess(null, "ffmpeg.exe " + arguments, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref startup, out var info))
throw new Win32Exception(Marshal.GetLastWin32Error());
CloseHandle(info.hProcess);
CloseHandle(info.hThread);
var process = Process.GetProcessById(info.dwProcessId);
Console.CancelKeyPress += (s, e) =>
process.WaitForExit();
Console.WriteLine("Abort.");
// end of program is here
;
process.WaitForExit();
Console.WriteLine("Exit.");
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
[DllImport("kernel32")]
private static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CreateProcess(
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
int dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
【讨论】:
【参考方案6】:尝试实际发送组合键Ctrl+C,而不是直接终止进程:
[DllImport("user32.dll")]
public static extern int SendMessage(
int hWnd, // handle to destination window
uint Msg, // message
long wParam, // first message parameter
long lParam // second message parameter
);
在 MSDN 上查找,您应该可以在那里找到发送 Ctrl+Key 组合所需的内容... 我知道你发送 Alt+Key 需要的消息是 WM_SYSTEMKEYDOWN 和 WM_SYSTEMKEYUP,不能告诉你关于 Ctrl...
【讨论】:
如果进程在没有窗口的情况下被隐藏怎么办?以上是关于如何将 ctrl+c 发送到 c# 中的进程?的主要内容,如果未能解决你的问题,请参考以下文章
Nodejs:将 Ctrl+C 发送到 Windows 上的子进程
将 Ctrl+C 事件发送到在 Windows 上使用 QProcess 启动的进程
如何将 control + L 和 control + C 发送到另一个应用程序?