Level Extreme platform
Subscription
Corporate profile
Products & Services
Support
Legal
Français
Articles
Search: 

Controlling the Event Log - Part 2
Roberto C. Ianni, June 1, 2004
The "EventLog" or "Event viewer" is something we use on a daily basis, to obtain information about what happened to our computer at a certain moment; for example, when an application generates an error, most of us will intuitively look at the EventLog to see what information it left for us.
Summary
The "EventLog" or "Event viewer" is something we use on a daily basis, to obtain information about what happened to our computer at a certain moment; for example, when an application generates an error, most of us will intuitively look at the EventLog to see what information it left for us.
Description
The "EventLog" or "Event viewer" is something we use on a daily basis, to obtain information about what happened to our computer at a certain moment; for example, when an application generates an error, most of us will intuitively look at the EventLog to see what information it left for us.

In the first part of this article, we saw the Windows API functions required to generate an Event in the Registry, and discussed several important related subjects such as managing pointers, structures, and other elements familiar to C++ developers, which, although unfamiliar to Visual FoxPro programmers, we can still manage with relative ease. In this second part, we will see how to query and make use of this information.

Consulting the generated events

In order to query the events generated by applications, we have to use four Windows API functions; this is all we need to obtain all the basic information. Although it is not complicated to obtain this information, there is some relative complexity, and it requires some time to understand well how it works. For the specific information, we will use a few additional API functions to obtain that which we can't get with the first three

But first, we have to know where we want to consult the events generated. By default, there are always three groups or containers: System, Application and Security. We can be quite sure of finding these on any terminal, and we can consult them, but apart from these three, there can be as many as we, or the applications creating them, want.

API Functions

We will now enumerate the four basic API functions that we will use to be able to consult the EventLog.

Función API Description
OpenEventLog Returnes a handle to an open EventLog.
GetNumberOfEventLogRecords Obtains the number of records which currently exists in the specified EventLog.
ReadEventLog Reads an integer number of events from the specified EventLog.
CloseEventLog Closes the open EventLog.
DECLARE LONG OpenEventLog IN "advapi32.dll" ;
   STRING lpUNCServerName, STRING lpSourceName
		
DECLARE LONG GetNumberOfEventLogRecords IN "advapi32.dll" ;
   LONG hEventLog, LONG @NumberOfRecords

DECLARE LONG ReadEventLog IN "advapi32.dll" ;
   LONG hEventLog, LONG dwReadFlags, ;
   LONG dwRecordOffset, STRING @lpBuffer, ;
   LONG nNumberOfBytesToRead, LONG @pnBytesRead, ;
   LONG @pnMinNumberOfBytesNeeded

DECLARE LONG CloseEventLog IN "advapi32.dll" ;
   LONG hEventLog
We will start by following the steps needed to read from the EventLog by looking at a diagram representing the process' logic. Later, we will explain some variants of these API functions, to continue later with the development of a method which reads from an event container and returns data to us.

Logic diagram

The diagram shown below is an example of how the access logic would be, to be able to access the "records". Each "record" in the EventLog is an event for us.

Figure 1: Logic diagram

As we can see in the graphic, it isn't complicated to obtain basic information on the event. Then, you might ask, where is the complication I mentioned previously?

The complications are in the part of the graphic labelled as "Reading the Record" (Lectura del Registro); the problem is that the API function ReadEventLog requires a pointer to a structure of type EVENTLOGRECORD, where it will leave us a set of records. For us VFP developers, this complicates matters, since our language doesn't have native support for variables of type structure.

This doesn't occur in C++, where the complexity of obtaining this information begins and ends in calling the four functions.

