如何在 C# 控制台应用程序中捕获 ctrl-c (SIGINT)

Posted

技术标签:

【中文标题】如何在 C# 控制台应用程序中捕获 ctrl-c (SIGINT)【英文标题】:How do I trap ctrl-c (SIGINT) in a C# console app 【发布时间】:2010-09-15 17:24:33 【问题描述】:

我希望能够在 C# 控制台应用程序中捕获 CTRL+C 以便我可以在退出之前进行一些清理。这样做的最佳方法是什么?

【问题讨论】:

【参考方案1】:

Console.CancelKeyPress 事件用于此目的。它是这样使用的:

public static void Main(string[] args)

    Console.CancelKeyPress += delegate 
        // call methods to clean up
    ;

    while (true) 

当用户按下 Ctrl + C 时,委托中的代码运行并且程序退出。这允许您通过调用必要的方法来执行清理。注意委托执行后没有代码。

在其他情况下这不会削减它。例如,如果程序当前正在执行无法立即停止的重要计算。在这种情况下,正确的策略可能是在计算完成后告诉程序退出。以下代码给出了如何实现的示例:

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

这段代码和第一个例子的不同之处在于e.Cancel被设置为true,这意味着在delegate之后继续执行。如果运行,程序会等待用户按 Ctrl + C。当这种情况发生时,keepRunning 变量会更改值,这会导致 while 循环退出。这是一种让程序优雅退出的方法。

【讨论】:

keepRunning 可能需要标记为volatile。否则主线程可能会将其缓存在 CPU 寄存器中,并且在执行委托时不会注意到值的变化。 这行得通,但它不会用 X 来限制窗口的关闭。请参阅下面的完整解决方案。也适用于 kill。 应该改为使用ManualResetEvent,而不是使用bool 对于在 Git-Bash、MSYS2 或 CygWin 中运行的任何其他人的一个小警告:您必须通过 winpty 运行 dotnet 才能使其工作(所以,winpty dotnet run)。否则,代理将永远不会运行。 注意 - CancelKeyPress 的事件是在线程池线程上处理的,这不是很明显:docs.microsoft.com/en-us/dotnet/api/…【参考方案2】:

参见 MSDN:

Console.CancelKeyPress Event

带有代码示例的文章:

Ctrl-C and the .NET console application

【讨论】:

其实那篇文章推荐的是P/Invoke,而CancelKeyPress只是在cmets中简单提及。一篇好文章是codeneverwritten.com/2006/10/… 这行得通,但它不会用 X 来限制窗口的关闭。请参阅下面的完整解决方案。也适用于 kill 我发现如果控制台关闭,Console.CancelKeyPress 将停止工作。使用 systemd 在 mono/linux 下运行应用程序,或者如果应用程序以“mono myapp.exe ***.com/questions/6546509/… 这是上述文章的永久链接:codeneverwritten.blogspot.com/2006/10/… 在搜索 Ctrl-C 处理时遇到我自己关于 SO 的文章很有趣。 :)【参考方案3】:

我想添加到Jonas' answer。在bool 上旋转将导致 100% 的 CPU 利用率,并在等待 CTRL+C 时浪费大量精力。

更好的解决方案是使用ManualResetEvent 实际“等待” 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();

【讨论】:

我认为关键是您将在 while 循环内完成所有工作,并且按 Ctrl+C 不会在 while 迭代中间中断;它将在爆发之前完成该迭代。 @pkr298 - 可惜人们没有投票赞成您的评论,因为它完全正确。我将编辑乔纳斯的答案,以澄清人们不会像乔纳森那样思考(这本身并不坏,但不是乔纳斯的意思) 请注意,虽然在 VS 调试控制台窗口中按下 Ctrl-C 时会调用 CancelKeyPress 事件处理程序,但设置 Cancel = true 无效。【参考方案4】:

这是一个完整的工作示例。粘贴到空的 C# 控制台项目中:

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

【讨论】:

这个 p/invokes 因此不是跨平台的 我只是加了这个,因为它是解决完整答案的唯一答案。这个是彻底的,允许您在任务计划程序下运行程序,而无需用户参与。你仍然有机会清理。在你的项目中使用 NLOG,你就有了一些可管理的东西。我想知道它是否会在 .NET Core 2 或 3 中编译。【参考方案5】:

这个问题非常类似于:

Capture console exit C#

这是我解决这个问题的方法,并处理了用户按 X 和 Ctrl-C 的问题。注意 ManualResetEvents 的使用。这些将导致主线程休眠,从而释放 CPU 以在等待退出或清理时处理其他线程。注意:有必要在 main 结束时设置 TerminationCompletedEvent。如果不这样做,则会由于操作系统在终止应用程序时超时而导致不必要的终止延迟。

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;
        
    

【讨论】:

【参考方案6】:

Console.TreatControlCAsInput = true; 为我工作。

【讨论】:

这可能会导致 ReadLine 需要为每个输入按两次 Enter 键。【参考方案7】:

我可以在退出前进行一些清理工作。这样做的最佳方法是什么 这才是真正的目标:陷阱出口,制作自己的东西。上面的答案都没有正确。因为,Ctrl+C 只是退出应用的众多方式之一。

dotnet c# 中需要什么 - 所谓的 取消令牌 传递给 Host.RunAsync(ct),然后,在退出信号陷阱中,对于 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;
     

...

【讨论】:

【参考方案8】:

检测 SIGTERM 和 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);

【讨论】:

不正确。这个页面上写的解决方案都不能在 LINUX 上运行。 Console.In.Peek()、CurrentDomain.blah blah()、Console.CancelKeyPress() 它们都只能在 Windows 中工作,而根本不能在 linux 中工作。这听起来很简单,但是当您的应用程序作为基于控制台的应用程序在容器中运行时,这是一个大问题,因为 kubernetes 必须杀死它而不是优雅地结束它 我的答案是用 .net core 5 编写的,并测试了我的主要操作系统 Ubuntu 21.10。 这里也一样,我告诉你它根本不起作用。我正在使用 ConsoleHost Console.CancelKeyPress 自 2022 年起为我工作。Win11、.NET 6、NativeAOT 编译的应用程序、OutputType = Exe。【参考方案9】:

在我的例子中,我将一个异步 lambda 传递给 Console.CancelKeyPress,但这是行不通的。

【讨论】:

以上是关于如何在 C# 控制台应用程序中捕获 ctrl-c (SIGINT)的主要内容,如果未能解决你的问题,请参考以下文章

在ruby中捕获Ctrl-c

从 .NET 应用程序 (C#) 捕获控制台输出

如何在 C# 中读取正则表达式捕获

如何仅在 Tesseract C# 中捕获数字

杀死正在运行的程序的方法以及如何捕获它们?

Windows Ctrl-C - 在命令行应用程序中清理本地堆栈对象