44

我正在为命令行可执行文件编写一个包装类。这个 exe 接受输入,stdin直到我进入Ctrl+C命令提示符 shell,在这种情况下,它会stdout根据输入打印输出。我想Ctrl+C在 C# 代码中模拟该按下,将 kill 命令发送到 .NETProcess对象。我试过打电话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();

输出始终为空,即使我从stdout手动运行 exe 时获取数据。

编辑:顺便说一句,这是 C# 2.0。

4

6 回答 6

47

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

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

  1. 将主 .NET 进程附加到要使用Ctrl+发出信号的进程的控制台C
  2. 通过禁用对信号的处理,防止主 .NET 进程因Ctrl+C事件而停止SetConsoleCtrlHandler()
  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,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(主控制台应用程序已经有一个控制台)。可以先调用FreeConsole()AttachConsole()调用,但这样做会导致原始 .NET 应用程序控制台丢失,这在大多数情况下是不可接受的。

这是我对这种情况的解决方案;它可以工作并且对 .NET 主进程控制台没有副作用:

  1. 创建小型支持 .NET 控制台程序,该程序接受来自命令行参数的进程 ID,在调用FreeConsole()之前丢失自己的控制台,并使用上述代码将+发送到目标进程。AttachConsole()CtrlC
  2. 主 .NET 控制台进程只是在需要向另一个控制台进程发送Ctrl+时在新进程中调用此实用程序。C
于 2015-03-26T08:48:58.603 回答
37

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

p.StandardInput.Close()

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

于 2008-11-12T19:27:34.873 回答
23

@alonl:用户正在尝试包装命令行程序。命令行程序没有消息泵,除非它们是专门创建的,即使是这种情况,Ctrl+C在 Windows 环境应用程序(默认情况下为副本)中的语义与它在命令中的语义不同-line 环境(中断)。

我把这个扔在一起了。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。它不仅仅是一个幻数。;))

于 2008-11-12T08:51:14.220 回答
10

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

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

就我而言,我希望父进程也能够处理事件CtrlC因此我需要区分用户在控制台上发送的事件和父进程发送给子进程的事件CtrlC我只是通过在将 - 发送给孩子时粗暴地设置/取消设置布尔标志来做到这一点Ctrl,然后在父母的-事件处理程序C中检查这个标志(即,如果发送-给孩子,则忽略。)CtrlCCtrlC

所以,代码看起来像这样:

//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;
}
于 2011-09-06T17:21:04.627 回答
1

FWIW,在我的情况下,我想要的是,从控制台进程创建一个子控制台进程(ffmpeg.exe)并支持清理CTRL-C在我的进程和子进程中处理(ffmpreg在按下CTRL时正常退出C这是我想继续工作的一个很好的功能)

我在这里找到的解决方案都没有工作,所以我只是互操作了 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);
于 2019-11-16T12:06:38.697 回答
-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 的信息...

于 2008-11-12T05:50:26.940 回答