278

I would like to be able to trap Ctrl+C in a C# console application so that I can carry out some cleanups before exiting. What is the best way of doing this?

Josh Correia
  • 3,807
  • 3
  • 33
  • 50
Nick Randell
  • 17,805
  • 18
  • 59
  • 74

8 Answers8

289

The Console.CancelKeyPress event is used for this. This is how it's used:

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

    while (true) {}
}

When the user presses Ctrl+C the code in the delegate is run and the program exits. This allows you to perform cleanup by calling necessary methods. Note that no code after the delegate is executed.

There are other situations where this won't cut it. For example, if the program is currently performing important calculations that can't be immediately stopped. In that case, the correct strategy might be to tell the program to exit after the calculation is complete. The following code gives an example of how this can be implemented:

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

The difference between this code and the first example is that e.Cancel is set to true, which means the execution continues after the delegate. If run, the program waits for the user to press Ctrl+C. When that happens the keepRunning variable changes value which causes the while loop to exit. This is a way to make the program exit gracefully.

John Cummings
  • 1,949
  • 3
  • 22
  • 38
Jonas
  • 4,107
  • 3
  • 21
  • 25
  • 31
    `keepRunning` may need to be marked `volatile`. The main thread might cache it on a CPU register otherwise, and won't notice the value change when the delegate is executed. – cdhowie Sep 20 '12 at 18:34
  • This works but it doesn't trap the closing of the window with the X. See my complete solution below. works with kill as well. – JJ_Coder4Hire Apr 10 '14 at 18:53
  • 17
    Should be changed to use a `ManualResetEvent` rather than spin on a `bool`. – Mizipzor Jul 03 '14 at 14:06
  • A small caveat for anyone else running running stuff in Git-Bash, MSYS2 or CygWin: You will have to run dotnet via winpty for this to work (So, `winpty dotnet run`). Otherwise, the delegate will never be run. – Samuel Lampa Feb 08 '19 at 10:08
  • Watch out- the event for CancelKeyPress is handled on a thread pool thread, which is not immediately obvious: https://learn.microsoft.com/en-us/dotnet/api/system.console.cancelkeypress?view=netframework-4.8 – Sam Rueby Apr 30 '19 at 14:08
  • If the Main function is paused by `Console.ReadLine` , this makes Ctrl + C pass an empty string to it . If the Main does a cleanup and returns after the Read , this causes a concurrency issue, does anyone know how to solve that ? This requires a functionality to synchronize state comparison and definition from 2 threads, what is the correct way to implement that ? – irvnriir Sep 01 '23 at 17:32
140

See MSDN:

Console.CancelKeyPress Event

Article with code samples:

Ctrl-C and the .NET console application

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
aku
  • 122,288
  • 32
  • 173
  • 203
  • 6
    Actually, that article recommends P/Invoke, and `CancelKeyPress` is mentioned only briefly in the comments. A good article is http://www.codeneverwritten.com/2006/10/ctrl-c-and-net-console-application.html – bzlm Aug 21 '10 at 07:55
  • 1
    This works but it doesn't trap the closing of the window with the X. See my complete solution below. works with kill as well – JJ_Coder4Hire Apr 10 '14 at 18:53
  • 1
    I have found that that Console.CancelKeyPress will stop working if the console is closed. Running an app under mono/linux with systemd, or if the app is run as "mono myapp.exe < /dev/null", a SIGINT will be sent to the default signal handler and instantly kill the app. Linux users may want to see http://stackoverflow.com/questions/6546509/detect-when-console-application-is-closing-killed – tekHedd Nov 14 '15 at 19:00
  • 3
    Here's the permalink for the article above: https://codeneverwritten.blogspot.com/2006/10/ctrl-c-and-net-console-application.html Funny to run across my own article on SO when searching for Ctrl-C handling. :) – Curt Nichols Aug 21 '20 at 17:04
132

I'd like to add to Jonas' answer. Spinning on a bool will cause 100% CPU utilization, and waste a bunch of energy doing a lot of nothing while waiting for CTRL+C.

The better solution is to use a ManualResetEvent to actually "wait" for the 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();
}
Mwiza
  • 7,780
  • 3
  • 46
  • 42
