¿Cómo atrapo ctrl-c (SIGINT) en una aplicación de consola C #

222

Me gustaría poder atrapar CTRL+ Cen una aplicación de consola C # para poder realizar algunas limpiezas antes de salir. Cual es la mejor manera de hacer esto?

Nick Randell
fuente

Respuestas:

127

Ver MSDN:

Console.CancelKeyPress Event

Artículo con ejemplos de código:

Ctrl-C y la aplicación de consola .NET

aku
fuente
66
En realidad, ese artículo recomienda P / Invoke, y CancelKeyPresssolo se menciona brevemente en los comentarios. Un buen artículo es codeneverwritten.com/2006/10/…
bzlm
Esto funciona pero no atrapa el cierre de la ventana con la X. Vea mi solución completa a continuación. funciona con kill también
JJ_Coder4Hire
1
He encontrado que esa Console.CancelKeyPress dejará de funcionar si la consola está cerrada. Ejecutando una aplicación en mono / linux con systemd, o si la aplicación se ejecuta como "mono myapp.exe </ dev / null", se enviará un SIGINT al manejador de señal predeterminado y la cerrará instantáneamente. Los usuarios de Linux pueden querer ver stackoverflow.com/questions/6546509/…
tekHedd
2
Enlace no roto para el comentario de @ bzlm: web.archive.org/web/20110424085511/http://…
Ian Kemp
@aku, ¿no hay una cantidad de tiempo para responder al SIGINT antes de que el proceso se cancele groseramente? No puedo encontrarlo en ningún lado, y estoy tratando de recordar dónde lo leí.
John Zabroski
224

El evento Console.CancelKeyPress se utiliza para esto. Así es como se usa:

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

    while (true) {}
}

Cuando el usuario presiona Ctrl + C, se ejecuta el código en el delegado y se cierra el programa. Esto le permite realizar la limpieza llamando a los métodos necesarios. Tenga en cuenta que no hay código después de que se ejecuta el delegado.

Hay otras situaciones en las que esto no es suficiente. Por ejemplo, si el programa está realizando cálculos importantes que no se pueden detener de inmediato. En ese caso, la estrategia correcta podría ser decirle al programa que salga después de que se complete el cálculo. El siguiente código da un ejemplo de cómo se puede implementar esto:

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 diferencia entre este código y el primer ejemplo es que e.Cancelse establece en verdadero, lo que significa que la ejecución continúa después del delegado. Si se ejecuta, el programa espera a que el usuario presione Ctrl + C. Cuando eso sucede, la keepRunningvariable cambia el valor que hace que el ciclo while salga. Esta es una manera de hacer que el programa salga con gracia.

Jonas
fuente
21
keepRunningpuede necesitar ser marcado volatile. De lo contrario, el hilo principal podría almacenarlo en caché en un registro de CPU y no notará el cambio de valor cuando se ejecute el delegado.
cdhowie
Esto funciona pero no atrapa el cierre de la ventana con la X. Vea mi solución completa a continuación. funciona con kill también.
JJ_Coder4Hire
13
Debe cambiarse para usar un en ManualResetEventlugar de girar en un bool.
Mizipzor
Una pequeña advertencia para cualquier otra persona que ejecute material de ejecución en Git-Bash, MSYS2 o CygWin: tendrá que ejecutar dotnet a través de winpty para que esto funcione (Entonces, winpty dotnet run). De lo contrario, el delegado nunca se ejecutará.
Samuel Lampa
Cuidado: el evento para CancelKeyPress se maneja en un subproceso de grupo de subprocesos, que no es inmediatamente obvio: docs.microsoft.com/en-us/dotnet/api/…
Sam Rueby
100

Me gustaría agregar a la respuesta de Jonas . Girar sobre un boolcausará un 100% de utilización de la CPU y desperdiciará un montón de energía sin hacer nada mientras espera CTRL+ C.

La mejor solución es usar a ManualResetEventpara "esperar" realmente el 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
fuente
28
Creo que el punto es que harías todo el trabajo dentro del ciclo while, y presionar Ctrl + C no se romperá en el medio de la iteración while; completará esa iteración antes de estallar.
pkr298
2
@ pkr298 - Lástima que las personas no voten tu comentario ya que es completamente cierto. Editaré su respuesta de Jonas para aclarar que la gente piense de la manera en que Jonathon lo hizo (lo cual no es inherentemente malo, pero no como Jonas quiso decir su respuesta)
M. Mimpen
Actualice la respuesta existente con esta mejora.
Mizipzor
27

Aquí hay un ejemplo de trabajo completo. pegar en proyecto de consola C # vacío:

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
fuente
8
Esta p / invoca, por tanto, no es multiplataforma
knocte
Acabo de agregar esto porque es la única respuesta que aborda una respuesta completa. Este es completo y le permite ejecutar el programa bajo el programador de tareas sin la participación del usuario. Todavía tienes la oportunidad de limpiar. Use NLOG en su proyecto y tendrá algo manejable. Me pregunto si se compilará en .NET Core 2 o 3.
BigTFromAZ
7

Esta pregunta es muy similar a:

Capture la salida de la consola C #

Así es como resolví este problema y traté con el usuario presionando la X y Ctrl-C. Observe el uso de ManualResetEvents. Esto hará que el subproceso principal se suspenda, lo que libera a la CPU para procesar otros subprocesos mientras espera la salida o la limpieza. NOTA: es necesario establecer TerminationCompletedEvent al final de main. De lo contrario, se produce una latencia innecesaria en la finalización debido al tiempo de espera del sistema operativo mientras se cierra la aplicación.

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;
        }
    }
}
Pablo
fuente
3

Console.TreatControlCAsInput = true; ha trabajado para mi

Hola
fuente
2
Esto puede hacer que ReadLine requiera dos pulsaciones Intro para cada entrada.
Grault
0

Puedo realizar algunas limpiezas antes de salir. ¿Cuál es la mejor manera de hacer esto? Ese es el verdadero objetivo: salir de la trampa, hacer tus propias cosas. Y otras respuestas anteriores no lo hacen bien. Porque, Ctrl + C es solo una de las muchas formas de salir de la aplicación.

Lo que en dotnet C # es necesaria para que - por lo que llamó la cancelación símbolo pasó a Host.RunAsync(ct)y, a continuación, en las trampas de las señales de salida, para Windows sería

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

...

Saltador
fuente