Win9xConHook.c   [plain text]


/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifdef WIN32

/*
 * Win9xConHook.dll - a hook proc to clean up Win95/98 console behavior.
 *
 * It is well(?) documented by Microsoft that the Win9x HandlerRoutine
 * hooked by the SetConsoleCtrlHandler never receives the CTRL_CLOSE_EVENT,
 * CTRL_LOGOFF_EVENT or CTRL_SHUTDOWN_EVENT signals.
 *
 * It is possible to have a second window to monitor the WM_ENDSESSION
 * message, but the close button still fails..
 *
 * There is a 16bit polling method for the close window option, but this
 * is CPU intensive and requires thunking.
 *
 * Attempts to subclass the 'tty' console fail, since that message thread
 * is actually owned by the 16 bit winoldap.mod process, although the
 * window reports it is owned by the process/thread of the console app.
 *
 * Win9xConHook is thunks the WM_CLOSE and WM_ENDSESSION messages,
 * first through a window hook procedure in the winoldap context, into
 * a subclass WndProc, and on to a second hidden monitor window in the
 * console application's context that dispatches them to the console app's
 * registered HandlerRoutine.
 */

/* This debugging define turns on output to COM1, although you better init
 * the port first (even using hyperterm).  It's the only way to catch the
 * goings on within system logoff/shutdown.
 * #define DBG 1
 */

#include <windows.h>

/* Variables used within any process context:
 *  hookwndmsg is a shared message to send Win9xConHook signals
 *  origwndprop is a wndprop atom to store the orig wndproc of the tty
 *  hookwndprop is a wndprop atom to store the hwnd of the hidden child
 *  is_service reminds us to unmark this process on the way out
 */
static UINT hookwndmsg = 0;
static LPCTSTR origwndprop;
static LPCTSTR hookwndprop;
static BOOL is_service = 0;
//static HMODULE hmodThis = NULL;

/* Variables used within the tty processes' context:
 *  is_tty flags this process;  -1 == unknown, 1 == if tty, 0 == if not
 *  hw_tty is the handle of the top level tty in this process context
 *  is_subclassed is toggled to assure DllMain removes the subclass on unload
 *  hmodLock is there to try and prevent this dll from being unloaded if the
 *           hook is removed while we are subclassed
 */
static int is_tty = -1;
static HWND hwtty = NULL;
static BOOL is_subclassed = 0;

// This simply causes a gpfault the moment it tries to FreeLibrary within
// the subclass procedure ... not good.
//static HMODULE hmodLock = NULL;

/* Variables used within the service or console app's context:
 *  hmodHook is the instance handle of this module for registering the hooks
 *  hhkGetMessage is the hook handle for catching Posted messages
 *  hhkGetMessage is the hook handle for catching Sent messages
 *  monitor_hwnd is the invisible window that handles our tty messages
 *  the tty_info strucure is used to pass args into the hidden window's thread
 */
static HMODULE hmodHook = NULL;
static HHOOK hhkGetMessage;
//static HHOOK hhkCallWndProc;
static HWND monitor_hwnd = NULL;

typedef struct {
    PHANDLER_ROUTINE phandler;
    HINSTANCE instance;
    HWND parent;
    INT type;
    LPCSTR name;
} tty_info;

/* These are the GetWindowLong offsets for the hidden window's internal info
 *  gwltty_phandler is the address of the app's HandlerRoutine
 *  gwltty_ttywnd is the tty this hidden window will handle messages from
 */
#define gwltty_phandler 0
#define gwltty_ttywnd 4

/* Forward declaration prototypes for internal functions
 */
static BOOL CALLBACK EnumttyWindow(HWND wnd, LPARAM retwnd);
static LRESULT WINAPI RegisterWindows9xService(BOOL set_service);
static LRESULT CALLBACK ttyConsoleCtrlWndProc(HWND hwnd, UINT msg,
                                              WPARAM wParam, LPARAM lParam);
static DWORD WINAPI ttyConsoleCtrlThread(LPVOID tty);
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg,
                                WPARAM wParam, LPARAM lParam);
