23

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.

4

2 回答 2

5

我花了几个小时试图自己弄清楚这一点。正如您所提到的,网络上充满了根本行不通的答案。很多人建议使用 GenerateConsoleCtrlEvent,但他们不提供任何上下文,他们只是提供无用的代码片段。下面的解决方案使用 GenerateConsoleCtrlEvent,但它可以工作。我已经测试过了。

请注意,这是一个 WinForms 应用程序,我正在启动和停止的进程是FFmpeg。我没有用其他任何东西测试过这个解决方案。我在这里使用 FFmpeg 录制视频并将输出保存到名为“video.mp4”的文件中。

下面的代码是我的 Form1.cs 文件的内容。这是 Visual Studio 在您创建 WinForms 解决方案时为您创建的文件。

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
}

}

于 2016-08-21T19:32:51.187 回答
3

这有点晚了,所以你可能不再使用它,但也许它会帮助其他人......

你多虑了。问题是您只能从共享同一控制台的进程中发出中断信号 - 解决方案应该相当明显。

创建一个控制台项目。该项目将以通常的方式启动目标应用程序:

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是重要的部分——这确保了两个应用程序将共享同一个控制台。

这允许您将中断发送到您的帮助应用程序,该应用程序也将传递给托管应用程序:

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

这将在启动后一秒钟破坏托管应用程序。轻松、安全。不是必需的CancelKeyPress- 我只是把它放在那里,以表明您可以破坏托管进程,并且仍然继续运行。在真正的助手应用程序中,这可以用于一些通知或类似的东西,但这并不是真正需要的。

现在您只需要一种方法来指示帮助应用程序发出中断命令 - 最简单的方法是只使用简单的控制台输入,但这可能会干扰托管应用程序。如果这不是你的选择,一个简单的互斥锁就可以了:

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.");
    }
}

这将等待互斥体上的信号或托管应用程序退出。要启动帮助应用程序,您需要做的就是:

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

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

要发出中断,只需释放互斥锁:

mutex.ReleaseMutex();

而已。如果您需要更严格的控制,使用命名管道之类的东西可能是更好的选择,但如果您只需要中断信号,互斥体就可以了。您需要传递的任何参数都可以作为参数传递给辅助应用程序,您甚至可以使用 shell 脚本来实现这一点(只需使用辅助应用程序来运行您需要运行和中断的任何内容)。

于 2015-07-23T11:35:27.757 回答