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.
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
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
Visual C++:
HANDLE OpenEventLog( LPCTSTR lpUNCServerName, // server name LPCTSTR lpSourceName // file name );
DECLARE LONG OpenEventLog IN "advapi32.dll" ; STRING lpUNCServerName, STRING lpSourceName
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.
BOOL GetNumberOfEventLogRecords( HANDLE hEventLog, // handle to event log PDWORD NumberOfRecords // buffer for number of records );
DECLARE LONG GetNumberOfEventLogRecords IN "advapi32.dll" ; LONG hEventLog, LONG @NumberOfRecords
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.
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 );
DECLARE LONG ReadEventLog IN "advapi32.dll" ; LONG hEventLog, LONG dwReadFlags, ; LONG dwRecordOffset, STRING @lpBuffer, ; LONG nNumberOfBytesToRead, LONG @pnBytesRead, ; LONG @pnMinNumberOfBytesNeeded
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.
BOOL CloseEventLog( HANDLE hEventLog // handle to event log );
DECLARE LONG CloseEventLog IN "advapi32.dll" ; LONG 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
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
DWORD:
PROCEDURE DataLength_Access IF EMPTY( NVL( This.DataLength, .F. ) ) This.DataLength = This.StringToLong( This.bDataLength ) ENDIF RETURN This.DataLength ENDPROC
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
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
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
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
Property Data:
PROCEDURE Data_Access IF EMPTY( NVL( This.Data, .F. ) ) This.Data = This.FormatBinary( This.bData ) ENDIF RETURN This.Data ENDPROC
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.
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
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
BOOL ClearEventLog( HANDLE hEventLog, // handle to event log LPCTSTR lpBackupFileName // name of backup file );
DECLARE LONG ClearEventLog IN "advapi32.dll" ; LONG hEventLog, STRING lpBackupFileName
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
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
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