static int HookProc(int hc, HWND *hwnd, UINT *msg,
                    WPARAM *wParam, LPARAM *lParam);
#ifdef DBG
static VOID DbgPrintf(LPTSTR fmt, ...);
#endif


/* DllMain is invoked by every process in the entire system that is hooked
 * by our window hooks, notably the tty processes' context, and by the user
 * who wants tty messages (the app).  Keep it light and simple.
 */
BOOL __declspec(dllexport) APIENTRY DllMain(HINSTANCE hModule, ULONG ulReason,
                                            LPVOID pctx)
{
    if (ulReason == DLL_PROCESS_ATTACH)
    {
        //hmodThis = hModule;
        if (!hookwndmsg) {
            origwndprop = MAKEINTATOM(GlobalAddAtom("Win9xConHookOrigProc"));
            hookwndprop = MAKEINTATOM(GlobalAddAtom("Win9xConHookThunkWnd"));
            hookwndmsg = RegisterWindowMessage("Win9xConHookMsg");
        }
#ifdef DBG
//        DbgPrintf("H ProcessAttach:%8.8x\r\n",
//                  GetCurrentProcessId());
#endif
    }
    else if ( ulReason == DLL_PROCESS_DETACH )
    {
#ifdef DBG
//        DbgPrintf("H ProcessDetach:%8.8x\r\n", GetCurrentProcessId());
#endif
        if (monitor_hwnd)
            SendMessage(monitor_hwnd, WM_DESTROY, 0, 0);
        if (is_subclassed)
            SendMessage(hwtty, hookwndmsg, 0, (LPARAM)hwtty);
        if (hmodHook)
        {
            if (hhkGetMessage) {
                UnhookWindowsHookEx(hhkGetMessage);
                hhkGetMessage = NULL;
            }
            //if (hhkCallWndProc) {
            //    UnhookWindowsHookEx(hhkCallWndProc);
            //    hhkCallWndProc = NULL;
            //}
            FreeLibrary(hmodHook);
            hmodHook = NULL;
        }
        if (is_service)
            RegisterWindows9xService(FALSE);
        if (hookwndmsg) {
            GlobalDeleteAtom((ATOM)origwndprop);
            GlobalDeleteAtom((ATOM)hookwndprop);
            hookwndmsg = 0;
        }
    }
    return TRUE;
}


/*  This group of functions are provided for the service/console app
 *  to register itself a HandlerRoutine to accept tty or service messages
 */


/*  Exported function that creates a Win9x 'service' via a hidden window,
 *  that notifies the process via the HandlerRoutine messages.
 */
BOOL __declspec(dllexport) WINAPI Windows9xServiceCtrlHandler(
        PHANDLER_ROUTINE phandler,
        LPCSTR name)
{
    /* If we have not yet done so */
    FreeConsole();

    if (name)
    {
        DWORD tid;
        HANDLE hThread;
        /* NOTE: this is static so the module can continue to
         * access these args while we go on to other things
         */
        static tty_info tty;
        tty.instance = GetModuleHandle(NULL);
        tty.phandler = phandler;
        tty.parent = NULL;
        tty.name = name;
        tty.type = 2;
        RegisterWindows9xService(TRUE);
        hThread = CreateThread(NULL, 0, ttyConsoleCtrlThread,
                               (LPVOID)&tty, 0, &tid);
        if (hThread)
        {
            CloseHandle(hThread);
            return TRUE;
        }
    }
    else /* remove */
    {
        if (monitor_hwnd)
            SendMessage(monitor_hwnd, WM_DESTROY, 0, 0);
        RegisterWindows9xService(FALSE);
        return TRUE;
    }
    return FALSE;
}


/*  Exported function that registers a HandlerRoutine to accept missing
 *  Win9x CTRL_EVENTs from the tty window, as NT does without a hassle.
 *  If add is 1 or 2, register the handler, if 2 also mark it as a service.
 *  If add is 0 deregister the handler, and unmark if a service
 */
