Как мне перехватить ctrl-c (SIGINT) в консольном приложении C #

222

Я хотел бы иметь возможность перехватывать CTRL+ Cв консольном приложении C #, чтобы я мог выполнить некоторые очистки перед выходом. Каков наилучший способ сделать это?

Ник Рэнделл
источник

Ответы:

127

Смотрите MSDN:

Событие Console.CancelKeyPress

Статья с примерами кода:

Ctrl-C и консольное приложение .NET

Ака
источник
6
На самом деле, эта статья рекомендует P / Invoke и CancelKeyPressупоминается лишь кратко в комментариях. Хорошая статья - codeneverwritten.com/2006/10/…
bzlm
Это работает, но не закрывает окно с помощью X. Смотрите мое полное решение ниже. работает и с kill
JJ_Coder4Hire
1
Я обнаружил, что Console.CancelKeyPress перестанет работать, если консоль закрыта. Запуск приложения под mono / linux с помощью systemd или если приложение запускается как «mono myapp.exe </ dev / null», SIGINT будет отправлен обработчику сигналов по умолчанию и сразу же убьет приложение. Пользователи Linux могут захотеть увидеть stackoverflow.com/questions/6546509/…
tekHedd
2
Непрерывная ссылка для комментария @ bzlm: web.archive.org/web/20110424085511/http://…
Ян Кемп
@aku, неужели у тебя нет времени, чтобы ответить на SIGINT, прежде чем процесс будет прерван? Я нигде не могу его найти, и я пытаюсь вспомнить, где я это читал.
Джон Заброски
224

Для этого используется событие Console.CancelKeyPress . Вот как это используется:

public static void Main(string[] args)
{
    Console.CancelKeyPress += delegate {
        // call methods to clean up
    };

    while (true) {}
}

Когда пользователь нажимает Ctrl + C, код в делегате запускается и программа завершается. Это позволяет выполнять очистку, вызывая необходимые методы. Обратите внимание, что после делегата код не выполняется.

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

class MainClass
{
    private static bool keepRunning = true;

    public static void Main(string[] args)
    {
        Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) {
            e.Cancel = true;
            MainClass.keepRunning = false;
        };

        while (MainClass.keepRunning) {
            // Do your work in here, in small chunks.
            // If you literally just want to wait until ctrl-c,
            // not doing anything, see the answer using set-reset events.
        }
        Console.WriteLine("exited gracefully");
    }
}

Разница между этим кодом и первым примером состоит в том, что e.Cancelустановлено значение true, что означает, что выполнение продолжается после делегата. При запуске программа ожидает, когда пользователь нажмет Ctrl + C. Когда это произойдет, keepRunningпеременная изменит значение, что приведет к выходу из цикла while. Это способ изящно завершить работу программы.

Jonas
источник
21
keepRunningвозможно, потребуется пометить volatile. В противном случае основной поток может кэшировать его в регистре ЦП и не заметит изменения значения при выполнении делегата.
cdhowie
Это работает, но не закрывает окно с помощью X. Смотрите мое полное решение ниже. также работает с kill.
JJ_Coder4Hire
13
Следует изменить, чтобы использовать ManualResetEventвместо вращения bool.
Мизипзор
Небольшое предостережение для всех, кто запускает запущенные программы в Git-Bash, MSYS2 или CygWin: вам нужно будет запустить dotnet через winpty, чтобы это работало (Итак, winpty dotnet run). В противном случае делегат никогда не будет запущен.
Самуэль Лампа
Остерегайтесь - событие CancelKeyPress обрабатывается в потоке пула потоков, что не сразу очевидно: docs.microsoft.com/en-us/dotnet/api/…
Сэм Рюби,
100

Я хотел бы добавить к ответу Джонаса . Вращение на boolбудет вызывать 100% загрузку процессора и тратить кучу энергии, ничего не делая, ожидая CTRL+ C.

Лучшее решение - использовать « ManualResetEventна самом деле» для ожидания CTRL+ C:

static void Main(string[] args) {
    var exitEvent = new ManualResetEvent(false);

    Console.CancelKeyPress += (sender, eventArgs) => {
                                  eventArgs.Cancel = true;
                                  exitEvent.Set();
                              };

    var server = new MyServer();     // example
    server.Run();

    exitEvent.WaitOne();
    server.Stop();
}
Джонатон Рейнхарт
источник
28
Я думаю, дело в том, что вы будете выполнять всю работу внутри цикла while, и нажатие Ctrl + C не будет прерываться в середине итерации while; это завершит эту итерацию, прежде чем вырваться.
pkr298
2
@ pkr298 - Жаль, что люди не голосуют за ваш комментарий, потому что он полностью правдив. Я отредактирую его ответ Джонаса, чтобы прояснить, как люди думают так, как поступил Джонатон (что не так уж плохо, как Джонас имел в виду его ответ)
М. Мимпен
Обновите существующий ответ этим улучшением.
Мизипзор
27

