C#コンソールアプリでctrl-c(SIGINT)をトラップする方法


222

終了する前にいくつかのクリーンアップを実行できるように、C#コンソールアプリケーションでCTRL+ をトラップできるようにしたいと思いCます。これを行う最良の方法は何ですか?

回答:


127

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 /…

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.Canceltrueに設定されていることです。つまり、デリゲートの後も実行が継続されます。実行されると、プログラムはユーザーが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)。それ以外の場合、デリゲートは実行されません。
Samuel Lampa

時計はすぐに明らかにされていませんスレッドプールのスレッド上で処理されるCancelKeyPressのためのイベントをOUT-:docs.microsoft.com/en-us/dotnet/api/...
サムRueby

100

追加したい ジョナスの答えます。aでスピンすると、boolCPU使用率が100%になり、CTRL+ を待機している間、大量のエネルギーを無駄に消費しCます。

より良い解決策はManualResetEventCTRL+ を実際に「待つ」ために使用すること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ループ内ですべての作業を行うことであり、Ctrl + Cを押しても、whileの反復の途中で中断しないことです。それが発生する前に、その反復を完了します。
2013

2
@ pkr298-完全に真実であるため、あまりにも悪い人々はあなたのコメントに投票しないでください。ジョナソンの答えを編集して、人々がジョナソンのやり方を理解するのを明確にします(本質的に悪いわけではありませんが、ジョナスが彼の答えを意味したわけではありません)
M.ミンペン

この改善で既存の回答を更新します。
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 / invokesはクロス
プラットフォームで

完全な答えに対処する唯一の答えだからです。これは完全であり、ユーザーが関与することなくタスクスケジューラの下でプログラムを実行できます。あなたはまだクリーンアップする機会を得ます。プロジェクトでNLOGを使用すると、扱いやすいものが得られます。.NET Core 2または3でコンパイルできるかどうか疑問に思います
BigTFromAZ

7

この質問は次の質問とよく似ています。

キャプチャーコンソール出口C#

これが私がこの問題を解決した方法であり、Ctrl-Cと同様にXを押すユーザーに対処しました。ManualResetEventsの使用に注意してください。これらにより、メインスレッドがスリープ状態になり、終了またはクリーンアップを待機している間、CPUが他のスレッドを処理できるようになります。注:メインの最後にTerminationCompletedEventを設定する必要があります。そうしないと、アプリケーションの強制終了中にOSがタイムアウトするため、終了時に不要な待ち時間が発生します。

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は入力ごとに2回のEnterキーを必要とする可能性があります。
Grault

0

終了する前に、いくつかのクリーンアップを実行できます。これを行うための最良の方法は何ですか?それが本当の目標です:自分自身のものを作るためのトラップ出口 そして、それを正しくしないように上記の隣の答え。Ctrl + Cは、アプリを終了する多くの方法の1つにすぎません。

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.