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


221

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

Answers:


126

请参阅MSDN:

Console.CancelKeyPress事件

带有代码示例的文章:

Ctrl-C和.NET控制台应用程序


6
实际上,该文章建议使用P / Invoke,并且CancelKeyPress在注释中仅简短提及。一篇好文章是codeneverwritten.com/2006/10/...
bzlm

这可以工作,但不会用X来关闭窗口。请参阅下面的完整解决方案。也可以与kill一起使用
JJ_Coder4Hire 2014年

1
我发现如果控制台关闭,该Console.CancelKeyPress将停止工作。在具有systemd的mono / linux下运行应用程序,或者如果该应用程序以“ mono myapp.exe </ dev / null”运行,则SIGINT将发送到默认信号处理程序并立即终止该应用程序。Linux用户可能希望看到stackoverflow.com/questions/6546509/...
tekHedd

2
@bzlm的评论的完整链接:web.archive.org/web/20110424085511/http
伊恩·肯普

@aku在程序粗鲁中止之前,您没有足够的时间响应SIGINT吗?我在任何地方都找不到它,我想回忆起我在哪里读的。
John Zabroski

223

Console.CancelKeyPress事件被用于此目的。使用方法如下:

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

    while (true) {}
}

当用户按Ctrl + C时,将运行委托中的代码,并退出程序。这使您可以通过调用necessairy方法来执行清理。请注意,执行委托后没有任何代码。

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

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,这意味着在委托之后继续执行。如果运行,程序将等待用户按Ctrl +C。在这种情况下,keepRunning变量会更改值,从而导致while循环退出。这是使程序正常退出的一种方法。


21
keepRunning可能需要标记volatile。主线程否则可能会将其缓存在CPU寄存器中,并且在执行委托时不会注意到值的更改。
cdhowie 2012年

这可以工作,但不会用X来关闭窗口。请参阅下面的完整解决方案。也可以与kill配合使用。
JJ_Coder4Hire 2014年

13
应该更改为使用ManualResetEvent而不是旋转bool
Mizipzor 2014年

对于在Git-Bash,MSYS2或CygWin中运行正在运行的东西的其他人来说,这是一个小警告:您必须通过winpty运行dotnet才能工作(因此,winpty dotnet run)。否则,将永远不会运行委托。
塞缪尔·兰帕

要小心了,对于CancelKeyPress该事件在线程池线程,这不是很明显的处理:docs.microsoft.com/en-us/dotnet/api/...
山姆Rueby

100

我想补充一下乔纳斯的答案。在a上旋转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();
}

28
我认为要点是,您将在while循环内完成所有工作,并且在while迭代的中间按Ctrl + C不会中断;它将在中断之前完成该迭代。
pkr298

2
@ pkr298-太糟糕了的人不会投票赞成您的评论,因为这是完全正确的。我将编辑乔纳斯(Jonas)的答案,以使人们从思考乔纳森(Jonathon)的方式中脱颖而出(虽然天生就不好,但并不像乔纳斯(Jonas)的意思那样)
M. Mimpen 2013年

通过此改进来更新现有答案。
Mizipzor 2014年

27

这是一个完整的工作示例。粘贴到空的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..");
        }
    }
}

8
因此,此p /调用不是

我加了一下,因为它是唯一可以解决完整答案的答案。这一过程很彻底,它使您可以在任务调度程序下运行程序,而无需用户参与。您仍然有机会进行清理。在项目中使用NLOG,您可以管理一些东西。我想知道它将在.NET Core 2还是3中进行编译
。– BigTFromAZ

7

这个问题非常类似于:

捕获控制台出口C#

这是我解决此问题的方法,并处理了用户同时按下X和Ctrl-C的问题。注意使用ManualResetEvents。这些将导致主线程进入休眠状态,从而使CPU在等待退出或清除时释放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;
        }
    }
}

3

Console.TreatControlCAsInput = true; 为我工作。


2
这可能导致ReadLine对每个输入要求两次Enter键。
Grault

0

我可以在退出前进行一些清理。做到这一点的最佳方法是什么,这才是真正的目标:陷阱出口,制造自己的东西。上面的答案还不够正确。因为,Ctrl + C只是退出应用程序的众多方法之一。

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

...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.