Wyjście z konsoli przechwytywania C #


93

Mam aplikację konsolową, która zawiera sporo wątków. Istnieją wątki, które monitorują określone warunki i kończą program, jeśli są one prawdziwe. To wypowiedzenie może nastąpić w dowolnym momencie.

Potrzebuję zdarzenia, które może zostać wywołane podczas zamykania programu, aby móc wyczyścić wszystkie inne wątki i poprawnie zamknąć wszystkie uchwyty plików i połączenia. Nie jestem pewien, czy jest już wbudowany w framework .NET, więc pytam, zanim napiszę własny.

Zastanawiałem się, czy doszło do zdarzenia podobnego do:

MyConsoleProgram.OnExit += CleanupBeforeExit;

2
Wiem, że to bardzo późny komentarz, ale tak naprawdę nie musisz tego robić, jeśli „zamykanie plików i połączeń” jest jedyną rzeczą, którą chcesz zrobić jako czyszczenie. Ponieważ system Windows zamyka już wszystkie dojścia związane z procesem podczas kończenia.
Sedat Kapanoglu

6
^ Tylko jeśli te zasoby należą do kończonego procesu. Jest to absolutnie konieczne, jeśli na przykład automatyzujesz ukrytą aplikację COM (powiedzmy, Word lub Excel) w tle i musisz ją zabić przed
zamknięciem

1
to ma krótką odpowiedź stackoverflow.com/questions/2555292/…
barlop

Odpowiedzi:


97

Nie jestem pewien, gdzie znalazłem kod w sieci, ale znalazłem go teraz w jednym z moich starych projektów. Umożliwi to wyczyszczenie kodu w konsoli, np. Gdy zostanie nagle zamknięta lub z powodu wyłączenia ...

[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)
{
  switch (sig)
  {
      case CtrlType.CTRL_C_EVENT:
      case CtrlType.CTRL_LOGOFF_EVENT:
      case CtrlType.CTRL_SHUTDOWN_EVENT:
      case CtrlType.CTRL_CLOSE_EVENT:
      default:
          return false;
  }
}


static void Main(string[] args)
{
  // Some biolerplate to react to close window event
  _handler += new EventHandler(Handler);
  SetConsoleCtrlHandler(_handler, true);
  ...
}

Aktualizacja

Dla tych, którzy nie sprawdzają komentarzy, wydaje się, że to rozwiązanie nie działa dobrze (lub wcale) w systemie Windows 7 . Mówi o tym poniższy wątek


4
Czy możesz tego użyć, aby anulować wyjście? Poza tym, gdy się wyłącza!
ingh.am

7
Działa świetnie, tylko bool Handler()musi return false;(nic nie zwraca w kodzie), więc zadziała. Jeśli zwróci true, system Windows wyświetli okno dialogowe „Zakończ proces teraz”. = D
Cipi

3
Wygląda na to, że to rozwiązanie nie działa z systemem Windows 7 w przypadku zdarzenia związanego z zamknięciem, zobacz social.msdn.microsoft.com/Forums/en/windowscompatibility/thread/ ...
CharlesB

3
Należy pamiętać, że umieszczenie punktu przerwania w metodzie „Handler” spowoduje zgłoszenie wyjątku NullReferenceException. Sprawdzono w VS2010, Windows 7.
Maxim,

10
To działało świetnie dla mnie w systemie Windows 7 (64-bitowym). Nie wiem, dlaczego wszyscy tak mówią. Jedyne większe modyfikacje, jakie wprowadziłem, to usunięcie instrukcji enum i switch oraz zwrócenie wartości false z metody - wszystkie moje porządki wykonuję w treści metody.
BrainSlugs83

25

W pełni działający przykład, działa z ctrl-c, zamykając okna X i zabijają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 boilerplate 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..");
        }
    }
}

2
Przetestowałem to w Handlersystemie Windows 7 ze wszystkimi komentarzami z wyjątkiem return truepętli i while do liczenia sekund. Aplikacja nadal działa na ctrl-c, ale zamyka się po 5 sekundach podczas zamykania z X.
Antonios Hadjigeorgalis

