How to cleanly shut down a console app started wit

2019-02-01 21:46发布

问题:

This is looking like an impossible task. Absolutely nothing I've found works. The question is how to cleanly close a console application started with Process.Start that has been started with no console window and without using shell execute: (ProcessStartInfo.CreateNoWindow = true; ProcessStartInfo.UseShellExecute = false;).

It is given that the application being started will shut down "cleanly" if it receives a ctrl-c or ctrl-break signal, but there seems to be no way to send it one that works (particularly GenerateConsoleCtrlEvent).

  • Process.Kill doesn't work. It leaves corrupt files behind due to abrupt killing of the process.
  • Process.CloseMainWindow doesn't work. There is no main window in this case, so the function returns false and does nothing.
  • Calling EnumThreadWindows on all threads for the process and sending a WM_CLOSE to every window does nothing, and there aren't any thread windows anyway.
  • GenerateConsoleCtrlEvent doesn't work. It's only useful for processes in the same group (which .NET gives you no control over), with an unwanted side effect of closing the calling process anyway. The function does not allow you to specify a process id.

Whoever can provide code that accepts a "Process" object started with the parameters above which results in a clean shutdown of the started process without affecting the calling process will be marked as the answer. Use 7z.exe (7-zip archiver) as an example console app, which begins compressing a large file, and will leave a corrupt, unfinished file behind if not terminated cleanly.

Until someone provides a functional example or code that leads to a functional example, this question is unanswered. I have seen dozens of people asking this question and dozens of answers online, and none of them work. .NET seems to provide no support for cleanly closing a console application given its process id, which is odd considering it's started with a .NET Process object. Part of the problem is the inability to create a process in a new process group, which makes using GenerateConsoleCtrlEvent useless. There has to be a solution to this.

回答1:

This is a bit late so you might not use it anymore, but perhaps it will help others...

You are overthinking this. The problem is that you can only signal a break from a process that shares the same console - the solution should be rather obvious.

Create a console project. This project will launch the target application the usual way:

var psi = new ProcessStartInfo();
psi.FileName = @"D:\Test\7z\7z.exe";
psi.WorkingDirectory = @"D:\Test\7z\";
psi.Arguments = "a output.7z input.bin";
psi.UseShellExecute = false;

var process = Process.Start(psi);

UseShellExecute is the important part - this ensures that the two applications are going to share the same console.

This allows you to send the break to your helper application, which will be passed to the hosted application as well:

Console.CancelKeyPress += (s, e) => e.Cancel = true;
Thread.Sleep(1000);
GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);

This will break the hosted application a second after it is started. Easily, safely. The CancelKeyPress isn't required - I only put it there to make it obvious that you can break the hosted process, and still keep on running. In the real helper application, this could be used for some notifications or something like that, but it's not really required.

Now you only need a way to signal the helper application to issue the break command - the easiest way would be to just use a simple console input, but that might interfere with the hosted application. If that's not an option for you, a simple mutex will work fine:

using (var mutex = Mutex.OpenExisting(args[0]))
using (var processWaitHandle = new SafeWaitHandle(process.Handle, false))
using (var processMre = new ManualResetEvent(false) { SafeWaitHandle = processWaitHandle })
{
    var which = WaitHandle.WaitAny(new WaitHandle[] { mutex, processMre });

    if (which == 0)
    {
        Console.WriteLine("Got signalled.");
        GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
    }
    else if (which == 1)
    {
        Console.WriteLine("Exitted normally.");
    }
}

This will wait either for a signal on the mutex, or for the hosted application to exit. To launch the helper application, all you need to do is this:

var mutexName = Guid.NewGuid().ToString();
mutex = new Mutex(true, mutexName);

var process = Process.Start(@"TestBreak.exe", mutexName);

And to issue the break, just release the mutex:

mutex.ReleaseMutex();

That's it. If you need tighter control, using something like a named pipe might be a better option, but if you only need the break signal, a mutex will do just fine. Any arguments you need to pass can be passed as arguments to the helper application, and you could even make this work with shell scripts (just use the helper application to run anything you need to run and break).



回答2:

I spent several hours trying to figure this one out myself. As you mentioned, the web is replete with answers that simply don't work. A lot of people suggest using GenerateConsoleCtrlEvent, but they don't provide any context, they just provide useless code snippets. The solution below uses GenerateConsoleCtrlEvent, but it works. I've tested it.

Note that this is a WinForms app and the process I'm starting and stopping is FFmpeg. I haven't tested the solution with anything else. I am using FFmpeg here to record a video and save the output to a file called "video.mp4".

The code below is the contents of my Form1.cs file. This is the file that Visual Studio creates for you when you create a WinForms solution.

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

namespace ConsoleProcessShutdownDemo {
    public partial class Form1 : Form {

    BackgroundWorker worker;
    Process currentProcess;

    public Form1() {
        InitializeComponent();
    }

    private void Worker_DoWork(object sender, DoWorkEventArgs e) {
        const string outFile = "video.mp4";

        var info = new ProcessStartInfo();
        info.UseShellExecute = false;
        info.CreateNoWindow = true;
        info.FileName = "ffmpeg.exe";
        info.Arguments = string.Format("-f gdigrab -framerate 60 -i desktop -crf 0 -pix_fmt yuv444p -preset ultrafast {0}", outFile);
        info.RedirectStandardInput = true;

        Process p = Process.Start(info);

        worker.ReportProgress(-1, p);
    }

    private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e) {
        currentProcess = (Process)e.UserState;
    }

    private void btnStart_Click(object sender, EventArgs e) {
        btnStart.Enabled = false;
        btnStop.Enabled = true;

        worker = new BackgroundWorker();

        worker.WorkerSupportsCancellation = true;
        worker.WorkerReportsProgress = true;
        worker.DoWork += Worker_DoWork;
        worker.ProgressChanged += Worker_ProgressChanged;

        worker.RunWorkerAsync();

    }

    private void btnStop_Click(object sender, EventArgs e) {
        btnStop.Enabled = false;
        btnStart.Enabled = true;

        if (currentProcess != null)
            StopProgram(currentProcess);
    }





    //MAGIC BEGINS


    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AttachConsole(uint dwProcessId);

    [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
    static extern bool FreeConsole();

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);

    [DllImport("Kernel32", SetLastError = true)]
    private static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);

    enum CtrlTypes {
        CTRL_C_EVENT = 0,
        CTRL_BREAK_EVENT,
        CTRL_CLOSE_EVENT,
        CTRL_LOGOFF_EVENT = 5,
        CTRL_SHUTDOWN_EVENT
    }

    private delegate bool HandlerRoutine(CtrlTypes CtrlType);

    public void StopProgram(Process proc) {

        int pid = proc.Id;

        FreeConsole();

        if (AttachConsole((uint)pid)) {

            SetConsoleCtrlHandler(null, true);
            GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0);

            Thread.Sleep(2000);

            FreeConsole();

            SetConsoleCtrlHandler(null, false);
        }

        proc.WaitForExit();

        proc.Close();
    }


    //MAGIC ENDS
}

}