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ć?

Odpowiedzi:


126

Zobacz MSDN:

Zdarzenie Console.CancelKeyPress

Artykuł z przykładowymi kodami:

Ctrl-C i aplikacja konsoli .NET


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.


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

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

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

3

Console.TreatControlCAsInput = true; pracował dla mnie.


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

...

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.