Как избежать принудительного уничтожения для остановки консольного приложения?

#c# #windows #console-application #kill-process

#c# #Windows #консольное приложение #kill-process

Вопрос:

Мне нужно иметь возможность корректно завершать работу консольного приложения по требованию.

Используя этот пост, я смог контролировать выход при получении событий CTRL. Тем не менее, я не могу остановить приложение по требованию, используя taskkill .

Если я попробую выполнить команду:

 taskkill /im myapp
  

он отвечает:

ОШИБКА: процесс «MyApp.exe » с PID 23296 не удалось завершить работу. Причина: этот процесс может быть завершен только принудительно (с параметром /F ).

Комментарии:

1. taskkill хочет изящно завершить работу, отправив WM_QUIT сообщение в главное окно. К сожалению, в консольных приложениях нет окон или циклов сообщений (если только они не стараются изо всех сил создавать их, то есть), поэтому у него нет вариантов для изящного завершения — в Windows нет согласованного механизма, который сигнализировал бы консольному приложению о необходимости корректного завершения. (Я не проверял, действительно ли создание цикла сообщений / невидимого окна позволит taskkill завершить его изящно — вы могли бы это проверить.)

2. Окно консоли имеет специальный корпус, чтобы иметь эффективного владельца для GetWindowProcessThreadId запроса. Это процесс, который был наименее недавно подключен к консоли, который обычно является процессом выделения. Если мы не принудительно убиваем этот ведущий процесс, он отправляет WM_CLOSE (не WM_QUIT ) в окно консоли, а консоль, в свою очередь, отправляет событие Ctrl Close всем процессам , подключенным к нему. У них есть 5 секунд для корректного завершения, прежде чем они будут принудительно завершены.

3. Если вам нужно такое поведение для любого консольного процесса, а не только для ведущего процесса, напишите небольшую программу / скрипт, который вызывает AttachConsole(pid) , а затем передает Ctrl C или Ctrl Break всем подключенным процессам (группе процессов 0) через GenerateConsoleCtrlEvent . Во-первых, для вашего собственного процесса вам нужно вызвать SetConsoleCtrlHandler , чтобы игнорировать Ctrl C или установить обработчик, который возвращает TRUE for CTRL_BREAK_EVENT , в зависимости от того, какое событие вы генерируете.

4. Вы даже можете реализовать принудительное завершение для всех подключенных процессов. Вызов GetConsoleProcessList . Откройте дескриптор для каждого процесса (без себя) с помощью terminate access. Вызов GenerateConsoleCtrlEvent . Подождите до 5 секунд, а затем вызовите TerminateProcess любой процесс, который еще не завершен.

5. @JeroenMostert, по крайней мере, в Windows 10, taskkill.exe перечисляет невидимые окна только для сообщений в дополнение к видимым окнам верхнего уровня, чтобы найти все окна, связанные с процессом. IIRC в предыдущих версиях не проверял окна только для сообщений.

Ответ №1:

Основываясь на многих вкладах, добавленных в этот пост, а также на обнаружении-console-application-exit-in-c, я создаю следующий помощник, чтобы корректно останавливать консольное приложение по сигналу taskkill, а также останавливать любые другие сигналы close controls:

 public class StopController : Form, IMessageFilter
{
    //logger
    private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    static private CancellationTokenSource cancellationTokenSource;

    public StopController(CancellationTokenSource cancellationTokenSource)
    {
        StopController.cancellationTokenSource = cancellationTokenSource;
        System.Windows.Forms.Application.AddMessageFilter(this);
        SetConsoleCtrlHandler(new HandlerRoutine(ConsoleCtrlCheck), true);
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        this.WindowState = FormWindowState.Minimized;
        this.ShowInTaskbar = false;
    }
    public bool PreFilterMessage(ref Message m)
    {
        if (m.Msg == 16)
        {
            log.Warn("Receiveing WF_Close event. Cancel was fired.");
            cancellationTokenSource.Cancel();
        }

        return true;
    }

    public static void Activate(CancellationTokenSource cancellationTokenSource)
    {            
        Task.Run(() => Application.Run(new StopController(cancellationTokenSource)));
    }
    #region unmanaged

    //must be static.
    private static bool ConsoleCtrlCheck(CtrlTypes ctrlType)
    {
        // Put your own handler here
        switch (ctrlType)
        {
            case CtrlTypes.CTRL_C_EVENT:

                log.Warn("CTRL C received!. Cancel was fired.");
                cancellationTokenSource.Cancel();
                break;

            case CtrlTypes.CTRL_BREAK_EVENT:

                log.Warn("CTRL BREAK received!. Cancel was fired.");
                cancellationTokenSource.Cancel();
                break;

            case CtrlTypes.CTRL_CLOSE_EVENT:

                log.Warn("Program being closed!. Cancel was fired.");
                cancellationTokenSource.Cancel();
                break;

            case CtrlTypes.CTRL_LOGOFF_EVENT:
            case CtrlTypes.CTRL_SHUTDOWN_EVENT:
                log.Warn("User is logging off!. Cancel was fired.");
                cancellationTokenSource.Cancel();
                break;

            default:
                log.Warn($"unknow type {ctrlType}");
                break;
        }
        return true;

    }
    // Declare the SetConsoleCtrlHandler function
    // as external and receiving a delegate.

    [DllImport("Kernel32")]
    public static extern bool SetConsoleCtrlHandler(HandlerRoutine Handler, bool Add);

    // A delegate type to be used as the handler routine
    // for SetConsoleCtrlHandler.
    public delegate bool HandlerRoutine(CtrlTypes CtrlType);

    // An enumerated type for the control messages
    // sent to the handler routine.
    public enum CtrlTypes
    {
        CTRL_C_EVENT = 0,
        CTRL_BREAK_EVENT,
        CTRL_CLOSE_EVENT,
        CTRL_LOGOFF_EVENT = 5,
        CTRL_SHUTDOWN_EVENT
    }

    #endregion
}
  

Использование:

   StopController.Activate(cancellationTokenSource);
  

Нет необходимости переходить на приложение Windows.