In Konsolenanwendung CTRL-C behandeln



  • Hallo,

    ich habe eine Konsolenanwendung und möchte, dass die Anwendung durch CTRL-C beendet wird. Vorher möchte ich aber eine Abfrage schalten, ob der Benutzer das wirklich möchte.

    Das Minimalbeispiel sieht so aus:

    #include <cstdio>
    #include <iostream>
    #include <limits>
    #include <windows.h>
    
    bool terminate_app 	= false;
    
    bool handler_busy = false;
    
    BOOL __stdcall myhandler( DWORD event )
    {
       // if( handler_busy ) return;
       // handler_busy = true;
       // 
       if( event == CTRL_C_EVENT )
       {
          // Versuch 1: alle Handler aushängen
          // ::SetConsoleCtrlHandler( myhandler, TRUE );
          
          // Versuch 2: nur eigenen Handler aushängen
          // ::SetConsoleCtrlHandler( myhandler, FALSE );
          
          std::cout << "Anwendung beenden (J/N):";
          std::cin.ignore( std::numeric_limits<std::streamsize>::max() );
          std::string input;
          std::getline( cin, input );
          if( !input.empty() && (input[0] == 'j' || input[0] == 'J' ) )
          {
             terminate_app = true;
          }
          // Handler wieder einhängen
          // ::SetConsoleCtrlHandler( myhandler, TRUE);
       }
       // handler_busy = false;
       return TRUE;
    }
    
    int _tmain( int argc, _TCHAR* argv[] )
    {
       ::SetConsoleCtrlHandler( myhandler, TRUE );
       while( !terminate_app )
       {
          ::Sleep( 50 );
       }
    }
    

    Das Ganze funktioniert, so lange ich nur 1x CTRL-C drücke, dann erscheint die Bestätigungsmeldung und wenn ich "J" eingebe beendet sich die Anwendung. Wenn ich jedoch mehrmals schnell hintereinander CTRL-C drücke passieren seltsame Dinge: Ich kann nichts mehr eingeben und die Anwendung lässt sich nicht mehr beenden oder sie beendet sich, ohne dass ich "J" gedrückt habe. Hängt vermutlich damit zusammen, was noch im Eingabestrom steht, obwohl ich den ja in Zeile 24 eigentlich leere.

    Ich habe jetzt mehrere Sachen erfolglos ausprobiert:

    • durch globale Variable sichergestellt, dass der Handler nur 1x angesprungen wird
    • den eigenen Handler aus- und wieder eingehängt
    • alle Handler ausgehängt und den eigenen wieder eingehängt

    Wie implementiert man sowas richtig?



  • Also ich habe das so gelöst (Visual Studio 2019-Windows 10):

    …
    #include <iostream>
    #include <csignal>
    #include <conio.h>
    
    bool end = false;
    void signalCtrlC(int i)
    {
      std::cout << "Ctrl-C gedrueckt...[j] für Ende:";
      signal( SIGINT, ::signalCtrlC );
      char ioch = _getch();
      if ( ioch == 'j' )
      {
        end = true;
      }
    }
    int main( int argc, char * argv[] )
    {
      signal( SIGINT, ::signalCtrlC );
      while( !::end );
    return 0;
    }
    

    Hab ich jetzt kurz aus dem Kopf abgeschrieben, weil aus der Firma gesendet. Jedenfalls kann ich auf den Tasten [Strg]+[c] stehen.



  • Danke für die Antwort, aber das geht genauso kaputt wie mein Ansatz mit SetConsoleCtrlHandler

    Hier mein Quelltext:

    #include <signal.h>
    #include <cstdio>
    #include <iostream>
    #include <conio.h>
    #include <windows.h>
    
    using namespace std;
    
    bool stop = false;
    void signal_handler( int cause )
    {
       cout << "Anwendung beenden (J/N): ";
       signal( SIGINT, signal_handler );
    
       char ch = _getch();
       stop = (ch == 'j' || ch == 'J');
    }
    
    int _tmain( int argc, _TCHAR* argv[] )
    {
       signal( SIGINT, signal_handler );
       while( !stop )
       {
          ::Sleep( 100 );
       }
       return 0;
    }
    


  • Kann das nicht kompilieren mit dem:

    int _tmain( int argc, _TCHAR* argv[] )
    

    mit

    int main( int argc, char * argv[] )
    

    geht es.
    Was auffällt, dass ich nur nach jedem zweiten [Ctrl]+[C] die Anzeige "Anwendung beenden (J/N): " bekomme. dann ein 'j' und es bricht ordendlich ab. Wenn keine Anzeige nach dem [Ctrl]+[C] und es wird ein 'j' eingegeben, wird erst nach dem nächsten [Ctrl]+[C] ohne weitere Eingabe abgebrochen.
    Ansonsten habe ich kein auffälliges Verhalten festegestellt.
    Ich kann auf auf Dauer [Ctrl]+[C] drücken ohne das etwas unvorhergesehenes passiert; ich hab Dein Sleep gegen ein

    cout << ".";
    

    ausgetauscht um zu sehen ob was passiert.





  • @Helmut-Jakoby
    Merkwürdig. Welches OS benutzt du? Ich habe hier Windows 10 64Bit Professional.
    Hab das Spielprojekt grad noch mal frisch aufgesetzt, wenn ich 2x CTRL-C drücke erscheint für jeden CTRL-C-Tastendruch die Meldung, aber ich kann nichts eingeben und das Programm lässt sich nicht beenden.



  • Benutzt du denn die Eingabeaufforderung (CMD.exe) oder die Powershell?



  • @DocShoe
    Also ich nutze Windows 10 Home Version 20H2 (64 Bit). Visual Studio 2019 Version 16.10.0.

    Alle Einstellungen kann ich Dir jetzt nicht nennen, aber ggf. wichtig; ISO C++17/-Standard (/std:c++17).

    Ich kann folgende Applikation sowohl im Debug- und Release-Modus (CMD) laufen lassen.

    !Achtung! Ich sitze am Arbeitsplatz in der Firma und habe das Beispiel von meinem Laptop 'abgeschrieben'.
    Da ich hier keinen C++ Compiler habe, konnte ich es nicht testen; es können Schreibfehler vorhanden sein.
    Wenn es Probleme beim Übersetzen gibt, helfe ich gerne. Kann ggf. nicht sofort antworten da ja grad auf Arbeit.

    //Minimalbeispiel
    #include <csignal>
    #include <cstdio>
    #include <iostream>
    #include <conio.h>
    #include <algorithm>
    
    void signalCtrlC(int i)
    {
      signal( SIGINT, ::signalCtrlC );
    }
    int main( int argc, char * argv[] )
    {
      std::string sInput;
      
      signal( SIGINT, ::signalCtrlC );
      std::cout << "Programm beenden mit [Q]\n";
      do
      {
        do
        {
          sInput.clear();
          std::cout << "\nEingabe:";
          if ( std::cin.fail() || std::cin.eof() )
          {
            std::cin.clear();
            break;
          }
          std::cout << "[" << sInput << "]\n";
        } while ( sInput != "q" );
        {
          char cIO;
          
          std::cout << "Programm jetzt beenden druecke'y'";
          cIO = _getche();
          sInput = cIO;
          std::transform( sInput.begin(), sInput.end(), sInput.begin(), ::tolower );
          if ( sInput != "y" )
          {
            std::cout << "\nEs wurde nicht 'y' gedrueckt, es geht weiter.";
          }
        }
      } while ( sInput != "y" );
    return 0;
    }
    


  • Also,

    nachdem ich mich weiter damit beschäftigt habe sind mir ein paar Sachen klar geworden. Mit dem CTRL-C Handler lässt sich das nicht lösen und in der Handlerfunktion auf Benutzereingaben zu warten ist eine blöde Idee. Das Drücken von CTRL-C unterbricht auch die Eingaben über std::in, was in meinem Programm oben passiert ist Folgendes:

    1. CTRL-C drücken: Handler wird angesprungen und bleibt in Zeile 26 stehen, um die Benutzereingabe zu lesen
    2. CTRL-C drücken: Die Benutzereingabe wird abgebrochen und der Handler erneut angesprungen. Aus irgendwelchen Gründen kann der Benutzer jetzt aber nichts mehr eingeben.
    3. nächstes Drücken von CTRL-C: goto 2)

    Meine Lösung sieht jetzt so aus, dass ich die Konsole in den Raw-Modus versetze, in dem CTRL-C nicht mehr gefangen wird und auch keinen Handler mehr anspringt. Stattdessen landet das Event als 0x03 im Eingabestrom der Konsole. Den kann ich über ReadFile lesen und entsprechend reagieren. Dazu muss ich noch ein paar Konsolenflags bei Bedarf ein- und ausschalten, aber jetzt funktioniert´s genau so, wie ich es haben will:

    #include <cctype>
    #include <iostream>
    #include <windows.h>
    
    void enable_console_flag( HANDLE console, DWORD flag )
    {
       DWORD mode = 0;
       BOOL const r1 = ::GetConsoleMode( console, &mode );
       mode |= flag;
       BOOL const r2 = ::SetConsoleMode( console, mode );
    }
    
    void disable_console_flag( HANDLE console, DWORD flag )
    {
       DWORD mode = 0;
       BOOL const r1 = ::GetConsoleMode( console, &mode );
       mode &= ~flag;
       BOOL const r2 = ::SetConsoleMode( console, mode );
    }
    
    int main()
    {
       HANDLE console_in = ::GetStdHandle( STD_INPUT_HANDLE  );
       HANDLE console_out = ::GetStdHandle( STD_OUTPUT_HANDLE );
       disable_console_flag( console_in, ENABLE_PROCESSED_INPUT ); // Konsole in Raw Modus versetzen
       disable_console_flag( console_in, ENABLE_LINE_INPUT); // Eingaben direkt verarbeiten, ohne auf CR zu warten
    
       for( ;; )
       {
          char input_buffer[16];
          DWORD bytes_read = 0;
          ::ReadFile( console_in, input_buffer, sizeof( input_buffer ), &bytes_read, nullptr );
    
          // alle Eingaben außer CTRL-C verwerfen
          if( bytes_read == 1 && input_buffer[0] == 0x03 )
          {
             std::cout << "Anwendung beenden (J/N)? ";
    
             enable_console_flag( console_in, ENABLE_LINE_INPUT ); // Eingabe erst bei CR bearbeiten, aktiviert automatisch ECHO
             ::ReadFile( console_in, input_buffer, sizeof( input_buffer ), &bytes_read, nullptr );
             ::FlushConsoleInputBuffer( console_in );
             disable_console_flag( console_in, ENABLE_LINE_INPUT); // zurück zum direkten Eingabemodus
    
             std::cout << "\r\n";
             if( bytes_read == 2 )
             {
                // nur abbrechen, wenn die beiden Tasten 'j' und <CR> gedrückt wurden
                char const key1 = std::tolower( input_buffer[0] );
                char const key2 = input_buffer[1];
                if( key1 == 'j' && key2 == '\r' )
                {
                   break;
                }
             }
          }
       }
       return 0;
    }
    

    Danke für eure Hilfe.


Anmelden zum Antworten