如何将 ctrl+c 发送到 C# 中的进程?

发布于 2024-08-26 18:09:08 字数 954 浏览 5 评论 0原文

我正在为命令行可执行文件编写一个包装类。该 exe 接受来自 stdin 的输入,直到我在命令提示符 shell 中按下 Ctrl+C,在这种情况下,它会根据以下内容将输出打印到 stdout:输入。我想在 C# 代码中模拟 Ctrl+C 按下,将终止命令发送到 .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。

I'm writing a wrapper class for a command line executable. This exe accepts input from stdin until I hit Ctrl+C in the command prompt shell, in which case it prints output to stdout based on the input. I want to simulate that Ctrl+C press in C# code, sending the kill command to a .NET Process object. I've tried calling Process.Kill(), but that doesn't seem to give me anything in the process's StandardOutput StreamReader. Might there be anything I'm not doing right? Here's the code I'm trying to use:

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();

The output is always empty, even though I get data back from stdout when I run the exe manually.

Edit: This is C# 2.0 by the way.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

饮湿 2024-09-02 18:09:08

尽管事实上使用 GenerateConsoleCtrlEvent() 发送 Ctrl+C 信号是正确的答案,但需要进行大量澄清才能使其正常工作不同的 .NET 应用程序类型。

如果您的 .NET 应用程序不使用自己的控制台(Windows Forms/WPF/Windows Service/ASP.NET),则基本流程是: 将

  1. 主 .NET 进程附加到要使用 < 发出信号的进程的控制台kbd>Ctrl+C
  2. 通过使用 SetConsoleCtrlHandler() 禁用信号处理,防止主 .NET 进程因 Ctrl+C 事件而停止。
  3. 使用 GenerateConsoleCtrlEvent()当前控制台生成控制台事件(processGroupId 应为零!答案为发送 p. SessionId 将不起作用并且不正确)。
  4. 等待发出信号的进程响应(例如,等待其退出)
  5. 恢复主进程的 Ctrl+C 处理并断开与控制台的连接。

以下代码片段说明了如何执行此操作:

Process p;
if (AttachConsole((uint)p.Id)) {
    SetConsoleCtrlHandler(null, true);
    try { 
        if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0))
            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 主进程控制台没有副作用:

  1. 创建小型支持 .NET 控制台程序,该程序从命令行参数接受进程 ID,在 FreeConsole() 之前丢失自己的控制台>AttachConsole() 调用并使用上述代码将 Ctrl+C 发送到目标进程。
  2. 当主 .NET 控制台进程需要将 Ctrl+C 发送到另一个控制台进程时,它只会在新进程中调用此实用程序。
  3. 在主 .NET 控制台进程中,在生成“杀手”进程(来自步骤 1)之前调用 SetConsoleCtrlHandler(null, true),并在之后调用 SetConsoleCtrlHandler(null, false)。否则你的主进程也会收到 Ctrl+C 并死亡

Despite the fact that using GenerateConsoleCtrlEvent() for sending Ctrl+C signal is the right answer, it needs significant clarification to get it to work in different .NET application types.

If your .NET application doesn't use its own console (Windows Forms/WPF/Windows Service/ASP.NET), the basic flow is:

  1. Attach the main .NET process to the console of the process that you want to signal with Ctrl+C.
  2. Prevent the main .NET process from stopping because of Ctrl+C event by disabling handling of the signal with SetConsoleCtrlHandler().
  3. Generate the console event for the current console with GenerateConsoleCtrlEvent() (processGroupId should be zero! The answer with code that sends p.SessionId will not work and is incorrect).
  4. Wait for the signaled process to respond (e.g. by waiting for it to exit)
  5. Restore Ctrl+C handling by main process and disconnect from console.

The following code snippet illustrates how to do that:

Process p;
if (AttachConsole((uint)p.Id)) {
    SetConsoleCtrlHandler(null, true);
    try { 
        if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0))
            return false;
        p.WaitForExit();
    } finally {
        SetConsoleCtrlHandler(null, false);
        FreeConsole();
    }
    return true;
}

where SetConsoleCtrlHandler(), FreeConsole(), AttachConsole() and GenerateConsoleCtrlEvent() are native WinAPI methods:

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);

Note that waiting for the targeted process to respond, typically by waiting for the process to exit, is critical. Otherwise, the Ctrl+C signal will remain in the current process's input queue and when handling is restored by the second call to SetConsoleCtrlHandler(), that signal will terminate the current process, rather than the targeted one.

Things become more complex if you need to send Ctrl+C from .NET console application. The above approach will not work because AttachConsole() returns false in this case (the main console app already has a console). It is possible to call FreeConsole() before AttachConsole() call, but doing so will result in the original .NET app console being lost, which is not acceptable in most cases.

Here is my solution for this case; it works and has no side effects for the .NET main process console:

  1. Create small supporting .NET console program that accepts process ID from command line arguments, loses its own console with FreeConsole() before the AttachConsole() call and sends Ctrl+C to the target process with code mentioned above.
  2. The main .NET console process just invokes this utility in a new process when it needs to send Ctrl+C to another console process.
  3. In the main .NET console process call SetConsoleCtrlHandler(null, true) before spawning the "killer"-process (from step 1) and SetConsoleCtrlHandler(null, false) after. Else your main process will also receive Ctrl+C and die
