Jak uwięzić ctrl-c (SIGINT) w aplikacji konsoli C #

221

Chciałbym móc pułapkować CTRL+ Cw aplikacji na konsolę C #, abym mógł przeprowadzić kilka porządków przed wyjściem. Jak najlepiej to zrobić?

Nick Randell
źródło

Odpowiedzi:

126

Zobacz MSDN:

Zdarzenie Console.CancelKeyPress

Artykuł z przykładowymi kodami:

Ctrl-C i aplikacja konsoli .NET

aku
źródło
6
W rzeczywistości ten artykuł zaleca P / Invoke i CancelKeyPressjest wspomniany tylko krótko w komentarzach. Dobry artykuł to codeneverwritten.com/2006/10/10/…
bzlm
Działa to, ale nie uwięzienie zamknięcia okna za pomocą X. Zobacz moje kompletne rozwiązanie poniżej. działa również z kill
JJ_Coder4Hire
1
Przekonałem się, że Console.CancelKeyPress przestanie działać, jeśli konsola zostanie zamknięta. Uruchamianie aplikacji pod mono / linux z systememd lub jeśli aplikacja działa jako „mono myapp.exe </ dev / null”, SIGINT zostanie wysłany do domyślnej procedury obsługi sygnału i natychmiast zabije aplikację. Użytkownicy Linuksa mogą chcieć zobaczyć stackoverflow.com/questions/6546509/...
tekHedd
2
Nieprzerwany link do komentarza @ bzlm: web.archive.org/web/20110424085511/http://…
Ian Kemp
@aku, czy nie ma czasu, aby odpowiedzieć na SIGINT, zanim proces zostanie przerwany? Nigdzie nie mogę go znaleźć i próbuję sobie przypomnieć, gdzie go przeczytałem.
John Zabroski
224

Do tego celu służy zdarzenie Console.CancelKeyPress . Oto jak jest używany:

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

    while (true) {}
}

Gdy użytkownik naciśnie Ctrl + C, kod w delegacie jest uruchamiany i program kończy działanie. Pozwala to na wykonanie czyszczenia przez wywołanie niezbędnych metod. Pamiętaj, że po wykonaniu zadania nie ma kodu.

Są inne sytuacje, w których to nie da rady. Na przykład, jeśli program wykonuje obecnie ważne obliczenia, których nie można natychmiast zatrzymać. W takim przypadku poprawną strategią może być nakazanie programowi zakończenia pracy po zakończeniu obliczeń. Poniższy kod podaje przykład tego, jak można to zaimplementować:

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");
    }
}

Różnica między tym kodem a pierwszym przykładem jest e.Cancelustawiona na true, co oznacza, że ​​wykonywanie jest kontynuowane po delegowaniu. Jeśli zostanie uruchomiony, program czeka, aż użytkownik naciśnie Ctrl + C. W takim przypadku keepRunningzmienna zmienia wartość, co powoduje wyjście pętli while. Jest to sposób na wdzięczne zakończenie programu.

Jonas
źródło
21
keepRunningmoże wymagać oznaczenia volatile. W przeciwnym razie główny wątek może buforować go w rejestrze procesora i nie zauważy zmiany wartości, gdy delegat zostanie wykonany.
cdhowie
Działa to, ale nie uwięzienie zamknięcia okna za pomocą X. Zobacz moje kompletne rozwiązanie poniżej. działa również z zabijaniem.
JJ_Coder4 Zatrudnij
13
Powinien zostać zmieniony, aby użyć ManualResetEventzamiast obracać na bool.
Mizipzor
Małe zastrzeżenie dla każdego, kto uruchamia rzeczy w Git-Bash, MSYS2 lub CygWin: Aby to zadziałało, musisz uruchomić dotnet przez winpty (Tak, winpty dotnet run). W przeciwnym razie delegat nigdy nie zostanie uruchomiony.
Samuel Lampa
Uważaj - zdarzenie CancelKeyPress jest obsługiwane w wątku puli wątków, co nie jest od razu oczywiste: docs.microsoft.com/en-us/dotnet/api/…
Sam Rueby 30.04.19
100

Chciałbym dodać do odpowiedzi Jonasa . Odwrócenie boolpowoduje 100% wykorzystanie procesora i marnuje mnóstwo energii, nie robiąc nic, czekając na CTRL+ C.

Lepszym rozwiązaniem jest użycie opcji ManualResetEvent„poczekaj” na 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();
}
Jonathon Reinhart
źródło
28
Myślę, że chodzi o to, że wykonasz całą pracę w pętli while, a naciśnięcie Ctrl + C nie spowoduje przerwania w trakcie iteracji podczas; wykona tę iterację przed wybuchem.
pkr298
2
@ pkr298 - Szkoda, że ​​ludzie nie głosują na twój komentarz, ponieważ jest to całkowicie prawda. Przeredaguję jego odpowiedź, aby wyjaśnić ludziom sposób myślenia Jonathona (co nie jest z natury złe, ale nie tak, jak Jonas miał na myśli swoją odpowiedź)
M. Mimpen,
Zaktualizuj istniejącą odpowiedź dzięki temu ulepszeniu.
Mizipzor
27

Oto kompletny przykład działania. wklej do pustego projektu konsoli 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
źródło
8
ten p / wywołuje zatem nie jest
krzyżykowy
Właśnie dodałem to, ponieważ jest to jedyna odpowiedź, która dotyczy pełnej odpowiedzi. Ten jest dokładny i pozwala uruchomić program w harmonogramie zadań bez udziału użytkownika. Nadal masz szansę na oczyszczenie. Użyj NLOG w swoim projekcie i masz coś do zarządzania. Zastanawiam się, czy skompiluje się w .NET Core 2 czy 3.
BigTFromAZ
7

To pytanie jest bardzo podobne do:

Przechwyć wyjście konsoli C #

Oto jak rozwiązałem ten problem i poradziłem sobie z naciśnięciem klawisza X, a także Ctrl-C. Zwróć uwagę na użycie ManualResetEvents. Powodują one uśpienie głównego wątku, co pozwala procesorowi przetwarzać inne wątki podczas oczekiwania na wyjście lub czyszczenie. UWAGA: Konieczne jest ustawienie TerminationCompletedEvent na końcu Main. Niezastosowanie się do tego powoduje niepotrzebne opóźnienie w zakończeniu z powodu przekroczenia limitu czasu systemu operacyjnego podczas zabijania aplikacji.

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;
        }
    }
}
Paweł
źródło
3

Console.TreatControlCAsInput = true; pracował dla mnie.

Cześć
źródło
2
Może to spowodować, że ReadLine będzie wymagało dwóch naciśnięć Enter dla każdego wejścia.
Grault
0

Mogę przeprowadzić porządki przed wyjściem. Jaki jest najlepszy sposób na zrobienie tego. To jest prawdziwy cel: zamknij wyjście, aby tworzyć własne rzeczy. I powyższe odpowiedzi nie są poprawne. Ponieważ Ctrl + C to tylko jeden z wielu sposobów na wyjście z aplikacji.

Co jest potrzebne w dotnet c # - tak zwany token anulowania przekazywany do, Host.RunAsync(ct)a następnie, w pułapkach sygnałów wyjścia, dla Windows byłby

    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
źródło