Wie fange ich Strg-C (SIGINT) in einer C # -Konsolen-App ein?


221

Ich möchte in der Lage sein, CTRL+ Cin einer C # -Konsolenanwendung abzufangen, damit ich vor dem Beenden einige Bereinigungen durchführen kann. Was ist der beste Weg, dies zu tun?

Antworten:


126

6
Tatsächlich empfiehlt dieser Artikel P / Invoke und CancelKeyPresswird in den Kommentaren nur kurz erwähnt. Ein guter Artikel ist codeneverwritten.com/2006/10/…
bzlm

Dies funktioniert, aber es schließt das Schließen des Fensters mit dem X nicht ein. Siehe meine vollständige Lösung unten. funktioniert auch mit kill
JJ_Coder4Hire

1
Ich habe festgestellt, dass Console.CancelKeyPress nicht mehr funktioniert, wenn die Konsole geschlossen wird. Wenn Sie eine App unter Mono / Linux mit systemd ausführen oder wenn die App als "mono myapp.exe </ dev / null" ausgeführt wird, wird ein SIGINT an den Standard-Signalhandler gesendet und die App wird sofort beendet. Linux-Benutzer möchten möglicherweise stackoverflow.com/questions/6546509/…
tekHedd

2
Nicht unterbrochener Link für @ bzlms Kommentar: web.archive.org/web/20110424085511/http://…
Ian Kemp

@aku gibt es nicht eine gewisse Zeit, die Sie auf das SIGINT antworten müssen, bevor der unhöfliche Prozess abgebrochen wird? Ich kann es nirgendwo finden und versuche mich zu erinnern, wo ich es gelesen habe.
John Zabroski

224

Das Console.CancelKeyPress Ereignis wird dafür verwendet. So wird es verwendet:

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

    while (true) {}
}

Wenn der Benutzer Strg + C drückt, wird der Code im Delegaten ausgeführt und das Programm beendet. Auf diese Weise können Sie eine Bereinigung durchführen, indem Sie die erforderlichen Methoden aufrufen. Beachten Sie, dass nach der Ausführung des Delegaten kein Code mehr vorhanden ist.

Es gibt andere Situationen, in denen dies nicht funktioniert. Zum Beispiel, wenn das Programm gerade wichtige Berechnungen durchführt, die nicht sofort gestoppt werden können. In diesem Fall besteht die richtige Strategie möglicherweise darin, das Programm anzuweisen, das Programm nach Abschluss der Berechnung zu beenden. Der folgende Code gibt ein Beispiel dafür, wie dies implementiert werden kann:

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

Der Unterschied zwischen diesem Code und dem ersten Beispiel besteht darin, dass e.Canceltrue festgelegt ist. Dies bedeutet, dass die Ausführung nach dem Delegaten fortgesetzt wird. Wenn es ausgeführt wird, wartet das Programm darauf, dass der Benutzer Strg + C drückt. In diesem Fall keepRunningändert die Variable den Wert, wodurch die while-Schleife beendet wird. Dies ist eine Möglichkeit, das Programm ordnungsgemäß zu beenden.


21
keepRunningmuss möglicherweise markiert werden volatile. Der Hauptthread kann es andernfalls in einem CPU-Register zwischenspeichern und bemerkt die Wertänderung nicht, wenn der Delegat ausgeführt wird.
CDhowie

Dies funktioniert, aber es schließt das Schließen des Fensters mit dem X nicht ein. Siehe meine vollständige Lösung unten. funktioniert auch mit kill.
JJ_Coder4Hire

13
Sollte geändert werden, um a ManualResetEventanstelle von Spin auf a zu verwenden bool.
Mizipzor

Eine kleine Einschränkung für alle anderen, die laufende Inhalte in Git-Bash, MSYS2 oder CygWin ausführen: Sie müssen dotnet über winpty ausführen, damit dies funktioniert (So, winpty dotnet run). Andernfalls wird der Delegat niemals ausgeführt.
Samuel Lampa

Achtung, das Ereignis für CancelKeyPress wird in einem Thread-Pool-Thread behandelt, was nicht sofort offensichtlich ist: docs.microsoft.com/en-us/dotnet/api/…
Sam Rueby

100

Ich möchte Jonas 'Antwort ergänzen . Das Drehen auf a boolführt zu einer 100% igen CPU-Auslastung und verschwendet viel Energie, während Sie auf CTRL+ warten C.

Die bessere Lösung besteht darin, a ManualResetEventzu verwenden, um tatsächlich auf das CTRL+ zu "warten" 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
Ich denke, der Punkt ist, dass Sie die ganze Arbeit innerhalb der while-Schleife erledigen würden und das Drücken von Strg + C nicht in der Mitte der while-Iteration unterbrochen wird. Diese Iteration wird vor dem Ausbruch abgeschlossen.
pkr298

2
@ pkr298 - Schade, dass Leute Ihren Kommentar nicht abstimmen, da er völlig wahr ist. Ich werde Jonas seine Antwort bearbeiten, um zu klären, dass die Leute nicht so denken wie Jonathon (was nicht von Natur aus schlecht ist, aber nicht so, wie Jonas seine Antwort meinte)
M. Mimpen

Aktualisieren Sie die vorhandene Antwort mit dieser Verbesserung.
Mizipzor

27

Hier ist ein vollständiges Arbeitsbeispiel. In leeres C # -Konsolenprojekt einfügen:

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
Diese p / Aufrufe sind daher nicht
plattformübergreifend

Ich habe dies nur hinzugefügt, weil es die einzige Antwort ist, die eine vollständige Antwort adressiert. Dieser ist gründlich und ermöglicht es Ihnen, das Programm unter dem Taskplaner ohne Benutzereingriff auszuführen. Sie haben immer noch die Möglichkeit aufzuräumen. Verwenden Sie NLOG in Ihrem Projekt und Sie haben etwas Verwaltbares. Ich frage mich, ob es in .NET Core 2 oder 3 kompiliert wird.
BigTFromAZ

7

Diese Frage ist sehr ähnlich zu:

Capture Console Exit C #

Hier ist, wie ich dieses Problem gelöst habe und wie der Benutzer sowohl das X als auch Strg-C gedrückt hat. Beachten Sie die Verwendung von ManualResetEvents. Dadurch wird der Hauptthread in den Ruhezustand versetzt, wodurch die CPU andere Threads verarbeiten kann, während sie entweder auf das Beenden oder auf die Bereinigung wartet. HINWEIS: Es ist erforderlich, das TerminationCompletedEvent am Ende von main festzulegen. Andernfalls tritt eine unnötige Latenz bei der Beendigung auf, da das Betriebssystem beim Beenden der Anwendung eine Zeitüberschreitung aufweist.

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; hat für mich gearbeitet.


2
Dies kann dazu führen, dass für ReadLine zwei Eingabetasten für jede Eingabe erforderlich sind.
Grault

0

Ich kann vor dem Beenden einige Aufräumarbeiten durchführen. Was ist der beste Weg, dies zu tun? Das ist das eigentliche Ziel: Trap Exit, um deine eigenen Sachen zu machen. Und weitere Antworten oben machen es nicht richtig. Weil Strg + C nur eine von vielen Möglichkeiten ist, die App zu beenden.

Was in dotnet c # dafür benötigt wird - sogenanntes Stornierungs-Token, das an Host.RunAsync(ct)Windows übergeben wird, und dann in Exit-Signal-Traps für 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;
     }

...

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.