Вот полный рабочий пример. вставьте в пустой консольный проект C #:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace TestTrapCtrlC {
    public class Program {
        static bool exitSystem = false;

        #region Trap application termination
        [DllImport("Kernel32")]
        private static extern bool SetConsoleCtrlHandler(EventHandler handler, bool add);

        private delegate bool EventHandler(CtrlType sig);
        static EventHandler _handler;

        enum CtrlType {
            CTRL_C_EVENT = 0,
            CTRL_BREAK_EVENT = 1,
            CTRL_CLOSE_EVENT = 2,
            CTRL_LOGOFF_EVENT = 5,
            CTRL_SHUTDOWN_EVENT = 6
        }

        private static bool Handler(CtrlType sig) {
            Console.WriteLine("Exiting system due to external CTRL-C, or process kill, or shutdown");

            //do your cleanup here
            Thread.Sleep(5000); //simulate some cleanup delay

            Console.WriteLine("Cleanup complete");

            //allow main to run off
            exitSystem = true;

            //shutdown right away so there are no lingering threads
            Environment.Exit(-1);

            return true;
        }
        #endregion

        static void Main(string[] args) {
            // Some biolerplate to react to close window event, CTRL-C, kill, etc
            _handler += new EventHandler(Handler);
            SetConsoleCtrlHandler(_handler, true);

            //start your multi threaded program here
            Program p = new Program();
            p.Start();

            //hold the console so it doesn’t run off the end
            while (!exitSystem) {
                Thread.Sleep(500);
            }
        }

        public void Start() {
            // start a thread and start doing some processing
            Console.WriteLine("Thread started, processing..");
        }
    }
}
JJ_Coder4Hire
источник
8
этот p / вызывает поэтому не кроссплатформенный
knocte
Я просто добавил это, потому что это единственный ответ, который касается полного ответа. Это полный и позволяет запускать программу в планировщике задач без участия пользователя. У тебя все еще есть шанс на уборку. Используйте NLOG в своем проекте, и у вас есть что-то управляемое. Интересно, скомпилируется ли он в .NET Core 2 или 3.
BigTFromAZ
7

Этот вопрос очень похож на:

Захватить консоль выхода C #

Вот как я решил эту проблему, и имел дело с пользователем, нажимающим X, а также Ctrl-C. Обратите внимание на использование ManualResetEvents. Это приведет к тому, что основной поток перейдет в спящий режим, что освободит ЦП для обработки других потоков в ожидании выхода или очистки. ПРИМЕЧАНИЕ. Необходимо установить TerminationCompletedEvent в конце main. Невыполнение этого требования приводит к ненужной задержке при завершении из-за истечения времени ожидания ОС при уничтожении приложения.

namespace CancelSample
{
    using System;
    using System.Threading;
    using System.Runtime.InteropServices;

    internal class Program
    {
        /// <summary>
        /// Adds or removes an application-defined HandlerRoutine function from the list of handler functions for the calling process
        /// </summary>
        /// <param name="handler">A pointer to the application-defined HandlerRoutine function to be added or removed. This parameter can be NULL.</param>
        /// <param name="add">If this parameter is TRUE, the handler is added; if it is FALSE, the handler is removed.</param>
        /// <returns>If the function succeeds, the return value is true.</returns>
        [DllImport("Kernel32")]
        private static extern bool SetConsoleCtrlHandler(ConsoleCloseHandler handler, bool add);

        /// <summary>
        /// The console close handler delegate.
        /// </summary>
        /// <param name="closeReason">
        /// The close reason.
        /// </param>
        /// <returns>
        /// True if cleanup is complete, false to run other registered close handlers.
        /// </returns>
        private delegate bool ConsoleCloseHandler(int closeReason);

        /// <summary>
        ///  Event set when the process is terminated.
        /// </summary>
        private static readonly ManualResetEvent TerminationRequestedEvent;

        /// <summary>
        /// Event set when the process terminates.
        /// </summary>
        private static readonly ManualResetEvent TerminationCompletedEvent;

        /// <summary>
        /// Static constructor
        /// </summary>
        static Program()
        {
            // Do this initialization here to avoid polluting Main() with it
            // also this is a great place to initialize multiple static
            // variables.
            TerminationRequestedEvent = new ManualResetEvent(false);
            TerminationCompletedEvent = new ManualResetEvent(false);
            SetConsoleCtrlHandler(OnConsoleCloseEvent, true);
        }

        /// <summary>
        /// The main console entry point.
        /// </summary>
        /// <param name="args">The commandline arguments.</param>
        private static void Main(string[] args)
        {
            // Wait for the termination event
            while (!TerminationRequestedEvent.WaitOne(0))
            {
                // Something to do while waiting
                Console.WriteLine("Work");
            }

            // Sleep until termination
            TerminationRequestedEvent.WaitOne();

            // Print a message which represents the operation
            Console.WriteLine("Cleanup");

            // Set this to terminate immediately (if not set, the OS will
            // eventually kill the process)
            TerminationCompletedEvent.Set();
        }

        /// <summary>
        /// Method called when the user presses Ctrl-C
        /// </summary>
        /// <param name="reason">The close reason</param>
        private static bool OnConsoleCloseEvent(int reason)
        {
            // Signal termination
            TerminationRequestedEvent.Set();

            // Wait for cleanup
            TerminationCompletedEvent.WaitOne();

            // Don't run other handlers, just exit.
            return true;
        }
    }
}
Павел
источник
3

Console.TreatControlCAsInput = true; работал для меня.

алло
источник
2
Это может привести к тому, что ReadLine потребует два нажатия Enter для каждого входа.
Grault
0

Я могу провести некоторые очистки перед выходом. Каков наилучший способ сделать это? Вот настоящая цель: выйти из ловушки, чтобы сделать свои собственные вещи. И более четкие ответы выше не делают это правильно. Потому что Ctrl + C - это только один из многих способов выхода из приложения.

Что для этого нужно в dotnet c # - так называемый токен отмены, который передается, Host.RunAsync(ct)а затем, в ловушках сигналов выхода, для Windows это будет

    private static readonly CancellationTokenSource cts = new CancellationTokenSource();
    public static int Main(string[] args)
    {
        // For gracefull shutdown, trap unload event
        AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
        {
            cts.Cancel();
            exitEvent.Wait();
        };

        Console.CancelKeyPress += (sender, e) =>
        {
            cts.Cancel();
            exitEvent.Wait();
        };

        host.RunAsync(cts);
        Console.WriteLine("Shutting down");
        exitEvent.Set();
        return 0;
     }

...

Vaulter
источник