Click here to Skip to main content
Click here to Skip to main content

Integrating autosave feature with the Windows Notepad application

, 21 Jul 2007 CPOL
Rate this:
Please Sign up or sign in to vote.
Illustration of Windows hooks for adding extra code in applications without the source available.

Introduction

This program demonstrates using Windows hooks for adding additional functionality to applications which do not have their source available.

How it works

The actual code for handling the autosave feature comes from a separate DLL. To include the DLL in the application, the executable file notepad.exe (shipped with Microsoft Windows) is modified. The import section of an executable contains the names of other DLLs it requires along with the functions required by Notepad from each DLL. The dumpbin utility (shipped with Microsoft Visual Studio) can be used to find these dependencies of an executable.

On my computer, I use the following command line:

C:\Program Files\Microsoft Visual Studio\VC98\Bin>DUMPBIN.EXE /imports 
                 c:\windows\system32\notepad.exe

and following is a section of the output I get on the screen:

- - - -

COMCTL32.dll
1001020 Import Address Table
10076EC Import Name Table
FFFFFFFF time date stamp
FFFFFFFF Index of first forwarder reference

773DD2ED 8 CreateStatusWindowW
----

Along with other DLLs, Notepad.exe requires COMCTL32.dll for a single function CreateStatusWindowW.

To include my own DLL (which is Autosave.dll), I replaced the name of the dependency (COMCTL32.dll) with Autosave.dll in the notepad.exe executable file. It can be easily done by modifying the exe file in a hex/binary editor. Note that the number of characters in the string "Autosave.dll" is the same as that in "Comctl32.dll" (12), so no issues. If the character length does not match, then we need to modify it by writing additional code, which is out of the scope for now.

Now when the user runs notepad.exe, the loader loads Autosave.dll also in the process address space.

However, the function CreateStatusWindowW is still required by Notepad (which resides in COMCTL32.dll). To solve this problem, I use the function forwarder approach. I export the function in my AutoSave.dll and tell the linker that this function is actually present in COMCTL32.dll.

This is achieved by using a single line in the DLL code:

#pragma comment(linker, "/export:CreateStatusWindowW=COMCTL32.CreateStatusWindowW")

The above line also loads COMCTL32.dll in the process address space and hence the dependencies are resolved.

Now since my DLL (autosave.dll) is finally loaded, I can add extra code in the process to add more features. I tried adding the autosave feature.

I use Windows hooks (WH_GETMESSAGE and WH_CALLWNDPROC) to do the job. The WH_GETMESSAGE hook is used to sniff (and can even modify) all the messages from the required thread's message queue. Similarly, the WH_CALLWNDPROC hook sniffs all the messages posted to the window procedure of the desired thread. Now since I can monitor and post messages to the Notepad window, the DLL starts a timer. Every time the timer expires, the DLL posts a WM_COMMAND message to the Notepad main window invoke File->Save menu option, thereby saving the content of the window.

Using the code

Following is a snapshot of the Autosave.dll source code.

#include <windows.h>
#include <stdio.h>

/* forward the function required by notepad.exe to the actual application */
#pragma comment(linker, "/export:CreateStatusWindowW=COMCTL32.CreateStatusWindowW")

#define AUTOSAVE_NOT_ASKED 1
#define AUTOSAVE_ACCEPTED 2
#define AUTOSAVE_DENIED 3

#define IDLE_TIMER_ID 0xdcba
/* any probably unique timer id value not used in notepad application */

#define IDLE_TIMER_INTERVAL (15 * 1000) /* 15 seconds */

#define WINDOW_CONTENTS_SAVED 1
#define WINDOW_CONTENTS_UNSAVED 2

/* global variables definitions */
HHOOK g_getmsg_hook = NULL;
/* Handle to hook for monitoring queued messages in the thread's queue */

HHOOK g_callwnd_hook = NULL;
/* Handle to Call window proc (for getting window handle only)*/

