ฉันจะดัก ctrl-c (SIGINT) ในแอปคอนโซล C # ได้อย่างไร


222

ฉันต้องการดักจับCTRL+ Cในแอปพลิเคชันคอนโซล C # เพื่อให้ฉันสามารถดำเนินการสะสางก่อนที่จะออก วิธีที่ดีที่สุดในการทำเช่นนี้คืออะไร?

คำตอบ:


127

ดู MSDN:

กิจกรรม Console.CancelKeyPress

บทความพร้อมตัวอย่างรหัส:

Ctrl-C และแอ็พพลิเคชันคอนโซล. NET


6
ที่จริงแล้วบทความนั้นแนะนำ P / Invoke และCancelKeyPressจะกล่าวถึงเพียงสั้น ๆ ในความคิดเห็น บทความที่ดีคือcodeneverwritten.com/2006/10/…
bzlm

ทำงานได้ แต่ไม่ดักจับการปิดหน้าต่างด้วย X ดูโซลูชันที่สมบูรณ์ของฉันด้านล่าง ทำงานร่วมกับ kill ได้เป็นอย่างดี
JJ_Coder4Hire

1
ฉันได้พบว่า Console.CancelKeyPress จะหยุดทำงานหากปิดคอนโซล การรันแอปภายใต้โมโน / ลินุกซ์ด้วย systemd หรือหากแอปทำงานเป็น "โมโน myapp.exe </ dev / null" ระบบจะส่ง SIGINT ไปยังตัวจัดการสัญญาณเริ่มต้นและฆ่าแอปทันที ผู้ใช้ Linux อาจต้องการดูstackoverflow.com/questions/6546509/…
tekHedd

2
ลิงก์ที่ไม่เสียสำหรับความคิดเห็นของ @ bzlm: web.archive.org/web/20110424085511/http://…
Ian Kemp

@aku ไม่มีเวลาที่คุณจะต้องตอบกลับ SIGINT ก่อนที่กระบวนการจะยกเลิก ฉันหามันไม่ได้ทุกที่และพยายามที่จะจำตำแหน่งที่ฉันอ่าน
John Zabroski

224

กระบวนการ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คือการตั้งค่าเป็นจริงซึ่งหมายความว่าการดำเนินการต่อหลังจากผู้รับมอบสิทธิ์ หากเรียกใช้โปรแกรมจะรอให้ผู้ใช้กด Ctrl + C เมื่อสิ่งนั้นเกิดขึ้นkeepRunningค่าการเปลี่ยนแปลงของตัวแปรจะทำให้ลูป while ออก นี่เป็นวิธีที่จะทำให้โปรแกรมออกจากโปรแกรมได้อย่างสวยงาม


21
keepRunningvolatileอาจจะต้องมีการทำเครื่องหมาย เธรดหลักอาจแคชในการลงทะเบียน CPU เป็นอย่างอื่นและจะไม่สังเกตเห็นการเปลี่ยนแปลงค่าเมื่อดำเนินการผู้รับมอบสิทธิ์
cdhowie

ทำงานได้ แต่ไม่ดักจับการปิดหน้าต่างด้วย X ดูโซลูชันที่สมบูรณ์ของฉันด้านล่าง ทำงานร่วมกับฆ่าเช่นกัน
JJ_Coder4Hire

13
ควรจะเปลี่ยนไปใช้สปินมากกว่าบนManualResetEvent bool
Mizipzor

ข้อแม้เล็ก ๆ สำหรับทุกคนที่ใช้งานสิ่งของใน Git-Bash, MSYS2 หรือ CygWin: คุณจะต้องเรียกใช้ dotnet ผ่าน winpty เพื่อให้มันทำงาน (ดังนั้นwinpty dotnet run) มิฉะนั้นผู้รับมอบสิทธิ์จะไม่ถูกเรียกใช้
ซามูเอล

ระวัง - เหตุการณ์สำหรับ CancelKeyPress ถูกจัดการบนเธรดพูลเธรดซึ่งไม่ชัดเจนในทันที: docs.microsoft.com/en-us/dotnet/api/…
Sam Rueby

100

ฉันต้องการที่จะเพิ่มคำตอบโจนัส ปั่นบนboolจะทำให้เกิดการใช้งาน CPU 100% และเสียพวงของพลังงานทำมากไม่มีอะไรในขณะที่รอให้+CTRLC

ทางออกที่ดีกว่าคือการใช้ a 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
ฉันคิดว่าประเด็นคือคุณจะต้องทำงานทั้งหมดในขณะที่ลูปและการกดปุ่ม Ctrl + C จะไม่หยุดในระหว่างการทำซ้ำในขณะที่; มันจะเสร็จสมบูรณ์ซ้ำก่อนที่จะแบ่งออก
pkr298

2
@ pkr298 - คนเลวเกินไปไม่โหวตความคิดเห็นของคุณเนื่องจากเป็นความจริงทั้งหมด ฉันจะแก้ไขคำตอบของโจนัสเพื่อชี้แจงให้ผู้คนเข้าใจวิธีที่ Jonathon ทำ (ซึ่งไม่เลวโดยเนื้อแท้ แต่ไม่ใช่อย่างที่โจนัสหมายถึงคำตอบของเขา)
M. Mimpen

อัปเดตคำตอบที่มีอยู่ด้วยการปรับปรุงนี้
Mizipzor

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 / invokes นี้จึงไม่ได้เป็น
แพลตฟอร์มข้าม

ฉันแค่บวกนี่เพราะมันเป็นคำตอบเดียวที่ตอบคำถามทั้งหมด อันนี้เป็นอย่างละเอียดและช่วยให้คุณสามารถเรียกใช้โปรแกรมภายใต้ตัวกำหนดเวลางานโดยไม่ต้องมีส่วนร่วมของผู้ใช้ คุณยังคงมีโอกาสได้ล้างข้อมูล ใช้ NLOG ในโครงการของคุณและคุณมีบางอย่างที่จัดการได้ ฉันสงสัยว่ามันจะรวบรวมใน. NET Core 2 หรือ 3
BigTFromAZ

7

คำถามนี้คล้ายกับ:

จับคอนโซลทางออก C #

นี่คือวิธีที่ฉันแก้ไขปัญหานี้และจัดการกับผู้ใช้ที่กดปุ่ม X และ Ctrl-C สังเกตการใช้ ManualResetEvents สิ่งเหล่านี้จะทำให้เธรดหลักเข้าสู่โหมดสลีปซึ่งทำให้ CPU ประมวลผลเธรดอื่นในขณะที่รอการออกหรือการล้างข้อมูล หมายเหตุ: มีความจำเป็นต้องตั้งค่า 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

ฉันสามารถทำการสะสางก่อนออกได้ อะไรคือวิธีที่ดีที่สุดในการทำ Thats นี้คือเป้าหมายที่แท้จริง: กับดักทางออกเพื่อสร้างสิ่งของของคุณเอง และคำตอบของนีกเธอร์ไม่ได้ทำให้ถูกต้อง เนื่องจาก 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;
     }

...

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.