4

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?

scobi
  • 14,252
  • 13
  • 80
  • 114
  • I'm having the same problem. GenerateConsoleCtrlEvent appears to be broken. It doesn't affect the process, whether it has a console window or not, and whether it's started with CREATE_NEW_PROCESS_GROUP or not. I've tried everything, and there appears to be no way to cleanly shut down a console application started with Process.Start or CreateProcess when it's not a part of the calling process's console window. – Triynko Apr 22 '13 at 19:12

3 Answers3

3

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.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • The trick is - I need a way to signal the child process to abort. Ctrl-c is the standard way to do this with console apps. Not all of the subprocesses we're running are under our control (such as cl.exe and gcc.exe). Thanks for the info about CREATE_NEW_PROCESS_GROUP. I'll try some hacks. – scobi Nov 18 '08 at 17:36
  • Yikes. Reflector tells me that Process.Start() is very complex. Would have to clone it just to set that single flag. Too much work at this time. – scobi Nov 18 '08 at 19:29
  • 1
    GenerateConsoleCtrlEvent does NOT even work when CREATE_NEW_PROCESS_GROUP is set. It doesn't work at all. Period. It doesn't even work whether you create a new console for it or not, whether you specify CREATE_NO_WINDOW, CREATE_NEW_CONSOLE, or DETATCHED_PROCESS... the new process never responds to either of the signals generated by GenerateConsoleCtrlEvent, despite the fact that the process (such as 7-zip [7z.exe] for example) is known to cleanly close in response to a ctrl+c or ctrl+break signal. GenerateConsoleCtrlEvent is useless. – Triynko Apr 22 '13 at 19:10
  • @Triynko, didn't you ask yourself why Ctrl+Break works but not Ctrl+C? It's just a different event code, so probably there's no technical limitation that prevents generating Ctrl+C. We know that setting a `NULL` handler makes a console app ignore Ctrl+C, and removing the `NULL` handler restores Ctrl+C. It turns out that creating a new process group sets the same flag in the process parameters `ConsoleFlags` that makes `kernel32!CtrlRoutine` ignore Ctrl+C (except for generating a `DBG_CONTRL_C` exception when a debugger is attached). It works fine if you unset this flag. – Eryk Sun Apr 16 '15 at 11:06
  • @Triynko, of course you need to be able to modify the child process to have it call `SetConsoleCtrlHandler(NULL, FALSE)`. That's easy if it's your own app or an open source app. Otherwise use DLL injection. – Eryk Sun Apr 16 '15 at 11:19
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.

Weeble
  • 17,058
  • 3
  • 60
  • 75
  • I haven't worked on this since - hasn't been a problem lately. But if I remember the problem right, I wanted to be able to tell a child process to abort, and for a lot of command-line apps that means sending a ctrl-c, or signal, or whatever it's called. GenerateConsoleCtrlEvent() is apparently the function to call but it wasn't working. It sounds like CREATE_NEW_PROCESS_GROUP is what I need to set, but the Process class does not support that. – scobi Nov 30 '09 at 22:25
1

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?

user8128167
  • 6,929
  • 6
  • 66
  • 79