Some variations

  1. The function OpenEventLog is the first one we are going to analyze for possible variations. First, we will see its real declaration for VC++, and how our declaration in Visual FoxPRo is going to look, and see what we can do with each of the parameters the function requires.

    Visual C++:

    HANDLE OpenEventLog(
       LPCTSTR lpUNCServerName,  // server name
       LPCTSTR lpSourceName      // file name
       );
    
    Visual FoxPro:
    DECLARE LONG OpenEventLog IN "advapi32.dll" ;
       STRING lpUNCServerName, STRING lpSourceName
    
    lpUNCServerName

    This is the parameter that informs the function the name of the server on which you want to open the EventLog, in the UNC (Universal Naming Convention) format; if this parameter is null, the operation will be done on the local machine.

    lpSourceName

    This parameter is the name of the log file that you want to open. It can be Application, Security, System, or a customized log. If the name of the log passed to this parameter doesn't exist, the Application log will be opened by default.

  2. The other function we will analyze is the function "GetNumberOfEventLogRecords". Once again, we will specify its real declaration in VC++, and its declaration in Visual FoxPro.

    Visual C++:

    BOOL GetNumberOfEventLogRecords(
       HANDLE hEventLog,        // handle to event log
       PDWORD NumberOfRecords   // buffer for number of records
    );
    
    Visual FoxPro:
    DECLARE LONG GetNumberOfEventLogRecords IN "advapi32.dll" ;
       LONG hEventLog, LONG @NumberOfRecords
    
    hEventLog

    This parameter is the Handle for opening the EventLog. This Handle is returned by the functions OpenEventLog or OpenBackupEventLog.

    NumberOfRecords

    This parameter is a pointer to a buffer which will hold the number of records in the Log container passed to the parameter hEventLog.

  1. The other function we will analyze is "ReadEventLog"; here, too, we will show its real declaration in VC++ and its declaration in Visual FoxPro.

    Visual C++:

    BOOL ReadEventLog(
       HANDLE hEventLog,                // handle to event log
       DWORD dwReadFlags,               // how to read log
       DWORD dwRecordOffset,            // initial record offset
       LPVOID lpBuffer,                 // buffer for read data
       DWORD nNumberOfBytesToRead,      // bytes to read
       DWORD *pnBytesRead,              // number of bytes read
       DWORD *pnMinNumberOfBytesNeeded  // bytes required
    );
    
    Visual FoxPro:
    DECLARE LONG ReadEventLog IN "advapi32.dll" 		;
       LONG hEventLog, LONG dwReadFlags,			;
       LONG dwRecordOffset, STRING @lpBuffer,		;
       LONG nNumberOfBytesToRead, LONG @pnBytesRead,	;
       LONG @pnMinNumberOfBytesNeeded
    
    hEventLog

    This parameter is the Handle for opening the EventLog. This Handle is returned by the function OpenEventLog.

    dwReadFlags

    This parameter specifies how the job of reading the records will be done.

    dwRecordOffset

    This parameter specifies the record number from where the reading should start. This parameter is ignored if the parameter dwReadFlags doesn't include EVENTLOG_SEEK_READ.

    lpBuffer

    This parameter is a pointer to the structure EVENTLOGRECORD so that the function can read the information from the EventLog. This parameter can't be NULL, even if the parameter nNumberOfBytesToRead is zero.

    nNumberOfBytesToRead

    This parameter specifies the size, in bytes, of the buffer. The function reeds as many events as fit in the buffer passed as a parameter, and it will never read an incomplete Log even if there is space in the buffer.

    pnBytesRead

    This function is a pointer to a variable that contains the number of bytes that the function copies to the parameter lpBuffer. This is the number of bytes read.

    pnMinNumberOfBytesNeeded

    This parameter is a pointer to a variable that contains the minimum number of bytes required for the function to read the next event. This value is only valid if the function ReadEventLog returns the value zero, and the last error is ERROR_INSUFFICIENT_BUFFER.

  1. The other function which we are going to analyze is "CloseEventLog". We will also specify its real declaration in VC++ and in Visual FoxPro.]

    Visual C++:

    BOOL CloseEventLog(
       HANDLE hEventLog   // handle to event log
    );
    
    Visual FoxPro:
    DECLARE LONG CloseEventLog IN "advapi32.dll"		;
       LONG hEventLog
    
    hEventLog

    This parameter is the Handle that you wish to close. This Handle should be returned by the functions OpenEventLog or OpenBackupEventLog.

Developing the method for reading

In this section we will concentrate in developing a method for class CEventLog that will be capable of obtaining all events that have been registered, and returning them. We are going to develop this on the basis of the logic diagram which we saw previously, but in this diagram, an important point still needs to be done, which we called "Reading the Record" (Lectura del Registro), and we mentioned that it has a certain complexity. So, before developing the method, we are going to analyze this.

The complexity is due to the fact that the OS API function ReadEventLog, in one of its parameters, needs to reserve a certain amount of memory in which the function can place its information; and the program that requests it must interpret it as a structure of type EVENTLOGRECORD.

It is here that the problem appears, in managing a structure from within VFP. I won't go into many details on how a structure really works, and how we can do to manage it freely in VFP, because this would be like writing a new article. Therefore, we will simply see how we can transform these memory segments into something recognizable from within VFP.

Through the parameter pnBytesRead of function ReadEventLog, we receive the records which, for us, are events, and I say records because the function doesn't return a single event, rather, it returns as many as can be stored in the amount of memory which I specify in parameter pnBytesRead. Then, to analyze how we can solve this problem, we will imagine that this pointer contains just a single record, and we will see how it is set up.

typedef struct _EVENTLOGRECORD { 
  DWORD  Length; 
  DWORD  Reserved; 
  DWORD  RecordNumber; 
  DWORD  TimeGenerated; 
  DWORD  TimeWritten; 
  DWORD  EventID; 

  WORD   EventType; 
  WORD   NumStrings; 
  WORD   EventCategory; 
  WORD   ReservedFlags; 
  DWORD  ClosingRecordNumber; 
  DWORD  StringOffset; 
  DWORD  UserSidLength; 
  DWORD  UserSidOffset; 
  DWORD  DataLength; 
  DWORD  DataOffset; 

Figure 2: Contents of a structure in memory

  // 
  // Followed by: 
  // 
  // TCHAR SourceName[] 
  // TCHAR Computername[] 
  // SID   UserSid 
  // TCHAR Strings[] 
  // BYTE  Data[] 
  // CHAR  Pad[] 
  // DWORD Length; 
  // 
} EVENTLOGRECORD, *PEVENTLOGRECORD; 