Przykro mi, ale przy użyciu tego kodu mogę uzyskać informację „Czyszczenie zakończone” tylko wtedy, gdy naciskam Ctrl + C, a nie gdy zamykam przyciskiem „X”; w tym drugim przypadku otrzymuję tylko „Wyjście z systemu z powodu zewnętrznego CTRL-C, zabicia procesu lub zamknięcia”, ale wydaje się, że konsola zamyka się przed wykonaniem pozostałej części Handlermetody {przy użyciu Win10, .NET Framework 4.6.1}
Giacomo Pirinoli

na Windows 10 działa dla mnie CTRL-C, X w oknie ORAZ zakończ proces w Menedżerze zadań.
JJ_Coder4Hire

8

Sprawdź również:

AppDomain.CurrentDomain.ProcessExit

7
Wygląda na to, że wychwytuje tylko wyjścia z powrotu lub środowiska. Exit, nie przechwytuje CTRL + C, CTRL + Break, ani faktycznego przycisku zamykania na konsoli.
Kit10

Jeśli osobno obsługujesz CTRL + C, Console.CancelKeyPressto ProcessExitzdarzenie faktycznie wywoływane po CancelKeyPresswykonaniu wszystkich programów obsługi zdarzeń.
Konard

5

Miałem podobny problem, tylko moja aplikacja konsolowa działałaby w nieskończonej pętli z jedną instrukcją wyprzedzającą na środku. Oto moje rozwiązanie:

class Program
{
    static int Main(string[] args)
    {
        // Init Code...
        Console.CancelKeyPress += Console_CancelKeyPress;  // Register the function to cancel event

        // I do my stuffs

        while ( true )
        {
            // Code ....
            SomePreemptiveCall();  // The loop stucks here wating function to return
            // Code ...
        }
        return 0;  // Never comes here, but...
    }

    static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
    {
        Console.WriteLine("Exiting");
        // Termitate what I have to terminate
        Environment.Exit(-1);
    }
}

4

Wygląda na to, że masz wątki bezpośrednio kończące aplikację? Być może byłoby lepiej, gdyby wątek sygnalizował główny wątek, aby powiedzieć, że aplikacja powinna zostać zakończona.

Po odebraniu tego sygnału główny wątek może czysto zamknąć inne wątki i ostatecznie zamknąć się.


3
Muszę się zgodzić z tą odpowiedzią. Wymuszenie zakończenia aplikacji, a następnie próba jej wyczyszczenia, nie jest dobrym rozwiązaniem. Kontroluj swoją aplikację, Noit. Nie pozwól, by cię kontrolowało.
Randolpho

1
Wątek stworzony przeze mnie bezpośrednio nie jest jedyną rzeczą, która może zamknąć moją aplikację. Ctrl-C i „przycisk zamykania” to inne sposoby zakończenia. Kod wysłany przez Franka, po drobnych modyfikacjach, pasuje idealnie.
ZeroKelvin

4

Odpowiedź ZeroKelvina działa w systemie Windows 10 x64, aplikacji konsoli .NET 4.6. Dla tych, którzy nie muszą zajmować się wyliczeniem CtrlType, oto naprawdę prosty sposób na podłączenie się do zamknięcia frameworka:

class Program
{
    private delegate bool ConsoleCtrlHandlerDelegate(int sig);

    [DllImport("Kernel32")]
    private static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandlerDelegate handler, bool add);

    static ConsoleCtrlHandlerDelegate _consoleCtrlHandler;

    static void Main(string[] args)
    {
        _consoleCtrlHandler += s =>
        {
            //DoCustomShutdownStuff();
            return false;   
        };
        SetConsoleCtrlHandler(_consoleCtrlHandler, true);
    }
}

