Как чисто закрыть консольное приложение, запущенное с Process.Start?

Это выглядит как невозможная задача. Абсолютно ничего, что я нашел, работает. Вопрос заключается в том, как полностью закрыть консольное приложение, запущенное с Process.Start, которое было запущено без консольного окна и без использования оболочки: (ProcessStartInfo.CreateNoWindow = true; ProcessStartInfo.UseShellExecute = false;).

Дано, что запущенное приложение будет закрыто "чисто", если оно получит сигнал ctrl-c или ctrl-break, но, похоже, нет способа отправить его, который работает (в частности GenerateConsoleCtrlEvent).

  • Process.Kill не работает. Он оставляет поврежденные файлы из-за резких убийство процесса.
  • Process.CloseMainWindow не работает. В этом случае нет главного окна, поэтому функция возвращает false и ничего не делает.
  • Вызов EnumThreadWindows для всех потоков процесса и отправка WM_CLOSE в каждое окно ничего не делает, и в любом случае нет окон нити.
  • GenerateConsoleCtrlEvent не работает. Это полезно только для процессов в той же группе (что .NET не дает вам контроля) с нежелательным побочным эффектом закрытия вызывающего процесса. Функция не позволяет указать идентификатор процесса.

Тот, кто может предоставить код, который принимает объект "Процесс", начатый с вышеприведенных параметров, который приводит к чистому завершению начатого процесса без влияния на вызывающий процесс, будет отмечен как ответ. Используйте 7z.exe(7-zip-архиватор) в качестве примера консольного приложения, которое начинает сжимать большой файл и оставит поврежденный, незавершенный файл позади, если он не будет завершен чисто.

Пока кто-то предоставляет функциональный пример или код, который приводит к функциональному примеру, этот вопрос не отвечает. Я видел, как десятки людей задавали этот вопрос и десятки ответов в Интернете, и никто из них не работает..NET, похоже, не поддерживает чистое закрытие консольного приложения, учитывая его идентификатор процесса, что является странным, учитывая, что он начался с объекта .NET Process. Частью проблемы является невозможность создания процесса в новой группе процессов, что делает использование GenerateConsoleCtrlEvent бесполезным. Должно быть решение этого.

Ответ 1

Это немного поздно, поэтому вы можете больше не использовать его, но, возможно, это поможет другим...

Ты слишком задумываешься об этом. Проблема в том, что вы можете только сигнализировать о прорыве от процесса, который использует одну и ту же консоль - решение должно быть довольно очевидным.

Создайте проект консоли. Этот проект запускает целевое приложение обычным способом:

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 не требуется - я только ставил его там, чтобы было очевидно, что вы можете разорвать размещенный процесс и продолжать работать. В реальном помощнике это может быть использовано для некоторых уведомлений или что-то в этом роде, но это действительно не требуется.

Теперь вам нужно только указать, чтобы вспомогательное приложение выдавало команду break. Самый простой способ - просто использовать простой ввод в консоль, но это может помешать размещенному приложению. Если это не вариант для вас, простой мьютекс будет работать нормально:

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();

Что это. Если вам нужен более жесткий контроль, использование чего-то вроде именованного канала может быть лучшим вариантом, но если вам нужен только сигнал разрыва, то мьютекс будет работать отлично. Любые аргументы, которые вам нужно передать, могут передаваться в качестве аргументов вспомогательному приложению, и вы даже можете выполнить эту работу со сценариями оболочки (просто используйте вспомогательное приложение для запуска всего, что вам нужно для запуска и разрыва).

Ответ 2

Я потратил несколько часов, пытаясь понять это сам. Как вы уже упоминали, сеть изобилует ответами, которые просто не работают. Многие люди предлагают использовать 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
}

}