In the definition of the structure in C++ we see that two data types are being used, one is a WORD and one is a DWORD. In memory, these occupy 2 and 4 bytes, respectively. Knowing this, we can consider that a record in memory is distributed as follows.


Figure 3: Positions in the structure



In this graphic, we can clearly see how part of the structure would be arranged in memory, but this structure is different to many others, since this particular structure doesn't have a fixed size as most structures do, rather, it has a fixed part and a variable part. The fixed part occupies 56 bytes, and the variable part is the content of the "Length" property, minus the fixed part. This creates yet another problem for us, but let's not despair; we will see how the variable part of the structure is distributed in memory.

Here we can see how the property "SourceName" starts in position 57, but we don't know where it ends. The end of the data is marked by the character "\0". This single character at the end of property "SourceName" is part of the new property "Computername", which works the same way as the previous one, ending with the character "\0". Now, if we observe how the property "UserSid" is made up, we can see that it starts at the offset defined in property "UserSidOffset", in other words, the starting marker for this property is defined in another property that tells it "it starts from here", and the end is marked by another property, "UserSidLength", which marks the number of characters that have to be counted from the starting position. The property "String" uses the same model as "UserSid", with the difference that there is nobody to tell it how many characters should be counted from the beginning, and neither would it be valid to count characters until a "\0" is encountered, since there can be as many of these as there are strings found to save in the event. To be able to find the end of this property, we need to find the start of the next one and subtract it from the beginning of this property, thus, we obtain a number of characters, as shown in the graphic.

Once we understand this, we can say, "Now I can read a record", and this is the step we require to be able to develop our method. As I told you before explaining all this, although it is not really complicated, it is a shock the first time you see something like this, since the majority of Fox programmers are not accustomed to handle this sort of problems. But I also told you it wouldn't be too complicated, and I believe that it wasn't.

Now, we should be prepared to develop our method which is capable of reading the EventLog, therefore, let's get started, and leave the theory behind for a while.

For this purpose, we will create a new method in the class CEventLog, which we will call GetRecordLogs; however, we will only see and explain the most important parts of the method, for this method is too big to show in its entirety, and actually, just by seeing the most important parts, you will notice that all other parts are quite similar; besides, you have the code which you can inspect.

PROCEDURE GetRecordLogs( sSourceName, pVecEvntRec )

*|-- Open the log.-
hOpenE = OpenEventLog( This.m_Machine, sSourceName )
pnBytesRead = 0
pnMinNumberOfBytesNeeded = 0
IF( hOpenE > 0 )	
   nNumberOfRecords = 0

   *|-- Searches the number of events available for this origin.-
   IF( GetNumberOfEventLogRecords( hOpenE, @nNumberOfRecords ) != 0 )
   
   *|-- Creates a vector that contains as many positions
   *|-- as events are found.-
      DIMENSION pVecEvntRec[nNumberOfRecords]

      DO WHILE ( ReadEventLog( ;
         hOpenE, EVENTLOG_FORWARDS_READ + EVENTLOG_SEQUENTIAL_READ, ;
         0x0, @pevlr, ;
         BUFFER_SIZE, @pnBytesRead, ;
         @pnMinNumberOfBytesNeeded ) != 0 )

         DO WHILE( pnBytesRead > 0 )
            *|-- New event.-
            pVecEvntRec[nActVec] = CREATEOBJECT("CEventRecord")
            pVecEvntRec[nActVec].SourceApp = sSourceName
            pVecEvntRec[nActVec].bLengt    = ;
                        SUBSTR( pevlr, pPonitRealPos, 4 )
            pevlrAux = SUBSTR( pevlr, pPonitRealPos,;
                         pVecEvntRec[nActVec].Length )

            *|-- Position of the second property.-
            pPonitPos      = 5

            pVecEvntRec[nActVec].bReserved = SUBSTR( pevlrAux,;
                         pPonitPos, 4 )
               pPonitPos  = pPonitPos + 4
            pVecEvntRec[nActVec].bRecordNumber = SUBSTR( pevlrAux,;
                         pPonitPos, 4 )
               pPonitPos  = pPonitPos + 4
            pVecEvntRec[nActVec].bTimeGenerated = SUBSTR( pevlrAux,;
                     pPonitPos, 4 )
            .........
            .........

            pVecEvntRec[nActVec].bEventType = SUBSTR( pevlrAux,;
                         pPonitPos, 2 )
               pPonitPos  = pPonitPos + 2
            pVecEvntRec[nActVec].bNumStrings = SUBSTR( pevlrAux,;
                         pPonitPos, 2 )
            .........
            .........

            pVecEvntRec[nActVec].bStringOffset  = SUBSTR( pevlrAux,;
                         pPonitPos, 4 )
               pPonitPos  = pPonitPos + 4
            pVecEvntRec[nActVec].bUserSidLength = SUBSTR( pevlrAux,;
                         pPonitPos, 4 )
            .........
            .........

            *|-- Origin of the event.-
            nPosAt = AT( CHR(0),    ;
            SUBSTR( pevlrAux, pPonitPos,    ;
               pVecEvntRec[nActVec].Length - pPonitPos )    ;
            )
            IF( nPosAt == 0 )
               pVecEvntRec[nActVec].SourceName = ""
            ELSE
               pVecEvntRec[nActVec].SourceName = SUBSTR( pevlrAux,;
                         pPonitPos,   nPosAt - 1 )
            ENDIF

            .........
            .........

            *|-- Binary part of the event.-
            IF( pVecEvntRec[nActVec].DataLength > 0 )
               pVecEvntRec[nActVec].bData = ;
               SUBSTR( pevlrAux, ;
                  pVecEvntRec[nActVec].DataOffset + 1,  ;
                  pVecEvntRec[nActVec].DataLength   ;
               )
            ELSE
               pVecEvntRec[nActVec].bData = ""
            ENDIF

            *|-- Move the pointer to the next record in the event.-
            pnBytesRead   = pnBytesRead   - pVecEvntRec[nActVec].Length
            pPonitRealPos = pPonitRealPos + pVecEvntRec[nActVec].Length

            *|-- Increment the index of the vectors
            nActVec       = nActVec + 1
         ENDDO

         pPonitRealPos = 1

      ENDDO
   ENDIF