何以畏孤独 2024-09-02 18:09:08

其实我刚刚找到了答案。谢谢你们的回答,但事实证明我所要做的就是这样:

p.StandardInput.Close()

这会导致我生成的程序完成从标准输入的读取并输出我需要的内容。

I've actually just figured out the answer. Thank you both for your answers, but it turns out that all i had to do was this:

p.StandardInput.Close()

which causes the program I've spawned to finish reading from stdin and output what i need.

浅语花开 2024-09-02 18:09:08

@alonl:用户正在尝试包装命令行程序。命令行程序没有消息泵,除非它们是专门创建的,即使是这样,Ctrl+C 在命令行中也不具有相同的语义。 Windows 环境应用程序(默认情况下复制)就像在命令行环境中一样 (Break)。

我把这个放在一起。 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 处于活动状态:True
4080 处于活动状态:False

希望有帮助!

(澄清一下:\x3 是十六进制字符 3 的十六进制转义序列,即 Ctrl+C。它不仅仅是一个神奇的数字.;))

@alonl: The user is attempting to wrap a command-line program. Command-line programs don't have message pumps unless they are specifically created, and even if that was the case, Ctrl+C doesn't have the same semantics in a Windows-environment application (copy, by default) as it does in a command-line environment (Break).

I threw this together. CtrlCClient.exe simply calls Console.ReadLine() and waits:

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();
}

My output seems to do what you want:

4080 is active: True
4080 is active: False

Hope that helps!

(To clarify: \x3 is the hex escape sequence for the hex character 3, which is Ctrl+C. It's not just a magic number. ;) )

残花月 2024-09-02 18:09:08

好的,这是一个解决方案。

发送Ctrl-C信号的方法是使用GenerateConsoleCtrlEvent。但是,此调用采用 processGroupdID 参数,并将 Ctrl-C 信号发送到组中的所有进程。如果不是因为无法在 .net 中生成与您(父进程)位于不同进程组中的子进程,那就没问题了。因此,当您发送GenerateConsoleCtrlEvent时,子进程都将处于不同的进程组中。您(家长)明白了。因此,您还需要捕获父级中的 Ctrl-C 事件,然后确定是否需要忽略它。

就我而言,我希望父级也能够处理 Ctrl-C 事件,因此我需要区分 Ctrl-C 用户在控制台上发送的事件,以及父进程发送给子进程的事件。我通过在将 Ctrl-C 发送给子级时简单地设置/取消设置布尔标志来做到这一点,然后在父级的 Ctrl< 中检查此标志。 /kbd>-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;
}

Ok, here is a solution.

The way to send the Ctrl-C signal is with GenerateConsoleCtrlEvent. HOWEVER, this call takes a processGroupdID parameter, and sends the Ctrl-C signal to all processes in the group. This would be fine if it weren't for the fact that there is no way spawn child process in .net that is in a different process group than you (the parent) are in. So, when you send the GenerateConsoleCtrlEvent, both the child AND YOU (THE PARENT) GET IT. So, you need to capture the Ctrl-C event in the parent too, and then determine if you ned to ignore it not.

In my case, I want the parent to be able to handle Ctrl-C events also, so I need to distnguish between Ctrl-C events sent by the user on the console, and those sent by the parent process to the child. I do this by just hackishly setting/unsetting a boolean flag while send the Ctrl-C to the child, and then checking for this flag in the parent's Ctrl-C event handler (ie. if send Ctrl-C to child, then ignore.)

So, the code would look something like this:

//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;
}
吹梦到西洲 2024-09-02 18:09:08

FWIW,就我而言,我想要的是从控制台进程创建一个子控制台进程(与此相关的 ffmpeg.exe)并支持干净的 CTRL-C 处理在我的进程和子进程中(当按下 CTRL-C 时 ffmpreg 正常退出,这是我想继续工作的一个很好的功能)

我在这里找不到任何解决方案正在工作,所以我只是互操作 Windows' CreateProcess 函数,它无需任何努力即可工作,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);

FWIW, in my case, what I wanted is, from a console process, create a child console process (ffmpeg.exe for that matter) and support clean CTRL-C handling in my process and in the child process (ffmpreg exits normally when CTRL-C is pressed which is a nice feature I wanted to keep working)

None of the solution I found here were working, so I just interop'd Windows' CreateProcess function and it just works w/o any effort, CTRL-C is automatically received by the child app and by the parent app, input and output streams are shared, etc. I was not able to reproduce that kind of code using the standard .NET Process class:

    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);
空心空情空意 2024-09-02 18:09:08

尝试实际发送组合键 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+组合键所需的内容...
我知道您发送 Alt+Key 所需的消息是 WM_SYSTEMKEYDOWN 和 WM_SYSTEMKEYUP,无法告诉您有关 Ctrl 的信息...

Try actually sending the Key Combination Ctrl+C, instead of directly terminating the process:

 [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
              );

Look it up on the MSDN, you should find what you need there in order to send the Ctrl+Key combination...
I know that the message you need for sending Alt+Key is WM_SYSTEMKEYDOWN and WM_SYSTEMKEYUP, can't tell you about Ctrl...

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文