Stuck on GenerateConsoleCtrlEvent in C# with conso

2019-01-26 00:24发布

问题:

I'm having the hardest time trying to get this to work, hoping one of you has done this before.

I have a C# console app that is running a child process which inherits its console. I want a ctrl-c caught by the outer app to be passed along to the inner app so that it can have a chance to shut down nicely.

I have some very simple code. I start a Process, then poll it with WaitForExit(10). I also have a CancelKeyPress handler registered, which sets a bool to true when it fires. The polling loop also checks this, and when it's true, it calls GenerateConsoleCtrlEvent() (which I have mapped through pinvoke).

I've tried a lot of combinations of params to GenerateConsoleCtrlEvent(). 0 or 1 for the first param, and either 0 or the child process's ID for the second param. Nothing seems to work. Sometimes I get a false back and Marshal.GetLastWin32Error() returns 0, and sometimes I get true back. But none cause the child app to receive a ctrl-c.

To be absolutely sure, I wrote a test C# app to be the child app which prints out what's going on with it and verified that manually typing ctrl-c when it runs does properly cause it to quit.

I've been banging my head against this for a couple hours. Can anyone give me some pointers on where to go with this?

回答1:

Not so sure this is a good approach. This only works if the child process is created with the CREATE_NEW_PROCESS_GROUP flag for CreateProcess(). The System.Diagnostics.Process class however does not support this.

Consider using the return value from the Main() method. There is already a unique value defined in the Windows SDK for Ctrl+C aborts, STATUS_CONTROL_C_EXIT or 0xC000013A. The parent process can get that return code from the Process.ExitCode property.



回答2:

Did you have any luck with this? My understanding is that when you press CTRL+C in a console, by default all the processes attached to the console receive it, not just the parent one. Here's an example:

Child.cs:

using System;

public class MyClass
{
    public static void CtrlCHandler(object sender, ConsoleCancelEventArgs args)
    {
        Console.WriteLine("Child killed by CTRL+C.");
    }
    public static void Main()
    {
        Console.WriteLine("Child start.");
        Console.CancelKeyPress += CtrlCHandler;
        System.Threading.Thread.Sleep(4000);
        Console.WriteLine("Child finish.");
    }
}

Parent.cs:

using System;

public class MyClass
{
    public static void CtrlCHandler(object sender, ConsoleCancelEventArgs args)
    {
        Console.WriteLine("Parent killed by CTRL+C.");
    }
    public static void Main()
    {
        Console.CancelKeyPress += CtrlCHandler;
        Console.WriteLine("Parent start.");
        System.Diagnostics.Process child = new System.Diagnostics.Process();
        child.StartInfo.UseShellExecute = false;
        child.StartInfo.FileName = "child.exe";
        child.Start();
        child.WaitForExit();
        Console.WriteLine("Parent finish.");
    }
}

Output:

Y:\>parent
Parent start.
Child start.
Parent killed by CTRL+C.
Child killed by CTRL+C.
^C
Y:\>parent
Parent start.
Child start.
Child finish.
Parent finish.

So I wouldn't have thought you'd need to do anything special. However, if you really need to generate CTRL+C events yourself, things might not be so easy. I'm not sure about the problems you describe, but as far as I can tell you can only send CTRL+C events to all the processes attached to a console window. If you detach a process, you can't send it CTRL+C events. If you want to be selective in which processes to send the CTRL+C events, you seem to need to create new console windows for every one. I've no idea if there's some way to do it without visible windows or when you want to redirect I/O using pipes.



回答3:

Here is my solution for sending ctrl-c to a process. FYI, I never got GenerateConsoleCtrlEvent to work.

Rather than using GenerateConsoleCtrlEvent, here is how I have found to send CTRL-C to a process. FYI, in this case, I didn't ever need to find the group process ID.

using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

public class ConsoleAppManager
{
    private readonly string appName;
    private readonly Process process = new Process();
    private readonly object theLock = new object();
    private SynchronizationContext context;
    private string pendingWriteData;