ENDIF
.........
.........

CloseEventLog( hOpenE )

RETURN nError

ENDPROC
With the code we have written, we are now well advanced with our method and with our second initial point, "Consulting the EventLog", but now, let's analyze what we have been doing, for there are still several things that haven't been explained.

As a first step, we see that the method clearly represents the logic diagram that we have used at the beginning of this point, therefore, we can conclude that the analysis part was correct. Next, we see that after opening the log that it receives as a parameter, what we do is call the function GetNumberOfEventLogRecords so that it can tell us how many records are in the open log, and we can thus redimension the other parameter that it receives as a vector. This is done because it will be this parameter which will take care of returning all the events that have been saved. Once we have assigned sufficient space to return the events, the process continues asking the function ReadEventLog as many events as it can save in the parameter "pevlr", which has a fixed size of 4096 bytes, but remember that this parameter is of the type EVENTLOGRECORD, and doesn't have a fixed size; rather, one that varies according to the amount of information contained in each event.

When the function ReadEventLog returns all the events that it could save in the 4096 bytes, what the process does is to scan through the parameter "pevlr" as a string and obtain one record at a time. This is possible since the first field in each event is the size of the record, thus, we can obtain it completely and then subtract, one at a time, each one of the fields and saved it in the vector we dimensioned previously. We can see this point clearly in the graphics that we showed previously, were reference was made to how the structure EVENTLOGRECORD was distributed in memory.

In this method, we also notice that for each record that we want to return, we create an object of another class, the class CEventRecord, which has all the properties that the structure EVENTLOGRECORD has. Also, we note that every time that we assign information to the object, we do no processing with the information we assign to it, we simply copy what the API function ReadEventLog returns, and nothing else. This is because the class CEventRecord is capable of realizing whether the contents have been translated or not. To do this, it uses an auxiliary property. To explain this in more detail, you can do this otherwise, making it start with "b", of "binary"; in this case, when we access the real property, the class will realize that the real property is empty, and will complete it with the one that has the information in memory format. This is also developed in this way for two additional important reasons. The first is that each method should carry out only one specific task and nothing more. The second is that we thus obtain much more speed in reading, since all conversions are done by the class CEventRecord for each of its properties.

From the class CEventRecord, we will see only some examples of conversion, and analyze, in detail, other things which are much more important.

Conversion of the types DWORD and WORD.

We do these conversions when we access the real properties; depending on the type, whether it is DWORD or WORD, it is done differently, because these variables use 4 and 2 bytes, respectively, in memory.

WORD:

PROCEDURE NumStrings_Access
   IF EMPTY( NVL( This.NumStrings, .F. ) )
      This.NumStrings = This.StringToShort( This.bNumStrings )
   ENDIF
   RETURN This.NumStrings
ENDPROC   
Here we see that when you try to access the property NumStrings of class CEventRecord, what you do is a conversion of the two bytes in property bNumStrings and assign it to property NumStrings. This is done through method StringToShort.

DWORD:

PROCEDURE DataLength_Access
   IF EMPTY( NVL( This.DataLength, .F. ) )
      This.DataLength = This.StringToLong( This.bDataLength )
   ENDIF
   RETURN This.DataLength
ENDPROC      
Here, when you try to access property DataLength of class CEventRecord, what is done is a conversion of the 4 bytes in property bDataLength, and assign it to property DataLength. This is done through method StringToLong.

All other type conversions are done in the same way, but there are some which, apart from these conversions, do other additional tasks, or only do other tasks. We mentioned these tasks at the beginning, when we said that with only four API functions we read the records, but to obtain additional information, we required other functions. Now, let's analyze one case at a time, and see what functions we need to use and why, for each property of class CEventRecord.

Property UserName:

What this property contains is the information about the user that generated the event, in the format \\DOMAIN\USER, which is exactly the same as we see in the EventViewer.

