Monday, December 2, 2013

Win32 Console Issues

Console Shutdown Issues

When using a console with your Windows application, it is possible for the user to close the console window using the system menu or clicking the 'x' button in the upper right corner of the console frame.  Doing this will cause the Windows application to terminate.  As far as I can tell, this functions like an application abort and does not give you the opportunity to clean up or gracefully shut down your application.  Worse yet, terminating the application may not be what the user expects. No warning is given or chance to cancel the impending shutdown.

The CtrlHandler Function

Windows does provide a way to hook a control handler chain which theoretically allows you to detect and act upon the close, ctrl-c, ctrl-break, and logoff events.  Unfortunately, there is no way to prevent the events.  Your event handling function can be registered when the Console object init function is run.

SetConsoleCtrlHandler( (PHANDLER_ROUTINE) CtrlHandler, TRUE );

The event processing function is declared as a static member of the Console object.

static bool CtrlHandler(DWORD fdwCtrlType);

Finally the event processing function is coded as part of the Console definition in Console.cpp.

bool Console::CtrlHandler(DWORD fdwCtrlType)
{
switch(fdwCtrlType) {
case CTRL_CLOSE_EVENT:
//
// do some cleanup
//
return false;
case CTRL_SHUTDOWN_EVENT:
case CTRL_BREAK_EVENT:
case CTRL_LOGOFF_EVENT:
return false;

default:
return false;
}
}

Since the event handler is coded as a static member, it cannot access any non-static function or member unless it first acquires an instance of the console object.  This is a limitation of how the Console object is currently coded.  This is not a problem if a static member is created which is a pointer to the current Console instance.

Console.h

class Console
{
    ...

public:
    static Console* m_pCons;
};

You must create an instance of m_pCons which can then be initialized in the console init function.

Console * m_pCons = 0;

bool Console::init()
{
   m_pcons = this;

   ...
}

Now inside the event handler you can use the Console instance pointer m_pCons to access any of the non-static members of the console object.

Doing this makes m_pCons a global pointer accessible anywhere in the code.  A better way to do this would be to create the console object as a singleton.  Use of the singleton pattern is not without its fair share of controversy in the C++ community. Personally, I see use of singletons as a benefit for amateur or hobbyist type applications, although in the long run, it is probably better to learn from the pros and emulate their methods.

Preventing the Close

Creating the event handler, really accomplishes very little in my opinion.  After you do what you can do accessing the various functions and methods of the Console object or any other global objects, the application will still terminate.  There seems to be no practical way to avoid it once it gets that far.  An alternative would be to prevent the user from accessing the close function in the first place.  As it turns out this is very simple to do.  Simply get the console window handle and use it to get the handle of the system menu.  Then delete the "close" menu.  This eliminates the close selection from the system menu and disables the 'x' button, preventing the user from inadvertently shutting down the application.

bool Console::init()
{
   ...

   if (!AllocConsole())
       return false;

   ...

    m_hCons = GetConsoleWindow();
    if (m_hCons <= 0)
        return false;
    HMENU hmenu = GetSystemMenu(m_hCons, FALSE);
    DeleteMenu(hmenu, SC_CLOSE, MF_BYCOMMAND);

    return true;
}

This works quite well at eliminating the accidental 'close' but it will not prevent the user from shutting down the application using a log off or other kind of termination.  That's okay because it is probably not a good idea to take all control away from the user.

A Better Way?

Considering the programmer has limited control over the console window without hooking deeply into its messaging system, the programmer must understand the vulnerabilities of using a console window and weigh the advantages with the disadvantages.  The console window is a ready-made device built to handle character-mode streams with minimal code.  On the other hand it does provide a way for the user to abort the application with almost no way to prevent it.  Disabling the close menu does provide a method to prevent an inadvertent termination of the application.  If the goal is to use the console window as a logging and messaging device, it makes sense to create it once when the application is started and not shut it down until the application has finished running.  With the console's window handle, it is easy to hide or show the console when necessary from your application without freeing it using the FreeConsole method.

A safer way, perhaps, to deal with logging and messaging is to create your own window designed to display logging text.  Since a window is a graphical device rather than a character-mode device, much more can be done, including adding controls like buttons and checkboxes.  Of course, then the console window is essentially a dialog box but you will have full control over how it is created and when it is destroyed.  I will no doubt revisit this topic in the future.

No comments:

Post a Comment