如何将 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(),但这似乎并没有给我在这个过程中的任何东西StandardOutputStreamReader。可能有什么我做的不对吗?这是我正在尝试使用的代码:

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 启动的进程

c# 将键盘命令发送到另一个窗口/进程

如何将 control + L 和 control + C 发送到另一个应用程序?

如何使用 pinvoke 将图像从 c 或 c++ 发送到 C#

linux中ctrl+z,ctrl+d,ctrl+c的区别