PROCEDURE UserName_Access
   IF EMPTY( NVL( This.UserName, .F. ) )
      This.UserName = This.GetUserInfo( This.prtSeg, This.UserSidLength )
   ENDIF
   RETURN This.UserName
ENDPROC

PROCEDURE GetUserInfo( lPtr, lLen )
   LOCAL lpSidNameUse
   LOCAL lRetu
   LOCAL cName
   LOCAL cDomain
   LOCAL sName
   LOCAL sDomain
   LOCAL sUsr
   IF( EMPTY( lPtr ) OR lLen <= 0)
      RETURN "N/A"
   ENDIF

   sUsr    = "N/A"      
   sName   = SPACE(255)
   sDomain = SPACE(255)
   cName   = LEN( sName ) + 1
   cDomain = LEN( sDomain ) + 1
   pArray = GlobalAlloc( GPTR, lLen )
   CopyMemory( pArray, lPtr, lLen )
   
   IF( IsValidSid( pArray ) != 0 )
      lngRet = LookupAccountSid( This.m_Machine, pArray, ;
                     @sName, @cName, @sDomain, @cDomain, @lpSidNameUse )
      IF lngRet > 0
         sUsr = "\\" + LEFT( sDomain, cDomain) + "\" +;
            LEFT( sName, cName )
      ENDIF
   ENDIF
   GlobalFree( pArray )
      
RETURN( sUsr )
ENDPROC
Here we can see that when you access property UserName, a call to method GetUserInfo is triggered, passing, as parameters, two properties: "prtSeg" and "UserSidLength", which contain the memory segment occupied by the SID structure, and its size.

Looking into the method GetUserInfo, this one uses five API functions, three for memory management, for the request; the copying; and freeing memory, which we already know. Then, it uses two additional functions which we haven't seen yet: IsValidSid and LookupAccountSid. What these do is to validate whether the memory address passed is valid as an SID (security identifier) structure, and the second accepts an SID as an input parameter, and recovers the account name belonging to the SID, and the name of the first domain where the SID was found.

With all this, we can obtain the name and the domain of the user who generated the record in the EventLog.

Property EventCategory:

PROCEDURE EventCategory_Access
   IF EMPTY( NVL( This.EventCategory, .F. ) )
      This.EventCategory = This.StringToShort( This.bEventCategory )
      This.EventCategory = This.GetEventCategory( This.EventCategory, This.SourceApp, This.SourceName )
      ENDIF
   RETURN This.EventCategory
ENDPROC

In this method, we see how, when property EventCategory is accessed, it transforms the string into a 2-byte short, in order to use it later as a parameter for the method GetEventCategory, which is responsible for searching the description of the category in case there is one.

The method that searches the categories will have to do three important tasks in order to obtain the information it needs. The tasks are accessing the system Registry; opening a DLL library; and searching for a message within an open DLL. Let's see the code, and then analyze it in detail.

PROCEDURE GetEventCategory( iCat, sRegSource, sSource )
   IF( iCat == 0 )
      RETURN "None"
   ENDIF

   LOCAL cKEY
   LOCAL cVALUE
   LOCAL sKEY
   LOCAL lReturn
   LOCAL sKeyValue
   LOCAL hKey
   LOCAL lKeyType
   LOCAL lLen
   LOCAL sFile
   LOCAL sNewFile
   LOCAL hMod
   LOCAL sDesc
   cKEY   = "SYSTEM\CurrentControlSet\Services\EventLog"
   cVALUE = "CategoryMessageFile"
      
   *|-- Part of the tree accessed.-
   sKEY    = cKEY + "\" + sRegSource + "\" + sSource
      
   *|-- Open the branch in the registry.-
   lReturn = RegOpenKeyEx( HKEY_LOCAL_MACHINE, sKEY, 0, KEY_READ, @hKey )
      
   IF( lReturn != ERROR_SUCCESS )
      RETURN "None" && An error was produced, but so as not to stop the process,
                && we return a generic value.-
   ENDIF
      
   *|-- Allocate space for the value of "CategoryMessageFile"
   sKeyValue = SPACE( MAX_BUFFER )
   lLen      = MAX_BUFFER
   lReturn   = RegQueryValueEx( hKey, cVALUE, 0, @lKeyType, @sKeyValue, @lLen )

   *|-- Checks whether a longer string is needed to accommodate the string.-
   IF( lReturn = ERROR_MORE_DATA )
       sKeyValue = SPACE( lLen )
       lReturn   = RegQueryValueEx( hKey, cVALUE, 0, @lKeyType, @sKeyValue, @lLen )
   ENDIF

   IF( lReturn != ERROR_SUCCESS )
      RETURN "None" && An error was produced, but so as not to stop the process,
                && we return a generic value.-
   ENDIF

   *|-- Filename.-
      sFile = LEFT( sKeyValue, lLen - 1)

   *|-- If the system returns with system environment variables,
   *|-- this has to be replaced by the real path of the file.-
   IF( AT( "%", sKeyValue ) != 0 )
      sNewFile = SPACE( MAX_PATH )
      lRet     = ExpandEnvironmentStrings( sFile, @sNewFile, MAX_PATH )

        IF( lRet > 0 )
            *|-- Copy everything up to the null.-
            sFile = SUBSTR( sNewFile, 1, AT( sNewFile, CHR(0) ) - 1 )
        ENDIF
   ENDIF

   hMod = LoadLibraryEx( sFile, 0, LOAD_LIBRARY_AS_DATAFILE )
   IF( hMod != 0 )

        sDesc = SPACE( BUFFER_SIZE )

        lRet  = FormatMessage( FORMAT_MESSAGE_FROM_HMODULE, ;
                                hMod, iCat, 0, @sDesc,       ;
                                BUFFER_SIZE, 0 )

        *|-- Copy everything up to the null.-
        IF( lRet > 0 )
          FreeLibrary( hMod )
          RegCloseKey( hKey )
            RETURN SUBSTR( sDesc, 1, lRet )
        ENDIF
    ENDIF
    FreeLibrary( hMod )
    RegCloseKey( hKey )

