如何干净地关闭通过Process.Start启动的控制台应用程序?

24
这似乎是一个不可能完成的任务。 我找到的所有方法都没有用。 问题是如何干净地关闭使用Process.Start启动的控制台应用程序,该程序已经启动但没有控制台窗口且未使用shell execute启动:(ProcessStartInfo.CreateNoWindow = true; ProcessStartInfo.UseShellExecute = false;)。
已知,如果该应用程序接收到ctrl-c或ctrl-break信号,则它将“干净”地关闭,但似乎没有可行的方法发送这样的信号(特别是GenerateConsoleCtrlEvent)。
  • Process.Kill无效。 强制终止进程会留下损坏的文件。
  • Process.CloseMainWindow 无效。 在这种情况下没有主窗口,因此该函数返回false并且不起作用。
  • 在处理进程的所有线程上调用EnumThreadWindows并向每个窗口发送WM_CLOSE不起作用,而且也没有任何线程窗口。
  • GenerateConsoleCtrlEvent不起作用。 它只对同一组中的进程有用(.NET无法控制),并带有关闭调用进程的副作用。 该函数不允许指定进程ID。
提供能够接受以以上参数启动的"Process"对象的代码,并导致已启动的进程干净地关闭而不影响调用进程的人将被标记为答案。以7z.exe (7-zip压缩程序)作为示例控制台应用程序,该应用程序开始压缩大文件,如果未正确终止,将留下损坏的未完成文件。
在有人提供可行示例或导致可行示例的代码之前,此问题仍未得到解答。 我看到许多人都在问这个问题,也看到了很多答案,但它们都不起作用。 .NET似乎没有提供支持以进程ID方式干净地关闭控制台应用程序的功能,这很奇怪,因为它是使用.NET Process对象启动的。 问题的一部分是无法在新进程组中创建进程,这使得使用GenerateConsoleCtrlEvent毫无意义。 必须有解决方案。

听起来对我来说正确的解决方案是 GenerateConsoleCtrlEvent。所以你只需要弄清楚如何在一个新组中创建进程。如果使用 .NET 进程包装器不可能实现,那就降到本地的 Win32 函数。使用 P/Invoke 没有任何伤害,因为你已经在为 GenerateConsoleCtrlEvent 这个函数使用它了。 - Cody Gray
1
与具有指针的非托管结构进行Interop不一定需要unsafe代码块。您可以在不支持原始指针的VB.NET中完成它。只是有点麻烦。您必须利用Marshal类中的方法。我不知道为什么您要避免使用P/Invoke,这似乎是不合逻辑的。是的,为了访问Win32的强大功能,您需要做更多的工作。没有包装器是完美的。这就是为什么在学习了C#之后,我回到了C++和Win32的原因之一。 - Cody Gray
很奇怪,你说“GenerateConsoleCtrlEvent”不起作用。不幸的是,我现在没有时间测试你的示例代码。 - Cody Gray
1
这个行得通吗:https://dev59.com/yXVC5IYBdhLWcg3weA8-#285041? - Steffen Winkler
一个解决你问题的方法可能是使用一些压缩库,比如Ionic.Zip。希望有支持取消提取过程的功能。 - gReX
显示剩余2条评论
2个回答

6

我花了好几个小时试图自己解决这个问题。正如你所提到的,网络上充斥着根本不起作用的答案。很多人建议使用GenerateConsoleCtrlEvent,但他们没有提供任何上下文,只提供了无用的代码片段。下面的解决方案使用GenerateConsoleCtrlEvent,但它是有效的。我已经测试过了。

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

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

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
}

}


4

可能有点晚了,你可能已经不需要它了,但也许它能帮助其他人...

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

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

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脚本使其工作(只需使用辅助应用程序运行任何需要运行和中断的内容即可)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接