Creating and Installing Message Handlers

Message Handler Overview, Configuring Message Handlers, Message Server Overview

This help section will explain pretty much everything you need to know to get a message handler up and running. The discussions below assume that you have a solid background with D/COM and accordingly don’t cover basic D/COM issues. Note that only Enterprise edition servers support the usage of message handlers.

The IPBToMH interface

The IPBToMH interface is defined in the PBToMH.idl and PBToMH.h files as follows:

typedef struct tagPBLOG_MESSAGE
    {
        DWORDLONG    dwlTime;
        SHORT    sGMTOffset;
        ULONG    ulMsgType;
        DWORD    dwPID;
        DWORD    dwTID;
    [string]    const WCHAR*    pszComponent;
    [string]    const WCHAR*    pszContext;
    [string]    const WCHAR*    pszMachine;
    [string]    const WCHAR*    pszProcess;
    [string]    const WCHAR*    pszModule;
    [string]    const WCHAR*    pszFile;
        ULONG    ulLine;
    [string]    const WCHAR*    pszMsg;
    } PBLOG_MESSAGE;

IPBToMH : public IUnknown
    {
    public:
    HRESULT OnInitialize(const WCHAR* pszName, const WCHAR* pszInitString);
    HRESULT OnReceive(USHORT* pusNumLogMessages, const PBLOG_MESSAGE plms[]);
    };

The elements of the PBLOG_MESSAGE structure correspond to the log message fields with the following exception. The dwlTime member represents the Coordinated Universal Time (UTC or GMT) for the message, expressed as the number of 100 nanosecond intervals elapsed since January 1, 1601 (the same definition as the Win32 FILETIME structure). The sGMTOffset member indirectly represents the source time field. This short integer contains the number of minutes the source machine’s time zone differs from GMT. Thus to get the source time field, add this number of minutes to the GMT time stamp. The local time field is obtained by correcting the dwlTime member to the time zone of the local machine. The ulMsgType member has been pre-validated and will always be equal to one of the predefined message types or a valid user type. (Valid user types are in the range 0x10000 – 0xFFFF0000 and always have the low word zero.) The string fields may be empty strings (L””) but will never be NULL.

The OnInitialize method is used to support the design and implementation of generically reusable message handlers that configure themselves dynamically as a function of initialization parameters. For instance, if the intent is to write a message handler to redirect log messages to a database then it would be nice to be able to write a fairly generic one that took the ODBC connect strings, table names, and such (or the name of an ini file or registry key containing same) in on the OnInitialize call and operated accordingly. As with the string fields of the log message structure, the pszName and pszInitString parameters may be empty strings (L””) but will never be NULL.

OnReceive is called when there are log messages to be passed to the message handler. If the message handler is flagged at install time to receive all the existing log messages currently in the message server then it will be called with these before any new messages. In all cases the list of potential log messages to be delivered is evaluated against the list of message keys and the FILTER that the message handler is configured with in order to determine which subset is allowed to be passed on the OnReceive call. If this subset contains no log messages then OnReceive is not called – that is, OnReceive is never called with *pusNumLogMessages == 0.

Normally a message handler would process all messages given to it on each OnReceive call, but this is not necessary. pusNumLogMessages is an [in, out] parameter, and on return specifies the number of log messages that were processed. This allows a message handler to temporarily discontinue processing in the middle of an OnReceive call and later pick up where it left off. The message server will keep track of the last message processed so that a message handler will always get each message once in sequence. Since the message server may need to shut down in a relatively short period of time, it is recommended that a message handler return from each call in a timely fashion. If lengthy processing of messages is required, the message handler should decide whether to copy all the messages, return, and continue processing, or just process a subset of the messages passed on a particular OnReceive call, setting *pusNumLogMessages to the number processed before returning. Note that returning with *pusNumLogMessages == 0 is considered an error and will result in unloading of the message handler. This is necessary because the message server has a limited amount of resources for caching messages. Since the message server guarantees that all messages will be delivered to each loaded message handler, the messages cannot be deleted until they have been processed by everyone. If for some reason a message handler (or forwarder or connected viewer) cannot process messages, it will be immediately taken out of the routing. The retry schedule will then be applied. When the message handler is next loaded, message processing will pick up where it left off, subject to messages rolling off the cache. If a message handler misses messages because they have been deleted while it was unloaded, internal notification number 1019 will be generated.

Calling sequence and threading issues

When the message server starts up it completes all of its initialization and then proceeds on to load and initialize the message handlers. The current implementation runs each message handler on its own thread but regardless of implementation changes to internal thread management, it is guaranteed that a particular object’s OnInitialize and OnReceive method will be called in the proper order and further that a given object’s OnReceive method will never be called simultaneously from multiple threads. This means that message handlers should always be coded as free threaded for optimal performance of the server and handler since there is no risk of a handler’s methods being called concurrently.