ENDPROC
The first thing we do here is to open the Registry at the position "HKEY_LOCAL_MACHINE", and within this, at "SYSTEM\CurrentControlSet\Services\EventLog"; to this string, we add the name of the application that generated the event. Once the Registry is open, the value of entry "CategoryMessageFile" is searched. This entry consists of the name of the file that contains the description of the categories generated by the events. In case this entry doesn't exist, the method informs that there is no category associated with the event; in case the entry does exist, the process verifies that the value returned contains the character "%". If the value contains this character, this means that the value has an environment variable, and we need to evaluate it to obtain the complete path for the file.

For example, when we open the entry "CategoryMessageFile" for the application "WMIAdapter", we might find that we get the value "%SystemRoot%\system32\WBEM\WMIApRes.dll", and this is not what we need to open the DLL, because we don't have the complete path. To obtain it, we use the API function "ExpandEnvironmentStrings" which might give us the value as "C:\Windows\system32\WBEM\WMIApRes.dll".

Once we have the name and the complete path to access the DLL that has the description of the events, we open it with the API function "LoadLibraryEx", simply passing the path so that the function can access it, and telling it that the DLL is accessed only to extract a certain message within the DLL.

Once the DLL is opened, we call the API function "FormatMessage", and call it to obtain the message from a previously opened DLL, and search the message, which is the category number passed to the method. With this information, the function searches within the DLL's resources and creates a message that is returned by a parameter passed as a pointer.

Now, all that still has to be done is to free the previously opened DLL and close the Registry entry.

Property EventType:

PROCEDURE EventType_Access
   IF EMPTY( NVL( This.EventType, .F. ) )
      This.EventType = This.GetEventType( This.StringToLong( This.bEventType ) )
   ENDIF
   RETURN This.EventType
ENDPROC

PROCEDURE GetEventType( nType )
LOCAL sType
   DO CASE
      CASE nType = EVENTLOG_SUCCESS
         sType = "Success"

      CASE nType = EVENTLOG_ERROR_TYPE
         sType = "Error"
      CASE nType = EVENTLOG_WARNING_TYPE
         sType = "Warning"
      CASE nType = EVENTLOG_INFORMATION_TYPE
         sType = "Information"
      CASE nType = EVENTLOG_AUDIT_SUCCESS
         sType = "Correct login"
      CASE nType = EVENTLOG_AUDIT_FAILURE
         sType = "Incorrect login"
      OTHERWISE
         sType = ""
   ENDCASE
   
RETURN sType
ENDPROC

These two methods don't need much explanation. First, when you try to access property "EventType", the data is transformed, and then, it searches for the description directly in a CASE, since the description depends on the type.

Property TimeGenerated/TimeWritten:

PROCEDURE TimeGenerated_Access
   IF EMPTY( NVL( This.TimeGenerated, .F. ) )
      This.TimeGenerated = This.GetTime( This.StringToLong( This.bTimeGenerated ) )
   ENDIF
   RETURN This.TimeGenerated
ENDPROC
The properties TimeGenerated and TimeWriten work exactly the same, and will usually return the same value; therefore, we will only see one of them here.
PROCEDURE GetTime( lngSeconds )
   LOCAL lpTZI
   LOCAL lngRet
   LOCAL Bias, DaylightBias
   LOCAL cSTART_DATE

   lpTZI       = REPLICATE( CHR( 0 ), TIME_ZONE_INFORMATION )
   cSTART_DATE = DATETIME( 1970, 1, 1, 0, 0, 0 ) && "01/01/1970 00:00:00"

    lngRet = GetTimeZoneInformation( @lpTZI )
    IF( lngRet != TIME_ZONE_ID_INVALID )
       Bias         = This.StringToLong( SUBSTR( lpTZI, 1,   4 ) )
       DaylightBias = This.StringToLong( SUBSTR( lpTZI, 168, 4 ) )
      lngSeconds   = lngSeconds - ( Bias * 60 ) - ( DaylightBias * 60 )
      cSTART_DATE  = cSTART_DATE + lngSeconds
    ENDIF
       
    RETURN cSTART_DATE
