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

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

【问题讨论】:

    标签: c# .net console


    【解决方案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 stackoverflow.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,但这是行不通的。

        【讨论】:

          猜你喜欢
          • 2012-03-17
          • 2013-11-14
          • 1970-01-01
          • 1970-01-01
          • 2011-11-22
          • 2012-02-07
          • 1970-01-01
          • 1970-01-01
          • 2020-12-05
          相关资源
          最近更新 更多