Как полностью завершить консольное приложение, запущенное с помощью Process.Start?

Это выглядит невыполнимой задачей. Абсолютно ничего из того, что я нашел, не работает. Вопрос в том, как чисто закрыть консольное приложение, запущенное с Process.Start, которое было запущено без окна консоли и без использования оболочки execute: (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 бесполезным. У этого должно быть решение.


person Triynko    schedule 22.04.2013    source источник
comment
Мне кажется, что правильное решение - GenerateConsoleCtrlEvent. Итак, вам просто нужно выяснить, как создать процесс в новой группе. Если это невозможно с помощью оболочки процесса .NET, перейдите к собственной функции Win32. Нет вреда в использовании P / Invoke, вы все равно делаете это для GenerateConsoleCtrlEvent.   -  person Cody Gray    schedule 22.04.2013
comment
Я еще не использую GenerateConsoleCtrlEvent (это не работает), и я бы предпочел не использовать P / Invoke. Структура SECURITY_ATTRIBUTES, используемая CreateProcess, имеет указатель, который требует от меня компиляции с unsafe, чего я не делаю. Другая проблема с CreateProcess заключается в том, что я не могу надежно использовать Process.EnableRaisingEvents и событие Exited, потому что процесс может завершиться до того, как я получу возможность вызвать Process.GetProcessById для создания экземпляра процесса .NET для назначения обработчика, который обычно сделано перед вызовом Process.Start. Меня заставляют переписывать большую часть логики Win32.   -  person Triynko    schedule 22.04.2013
comment
Как я уже сказал, это все равно не работает. Несмотря на создание процесса с CREATE_NEW_PROCESS_GROUP, GenerateConsoleCtrlEvent не влияет на группу, что определяется идентификатором процесса. Я попытался передать константу для CTRL + C, а затем CTRL + BREAK, и ничего не получилось. Кроме того, вместе с CREATE_NEW_PROCESS_GROUP я попробовал оба флага для CREATE_NO_WINDOW или DETACHED_PROCESS (по отдельности, поскольку они исключают друг друга), и, хотя оба они достигают процесса без консоли, который не мешает работе с консольным окном моего основного приложения, GenerateConsoleCrtlEvent метод не работает.   -  person Triynko    schedule 22.04.2013
comment
Взаимодействие с неуправляемыми структурами, имеющими указатели, абсолютно не требует unsafe блоков кода. Вы можете сделать это в VB.NET, который не поддерживает необработанные указатели. Просто немного больше проблем. Вы должны использовать методы класса Marshal. Я понятия не имею, почему еще вы бы избегали использования P / Invoke, это просто кажется нелогичным. Да, чтобы получить доступ к мощи Win32, вам нужно проделать намного больше работы. Нет идеальной обертки. Это одна из причин, почему я вернулся к C ++ и Win32 после изучения C #.   -  person Cody Gray    schedule 23.04.2013
comment
Странно, однако, что вы говорите, что GenerateConsoleCtrlEvent не работает. К сожалению, сейчас у меня нет времени тестировать ваш пример кода.   -  person Cody Gray    schedule 23.04.2013
comment
это работает: stackoverflow.com/a/285041/937093?   -  person Steffen Winkler    schedule 24.06.2015
comment
Обходным решением вашей проблемы может быть использование некоторой zip-библиотеки, такой как Ionic.Zip. Надеюсь, что есть поддержка для отмены Extract-Process.   -  person gReX    schedule 23.07.2015


Ответы (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
}

}

person Michael    schedule 21.08.2016

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

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

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

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

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

person Luaan    schedule 23.07.2015