    public ConsoleAppManager(string appName)
    {
        this.appName = appName;

        this.process.StartInfo.FileName = this.appName;
        this.process.StartInfo.RedirectStandardError = true;
        this.process.StartInfo.StandardErrorEncoding = Encoding.UTF8;

        this.process.StartInfo.RedirectStandardInput = true;
        this.process.StartInfo.RedirectStandardOutput = true;
        this.process.EnableRaisingEvents = true;
        this.process.StartInfo.CreateNoWindow = true;

        this.process.StartInfo.UseShellExecute = false;

        this.process.StartInfo.StandardOutputEncoding = Encoding.UTF8;

        this.process.Exited += this.ProcessOnExited;
    }

    public event EventHandler<string> ErrorTextReceived;
    public event EventHandler ProcessExited;
    public event EventHandler<string> StandartTextReceived;

    public int ExitCode
    {
        get { return this.process.ExitCode; }
    }

    public bool Running
    {
        get; private set;
    }

    public void ExecuteAsync(params string[] args)
    {
        if (this.Running)
        {
            throw new InvalidOperationException(
                "Process is still Running. Please wait for the process to complete.");
        }

        string arguments = string.Join(" ", args);

        this.process.StartInfo.Arguments = arguments;

        this.context = SynchronizationContext.Current;

        this.process.Start();
        this.Running = true;

        new Task(this.ReadOutputAsync).Start();
        new Task(this.WriteInputTask).Start();
        new Task(this.ReadOutputErrorAsync).Start();
    }

    public void Write(string data)
    {
        if (data == null)
        {
            return;
        }

        lock (this.theLock)
        {
            this.pendingWriteData = data;
        }
    }

    public void WriteLine(string data)
    {
        this.Write(data + Environment.NewLine);
    }

    protected virtual void OnErrorTextReceived(string e)
    {
        EventHandler<string> handler = this.ErrorTextReceived;

        if (handler != null)
        {
            if (this.context != null)
            {
                this.context.Post(delegate { handler(this, e); }, null);
            }
            else
            {
                handler(this, e);
            }
        }
    }

    protected virtual void OnProcessExited()
    {
        EventHandler handler = this.ProcessExited;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }

    protected virtual void OnStandartTextReceived(string e)
    {
        EventHandler<string> handler = this.StandartTextReceived;

        if (handler != null)
        {
            if (this.context != null)
            {
                this.context.Post(delegate { handler(this, e); }, null);
            }
            else
            {
                handler(this, e);
            }
        }
    }

    private void ProcessOnExited(object sender, EventArgs eventArgs)
    {
        this.OnProcessExited();
    }

    private async void ReadOutputAsync()
    {
        var standart = new StringBuilder();
        var buff = new char[1024];
        int length;

        while (this.process.HasExited == false)
        {
            standart.Clear();

            length = await this.process.StandardOutput.ReadAsync(buff, 0, buff.Length);
            standart.Append(buff.SubArray(0, length));
            this.OnStandartTextReceived(standart.ToString());
            Thread.Sleep(1);
        }

        this.Running = false;
    }

    private async void ReadOutputErrorAsync()
    {
        var sb = new StringBuilder();

        do
        {
            sb.Clear();
            var buff = new char[1024];
            int length = await this.process.StandardError.ReadAsync(buff, 0, buff.Length);
            sb.Append(buff.SubArray(0, length));
            this.OnErrorTextReceived(sb.ToString());
            Thread.Sleep(1);
        }
        while (this.process.HasExited == false);
    }

    private async void WriteInputTask()
    {
        while (this.process.HasExited == false)
        {
            Thread.Sleep(1);

            if (this.pendingWriteData != null)
            {
                await this.process.StandardInput.WriteLineAsync(this.pendingWriteData);
                await this.process.StandardInput.FlushAsync();

                lock (this.theLock)
                {
                    this.pendingWriteData = null;
                }
            }
        }
    }
}

Then, in actually running the process and sending the CTRL-C in my main app:

            DateTime maxStartDateTime = //... some date time;
            DateTime maxEndDateTime = //... some later date time
            var duration = maxEndDateTime.Subtract(maxStartDateTime);
            ConsoleAppManager appManager = new ConsoleAppManager("myapp.exe");
            string[] args = new string[] { "args here" };
            appManager.ExecuteAsync(args);
            await Task.Delay(Convert.ToInt32(duration.TotalSeconds * 1000) + 20000);

            if (appManager.Running)
            {
                // If stilll running, send CTRL-C
                appManager.Write("\x3");
            }

For details, please see Redirecting standard input of console application and Windows how to get the process group of a process that is already running?