ENDPROC
The procedure to obtain these dates is relatively simple, you only have to examine it carefully. The first thing that it does is obtain a 32-bit number that corresponds to an offset, in seconds, from a certain date, that is, method "GetTime" receives a 32-bit number that contains the time, in seconds, that has passed after a certain date, this date being January 1, 1970. Then, the API function "GetTimeZoneInformation" is used to obtain information about the time zone where the event was generated. With this information, the offset passed as a parameter is adjusted and added, or subtracted, to the start date.

Property Data:

PROCEDURE Data_Access
   IF EMPTY( NVL( This.Data, .F. ) )
      This.Data = This.FormatBinary( This.bData )
   ENDIF
   RETURN This.Data
ENDPROC
When this property is accessed, it calls method "FormatBinary". FormatBinary is too big to show it here, instead, I will explain what it does, and explain one special point in more detail.

What method FormatBinary basically does is to complete the binary information saved to the event. The information is added only to make it look as it does in the EventView, that is, adding a hexademical translation to each character. Let's see an example:

Real information saved into the event: "Error--- vuelco binario" (Error, binary dump).

Información visualizada en el EventView:
0000: 45 72 72 6f 72 2d 2d 2d   Error---
0008: 76 75 65 6c 63 6f 20 62   vuelco b
0010: 69 6e 61 72 69 6f 00      inario.
Here we see that the event's binary information is divided into three parts: first, the line number; second, the numbrs, in hexadecimal, that represents the characters of the line; and finally, the binary line in itself. You can also see that there are only eight characters per line. In summary, this is what method FormatBinary does, but here we will stop briefly to see how the conversion of character to hexadecimal can be achieved.

First, we have to consider our scope. We know that in the ASCII table we have 256 possibilities, which go from character 0 to 255; therefore, the hexadecimal value goes from 0x0 to 0xFF. Now, let's see a way which I find interesting, to understand this change of base.

The first thing we have to do is to obtain the modulus (remainder) of the division of a number (n) by 16. We obtain the hexadecimal representation of this number, and continue with the following. The next number will be the integer result of the starting number divided by 16. We repeat the cycle, until we end up with a zero. We have to consider that the first number processed is the last number or letter in hexadecimal format.

Let's see a small example:

PROCEDURE TransformToHex( lnNum )
   LOCAL sHexa, nMod
   sHexa = ""

   DO WHILE( lnNum > 0 )
      nMod  = lnNum % 16
      IF( nMod > 9 )
         nMod = nMod - 10
         sHexa = CHR( 65 + nMod ) + sHexa
      ELSE
         sHexa = STR( nMod, 1, 0 ) + sHexa
      ENDIF
      lnNum = INT( lnNum / 16 )
   ENDDO 
   RETURN( sHexa )
ENDPROC

Thus, we have transformed a decimal number to hexadecimal, to be able to use it in property "Data". Actually, VFP already has a function that allows you to transform a decimal number to hexadecimal, and, simply because it is a native function, it works much faster than the one developed by us. Anyway, it is useful to know how this transformation can be done.

Let's see a small example with VFP:

PROCEDURE TransformToHex( lnNum )
   RETURN TRANSFORM( lnNum , "@0" )
ENDPROC
Thus we have finished our second point, which was, how to be able to query the EventLog for events generated.

Erasing the events

Erasing the events generated in the EventLog is really simple, after having done the previous tasks.

To be able to erase this information, we only have to use the API function ClearEventLog, after opening the EventLog.

API functions

API Function Description
ClearEventLog Erases the information stored in the specified EventLog. Optionally, you can ask it to save the information before erasing it.

Visual C++:

BOOL ClearEventLog(
   HANDLE hEventLog,          // handle to event log
   LPCTSTR lpBackupFileName   // name of backup file
);
Visual FoxPro:
DECLARE LONG ClearEventLog IN "advapi32.dll" ;
   LONG hEventLog, STRING lpBackupFileName
hEventLog

This parameter receives the Handle returned by function OpenEventLog.

lpBackupFileName

This parameter specifies the name of the file to which the EventLog will be backed up, before being erased. If the file exists, the function will fail.

If this parameter is NULL, the EventLog is not backed up.

Developing the method

To develop this method, the only thing we have to do is open the container of the EventLog which we want to erase, and just erase it. For this purpose, we will develop two methods, one that accepts a filename to do the backup, and the other one only the name of the container to erase.

PROCEDURE ClearEventLog( sSourceName )
   RETURN ClearEventLogEx( sSourceName, NULL )
ENDPROC

PROCEDURE ClearEventLogEx( sSourceName, sFileBackUp )
   LOCAL hOpenE
   LOCAL nErr
   hOpenE = OpenEventLog( This.m_Machine, sSourceName )
   nErr    = 0
   IF( hOpenE > 0 )   
      IF( ClearEventLog( hOpenE, sFileBackUp ) != 0 )
         nErr = GetLastError()
      ENDIF
   ELSE
      nErr = GetLastError()
   ENDIF
   CloseEventLog( hOpenE )
   
   RETURN nErr