BOOL __declspec(dllexport) WINAPI FixConsoleCtrlHandler(
        PHANDLER_ROUTINE phandler,
        INT add)
{
    HWND parent;

    if (add)
    {
        HANDLE hThread;
        DWORD tid;
        /* NOTE: this is static so the module can continue to
         * access these args while we go on to other things
         */
        static tty_info tty;
        EnumWindows(EnumttyWindow, (LPARAM)&parent);
        if (!parent) {
#ifdef DBG
            DbgPrintf("A EnumttyWindow failed (%d)\r\n", GetLastError());
#endif
            return FALSE;
        }
        tty.instance = GetModuleHandle(NULL);
        tty.phandler = phandler;
        tty.parent = parent;
        tty.type = add;
        if (add == 2) {
            tty.name = "ttyService";
            RegisterWindows9xService(TRUE);
        }
        else
            tty.name = "ttyMonitor";
        hThread = CreateThread(NULL, 0, ttyConsoleCtrlThread,
                               (LPVOID)&tty, 0, &tid);
        if (!hThread)
            return FALSE;
        CloseHandle(hThread);
        hmodHook = LoadLibrary("Win9xConHook.dll");
        if (hmodHook)
        {
            hhkGetMessage = SetWindowsHookEx(WH_GETMESSAGE,
              (HOOKPROC)GetProcAddress(hmodHook, "GetMsgProc"), hmodHook, 0);
            //hhkCallWndProc = SetWindowsHookEx(WH_CALLWNDPROC,
            //  (HOOKPROC)GetProcAddress(hmodHook, "CallWndProc"), hmodHook, 0);
        }
        return TRUE;
    }
    else /* remove */
    {
        if (monitor_hwnd) {
            SendMessage(monitor_hwnd, WM_DESTROY, 0, 0);
        }
        if (hmodHook)
        {
            if (hhkGetMessage) {
                UnhookWindowsHookEx(hhkGetMessage);
                hhkGetMessage = NULL;
            }
            //if (hhkCallWndProc) {
            //    UnhookWindowsHookEx(hhkCallWndProc);
            //    hhkCallWndProc = NULL;
            //}
            FreeLibrary(hmodHook);
            hmodHook = NULL;
        }
        if (is_service)
            RegisterWindows9xService(FALSE);
        return TRUE;
    }
    return FALSE;
}


/*  The following internal helpers are only used within the app's context
 */

/* ttyConsoleCreateThread is the process that runs within the user app's
 * context.  It creates and pumps the messages of a hidden monitor window,
 * watching for messages from the system, or the associated subclassed tty
 * window.  Things can happen in our context that can't be done from the
 * tty's context, and visa versa, so the subclass procedure and this hidden
 * window work together to make it all happen.
 */
static DWORD WINAPI ttyConsoleCtrlThread(LPVOID tty)
{
    WNDCLASS wc;
    MSG msg;
    wc.style         = CS_GLOBALCLASS;
    wc.lpfnWndProc   = ttyConsoleCtrlWndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 8;
    wc.hInstance     = NULL;
    wc.hIcon         = NULL;
    wc.hCursor       = NULL;
    wc.hbrBackground = NULL;
    wc.lpszMenuName  = NULL;
    if (((tty_info*)tty)->parent)
        wc.lpszClassName = "ttyConHookChild";
    else
        wc.lpszClassName = "ApacheWin95ServiceMonitor";

    if (!RegisterClass(&wc)) {
#ifdef DBG
        DbgPrintf("A proc %8.8x Error creating class %s (%d)\r\n",
                  GetCurrentProcessId(), wc.lpszClassName, GetLastError());
#endif
        return 0;
    }

    /* Create an invisible window */
    monitor_hwnd = CreateWindow(wc.lpszClassName, ((tty_info*)tty)->name,
                                WS_OVERLAPPED & ~WS_VISIBLE,
                                CW_USEDEFAULT, CW_USEDEFAULT,
                                CW_USEDEFAULT, CW_USEDEFAULT,
                                NULL, NULL,
                                ((tty_info*)tty)->instance, tty);

    if (!monitor_hwnd) {
#ifdef DBG
        DbgPrintf("A proc %8.8x Error creating window %s %s (%d)\r\n",
                  GetCurrentProcessId(), wc.lpszClassName,
                  ((tty_info*)tty)->name, GetLastError());
#endif
        return 0;
    }

    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    /* Tag again as deleted, just in case we missed WM_DESTROY */
    monitor_hwnd = NULL;
    return 0;
}


/* This is the WndProc procedure for our invisible window.
 * When our subclasssed tty window receives the WM_CLOSE, WM_ENDSESSION,
 * or WM_QUERYENDSESSION messages, the message is dispatched to our hidden
 * window (this message process), and we call the installed HandlerRoutine
 * that was registered by the app.
 */
static LRESULT CALLBACK ttyConsoleCtrlWndProc(HWND hwnd, UINT msg,
                                              WPARAM wParam, LPARAM lParam)
{
    if (msg == WM_CREATE)
    {
        tty_info *tty = (tty_info*)(((LPCREATESTRUCT)lParam)->lpCreateParams);
        SetWindowLong(hwnd, gwltty_phandler, (LONG)tty->phandler);
        SetWindowLong(hwnd, gwltty_ttywnd, (LONG)tty->parent);
#ifdef DBG
        DbgPrintf("A proc %8.8x created %8.8x %s for tty wnd %8.8x\r\n",
                  GetCurrentProcessId(), hwnd,
                  tty->name, tty->parent);
#endif
        if (tty->parent) {
            SetProp(tty->parent, hookwndprop, hwnd);
            PostMessage(tty->parent, hookwndmsg,
                        tty->type, (LPARAM)tty->parent);
        }
        return 0;
    }
    else if (msg == WM_DESTROY)
    {
        HWND parent = (HWND)GetWindowLong(hwnd, gwltty_ttywnd);
#ifdef DBG
        DbgPrintf("A proc %8.8x destroyed %8.8x ttyConHookChild\r\n",
                  GetCurrentProcessId(), hwnd);
#endif
        if (parent) {
            RemoveProp(parent, hookwndprop);
            SendMessage(parent, hookwndmsg, 0, (LPARAM)parent);
        }
        monitor_hwnd = NULL;
    }
    else if (msg == WM_CLOSE)
    {
        PHANDLER_ROUTINE phandler =
            (PHANDLER_ROUTINE)GetWindowLong(hwnd, gwltty_phandler);
        LRESULT rv = phandler(CTRL_CLOSE_EVENT);
#ifdef DBG
        DbgPrintf("A proc %8.8x invoked CTRL_CLOSE_EVENT "
                  "returning %d\r\n",
                  GetCurrentProcessId(), rv);
#endif
        if (rv)
            return !rv;
    }
    else if ((msg == WM_QUERYENDSESSION) || (msg == WM_ENDSESSION))
    {
        if (lParam & ENDSESSION_LOGOFF)
        {
            PHANDLER_ROUTINE phandler =
                (PHANDLER_ROUTINE)GetWindowLong(hwnd, gwltty_phandler);
            LRESULT rv = phandler(CTRL_LOGOFF_EVENT);
#ifdef DBG
            DbgPrintf("A proc %8.8x invoked CTRL_LOGOFF_EVENT "
                      "returning %d\r\n",
                      GetCurrentProcessId(), rv);
#endif
            if (rv)
                return ((msg == WM_QUERYENDSESSION) ? rv : !rv);
        }
        else
        {
            PHANDLER_ROUTINE phandler =
                (PHANDLER_ROUTINE)GetWindowLong(hwnd, gwltty_phandler);
            LRESULT rv = phandler(CTRL_SHUTDOWN_EVENT);
#ifdef DBG
            DbgPrintf("A proc %8.8x invoked CTRL_SHUTDOWN_EVENT "
                      "returning %d\r\n", GetCurrentProcessId(), rv);
#endif
            if (rv)
                return ((msg == WM_QUERYENDSESSION) ? rv : !rv);
        }
    }
    return (DefWindowProc(hwnd, msg, wParam, lParam));
}


/*  The following internal helpers are invoked by the hooked tty and our app
 */


/*  Register or deregister the current process as a Windows9x style service.
 *  Experience shows this call is ignored across processes, so the second
 *  arg to RegisterServiceProcess (process group id) is effectively useless.
 */
static LRESULT WINAPI RegisterWindows9xService(BOOL set_service)
{
    static HINSTANCE hkernel;
    static DWORD (WINAPI *register_service_process)(DWORD, DWORD) = NULL;
    BOOL rv;

    if (set_service == is_service)
        return 1;

#ifdef DBG
    DbgPrintf("R %s proc %8.8x as a service\r\n",
              set_service ? "installing" : "removing",
              GetCurrentProcessId());
#endif

    if (!register_service_process)
    {
        /* Obtain a handle to the kernel library */
        hkernel = LoadLibrary("KERNEL32.DLL");
        if (!hkernel)
            return 0;

        /* Find the RegisterServiceProcess function */
        register_service_process = (DWORD (WINAPI *)(DWORD, DWORD))
                         GetProcAddress(hkernel, "RegisterServiceProcess");
        if (register_service_process == NULL) {
            FreeLibrary(hkernel);
            return 0;
        }
    }

    /* Register this process as a service */
    rv = register_service_process(0, set_service != FALSE);
    if (rv)
        is_service = set_service;

    if (!is_service)
    {
        /* Unload the kernel library */
        FreeLibrary(hkernel);
        register_service_process = NULL;
    }
    return rv;
}


/*
 * This function only works when this process is the active process
 * (e.g. once it is running a child process, it can no longer determine
 * which console window is its own.)
 */
static BOOL CALLBACK EnumttyWindow(HWND wnd, LPARAM retwnd)
{
    char tmp[8];
    if (GetClassName(wnd, tmp, sizeof(tmp)) && !strcmp(tmp, "tty"))
    {
        DWORD wndproc, thisproc = GetCurrentProcessId();
        GetWindowThreadProcessId(wnd, &wndproc);
        if (wndproc == thisproc) {
            *((HWND*)retwnd) = wnd;
            return FALSE;
        }
    }
    return TRUE;
}


/* The remaining code all executes --in the tty's own process context--
 *
 * That means special attention must be paid to what it's doing...
 */

/* Subclass message process for the tty window
 *
 * This code -handles- WM_CLOSE, WM_ENDSESSION and WM_QUERYENDSESSION
 * by dispatching them to the window identified by the hookwndprop
 * property atom set against our window.  Messages are then dispatched
 * to origwndprop property atom we set against the window when we
 * injected this subclass.  This trick did not work with simply a hook.
 */
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg,
                                WPARAM wParam, LPARAM lParam)
{
    WNDPROC origproc = (WNDPROC) GetProp(hwnd, origwndprop);
    if (!origproc)
        return 0;

    if (msg == WM_NCDESTROY
        || (msg == hookwndmsg && !LOWORD(wParam) && (HWND)lParam == hwnd))
    {
        if (is_subclassed) {
#ifdef DBG
            DbgPrintf("W proc %08x hwnd:%08x Subclass removed\r\n",
                      GetCurrentProcessId(), hwnd);
#endif
            if (is_service)
                RegisterWindows9xService(FALSE);
            SetWindowLong(hwnd, GWL_WNDPROC, (LONG)origproc);
            RemoveProp(hwnd, origwndprop);
            RemoveProp(hwnd, hookwndprop);
            is_subclassed = FALSE;
            //if (hmodLock)
            //    FreeLibrary(hmodLock);
            //hmodLock = NULL;
        }
    }
    else if (msg == WM_CLOSE || msg == WM_ENDSESSION
                             || msg == WM_QUERYENDSESSION)
    {
        HWND child = (HWND)GetProp(hwnd, hookwndprop);
        if (child) {
#ifdef DBG
            DbgPrintf("W proc %08x hwnd:%08x forwarded msg:%d\r\n",
                      GetCurrentProcessId(), hwnd, msg);
#endif
            return SendMessage(child, msg, wParam, lParam);
        }
    }
    return CallWindowProc(origproc, hwnd, msg, wParam, lParam);
}


/* HookProc, once installed, is responsible for subclassing the system
 * tty windows.  It generally does nothing special itself, since
 * research indicates that it cannot deal well with the messages we are
 * interested in, that is, WM_CLOSE, WM_QUERYSHUTDOWN and WM_SHUTDOWN
 * of the tty process.
 *
 * Respond and subclass only when a WM_NULL is received by the window.
 */
int HookProc(int hc, HWND *hwnd, UINT *msg, WPARAM *wParam, LPARAM *lParam)
{
    if (is_tty == -1 && *hwnd)
    {
        char ttybuf[8];
        HWND htty;
        hwtty = *hwnd;
        while (htty = GetParent(hwtty))
            hwtty = htty;
        is_tty = (GetClassName(hwtty, ttybuf, sizeof(ttybuf))
                  && !strcmp(ttybuf, "tty"));
#ifdef DBG
        if (is_tty)
            DbgPrintf("H proc %08x tracking hwnd %08x\r\n",
                      GetCurrentProcessId(), hwtty);
#endif
    }

    if (*msg == hookwndmsg && *wParam && *lParam == (LPARAM)hwtty && is_tty)
    {
        WNDPROC origproc = (WNDPROC)GetWindowLong(hwtty, GWL_WNDPROC);
        //char myname[MAX_PATH];
        //if (GetModuleFileName(hmodThis, myname, sizeof(myname)))
        //    hmodLock = LoadLibrary(myname);
        SetProp(hwtty, origwndprop, origproc);
        SetWindowLong(hwtty, GWL_WNDPROC, (LONG)WndProc);
        is_subclassed = TRUE;
#ifdef DBG
        DbgPrintf("H proc %08x hwnd:%08x Subclassed\r\n",
                  GetCurrentProcessId(), hwtty);
#endif
        if (LOWORD(*wParam) == 2)
            RegisterWindows9xService(TRUE);
    }

    return -1;
}


/*
 * PostMessage Hook:
 */
LRESULT __declspec(dllexport) CALLBACK GetMsgProc(INT hc, WPARAM wParam,
                                                  LPARAM lParam)
{
    PMSG pmsg;

    pmsg = (PMSG)lParam;

    if (pmsg) {
        int rv = HookProc(hc, &pmsg->hwnd, &pmsg->message,
                          &pmsg->wParam, &pmsg->lParam);
        if (rv != -1)
            return rv;
    }
    /*
     * CallNextHookEx apparently ignores the hhook argument, so pass NULL
     */
    return CallNextHookEx(NULL, hc, wParam, lParam);
}


/*
 * SendMessage Hook:
 */
LRESULT __declspec(dllexport) CALLBACK CallWndProc(INT hc, WPARAM wParam,
                                                   LPARAM lParam)
{
    PCWPSTRUCT pcwps = (PCWPSTRUCT)lParam;

    if (pcwps) {
        int rv = HookProc(hc, &pcwps->hwnd, &pcwps->message,
                          &pcwps->wParam, &pcwps->lParam);
        if (rv != -1)
            return rv;
    }
    /*
     * CallNextHookEx apparently ignores the hhook argument, so pass NULL
     */
    return CallNextHookEx(NULL, hc, wParam, lParam);
}


#ifdef DBG
VOID DbgPrintf(
    LPTSTR fmt,
    ...
    )
{
    static HANDLE mutex;
    va_list marker;
    TCHAR szBuf[256];
    DWORD t;
    HANDLE gDbgOut;

    va_start(marker, fmt);
    wvsprintf(szBuf, fmt, marker);
    va_end(marker);

    if (!mutex)
        mutex = CreateMutex(NULL, FALSE, "Win9xConHookDbgOut");
    WaitForSingleObject(mutex, INFINITE);
    gDbgOut = CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0,
                         NULL, OPEN_EXISTING, FILE_FLAG_WRITE_THROUGH, NULL);
    WriteFile(gDbgOut, szBuf, strlen(szBuf), &t, NULL);
    CloseHandle(gDbgOut);
    ReleaseMutex(mutex);
}
#endif

#endif /* WIN32 */