Làm cách nào để bẫy ctrl-c (SIGINT) trong ứng dụng bảng điều khiển C #


221

Tôi muốn có thể bẫy CTRL+ Ctrong ứng dụng bảng điều khiển C # để tôi có thể thực hiện một số dọn dẹp trước khi thoát. Cách nào là tốt nhất để thực hiện việc này?

Câu trả lời:


126

6
Trên thực tế, bài viết đó đề xuất P / Gọi, và CancelKeyPresschỉ được đề cập ngắn gọn trong các ý kiến. Một bài viết hay là codeneverwritten.com / 2006/10 / b
bzlm

Điều này hoạt động nhưng nó không bẫy được việc đóng cửa sổ với X. Xem giải pháp hoàn chỉnh của tôi bên dưới. cũng hoạt động với kill
JJ_Coder4Hire

1
Tôi đã thấy rằng Console.CatteryKeyPress sẽ ngừng hoạt động nếu đóng bàn điều khiển. Chạy một ứng dụng trong mono / linux với systemd hoặc nếu ứng dụng được chạy dưới dạng "mono myapp.exe </ dev / null", SIGINT sẽ được gửi đến trình xử lý tín hiệu mặc định và ngay lập tức tắt ứng dụng. Người dùng Linux có thể muốn xem stackoverflow.com/questions/6546509/ từ
tekHedd

2
Liên kết không bị hỏng cho nhận xét của @ bzlm: web.archive.org/web/20110424085511/http://
Ian Kemp

@aku không có thời gian bạn phải trả lời SIGINT trước khi quá trình hủy bỏ thô lỗ? Tôi không thể tìm thấy nó ở bất cứ đâu, và tôi đang cố nhớ lại nơi tôi đã đọc nó.
John Zabroski

224

Sự kiện Console.CatteryKeyPress được sử dụng cho việc này. Đây là cách nó được sử dụng:

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

    while (true) {}
}

Khi người dùng nhấn Ctrl + C, mã trong ủy nhiệm được chạy và chương trình thoát. Điều này cho phép bạn thực hiện dọn dẹp bằng cách gọi các phương thức cần thiết. Lưu ý rằng không có mã nào sau khi ủy nhiệm được thực thi.

Có những tình huống khác mà điều này sẽ không cắt nó. Ví dụ: nếu chương trình hiện đang thực hiện các tính toán quan trọng không thể dừng ngay lập tức. Trong trường hợp đó, chiến lược chính xác có thể là yêu cầu chương trình thoát ra sau khi tính toán hoàn tất. Đoạn mã sau đây đưa ra một ví dụ về cách thực hiện điều này:

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

Sự khác biệt giữa mã này và ví dụ đầu tiên là e.Cancelđược đặt thành true, có nghĩa là việc thực thi tiếp tục sau khi ủy nhiệm. Nếu chạy, chương trình sẽ đợi người dùng nhấn Ctrl + C. Khi điều đó xảy ra, keepRunningbiến số thay đổi giá trị khiến vòng lặp while thoát ra. Đây là một cách để làm cho chương trình thoát một cách duyên dáng.


21
keepRunningcó thể cần được đánh dấu volatile. Chủ đề chính có thể lưu trữ nó trên một thanh ghi CPU nếu không sẽ không nhận thấy sự thay đổi giá trị khi ủy nhiệm được thực thi.
cdhowie

Điều này hoạt động nhưng nó không bẫy được việc đóng cửa sổ với X. Xem giải pháp hoàn chỉnh của tôi bên dưới. làm việc với kill là tốt.
JJ_Coder4Hire

13
Nên thay đổi để sử dụng ManualResetEventthay vì quay trên a bool.
Mizipzor

Một cảnh báo nhỏ cho bất kỳ ai khác đang chạy công cụ trong Git-Bash, MSYS2 hoặc CygWin: Bạn sẽ phải chạy dotnet thông qua winpty để điều này hoạt động (Vì vậy, winpty dotnet run). Nếu không, đại biểu sẽ không bao giờ được điều hành.
Samuel Lampa

Xem ra - sự kiện cho CancKeyPress được xử lý trên một chuỗi nhóm luồng, điều này không rõ ràng ngay lập tức: docs.microsoft.com/en-us/dotnet/api/ Lỗi
Sam

100

Tôi muốn thêm vào câu trả lời của Jonas . Việc quay vòng trên boolsẽ gây ra việc sử dụng CPU 100% và lãng phí rất nhiều năng lượng mà không làm được gì nhiều trong khi chờ đợi CTRL+ C.

Giải pháp tốt hơn là sử dụng một ManualResetEventđể thực sự "chờ" cho 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
Tôi nghĩ vấn đề là bạn sẽ thực hiện tất cả công việc bên trong vòng lặp while và nhấn Ctrl + C sẽ không bị hỏng ở giữa vòng lặp while; nó sẽ hoàn thành việc lặp lại đó trước khi thoát ra.
pkr298

2
@ pkr298 - Quá tệ mọi người không bỏ phiếu nhận xét của bạn vì nó hoàn toàn đúng. Tôi sẽ chỉnh sửa câu trả lời của Jonas để làm rõ mọi người khỏi suy nghĩ theo cách Jonathon đã làm (điều này vốn không tệ nhưng không phải như Jonas có nghĩa là câu trả lời của anh ấy)
M. Mimpen 2/12/13

Cập nhật câu trả lời hiện có với cải tiến này.
Mizipzor

27

Dưới đây là một ví dụ làm việc hoàn chỉnh. dán vào dự án giao diện điều khiển C # trống:

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 / invokes này do đó không phải là nền tảng chéo
knocte

Tôi chỉ cộng với điều này bởi vì đó là câu trả lời duy nhất giải quyết một câu trả lời hoàn chỉnh. Điều này là kỹ lưỡng và cho phép bạn chạy chương trình theo lịch trình tác vụ mà không có sự tham gia của người dùng. Bạn vẫn có cơ hội để dọn dẹp. Sử dụng NLOG trong dự án của bạn và bạn có một cái gì đó có thể quản lý được. Tôi tự hỏi liệu nó sẽ biên dịch trong .NET Core 2 hay 3.
BigTFromAZ

7

Câu hỏi này rất giống với:

Chụp bàn điều khiển thoát C #

Đây là cách tôi giải quyết vấn đề này và xử lý người dùng nhấn X cũng như Ctrl-C. Lưu ý việc sử dụng ManualResetEvents. Những điều này sẽ khiến luồng chính ngủ, giải phóng CPU để xử lý các luồng khác trong khi chờ thoát hoặc dọn dẹp. LƯU Ý: Cần đặt TerminationCompletedEvent ở cuối chính. Không làm như vậy gây ra độ trễ không cần thiết trong việc chấm dứt do hệ điều hành hết thời gian trong khi giết ứng dụng.

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; đã làm việc cho tôi.


2
Điều này có thể khiến ReadLine yêu cầu hai lần nhấn Enter cho mỗi lần nhập.
Grault

0

Tôi có thể thực hiện một số dọn dẹp trước khi thoát. Cách tốt nhất để làm điều này Thats là mục tiêu thực sự: thoát bẫy, để tạo ra công cụ của riêng bạn. Và neigther câu trả lời ở trên không làm cho nó đúng. Bởi vì, Ctrl + C chỉ là một trong nhiều cách để thoát ứng dụng.

Cái gì trong dotnet c # là cần thiết cho nó - cái gọi là mã thông báo hủy được chuyển đến Host.RunAsync(ct)và sau đó, trong bẫy tín hiệu thoát, đối với Windows, nó sẽ là

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

...

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.