ENDPROC
As we can clearly see in the code, these methods are not at all complex, compared to the methods we developed earlier, but still, we will review them briefly.

The method ClearEventLogEx accepts two parameters. The first parameter is the name of the container to erase, and the second, the name of the file where the backup will be done. The method ClearEventLog only accepts the name of the container to erase.

As we can see, the first thing that method ClearEventLogEx does is open the container with the function OpenEventLog. Then, if it can open it, it simply calls function ClearEventLog with the parameter of the EventLog Handle and the name of the file where you want to have the container backed up before erasing it.

Backing the events up

To back up the events, the method is quite similar to erasing them; we just have to open the container and call an API function, in this case, BackupEventLog.

We won't go into details, methinks the previous explanation is sufficient. I will simply show the code and explain it briefly.

PROCEDURE BackupEventLog( sSourceName, sFileBackUp )
   LOCAL hOpenE
   LOCAL nErr
   hOpenE = OpenEventLog( This.m_Machine, sSourceName )
   nErr    = 0
   IF( hOpenE > 0 )   
      IF( BackupEventLog( hOpenE, sFileBackUp ) != 0 )
         nErr = GetLastError()
      ENDIF
   ELSE

      nErr = GetLastError()
   ENDIF
   CloseEventLog( hOpenE )
   
   RETURN nErr
ENDPROC
As we can see, it is really quite similar to method ClearEventLogEx; the first thing we have to do is open the event container that is passed as a parameter, and then call the API function BackupEventLog. The Handle of the open container is passed to this API function, together with the name of the file where the backup will be done.

Conclusions

In conclusion, we can see that we can easily obtain and generate information in the operating system's EventLog, without having to use third-party applications; rather, we remain in control.

To be able to do this, we have developed a class that we called CEventLog, an open-source class that is in no way behind other products, and that has the advantage that it is code which we can control ourselves, in all of its details.

I hope this article was useful for you, for some task that you want to do. You should be able to use it in many different ways, for example, to inform about errors in an application.

I thank you for reading the article; for me, it was a pleasure to write it.

Source Code

Roberto C. Ianni, Banco Hipotecario
Roberto Ianni (Buenos Aires, Argentina) is a professional software developer for the last 4 years. He has programmed in C++, VFP, VC++, C#, and has lots of experience in VFP and VC++. He currently works in the Banco Hipotecario as an Analyst-Developer.
More articles from this author
Roberto C. Ianni, December 1, 2004
Today we are going to talk about data compression. This is something we do frequently, something which is useful for many different tasks.
Roberto C. Ianni, May 1, 2004
The "EventLog" or "Event viewer" is something we use every day, to obtain information about what happened to our computer at a certain moment. For example, when an application generates an error, most of us instinctively go to the EventLog to see what information it left for us.
Roberto C. Ianni, July 1, 2004
One of the great limitations I find in VFP is the lack of low-level manipulation I can obtain from it. For many, this might not be an inconvenience, but several times I have encountered a problem which can't be solved with Fox, and it is then that I use a DLL or an FLL.
Roberto C. Ianni, August 1, 2004
As an objective for this issue, I want to propose the following points, to complete the first part of the development of an FLL. Advanced development with an FLL; Accessing VFP data; Executing VFP commands from a FLL; MultiThreading; FLL with VFP 9.0; Executing Assembler from VFP.
Roberto C. Ianni, October 1, 2004
This article is the continuation of "Sending email through Visual FoxPro without additional components", published last March. Back when I wrote this article, I didn't think it would get such a huge response, but it did, hence, due to the mails coming from everywhere I decided to write again on that...
Roberto C. Ianni, November 1, 2004
In this second part we will learn how to: Implement MIME, Attach binary files, the final implementation of the WSendMail class, and the use of the WSendMail class.
Roberto C. Ianni, May 1, 2005
Windows Control Panel holds the configuration from several of the operating system's applications, and from some other applications that provide an applet to appear there. Learn how to build this kind of applets to give your application a professional configuration mechanism.
Roberto C. Ianni, April 1, 2005
The intention of this article is to finish with the use of POP3 (Post Office Protocol 3), implementing everything we have seen so far.
Roberto C. Ianni, January 1, 2005
This article is the beginning of receiving email from VFP; this is complemented with the articles you have already seen about sending email from VFP. To be able to do this, we will have to use the POP3 protocol (Post Office Protocol 3).
Roberto C. Ianni, February 1, 2005
We will continue some of the points mentioned in the previous article, in order to be able to implement message reception. The points to be developed are: POP3 Authentication (both Flat and MD5 authentication methods) and EMail parsing.
Roberto C. Ianni, March 1, 2004
Many of us send email using third-party tools such as Outlook Express or Microsoft Outlook, among others, and most likely have encountered registration problems in the client, licencing problems, OLE errors that only occurr at the client's site but not in our developer environment. Also problems if ...
Roberto C. Ianni, August 1, 2006
The present article is one of those that Martín Salías calls "low level fox", the general idea is to develop a service for the operative system whose only goal will be to run a VFP application in charge of handling the business logic.