Everything we will explain about the EventLog will work on Windows NT/2000/XP terminals, from Windows NT 3.1 and later, but not on Windows 95/98/Me terminals, since they don't have this feature.
A while ago, our colleague Cleber Ferrari wrote an article about the EventLog in the Universal Thread Magazine, but from a somewhat different point of view. In his article, he used components of the Windows Script Host, while we will now see what these COM components really do to register an event in the "Event viewer".
Objetives
We will define, as our main objective, to obtain a clear understanding of how the operating system works for registering, consulting or deleting an event from its event registry.
For these purposes, we will only use the API function provided by the operating system, and Visual FoxPro, without any other third-party services.
In order to do this, we will explain the following points:
Generating a new event
To generate a new event in the Eventlog, what we have to consider is how we would do to generate any kind of event. As a first step, we should write down such an event. As a second step, we should report information about the event we registered. For the operating system, it is exactly the same, but we have some more complexity in Visual FoxPro, since these function, which the operating system provides us, require pointers, which is something common in Visual C++, but not for us Visual FoxPro developers. But don't despair, this isn't so bad.
API Functions
We will now list the API functions that we use to generate a new event in the EventLog.
Let's see the function declarations in Visual FoxPro:
DECLARE LONG RegisterEventSource IN "advapi32.dll" STRING lpUNCServerName, STRING lpSourceName DECLARE LONG DeregisterEventSource IN "advapi32.dll" LONG hEventLog DECLARE INTEGER ReportEvent IN "advapi32.dll" ; LONG hEventLog, INTEGER wType, INTEGER wCategory, ; INTEGER dwEventID, INTEGER lpUserSid, INTEGER wNumStrings, ; INTEGER dwDataSize, LONG lpStrings, LONG lpRawData DECLARE LONG GetLastError IN "kernel32" DECLARE RtlMoveMemory IN "Win32API" AS CopyMemory LONG hpvDest, STRING @hpvSource, LONG cbCopy DECLARE LONG GlobalAlloc IN "kernel32" LONG wFlags, LONG dwBytes DECLARE LONG GlobalFree IN "kernel32" LONG HMEM
Figure 1: Logical Diagram
We will here make a graphic of the logic we mentioned previously, to generate a new event in the EventLog.
In this diagram we see that there are four API functions, "GlobalAlloc"; "CopyMemory"; "GlobalFree" and "DeregisterEventSource", which we didn't name in the brief analysis we did previously, but if we watch carefully, the functions "GlobalAlloc"; "CopyMemory" and "GlobalFree" are functions for allocating, copying and freeing memory, and these are the functions that, in the previous analysis, we resumed with the word "pointer". On the other hand, there is the function "DeregisterEventSource" which we have to use to free the event.
Some variants
Visual C++:
HANDLE RegisterEventSource( LPCTSTR lpUNCServerName, // server name LPCTSTR lpSourceName // source name );
DECLARE LONG RegisterEventSource IN "advapi32.dll" ; STRING lpUNCServerName, STRING lpSourceName
This parameter is the one that informs the function the name of the server to which you want to connect, in the UNC (Universal Naming Convention) format. If this parameter is null, the registration will be done on the local machine.
lpSourceName
This parameter is the name of the origin which the Handle returned by the function references. This parameter is commonly used with the application name, since, when we want to see the events in the "EventLog", we can find it easily, sorting or filtering on the origin.
BOOL ReportEvent( HANDLE hEventLog, // handle to event log WORD wType, // event type WORD wCategory, // event category DWORD dwEventID, // event identifier PSID lpUserSid, // user security identifier WORD wNumStrings, // number of strings to merge DWORD dwDataSize, // size of binary data LPCTSTR *lpStrings, // array of strings to merge LPVOID lpRawData // binary data buffer );
DECLARE INTEGER ReportEvent IN "advapi32.dll" ; INTEGER hEventLog, INTEGER wType, INTEGER wCategory, ; INTEGER dwEventID, INTEGER lpUserSid, INTEGER wNumStrings, ; INTEGER dwDataSize, INTEGER lpStrings, INTEGER lpRawData
It is this parameter which receives the Handle returned by function RegisterEventSource.
wType
This parameter specifies the type of event that will be generated. It can be one of the following:
wCategory
This parameter specifies the message category. It is used to organize events, and to be able to sort or filter them in the EventViewer in its column "Category".
dwEventID
This parameter indicates the event's ID, associated to the source of the event, also used to sort or filter them in the Event Viewer in its column "Event".
lpUserSid
This parameter identifies a pointer with a user's security identification. This can also be null, in case security is not required. We will do the latter in our examples.
wNumStrings
This parameter indicates the number of strings which are passed in the array passed to parameter lpStrings. If this is zero, it means that no string is used for the event. dwDataSize
This parameter indicates the size, in bytes, of the array which contains the strings to be saved. If it is zero, it means that no strings are used for the event.
lpStrings
This parameter is a pointer to the buffer that contains the string array with the information to be saved with the event. This parameter can be null, in case there is no string for the event. The limit that this array can have is 32K characters.
lpRawData
This parameter is a pointer to the information in binary format.
HGLOBAL GlobalAlloc( UINT uFlags, // allocation attributes SIZE_T dwBytes // number of bytes to allocate );
DECLARE LONG GlobalAlloc IN "kernel32" LONG wFlags, LONG dwBytes
This parameter specifies the mode in which the function allocates the requested memory. This can be one of the values from the list below, or a combination of them, except for some we will mention.
dwBytes
This parameter specifies the number of bytes that should be reserved.
Developing a class
Up until now, we have already talked in detail about what we can and should do to save a new log to the EventLog. Now, we can start to program.
The first thing we'll see is how to save information to the log, if this information is in binary mode, and not in text mode.
DEFINE CLASS CEventLog AS CUSTOM m_Machine = NULL PROCEDURE LogBinaryEvent( sString, iLogType, iCategoty, iEventID, sAplication ) LOCAL hEventLog LOCAL hMsgs LOCAL cbStringSize LOCAL nError nError = 0 *|-- Registers the event.- hEventLog = RegisterEventSource( This.m_Machine, sAplication ) IF( hEventLog == NULL ) RETURN GetLastError() ENDIF *|-- Get the memory.- cbStringSize = LEN(sString) + 1 hMsgs = GlobalAlloc( GMEM_ZEROINIT, cbStringSize ) IF( hMsgs > 0 ) *|-- Copy the message to the pointer obtained.- CopyMemory( hMsgs, sString, cbStringSize ) IF( ReportEvent(hEventLog, ; iLogType, iCategoty, ; iEventID, 0x0, ; 0x0, cbStringSize, ; hMsgs, hMsgs ) = 0 ) nError = GetLastError() ENDIF ELSE nError = GetLastError() ENDIF *|-- Release the given memory.- IF( GlobalFree( hMsgs ) != 0 AND nError == 0 ) nError = GetLastError() ENDIF *|-- Deregisters event.- IF( DeregisterEventSource( hEventLog ) == 0 AND nError == 0 ) nError = GetLastError() ENDIF RETURN nError ENDPROC
We can see that this code reflects the logical diagram which we saw previously, but we will analyze it in more detail.
As a first step, what we do is register the event, passing, as parameters, the name of the terminal on which it will act, and the name of the application that generates the log. Then, we request sufficient memory to be able to store the information which we wish to save, passing as parameters to this function GMEM_ZEROINIT and the size we need; thus, we have the entire memory initialized to zero, but let's stop here for now.
First, why it is necessary to request memory, to then copy something which we already have in memory? It doesn't sound logical, right? Well, the answer is easy, and difficult to accept, at least for me. FOX doesn't manage pointers, therefore, we need to request memory from a function, and to have it give us the address of the memory which this pointer uses physically, so that we can next copy what we have in memory to another memory address which is thus marked as our pointer returned by the function. But once again, we encounter the word "pointer"; I will try to explain what, exactly, is a pointer.
A pointer is a variable that contains the address of another variable. A computer has a sort of array of memory cells, consecutively numbered. Thus, a byte can be a char; two bytes can be an integer, and four consecutive bytes can be a long integer. Pointers are used a lot in C/C++, and use 4 bytes in memory as an "unsigned long", mainly because:
Example:
Figure 2: A variable pointing to another variable
A second important point is to understand the reason of the parameter GMEM_ZEROINIT. Why do we need to initialize the memory to zero? This is because the memory which the operating system gave us could have been used by a previous process, which used it and later freed it, but did it leave it clean? In most cases, the memory which they give us has garbage; at least, for us it is garbage, because we can't interpret what is stored in this memory positions; only the process that wrote to that memory knows how to do that.
Continuing with our method LogBinaryEvent, after it asks the operating system for memory, it copies the memory contained in the string which we passed as a parameter to the method, to the memory address that we allocate. Once this is done, we only need to report the event with all the properties that we defined as a parameter, for example, the log type, the category, and the event number.
Once we reported the event, the only thing left to do is to free the memory requested, and de-register the event. Freeing memory is very important, since otherwise, the memory we requested will continue being reserved, and can't be assigned to any other process that asks it, since we are using it.
Let's see an example of how the method is executed, and how it would affect the event viewer.
SET PROCEDURE TO EventLog.prg oo = CREATEOBJECT("CEventLog") ?oo.LogBinaryEvent( "Error---binary dump",; && Information to save EVENTLOG_ERROR_TYPE,; && Error type 0,; && Error category 0,; && Number of event "RobertApp" ) && Name of the application RELEASE oo
Figure 3: Registered event
Figure 4: Detail of the event registered
And if we open the event (to see its contents), we will see something like this:
Here we can see that the error description doesn't contain the information we saved; this is further down, where it says "Data:", since this part is reserved for writing binary data of the event.
With this method LogBinaryEvent, we already have the possibility of saving information into the event's binary data, but how should we save information in the the event's description? To do this, we have to understand that the way of doing this is similar to saving binary data, the difference being that we have to change the parameters we pass to the API function ReportEvent. The required changes are relatively simple with respect to parameters; we only need to change the parameters wNumStrings and lpStrings; that would be enough. The problem, however, is to generate the parameter lpStrings.
Let's see why.
Let's remember that the parameter lpStrings for the API function ReportEvent is a pointer to an array, and this is the problem for us. We already saw what is a pointer, but what is a pointer to an array? It is basically the same as a pointer, but the variable pointed to, by the pointer type variable, is different; it is an array. Let's try to explain how an array is distributed in physical memory.
A matrix is similar to a vector, and in some cases they are closely related, with the difference that a vector is one-dimensional, and a matrix is two-dimensional. Thus, for our example, it is simpler to start explaining what a vector is, and to later continue with a matrix, and see its similarities and relationships.
Arrays are structures that consist of a number of homogenuous elements, grouped under a single name; each element is distinguished from others by its position.
A vector has certain characteristics or properties:
Let's analyze the last two of these characteristics. The first says that all elements of a vector have to be of the same type. Although this is true, many of you might be asking: "But in VFP, I can declare a vector, and assign any type I want to each element; then, why do you say this?" The answer is simple: although we don't notice it, all the elements are of the same data type; the data type is VARIANT, therefore we have this possibility.
Now, let's see how a vector is defined in VFP, and let's contrast this with C++; thus we can notice the difference, what we can do with VFP vectors, thanks to the VARIANT data type, and what we can do in C++.
Visual FoxPro:
DIMENSION a[2] a[0] = 0 a[1] = "aa" ?a[1] ?a[2]
int a[2]; a[0] = 0; a[1] = 1; printf( "%i %i", a[0], [1] );
Figure 5: Pointer to a vector - The variable "pa" points to vector "a"
In this graphic we see three important things we have been talking about. The first one is how a pointer variable points to a vector; the second, that all elements of a vector are stored in contiguous positions in physical memory; and third, that the elements of a vector can be accessed either directly or indirectly. The first two points, I believe, can be understood easily with the graphic, but the third one I will explain in a little more detail.
Directly accessing elements of a vector is the most natural thing for us VFP developers; this would be accessing elements by their index or subscript, for instance:
a[0] = 0 a[1] = "aa"
int *pa; // Variable of type pointer. int a[2]; // Vector with two positions. // Direct access a[0] = 0; a[1] = 1; // Indirect access pa = &a[0]; // The address of the first element of vector "a" // is assigned to "pa" printf( "%i %i", *( pa ), *( pa + 1 ) );
The indirect access is called "pointer arithmethic", and for our example, of saving into the EventLog, we will use it in a very simple way to create the array that needs the parameter lpStrings of the API function ReportEvent, to be able to save the information into the event detail.
With this, we should have the concept of what is a vector quite clear; how it works and how it is stored in physical memory. But now, what is an array? Surely many of you have a good idea of whan an array is, but let's remember that up until now we saw what a vector of numbers is, and we need an array of strings.
The differences between an array of characters and a numeric array are big, but similar, since a vector of numbers only stores one value per index, whereas a character array stores several values, one for each character, for each index.
Once we understand this, we have to create an array of characters; for this purpose, we have to know how to have a vector of only one position contain (n) values, but actually we won't be inventing anything. If we think about it, C++ has been doing this for a long time, we only have to see how it is done, and let me anticipate that it isn't anything complicated, and if you have understood the concept of a vector well, it shouldn't be difficult to understand what is an array of characters.
If we remember the graphic of a pointer to a vector, we will realice that if, instead of saving numbers, we save characters in the vector, we already have what we require, how to save, in one single value, (n) values, having thus a pointer (a single value) that points to a vector that contains (n) values, as shown in the graphic of the pointer to a vector.
We have thus almost solved our problem, but we still need to know how to make our array contain (n) positions with (n) values, but surely you have already realized that this can be solved easily with two vectors as a minimum, one containing the addresses (pointers), and another vector, or vectors, containing the values.
Let's see, then, how in C++ we can create an array in memory.
Figure 6: The array "a" points to different parts of vector "v"
Thus, in position zero, vector "a" points to a memory address where there is a vector "v" of characters, and in its position one, vector "a" points to another memory address of the same vector"v" where another set of characters is stored. Surely you will ask yourself, how the position zero of vector "a" knows when its character string ends, and how come it doesn't use characters from position one of vector "a". This is solved using a control character that identifies the end of the string; this is character zero, that many use in C++ as "\0".
This is how C++ does this job, but I must add that this isn't the only one, since the first vector "a" only contains the memory addresses where the character string, terminated with "\0", is saved, therefore, it isn't necessary to have a single vector "v" contain the entire information from all positions of the first vector "a", rather, there might be (n) vectors "v" that contain only their information, vector "a" being related to them through their memory address. This would be as follows:
Figure 7: Vector "a" points to different vectors, which contain the characters
Perhaps for many the second method is easier to understand than the first, but I believe it isn't the best, since we have to request memory once for each of the pointers; thus, respecting the decision of Dennis Ritchie (creator of the C language), I will use the first option.
As you have seen after all this theory, a pointer isn't anything taboo, and an array described in memory isn't anything magical either. What happens here is that VFP, and most high-level languages, hide these details from us, to make our development easier, but here we want to use an API function that requires an array in the C++ style, for the simple reason that most of the operating system's APIs are developed in C++.
But first, perhaps you have wondered that the parameter lpStrings is an array, wherease we actually only want to save an error description; so why should it be an array? This parameter is an array, because this gives us the possibility of saving (n) descriptions of the event we generate, as long as the total size of the array doesn't surpass 32K characters.
After understanding all this, we will now undertake to develop a method that can save a string array into the event. Here it is:
PROCEDURE LogArrayEvent( sArrayMsg, iLogType, iCategoty, iEventID, sAplication ) LOCAL nMsgCount LOCAL nMemAlloc LOCAL pArray LOCAL pMsgsAux LOCAL hEventLog LOCAL nError LOCAL cAddrLst nError = 0 nMemAlloc = 0 nMsgCount = ALEN( sArrayMsg, 1 ) *|-- Registers the event.- hEventLog = RegisterEventSource( This.m_Machine, sAplication ) IF( hEventLog == NULL ) RETURN GetLastError() ENDIF *|-- Calculates the amount of memory needed for the vector.- FOR i = 1 TO nMsgCount nMemAlloc = nMemAlloc + LEN( sArrayMsg[i] ) + 1 ENDFOR *|-- Gets the memory for the array. *|-- Multiplies the array quantity by 4 *|-- as every pointer needs 4 bytes of memory *|-- This is the size of a C++ unsingned long .- pArray = GlobalAlloc( GMEM_ZEROINIT, nMsgCount * 4 ) *|-- Gets the memory for the messages .- pMsgs = GlobalAlloc( GMEM_ZEROINIT, nMemAlloc ) *|-- Initializes the variable containing the copy of the memory pointed *|-- to the messages vector.- cAddrLst = "" IF( pArray > 0 AND pMsgs > 0 ) pMsgsAux = pMsgs *|-- Copies the vector to the returned memory address *|-- Access the vector trough indirect access.- FOR i = 1 TO nMsgCount CopyMemory( pMsgsAux, sArrayMsg[i] + CHR(0), LEN( sArrayMsg[i] ) + 1 ) *|-- Stores the vector address.- cAddrLst = cAddrLst + This.NumToDWord( pMsgsAux ) *|-- Moves to the next element.- pMsgsAux = pMsgsAux + LEN( sArrayMsg[i] ) + 1 ENDFOR *|-- Fills the vector with the pointers to the vector containing the messages.- CopyMemory( pArray, cAddrLst , LEN(cAddrLst) )
IF( ReportEvent( hEventLog, ; iLogType, iCategoty, ; iEventID, 0x0, ; nMsgCount, 0x0, ; pArray, 0x0 ) = 0 ) nError = GetLastError() ENDIF ELSE nError = GetLastError() ENDIF *|-- Releases the given memory.- IF( GlobalFree( pMsgs ) != 0 AND nError == 0 ) nError = GetLastError() ENDIF IF( GlobalFree( pArray ) != 0 AND nError == 0 ) nError = GetLastError() ENDIF *|-- Deregisters the event.- IF( DeregisterEventSource( hEventLog ) == 0 AND nError == 0 ) nError = GetLastError() ENDIF RETURN nError ENDPROC
We can continue by explaining that this code reflects the logical diagram that we saw when we started to analyze how to save into the EventLog. Also, we can see that this is very similar to the method LogBinaryEvent. This is because the structure of how to save the event is exactly the same; the only difference is that the method LogArrayEvent generates an array dynamically in memory in the best C++ style, to pass it to the API function ReportEvent.
Let's then analyze the variations to see how to code, in VFP, the creation of an array according to all the theory that we have been seeing.
As a first difference, we see that we have calculated the size of each array position that is passed to the method, and accumulated it into a variable "nMemAlloc", but you see that in this line, the size of the string is increased by one. If we remember the theory, it says that to detect the size of the string we have to place a "\0" that identifies the end of the string; it is for this reason that we add one to the real size of the string. This is the amount of memory that we must then request from the operating system, to create our message vector.
The second difference that we see is in requesting memory from the operating system. Here, we request memory for two vectors, the first for managing the pointers (for us, the array), and the second is the one that will contain the information of each array index.
In order to do this, we manage two memory requests, one to request the total of characters for the array index that is passed to us, one by one, for each index, for the end of the string. On the other hand, we have the request for memory to generate the vector that will contain the pointers to each of the strings, and here we should remember that a pointer uses 4 bytes in memory. Therefore, a vector of two consecutive positions would use 8 bytes in memory, one of three positions, 12 bytes, etc.
The third difference that we see is in the generation of the vector that contains the messages. For this purpose, we loop through the array passed to the method, and copy it into memory, one element at a time, moving through the vector indirectly, thanks to "pointer arithmetic".
We also note that we have called the method Long2ToString. What this one does is convert the number passed by parameter into a string, and this string is the representation of the number stoed in the computer's physical memory. We have already used this method previously, for instance in the article "Sending emails from Visual FoxPro without additional components".
All these transformations done to the pointers are saved into an auxiliary variable, and then copied to the vector, which will thus be an array that contains the pointer addresses of each of the character strings, thus forming an array.
Once we have done this, the only thing left todo is to report the event, and that's it.
Let's see an example of how to execute the method and how it would look in the event viewer.
SET PROCEDURE TO EventLog.prg oo = CREATEOBJECT("CEventLog") DIMENSION a[2] a[1] = "The first message - Roberto" a[2] = "The second message - Ianni!!!" ?oo.LogArrayEvent( @a,; && Information to save EVENTLOG_ERROR_TYPE,; && Error type 0,; && Error category 0,; && Event number "RobertApp" ) && Application name RELEASE oo
Figure 8: Registered event
Figure 9: Detail of the registered event
And if we open the event to see its contents, it will look something like this:
We can see now that the message was saved in the part of the event description, and not in the binary part as is done by the method LogBinaryEvent; this latter part remains empty.
We should note that there is a "," (comma) between each message that we pass to the array; this identifies the items in the array we pass.
In case we want to pass one single message, all we have to do is pass a single item to the array, and that's it, but to finish this class, let's create a new method that does this. The method would look like this:
PROCEDURE LogStringEvent( sStringMsg, iLogType, iCategoty, iEventID, sAplication ) DIMENSION arrMsg[1] arrMsg[1] = sStringMsg RETURN This.LogArrayEvent( @arrMsg, iLogType, iCategoty, iEventID, sAplication ) ENDPROC
SET PROCEDURE TO EventLog.prg oo = CREATEOBJECT("CEventLog") ?oo.LogStringEvent( "Hy, I'm a string",; // Information to save EVENTLOG_ERROR_TYPE,; // Error type 0,; // Error category 0,; // Event number "RobertApp"; // Application name ) RELEASE oo
This new method that we develop is a mixture of the two we already developed. For this case, we would be passing the complete information of the parameters wNumStrings; dwDataSize; lpStrings and lpRawData of the API function API ReportEvent; with this, we already have our new method.
The new method will be called LogEventEx, and I understand that it is not worthwhile to analyze it, since it is a mixture of the methods LogArrayEvent and LogBinaryEvent, which we already know well. What we can do, however, is to see the result of choosing this new method, to see the result of executing it.
SET PROCEDURE TO EventLog.prg oo = CREATEOBJECT("CEventLog") DIMENSION a[2] a[1] = "The first message - Roberto" a[2] = "The second message - Ianni!!!" ?oo.LogEventEx( @a, // Array to save " Error---binary dump",; // Binary information to save EVENTLOG_ERROR_TYPE,; // Error type 0,; // Error category 0,; // Event number "RobertApp"; // Application name ) RELEASE oo
Figure 10: Event registered with both descriptions
If we desire, as before, to pass a single message, the only thing we have to do is to pass a single item to the array, and that's it, but to complete this class, we will create a new method that does this. This method will be something like this:
PROCEDURE LogEventExEx( sStringMsg, sStringBin, iLogType, iCategoty, iEventID, sAplication ) DIMENSION arrMsg[1] arrMsg[1] = sStringMsg RETURN This.LogEventEx( @arrMsg, sStringBin, iLogType, iCategoty, iEventID, sAplication ) ENDPROC
SET PROCEDURE TO EventLog.prg oo = CREATEOBJECT("CEventLog") ?oo.LogEventExEx( "Roberto Ianni",; // Binary information to save " Error---binary dump",; // Binary information to save EVENTLOG_ERROR_TYPE,; // Error type 0,; // Error category 0,; // Number of event "RobertApp"; // Application name ) RELEASE oo
Thus, we have finished our methods, which have the capability of saving to the EventLog, without requiring any external tool.
In the next issue
Up to this point, we have covered a series of resources to access functions of the Windows API, managing concepts which are not so familiar for Visual FoxPro developers, such as pointers, vectors and arrays.
It is important to remember that once we have finished these generic functions, in many cases we can reuse them further on, to call other API functions. The most important thing is to have the concepts clear, so as to know in which cases we need what. It is not important to have a complete dominion of low-level programming, to use it on a daily basis, as C++ programmers do.
In the next part of this article, we will continue this process, detailing all the techniques required to consult the events generated. On the way, we will learn more about how to interact with the Windows API functions.