HWND g_hwnd = NULL; /* Handle to notepad main window */
unsigned char g_save_mode = AUTOSAVE_NOT_ASKED;
/* option selected by the user */

unsigned char g_window_contents_status = WINDOW_CONTENTS_SAVED;
/* current notepad window contents status */


/* Hook procedure to get initial messages */
LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK CallWndProc(int code, WPARAM wParam, LPARAM lParam);

/* DLL ENTRY POINT FUNCTION */
BOOL WINAPI DllMain(HINSTANCE hInst, DWORD dwReason, void *gbg_ptr)
{
    DWORD thread_id = 0;
    switch(dwReason)
    {
        case DLL_PROCESS_ATTACH: /* This is called only once at startup */
            /* Install the GetMessage and CallWndProc Hook into the application */
            thread_id = GetCurrentThreadId();
            g_getmsg_hook = SetWindowsHookEx(WH_GETMESSAGE, 
                                      GetMsgProc, hInst, thread_id);
            g_callwnd_hook = SetWindowsHookEx(WH_CALLWNDPROC, 
                                CallWndProc, hInst, thread_id);
            if (g_getmsg_hook == NULL)
            {
                MessageBox(NULL, TEXT("Unable to initialize the autosave feature"), 
                           TEXT("Error"), 0);
            }
            break;

        case DLL_PROCESS_DETACH: /* This is called only once at the time of termination */
            if (g_getmsg_hook != NULL) 
            { 
                UnhookWindowsHookEx(g_getmsg_hook);
            }
            break;
    }
    return TRUE;
}

/* Following routine gets called at startup to ask the user whether to turn the 
* autosave feature ON
*/
void AskForAutoSaveEnable()
{
    unsigned char users_reply;

    if (g_save_mode == AUTOSAVE_NOT_ASKED)
    {
        /* Ask the user at startup to enable the autosave feature */
        users_reply = MessageBox(NULL, TEXT(
          "Do you want to enable the automatic save on idle feature"), \
          TEXT("Program option"), MB_YESNO|MB_DEFBUTTON1);

        if (users_reply == IDYES)
        /* User accepted to turn the autosave feature ON */
        {
            g_save_mode = AUTOSAVE_ACCEPTED;
            SetTimer(g_hwnd, IDLE_TIMER_ID, IDLE_TIMER_INTERVAL, NULL); 
        }
        else /* User rejected it. Now no use of keeping the hook ON. Cleanup */
        {
            UnhookWindowsHookEx(g_getmsg_hook);
            g_getmsg_hook = NULL;
            g_save_mode = AUTOSAVE_DENIED;
        }
    }
    return;
}

/*
* Hook procedure to listen to keyboard inputs and timer messages.
*/
LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
    MSG *msg_str = NULL;
    static unsigned char control_key_down = FALSE;

    if (code < 0)
    {
        /* Action recommended by the Hooks functionality */
        return CallNextHookEx(g_getmsg_hook, code, wParam, lParam);
    }

    msg_str = (MSG *)lParam;

    /* User did some keyboard event. If idle timer is running, restart it */
    if (g_save_mode == AUTOSAVE_ACCEPTED && msg_str->message == WM_KEYDOWN)
    {
        KillTimer(g_hwnd, IDLE_TIMER_ID);
        SetTimer(g_hwnd, IDLE_TIMER_ID, IDLE_TIMER_INTERVAL, NULL);
    }

    /* User typed something, contents need to be saved again now */
    if (msg_str->message == WM_CHAR)
    {
        g_window_contents_status = WINDOW_CONTENTS_UNSAVED;
    }

    /*
    * Idle timer expired. Save the contents of the window if not
    * already saved
    */
    if (msg_str->hwnd == g_hwnd && msg_str->message == WM_TIMER && 
    msg_str->wParam == IDLE_TIMER_ID)
    {
        if (g_window_contents_status == WINDOW_CONTENTS_UNSAVED)
        {
            g_window_contents_status = WINDOW_CONTENTS_SAVED;
            PostMessage(g_hwnd, WM_COMMAND, MAKEWORD(3, 0), 0);
            /* Save menu option has ID 3 => Spy++ */ 
        }
        SetTimer(g_hwnd, IDLE_TIMER_ID, IDLE_TIMER_INTERVAL, NULL);
    }
    return CallNextHookEx(g_getmsg_hook, code, wParam, lParam);
}