Zwrócenie FALSE z procedury obsługi mówi frameworkowi, że nie "obsługujemy" sygnału sterującego i używana jest następna funkcja obsługi z listy programów obsługi dla tego procesu. Jeśli żaden z programów obsługi nie zwraca wartości TRUE, wywoływana jest domyślna procedura obsługi.

Należy zauważyć, że gdy użytkownik wylogowuje się lub zamyka, system Windows nie wywołuje wywołania zwrotnego, ale jest natychmiast przerywany.


3

Jest dla aplikacji WinForms;

Application.ApplicationExit += CleanupBeforeExit;

W przypadku aplikacji konsolowych spróbuj

AppDomain.CurrentDomain.DomainUnload += CleanupBeforeExit;

Ale nie jestem pewien, w którym momencie zostanie to wywołane lub czy zadziała z bieżącej domeny. Podejrzewam, że nie.


Dokumentacja pomocy dotycząca DomainUnload mówi: „Delegat EventHandler dla tego zdarzenia może wykonać wszelkie czynności związane z zakończeniem, zanim domena aplikacji zostanie zwolniona”. Wygląda więc na to, że działa w obecnej domenie. Jednak może to nie działać dla jego potrzeb, ponieważ jego wątki mogą utrzymywać domenę.
Rob Parker

2
Obsługuje tylko CTRL + C i CTRL + Close, nie przechwytuje się poprzez powrót, Environment.Exit ani kliknięcie przycisku zamykania.
Kit10

Nie łapie dla mnie CTRL + C z Mono w systemie Linux.
starbeamrainbowlabs

2

Visual Studio 2015 + Windows 10

  • Pozwól na czyszczenie
  • Aplikacja z pojedynczą instancją
  • Trochę złocenia

Kod:

using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;

namespace YourNamespace
{
    class Program
    {
        // if you want to allow only one instance otherwise remove the next line
        static Mutex mutex = new Mutex(false, "YOURGUID-YOURGUID-YOURGUID-YO");

        static ManualResetEvent run = new ManualResetEvent(true);

        [DllImport("Kernel32")]
        private static extern bool SetConsoleCtrlHandler(EventHandler handler, bool add);                
        private delegate bool EventHandler(CtrlType sig);
        static EventHandler exitHandler;
        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 ExitHandler(CtrlType sig)
        {
            Console.WriteLine("Shutting down: " + sig.ToString());            
            run.Reset();
            Thread.Sleep(2000);
            return false; // If the function handles the control signal, it should return TRUE. If it returns FALSE, the next handler function in the list of handlers for this process is used (from MSDN).
        }


        static void Main(string[] args)
        {
            // if you want to allow only one instance otherwise remove the next 4 lines
            if (!mutex.WaitOne(TimeSpan.FromSeconds(2), false))
            {
                return; // singleton application already started
            }

            exitHandler += new EventHandler(ExitHandler);
            SetConsoleCtrlHandler(exitHandler, true);

            try
            {
                Console.BackgroundColor = ConsoleColor.Gray;
                Console.ForegroundColor = ConsoleColor.Black;
                Console.Clear();
                Console.SetBufferSize(Console.BufferWidth, 1024);

                Console.Title = "Your Console Title - XYZ";

                // start your threads here
                Thread thread1 = new Thread(new ThreadStart(ThreadFunc1));
                thread1.Start();

                Thread thread2 = new Thread(new ThreadStart(ThreadFunc2));
                thread2.IsBackground = true; // a background thread
                thread2.Start();

                while (run.WaitOne(0))
                {
                    Thread.Sleep(100);
                }

                // do thread syncs here signal them the end so they can clean up or use the manual reset event in them or abort them
                thread1.Abort();
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.Write("fail: ");
                Console.ForegroundColor = ConsoleColor.Black;
                Console.WriteLine(ex.Message);
                if (ex.InnerException != null)
                {
                    Console.WriteLine("Inner: " + ex.InnerException.Message);
                }
            }
            finally
            {                
                // do app cleanup here

                // if you want to allow only one instance otherwise remove the next line
                mutex.ReleaseMutex();

                // remove this after testing
                Console.Beep(5000, 100);
            }
        }

        public static void ThreadFunc1()
        {
            Console.Write("> ");
            while ((line = Console.ReadLine()) != null)
            {
                if (line == "command 1")
                {

                }
                else if (line == "command 1")
                {

                }
                else if (line == "?")
                {

                }

                Console.Write("> ");
            }
        }


        public static void ThreadFunc2()
        {
            while (run.WaitOne(0))
            {
                Thread.Sleep(100);
            }

           // do thread cleanup here
            Console.Beep();         
        }

    }
}

Ciekawe, że wydaje się to najbardziej solidną odpowiedzią. Należy jednak zachować ostrożność przy zmianie rozmiaru bufora konsoli: jeśli wysokość bufora jest mniejsza niż wysokość okna, program zgłosi wyjątek podczas uruchamiania.
John Zabroski

1

Link wspomniano przez Charle B w komentarzu do FLQ

Głęboko mówi:

SetConsoleCtrlHandler nie będzie działać w systemie Windows7, jeśli połączysz się z user32

W innym miejscu w wątku sugeruje się utworzenie ukrytego okna. Więc tworzę winform iw onload podłączyłem się do konsoli i uruchomiłem oryginalny plik Main. A potem SetConsoleCtrlHandle działa dobrze (SetConsoleCtrlHandle jest wywoływana zgodnie z sugestią flq)

public partial class App3DummyForm : Form
{
    private readonly string[] _args;

    public App3DummyForm(string[] args)
    {
        _args = args;
        InitializeComponent();
    }

    private void App3DummyForm_Load(object sender, EventArgs e)
    {
        AllocConsole();
        App3.Program.OriginalMain(_args);
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool AllocConsole();
}

Właściwie to nie działa. Mam aplikację WFP z wieloma oknami i używam konsoli ( AllocConsolejak w twoim przykładzie), aby wyświetlić dodatkowe informacje. Problem polega na tym, że cała aplikacja (wszystkie Windows) zostanie zamknięta, jeśli użytkownik kliknie (X) w oknie konsoli. Te SetConsoleCtrlHandlerprace, ale zatrzymanie aplikacji w każdym razie przed dowolny kod w programie obsługi wykonane (widzę wartości graniczne zwolniony i tuż potem zatrzymanie APP).
Mike Keskinov

Ale znalazłem rozwiązanie, które na mnie działa - prosty przycisk zamykania WYŁĄCZONY . Zobacz: stackoverflow.com/questions/6052992/…
Mike Keskinov

0

Dla zainteresowanych VB.net. (Przeszukałem Internet i nie mogłem znaleźć odpowiednika) Tutaj jest przetłumaczony na vb.net.

    <DllImport("kernel32")> _
    Private Function SetConsoleCtrlHandler(ByVal HandlerRoutine As HandlerDelegate, ByVal Add As Boolean) As Boolean
    End Function
    Private _handler As HandlerDelegate
    Private Delegate Function HandlerDelegate(ByVal dwControlType As ControlEventType) As Boolean
    Private Function ControlHandler(ByVal controlEvent As ControlEventType) As Boolean
        Select Case controlEvent
            Case ControlEventType.CtrlCEvent, ControlEventType.CtrlCloseEvent
                Console.WriteLine("Closing...")
                Return True
            Case ControlEventType.CtrlLogoffEvent, ControlEventType.CtrlBreakEvent, ControlEventType.CtrlShutdownEvent
                Console.WriteLine("Shutdown Detected")
                Return False
        End Select
    End Function
    Sub Main()
        Try
            _handler = New HandlerDelegate(AddressOf ControlHandler)
            SetConsoleCtrlHandler(_handler, True)
     .....
End Sub

Powyższe rozwiązanie nie działa dla mnie framework vb.net 4.5 ControlEventType nie jest rozwiązywany. Udało mi się wykorzystać ten pomysł jako rozwiązanie stackoverflow.com/questions/15317082/ ...
glant
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.