Come posso intercettare ctrl-c (SIGINT) in un'app console C #


222

Vorrei essere in grado di intercettare CTRL+ Cin un'applicazione console C # in modo da poter eseguire alcune pulizie prima di uscire. Qual è il modo migliore per fare questo?

Risposte:


127

Vedi MSDN:

Evento Console.CancelKeyPress

Articolo con esempi di codice:

Ctrl-C e l'applicazione console .NET


6
In realtà, quell'articolo raccomanda P / Invoke ed CancelKeyPressè menzionato solo brevemente nei commenti. Un buon articolo è codeneverwritten.com/2006/10/…
bzlm

Funziona ma non intrappola la chiusura della finestra con la X. Vedi la mia soluzione completa di seguito. funziona anche con kill
JJ_Coder4Hire

1
Ho scoperto che Console.CancelKeyPress smetterà di funzionare se la console è chiusa. Eseguendo un'app in mono / linux con systemd, o se l'app viene eseguita come "mono myapp.exe </ dev / null", un SIGINT verrà inviato al gestore di segnali predefinito e interromperà immediatamente l'app. Gli utenti Linux possono voler vedere stackoverflow.com/questions/6546509/...
tekHedd

2
Link non interrotto per il commento di @ bzlm: web.archive.org/web/20110424085511/http://…
Ian Kemp

@aku non c'è tempo per rispondere a SIGINT prima che il processo si interrompa? Non riesco a trovarlo da nessuna parte e sto cercando di ricordare dove l'ho letto.
John Zabroski,

224

Il Console.CancelKeyPress evento è usato per questo. Ecco come viene utilizzato:

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

    while (true) {}
}

Quando l'utente preme Ctrl + C, il codice nel delegato viene eseguito e il programma termina. Ciò consente di eseguire la pulizia chiamando i metodi necessari. Si noti che nessun codice dopo l'esecuzione del delegato.

Ci sono altre situazioni in cui questo non lo taglierà. Ad esempio, se il programma sta attualmente eseguendo calcoli importanti che non possono essere immediatamente arrestati. In tal caso, la strategia corretta potrebbe essere quella di dire al programma di uscire dopo il completamento del calcolo. Il codice seguente fornisce un esempio di come implementarlo:

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

La differenza tra questo codice e il primo esempio è che e.Cancelè impostato su true, il che significa che l'esecuzione continua dopo il delegato. Se eseguito, il programma attende che l'utente prema Ctrl + C. Quando ciò accade, la keepRunningvariabile cambia valore che provoca la chiusura del ciclo while. Questo è un modo per far uscire il programma con garbo.


21
keepRunningpotrebbe essere necessario essere contrassegnati volatile. In caso contrario, il thread principale potrebbe memorizzarlo nella cache su un registro CPU e non noterà la modifica del valore quando viene eseguito il delegato.
cdhowie,

Funziona ma non intrappola la chiusura della finestra con la X. Vedi la mia soluzione completa di seguito. funziona anche con kill.
JJ_Coder4Hire

13
Dovrebbe essere cambiato per usare a ManualResetEventanziché spin su a bool.
Mizipzor,

Un piccolo avvertimento per chiunque altro esegua roba in esecuzione in Git-Bash, MSYS2 o CygWin: per farlo funzionare dovrai eseguire dotnet tramite winpty (So, winpty dotnet run). Altrimenti, il delegato non verrà mai eseguito.
Samuel Lampa,

Fai attenzione: l'evento per CancelKeyPress viene gestito su un thread del pool di thread, il che non è immediatamente ovvio: docs.microsoft.com/en-us/dotnet/api/…
Sam Rueby,

100

Vorrei aggiungere la risposta di Jonas . Girare su una boolcausa il 100% di utilizzo della CPU e sprecherà un sacco di energia senza fare molto durante l'attesa di CTRL+ C.

La soluzione migliore è utilizzare a ManualResetEventper "attendere" effettivamente il 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
Penso che il punto sia che faresti tutto il lavoro all'interno del ciclo while e colpire Ctrl + C non si interromperà nel mezzo dell'iterazione while; completerà quell'iterazione prima di scoppiare.
pkr298,

2
@ pkr298 - Le persone troppo cattive non votano il tuo commento perché è del tutto vero. Modificherò Jonas la sua risposta per chiarire la gente dal pensare come Jonathon (che non è intrinsecamente cattivo ma non come Jonas intendeva la sua risposta)
M. Mimpen,

Aggiorna la risposta esistente con questo miglioramento.
Mizipzor,

27

Ecco un esempio di lavoro completo. incolla nel progetto console C # vuoto:

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
questo p /
invoke

Ho appena aggiunto questo perché è l'unica risposta che risponde a una risposta completa. Questo è approfondito e ti consente di eseguire il programma sotto l'utilità di pianificazione senza coinvolgimento dell'utente. Hai ancora la possibilità di ripulire. Usa NLOG nel tuo progetto e hai qualcosa di gestibile. Mi chiedo se verrà compilato in .NET Core 2 o 3.
BigTFromAZ

7

Questa domanda è molto simile a:

Capture console exit C #

Ecco come ho risolto questo problema e ho affrontato l'utente colpendo la X e Ctrl-C. Si noti l'uso di ManualResetEvents. Ciò causerà la sospensione del thread principale che libera la CPU per elaborare altri thread in attesa dell'uscita o della pulizia. NOTA: è necessario impostare TerminationCompletedEvent alla fine di main. In caso contrario, si causa latenza non necessaria nella risoluzione a causa del timeout del sistema operativo durante l'uccisione dell'applicazione.

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; ha funzionato per me.


2
Ciò può comportare che ReadLine richieda due presse Invio per ciascun ingresso.
Grault,

0

Posso effettuare alcune pulizie prima di uscire. Qual è il modo migliore per farlo Questo è il vero obiettivo: uscire dalla trappola, creare le tue cose. E le altre risposte sopra non lo rendono giusto. Perché, Ctrl + C è solo uno dei molti modi per uscire dall'app.

Ciò che in dotnet c # è necessario per questo - il cosiddetto token di annullamento passato a Host.RunAsync(ct)e quindi, nelle trappole dei segnali di uscita, per Windows sarebbe

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

...

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.