Jonathon Reinhart
  • 132,704
  • 33
  • 254
  • 328
  • 32
    I think the point is that you would do all the work inside the while loop, and hitting Ctrl+C will not break in the middle of the while iteration; it will complete that iteration before breaking out. – pkr Feb 14 '13 at 22:32
  • 3
    @pkr298 - Too bad people don't vote your comment up since it's entirely true. I'll edit Jonas his answer to clarify people from thinking the way Jonathon did (which is not inherently bad but not as Jonas meant his answer) – M. Mimpen Dec 02 '13 at 11:43
  • Note that although the `CancelKeyPress` event handler is called when pressing Ctrl-C in a VS debug console window, setting `Cancel = true` has no effect. – StackOverthrow Jan 04 '21 at 19:53
  • If the Main function is paused by `Console.ReadLine` , this makes Ctrl + C pass an empty string to it . If the Main does a cleanup and returns after the Read , this causes a concurrency issue, does anyone know how to solve that ? This requires a functionality to synchronize state comparison and definition from 2 threads, what is the correct way to implement that ? – irvnriir Sep 01 '23 at 17:33
33

Here is a complete working example. paste into empty C# console project:

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
  • 4,706
  • 1
  • 37
  • 25
  • 12
    this p/invokes therefore is not crossplatform – knocte Apr 22 '17 at 09:33
  • 1
    I just plus'd this because it's the only answer that addresses a complete answer. This one is thorough and allows you to run the program under the task scheduler with no user involvement. You still get a chance to cleanup. Use NLOG in your project and you have something manageable. I wonder if it'll compile in .NET Core 2 or 3. – BigTFromAZ Sep 05 '19 at 21:35
8

This question is very similar to:

Capture console exit C#

Here is how I solved this problem, and dealt with the user hitting the X as well as Ctrl-C. Notice the use of ManualResetEvents. These will cause the main thread to sleep which frees the CPU to process other threads while waiting for either exit, or cleanup. NOTE: It is necessary to set the TerminationCompletedEvent at the end of main. Failure to do so causes unnecessary latency in termination due to the OS timing out while killing the application.

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;
        }
    }
}
Community
  • 1
  • 1
Paul
  • 1,011
  • 10
  • 13
7

Detect SIGTERM and Ctrl+C:

CancellationTokenSource ctSource = new();
CancellationToken ct = ctSource.Token;

void ExitHandler()
{
    // You can add any arbitrary global clean up
    Console.WriteLine("Exiting...");
    ctSource.Cancel();
}

// Assign exit handler to be called when the process is terminated
// or the user hits CTRL+C
AppDomain.CurrentDomain.ProcessExit += (sender, args) => ExitHandler();
Console.CancelKeyPress += (sender, args) => ExitHandler();

// Then you can use the cancellation token to check for exit:
Console.WriteLine("Ready to gracefully shut down!");
while (!ct.IsCancellationRequested)
{
    Console.WriteLine($"Exit not detected, waiting another 10s.");
    Task.Delay(10000, ct).Wait(ct);
}
Josh Correia
  • 3,807
  • 3
  • 33
  • 50
Alby
  • 321
  • 2
  • 6
  • 1
    Not true. none of the solutions written on this page work on LINUX at all. Console.In.Peek(), CurrentDomain.blah blah(), Console.CancelKeyPress() they all work in windows only and not in linux at all. And that may sound simple but it is a big problem when your app is running in container as a console based app because kubernetes has to kill it instead of gracefully ending it – Simple Fellow Dec 18 '21 at 11:48
  • My answer was written in .net core 5 and tested Ubuntu 21.10 which is my main OS. – Alby Dec 19 '21 at 13:11
  • same here and I'm telling you it doesnt work at all. I'm using ConsoleHost – Simple Fellow Dec 20 '21 at 04:25
  • Console.CancelKeyPress worked for me as of 2022. Win11, .NET 6, NativeAOT-compiled app, OutputType = Exe. – DARKGuy Jan 24 '22 at 00:23
  • 1
    As of .NET 6 on Linux I can attest that hooking ProcessExit does work. This captures SIGTERM as documented. – N8allan May 05 '22 at 21:51
5

I can carry out some cleanups before exiting. What is the best way of doing this Thats is the real goal: trap exit, to make your own stuff. And neigther answers above not makeing it right. Because, Ctrl+C is just one of many ways to exiting app.

What in dotnet c# is needed for it - so called cancellation token passed to Host.RunAsync(ct) and then, in exit signals traps, for Windows it would be

    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
  • 196
  • 2
  • 6
3

Console.TreatControlCAsInput = true; has worked for me.

Community
  • 1
  • 1
hallo
  • 31
  • 1