Once the message server has an open interface on the message handler object, it calls OnInitialize with the initialization string specified in its configuration settings. After the successful completion of initialization, the server checks to see if there are any log messages that should be passed on to the handler. There are a number of scenarios where there could be log messages already waiting. There may be device drivers or other NT services that log messages before the message server even gets started. (Those messages will simply sit in the IPC buffer until the server starts up and comes around to get them.) There may be messages generated because of shutdown errors, which couldn’t be passed on until the subsequent restart. Previous operations with the handler may have encountered an error that caused it to be aborted by the server until the current restart. Or, most commonly, the handler was just installed and was configured to receive all existing messages rather than to just start with ones that arrive subsequent to the install. In any case, once the initialization process completes, the handler thread drops into a dormant state until new log messages come in to be dispatched.

Error handling

The current implementation of the message server treats all error returns on all COM calls as fatal. That is, if any of the COM calls in the object creation (e.g. CoInitializeEx and CoCreateInstanceEx) or on the IPBToMH interface (OnInitialize and OnReceive) ever return anything other than S_OK then that particular message handler will be unloaded. Unless the failure is unrecoverable (e.g. CoInitalizeEx fails or a valid CLSID cannot be obtained), the message handler will be reloaded according to the retry schedule specified in the configuration of the message server. A return of S_FALSE from OnInitialize or OnReceive will be treated as a request to be unloaded without any error logging. A return of E_FAIL will also result in unloading, but an error message will be logged as specified by the retry schedule. On return from OnReceive, an invalid value for the pusNumLogMessages parameter (equal to zero or greater than the value passed in) will also be treated as an error. Further, if OnReceive returns anything other than S_OK, S_FALSE, or E_FAIL, the value of the return parameter will be assumed to be zero, that is any messages presented on the current call will be re-presented the next time the message handler is loaded.

Another aspect of error handling also comes into play when the message handler is called by the server. When the server is about to call a handler, it sets a flag in the registry so that if a message handler has a bug in it that takes out the server the offender can be hopefully identified and ratted on appropriately. The calls are also wrapped with exception handling code but note that when there’s an in-proc message handler in the game all bets are off. If it’s in the message server’s process space it can break anything and in ways that may not be detected for days. The state flags and exception handling only help to identify and isolate things – they cannot be relied upon as definitive gospel. If in-proc message handler A walks over the top of the interface pointer for message handler B and does no other detectable damage then A is going to come out looking innocent while the call to B crashes with all the state flags and exception handling identifying B as the culprit.

Miscellaneous development and deployment issues

Creating a message handler

Implementing a message handler involves the following steps:

1. Create the IDL code.

2. Implement the interface in code.

3. Create registry entries to instruct the message server to load the message handler.

These steps are further outlined below. These instructions are based on the simplest way to use the Microsoft Developer Studio wizards to create a message handler. COM experts are welcome to suggest other methods.

Creating the IDL code

1. Generate a new project using the ATL COM AppWizard. Note the following:

2. Add a new, simple, ATL object to the project.

3. Make the following changes to the generated IDL file:

Implementing the interface in code

1. Add the following code to the class definition that the wizard created. If the object is named “MyObj” the CMyObj class will be defined in MyObj.h.

public:

    virtual HRESULT STDMETHODCALLTYPE OnInitialize( /* [string][in] */ const WCHAR __RPC_FAR *pszName, /* [string][in] */ const WCHAR __RPC_FAR *pszInitString);
    virtual HRESULT STDMETHODCALLTYPE OnReceive( /* [out][in] */ USHORT __RPC_FAR *pusNumLogMessages, /* [size_is][in] */ const PBLOG_MESSAGE __RPC_FAR plms[  ]);

2. Add the following code to the class implementation file (e.g. CMyObj.cpp). Note the placeholder class name “CMyObj.”

    #include <PBToMH_i.c>
    HRESULT STDMETHODCALLTYPE CMyObj::OnInitialize( /* [string][in] */ const WCHAR __RPC_FAR *pszName, /* [string][in] */ const WCHAR __RPC_FAR *pszInitString)
    {
    // Process initialization args here.
    return S_OK;
    }

    HRESULT STDMETHODCALLTYPE CMyObj::OnReceive( /* [out][in] */ USHORT __RPC_FAR *pusNumLogMessages, /* [size_is][in] */ const PBLOG_MESSAGE __RPC_FAR plms[  ])
    {
    // Handle messages here.
    return S_OK;
    }

3. If the message handler uses MFC, add the AFX_MANAGE_STATE macro with the appropriate argument (AfxGetStaticModuleState or AfxGetAppModuleState) at the top of both these functions.

4. The following files must be somewhere on the INCLUDE path:

5. All that’s left is to add custom code and build! Note that unless the project supports interfaces other than IPBToMH, it is not necessary to build any proxy/stub code for the message handler.

Configuration

Once the message handler COM object has been built and registered, it’s necessary to inform the message server to load it. See Configuring Message Handlers for further information.