/*
* Call window procedure hook function. The sole purpose of the function is
* just to get the handle of the notepad main window
*/
LRESULT CALLBACK CallWndProc(int code, WPARAM wParam, LPARAM lParam)
{
    CWPSTRUCT *msg_str = NULL;
    char window_class_name[50];

    if (code != HC_ACTION)
    {
        return CallNextHookEx(g_callwnd_hook, code, wParam, lParam);
    }

    msg_str = (CWPSTRUCT *)lParam;
    if (msg_str->message == WM_CREATE)
    {
        GetClassName(msg_str->hwnd, window_class_name, 
                     sizeof(window_class_name) - 1);
        if (strcmp(window_class_name, "Notepad") == 0)
        {
            g_hwnd = msg_str->hwnd;
            AskForAutoSaveEnable();
            UnhookWindowsHookEx(g_callwnd_hook);
        }
    }
    return CallNextHookEx(g_callwnd_hook, code, wParam, lParam);
}

Some tricky internals

I used both WH_GETMESSAGE and WH_CALLWNDPROC. I was better off with using just WH_GETMESSAGE to monitor the keyboard events which were all I needed (almost all). However, to get the handle of the main Notepad window, I listened on the WM_CREATE message (which is not a queued message, so is not available through WH_GETMESSAGE).

Also, the WM_CREATE message is received multiple times in the hook procedure, therefore I had to check on the class name to find out the message for the main window. (I retrieved the class name from Spy++, shipped with Microsoft Visual Studio, which is "Notepad").

Windows regenerates notepad.exe kept in the system32 folder if it finds it missing/modified. Therefore you store the modified notepad.exe in a separate folder along with AutoSave.dll.

One little thing I was unable to understand about notepad.exe was that I was not able to capture keyboard accelerator events in GetMessageProc. Therefore, I decided to leave the idea of monitoring the save command given by the user (which had an initial intention of avoiding saving the contents again unnecessarily if the user has already saved the contents before the timer expired the next time).

Final word and warning

The purpose of this article was just to illustrate a method for modifying applications which do not have their source available.

The application has not been tried on all platforms and a lot of complicated scenarios have not been tested. It is recommended to try this approach on executables which have known logic.

History and further...

First document.

Besides numerous improvements that can be made, one that I can think of is using some utility like resource hacker to add another menu option of autosave in the File menu of notepad.exe and letting the user decide at runtime when to start/stop autosave.

However, the same is also possible by writing additional code using Windows ImgHelper.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

manik singhal
Web Developer
United States United States
No Biography provided

Comments and Discussions

 
QuestionInfringement? PinmemberSharjith24-May-12 8:32 
Generalit doesn't work on windows 7 Pinmembergndnet17-Sep-10 7:19 
GeneralNo wonder windows crashes PinmemberJim Crafton23-Jul-07 5:39 
AnswerRe: No wonder windows crashes Pinmembermanik singhal23-Jul-07 7:36 
GeneralRe: No wonder windows crashes PinmemberJim Crafton23-Jul-07 7:44 
GeneralRe: No wonder windows crashes Pinmemberhamo200823-Jul-07 18:53 
GeneralRe: No wonder windows crashes PinmemberJim Crafton23-Jul-07 18:56 
GeneralRe: No wonder windows crashes Pinmemberhamo200824-Jul-07 6:45 
QuestionGreat Work! [modified] PinmemberCoolboy12321-Jul-07 18:36 
AnswerRe: Great Work! Pinmembermanik singhal23-Jul-07 7:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 21 Jul 2007
Article Copyright 2007 by manik singhal
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid