Montag, 5. März 2012

Beenden einer Konsolenapplikation abfangen

Manchmal wird eine Anwendung, zum Beispiel durch den Benutzer, während einer kritischen Aktion beendet. Möchte man diese Aktion noch zuende führen oder anderweitigen Code zum Beenden ausführen, kann man in Windows Forms-Applikationen einfach die Ereignisse FormClosed und FormClosing benutzen.
Bei Konsolenanwendungen stehen diese nicht zur Verfügung. Um aber doch auf das unerwartete Beenden der Anwendung reagieren zu können, benutzen wir die Funktion SetConsoleCtrlHandler().
Mit dieser können wir bei der Anwendung eine Funktion registrieren, welche aufgerufen wird, falls das Programm beendet wird.
SetConsoleCtrlHandler() erwartet 2 Parameter, der erste ist das Delegate auf die zu nutzende Funktion und der 2 ein Boolean Wert, welcher angibt, ob der Handler hinzugefügt oder entfernt werden soll.
Die Funktion, welche beim Beenden aufgerufen wird, erhält als Parameter eine Enumeration, welche das Ereignis spezifiziert, welches das Beenden hervorruft, und muss immer false zurückgeben.
Im Code legen wir zuerst ein Delegate von der geforderten Signatur an:
private delegate bool EventHandler(CtrlType e)

Dann legen wir eine Variable mit dem eben erzeugten Typ an:
static EventHandler ConsoleCloseHandler
.
Dieser weisen wir die zu benutzende Funktion zu und registrieren diese bei der Anwendung:
ConsoleCloseHandler += new EventHandler(Console_Closed);
SetConsoleCtrlHandler(ConsoleCloseHandler, true);

In der Funktion Console_Closed() ist der Grund des Beendes im Argument gespeichert, dieser kann abgefragt und verwendet werden.
Die von mir benutzte Aufzählung enthält 4 Ereignisse: Das Schließen der Konsole über die Tastenkombination Strg + C, das herkömmliche Schließen (z.B. durch Klicken von "X"), das Schließen wegen Abmeldung des Benutzers und das Schließen wegen Herunterfahrens.
Ich hoffe der folgende Quellcode verdeutlicht das Prinzip:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Runtime.InteropServices;

namespace ConsoleApplication1
{
    class Program
    {
        [DllImport("Kernel32")]
        private static extern bool SetConsoleCtrlHandler(EventHandler handler, bool add);

        enum CtrlType
        {
            CTRL_C_EVENT = 0,
            CTRL_CLOSE_EVENT = 2,
            CTRL_LOGOFF_EVENT = 5,
            CTRL_SHUTDOWN_EVENT = 6
        }

        private delegate bool EventHandler(CtrlType e);
        static EventHandler ConsoleCloseHandler;

        static void Main(string[] args)
        {
            ConsoleCloseHandler += new EventHandler(Console_Closed);
            SetConsoleCtrlHandler(ConsoleCloseHandler, true);

            while (true)
            {
                // Endlosschleife
            }
        }

        private static bool Console_Closed(CtrlType e)
        {
            switch (e)
            {
                case CtrlType.CTRL_C_EVENT:
                    Console.WriteLine("Ctrl + C");
                    Console.ReadLine();
                    break;
                case CtrlType.CTRL_LOGOFF_EVENT:
                    Console.WriteLine("Log Off");
                    Console.ReadLine();
                    break;
                case CtrlType.CTRL_SHUTDOWN_EVENT:
                    Console.WriteLine("Shutdown");
                    Console.ReadLine();
                    break;
                case CtrlType.CTRL_CLOSE_EVENT:
                    Console.WriteLine("Close");
                    Console.ReadLine();
                    break;
                default:
                    Console.WriteLine("other");
                    Console.ReadLine();
                    break;
            }

            return true;
        }
    }
}

Kommentare:

  1. schöne Sache, um z.B. noch schnell ein Logfile vorm Schließen zu erstellen, da zwangsläufig jeder auf den X-Button drückt, wenn er etwas falsch gemacht bzw. falsch eingegeben hat.
    Dazu einfach die Switch-Cases ergänzen.

    nebenbei: Hat jemand den Logoff-Fall auf einem Win7 oder Win7 x64 Betriebssystem ausprobiert? Auf einem XP funktionierts bei mir, und auf dem Win7 hat er das Programm eiskalt beendet, ohne die Sachen auszuführen, die ich noch angegeben hatte.

    Grüße
    Tanja

    AntwortenLöschen
  2. Hi Tanja,

    stimmt, bei Windows 7 scheint der Logoff-Fall wirklich nicht zu funktionieren.
    Probier's mal über wtsregistersessionnotification (siehe http://www.pinvoke.net/default.aspx/wtsapi32.wtsregistersessionnotification), ich denke das könnte helfen?

    Grüße
    Oliver

    AntwortenLöschen
  3. Hi Oliver,
    wenn ich just im gleichen Moment, wo die Konsole geschlossen wird durch den Benutzer, Daten in eine MySQL schreibe, wie kann ich dann sichergehen, dass noch alle Schreibvorgänge abgeschlossen werden, bzw. dieser Schließvorgang nicht zu Störung des Schreibvorgangs führt?

    AntwortenLöschen
    Antworten
    1. Hallo,
      wieso lässt sich obige Methode nicht anwenden oder überlegst du generell wie du es damit umsetzen kannst?
      Du könntest z.B. "critical sections" um die Schreibvorgänge erstellen, also am Anfang des Schreibvorgangs eine Variable auf true setzen und am Ende auf false. Dann prüfst du wie oben beim Schließen ab ob die Variable auf true steht, wenn ja, wartest du noch mit dem Schließen.

      Löschen
  4. Oder man schreibt an's Ende Console.ReadKey(); hin.
    Pausiert aber nur den Abbruch bis zum nächsten Tastendruck

    AntwortenLöschen