Bagaimana cara menjebak ctrl-c (SIGINT) di aplikasi konsol C #

222

Saya ingin dapat menjebak CTRL+ Cdi aplikasi konsol C # sehingga saya dapat melakukan beberapa pembersihan sebelum keluar. Apa cara terbaik untuk melakukan ini?

Nick Randell
sumber

Jawaban:

127

Lihat MSDN:

Acara Konsol.CancelKeyPress

Artikel dengan contoh kode:

Ctrl-C dan aplikasi .NET console

aku
sumber
6
Sebenarnya, artikel itu merekomendasikan P / Invoke, dan CancelKeyPresshanya disebutkan secara singkat di komentar. Artikel yang bagus adalah codeneverwritten.com/2006/10/…
bzlm
Ini berfungsi tetapi tidak memerangkap penutupan jendela dengan X. Lihat solusi lengkap saya di bawah ini. bekerja dengan kill juga
JJ_Coder4Hire
1
Saya telah menemukan bahwa Console.CancelKeyPress akan berhenti bekerja jika konsol ditutup. Menjalankan aplikasi di bawah mono / linux dengan systemd, atau jika aplikasi dijalankan sebagai "mono myapp.exe </ dev / null", SIGINT akan dikirim ke penangan sinyal default dan langsung mematikan aplikasi. Pengguna Linux mungkin ingin melihat stackoverflow.com/questions/6546509/…
tekHedd
2
Tautan tidak terputus untuk komentar @ bzlm: web.archive.org/web/20110424085511/http://…
Ian Kemp
@aku tidak ada waktu untuk membalas SIGINT sebelum proses kasar dibatalkan? Saya tidak dapat menemukannya di mana pun, dan saya mencoba mengingat di mana saya membacanya.
John Zabroski
224

Acara Console.CancelKeyPress digunakan untuk ini. Ini adalah bagaimana ini digunakan:

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

    while (true) {}
}

Ketika pengguna menekan Ctrl + C kode dalam delegasi dijalankan dan program keluar. Ini memungkinkan Anda untuk melakukan pembersihan dengan memanggil metode yang diperlukan. Perhatikan bahwa tidak ada kode setelah delegasi dieksekusi.

Ada situasi lain di mana ini tidak akan memotongnya. Misalnya, jika program saat ini melakukan perhitungan penting yang tidak dapat segera dihentikan. Dalam hal itu, strategi yang tepat mungkin adalah memberi tahu program untuk keluar setelah perhitungan selesai. Kode berikut memberikan contoh bagaimana ini dapat diterapkan:

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

Perbedaan antara kode ini dan contoh pertama adalah yang e.Canceldisetel ke true, yang berarti eksekusi berlanjut setelah delegasi. Jika dijalankan, program menunggu pengguna menekan Ctrl + C. Ketika itu terjadi, keepRunningnilai perubahan variabel yang menyebabkan loop sementara keluar. Ini adalah cara untuk membuat program keluar dengan anggun.

Jonas
sumber
21
keepRunningmungkin perlu ditandai volatile. Utas utama mungkin menyimpannya di register CPU jika tidak, dan tidak akan melihat perubahan nilai ketika delegasi dieksekusi.
cdhowie
Ini berfungsi tetapi tidak memerangkap penutupan jendela dengan X. Lihat solusi lengkap saya di bawah ini. bekerja dengan kill juga.
JJ_Coder4Hire
13
Harus diubah untuk menggunakan ManualResetEventspin daripada bool.
Mizipzor
Peringatan kecil untuk siapa pun yang menjalankan menjalankan barang di Git-Bash, MSYS2 atau CygWin: Anda harus menjalankan dotnet via winpty agar ini berfungsi (Jadi, winpty dotnet run). Kalau tidak, delegasi tidak akan pernah dijalankan.
Samuel Lampa
Watch out- acara untuk CancelKeyPress ditangani pada utas thread pool, yang tidak segera jelas: docs.microsoft.com/en-us/dotnet/api/…
Sam Rueby
100

Saya ingin menambahkan jawaban Jonas . Berputar pada a boolakan menyebabkan utilisasi CPU 100%, dan membuang banyak energi tanpa melakukan apa-apa sambil menunggu CTRL+ C.

Solusi yang lebih baik adalah menggunakan a ManualResetEventuntuk benar-benar "menunggu" untuk 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();
}
Jonathon Reinhart
sumber
28
Saya pikir intinya adalah bahwa Anda akan melakukan semua pekerjaan di dalam loop sementara, dan menekan Ctrl + C tidak akan pecah di tengah iterasi sementara; itu akan menyelesaikan iterasi sebelum pecah.
pkr298
2
@ pkr298 - Sayang sekali orang tidak memilih komentar Anda karena itu sepenuhnya benar. Saya akan mengedit jawaban Jonas untuk mengklarifikasi orang agar tidak berpikir seperti yang dilakukan Jonathon (yang secara inheren tidak buruk tetapi tidak seperti yang Jonas maksudkan jawabannya)
M. Mimpen
Perbarui jawaban yang ada dengan peningkatan ini.
Mizipzor
27

Ini adalah contoh kerja yang lengkap. rekatkan ke proyek konsol C # kosong:

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..");
        }
    }
}
JJ_Coder4Hire
sumber
8
Oleh karena itu
permintaan
Saya hanya menambahkan ini karena itu satu-satunya jawaban yang membahas jawaban lengkap. Yang ini menyeluruh dan memungkinkan Anda untuk menjalankan program di bawah penjadwal tugas tanpa keterlibatan pengguna. Anda masih mendapatkan kesempatan untuk membersihkan. Gunakan NLOG dalam proyek Anda dan Anda memiliki sesuatu yang dapat dikelola. Saya ingin tahu apakah itu akan dikompilasi dalam .NET Core 2 atau 3.
BigTFromAZ
7

Pertanyaan ini sangat mirip dengan:

Tangkap keluar konsol C #

Inilah cara saya memecahkan masalah ini, dan menangani pengguna yang memukul X serta Ctrl-C. Perhatikan penggunaan ManualResetEvents. Ini akan menyebabkan thread utama tertidur yang membebaskan CPU untuk memproses thread lain sambil menunggu keluar, atau pembersihan. CATATAN: Perlu untuk mengatur TerminationCompletedEvent di akhir main. Kegagalan untuk melakukan hal itu menyebabkan latensi yang tidak perlu dalam penghentian karena waktu OS habis sementara membunuh aplikasi.

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;
        }
    }
}
Paul
sumber
3

Console.TreatControlCAsInput = true; telah bekerja untuk saya.

hallo
sumber
2
Ini dapat menyebabkan ReadLine membutuhkan dua tekan Enter untuk setiap input.
Grault
0

Saya dapat melakukan beberapa pembersihan sebelum keluar. Apa cara terbaik untuk melakukan ini? Itulah tujuan sebenarnya: perangkap keluar, untuk membuat barang-barang Anda sendiri. Dan jawaban yang lebih baik di atas tidak memperbaikinya. Karena, Ctrl + C hanyalah salah satu dari banyak cara untuk keluar dari aplikasi.

Apa yang ada di dotnet c # diperlukan untuk itu - yang disebut token pembatalan diteruskan ke Host.RunAsync(ct)dan kemudian, dalam perangkap sinyal keluar, untuk Windows akan menjadi

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

...

Vaulter
sumber