Como faço para interceptar ctrl-c (SIGINT) em um aplicativo de console C #

222

Gostaria de poder interceptar CTRL+ Cem um aplicativo de console C # para que eu possa executar algumas limpezas antes de sair. Qual a melhor maneira para fazer isto?

Nick Randell
fonte

Respostas:

127

Consulte MSDN:

Evento Console.CancelKeyPress

Artigo com exemplos de código:

Ctrl-C e o aplicativo de console .NET

aku
fonte
6
Na verdade, esse artigo recomenda P / Invoke e CancelKeyPressé mencionado apenas brevemente nos comentários. Um bom artigo é codeneverwritten.com/2006/10/…
bzlm
Isso funciona, mas não impede o fechamento da janela com o X. Veja minha solução completa abaixo. trabalha com kill bem
JJ_Coder4Hire
1
Descobri que o Console.CancelKeyPress deixará de funcionar se o console estiver fechado. Executando um aplicativo em mono / linux com systemd, ou se o aplicativo for executado como "mono myapp.exe </ dev / null", um SIGINT será enviado ao manipulador de sinal padrão e matará o aplicativo instantaneamente. Os usuários do Linux podem querer ver o stackoverflow.com/questions/6546509/…
tekHedd
2
Link não quebrado para o comentário de @ bzlm: web.archive.org/web/20110424085511/http://…
Ian Kemp #
@aku, não há um período de tempo necessário para responder ao SIGINT antes que o processo seja interrompido? Não consigo encontrá-lo em lugar nenhum e estou tentando lembrar onde o li.
John Zabroski 27/09/19
224

O evento Console.CancelKeyPress é usado para isso. É assim que é usado:

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

    while (true) {}
}

Quando o usuário pressiona Ctrl + C, o código no delegado é executado e o programa sai. Isso permite que você execute a limpeza chamando os métodos necessários. Observe que nenhum código após a execução do delegado.

Existem outras situações em que isso não é suficiente. Por exemplo, se o programa estiver executando cálculos importantes que não podem ser interrompidos imediatamente. Nesse caso, a estratégia correta pode ser solicitar ao programa que saia após o término do cálculo. O código a seguir fornece um exemplo de como isso pode ser implementado:

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

A diferença entre esse código e o primeiro exemplo é e.Canceldefinido como true, o que significa que a execução continua após o delegado. Se executado, o programa espera que o usuário pressione Ctrl + C. Quando isso acontece, a keepRunningvariável altera o valor, fazendo com que o loop while saia. Esta é uma maneira de fazer com que o programa saia normalmente.

Jonas
fonte
21
keepRunningpode precisar ser marcado volatile. Caso contrário, o encadeamento principal pode armazená-lo em cache em um registro da CPU e não notará a alteração do valor quando o delegado for executado.
Cdhowie
Isso funciona, mas não impede o fechamento da janela com o X. Veja minha solução completa abaixo. trabalha com kill também.
precisa saber é o seguinte
13
Deve ser alterado para usar um em ManualResetEventvez de girar em um bool.
Mizipzor 3/07
Uma pequena ressalva para qualquer pessoa executando coisas em execução no Git-Bash, MSYS2 ou CygWin: Você precisará executar o dotnet via winpty para que isso funcione (Então, winpty dotnet run). Caso contrário, o delegado nunca será executado.
Samuel Lampa
O relógio out- o evento para CancelKeyPress é tratado em um thread do pool, que não é imediatamente óbvio: docs.microsoft.com/en-us/dotnet/api/...
Sam Rueby
100

Eu gostaria de adicionar à resposta de Jonas . Girar em um boolcausará 100% de utilização da CPU e desperdiçará muita energia sem fazer nada enquanto aguarda CTRL+C .

A melhor solução é usar um ManualResetEventpara realmente "esperar" pelo 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
fonte
28
Eu acho que o ponto é que você faria todo o trabalho dentro do loop while, e pressionar Ctrl + C não será interrompido no meio da iteração while; ele concluirá a iteração antes de iniciar.
pkr298
2
@ pkr298 - Pena que as pessoas não votam no seu comentário, pois é inteiramente verdade. Vou editar Jonas sua resposta para esclarecer as pessoas de pensar a forma como Jonathon fez (que não é inerentemente mau, mas não como Jonas significava a sua resposta)
M. Mimpen
Atualize a resposta existente com esta melhoria.
Mizipzor 3/07
27

Aqui está um exemplo de trabalho completo. cole no projeto de console C # vazio:

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
fonte
8
este p / invoca, por conseguinte, não é crossplataform
knocte
Eu apenas acrescentei isso porque é a única resposta que aborda uma resposta completa. Este é completo e permite executar o programa no agendador de tarefas sem o envolvimento do usuário. Você ainda tem a chance de limpar. Use NLOG em seu projeto e você terá algo gerenciável. Eu me pergunto se ele vai compilar no .NET Core 2 ou 3.
BigTFromAZ
7

Esta pergunta é muito semelhante a:

Capturar saída do console C #

Aqui está como eu resolvi esse problema e lidei com o usuário pressionando o X e Ctrl-C. Observe o uso de ManualResetEvents. Isso fará com que o encadeamento principal fique inativo, o que libera a CPU para processar outros encadeamentos enquanto aguarda a saída ou a limpeza. NOTA: É necessário definir o TerminationCompletedEvent no final do main. Não fazer isso causa latência desnecessária na finalização devido ao tempo limite do sistema operacional ao matar o aplicativo.

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;
        }
    }
}
Paulo
fonte
3

Console.TreatControlCAsInput = true; trabalhou para mim.

olá
fonte
2
Isso pode fazer com que o ReadLine exija duas impressoras Enter para cada entrada.
Grault
0

Posso fazer algumas limpezas antes de sair. Qual é a melhor maneira de fazer isso? Esse é o objetivo real: sair da armadilha, para fazer suas próprias coisas. E as respostas mais recentes acima não a acertam. Porque, Ctrl + C é apenas uma das muitas maneiras de sair do aplicativo.

O que é necessário no dotnet c # - o chamado token de cancelamento passado para Host.RunAsync(ct)e, em armadilhas de sinais de saída, para o Windows seria

    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
fonte