Use Access and VERSION.DLL to track system file versions and get some control over DLLs and the Windows environment.
Have you ever been afraid of installing new software on your PC in case it changes your system files for the worse? I know I have. I'd hesitate before installing new software and then, when I did, watch in alarm as the setup program started writing files to the Windows and Windows\System directories. I'd worry that the software I couldn't resist trying out would leave me with more problems than it was worth.
This paranoia is only natural. As a developer, I'm well aware of the power of DLLs and how easy it is to incorrectly use options like "Always Overwrite" in setup wizards. Some of the better wizards save files that have been replaced in a backup directory, but that's not much help without a way of comparing those files with the ones they replaced.
Rather than continue to worry, I decided to use Access and VERSION.DLL to tackle this problem. Even if you don't worry about what installation programs are doing to you, the routines in this article will be very helpful should you ever need to determine the version of a file. I had to, for instance, when writing TAPI software that required the newer Unimodem V driver. This approach can also be used to compare DLLs between development and production PCs when chasing illusive environment problems.
File version information
Most people are aware that, under Windows 95, they can select a file in Explorer, right click it, and examine the File Version Information displayed in the Version Tab (see Figure 1). The Version tab gets its information from the Version Resource, which is (or should be) embedded into the files with the Resource Compiler (RC.EXE) during development.
Microsoft provides access to this information through a set of API calls. The routines to which these calls are made reside in VER.DLL (for 16-bit software like Access 2) and VERSION.DLL (for 32-bit software like Access 95/97). To keep things simple, I'll limit myself to providing the code for VERSION.DLL for Access 95 and Access 97.
While you can retrieve the version information for any file, the ones I wanted to keep track of were the shared DLLs. Since many programs use these DLLs, a setup routine that replaces one of them can have a significant impact on other, unrelated programs.
These DLLs are normally located in the Windows and Windows\System folders, so I decided to start by tracking all files with version information located in these folders. To do this, I'd create a routine that would store all the version information on all the files in any directory, as of some date. At a later date (after an installation of a new program, for instance), I'd gather the information again and compare it with the original data.
Obviously, such a simple approach has its limitations, even if all you want to do is protect yourself against rogue installations. For instance, I wouldn't be able to detect a setup program that installed a file that had the same version information as the file it replaced, even though the two versions were different. This procedure is also no help where a file had no version information, or was stored in some folder other than the ones I was tracking. Still, this was as good a place to start as any.
Capturing version information
To track changes, I created a table to hold the version infor-mation of the files. I could then compare those versions simply by using queries to determine which files had been upgraded, downgraded, or added to the two folders I was tracking.
Initially, I was planning to use a single table with a date field to allow comparison between different snapshots. Although this is the more flexible and normalized design, it turned out to be a lot slower than using separate, but identical, tables to hold the current and previous versions for comparison (Table 1 shows the final record layout).
Table 1. The file version information table.
In this table, the FilePath provides the fully qualified name to the file. FileVersionMS holds the major version number (the 2 in version 2.01), and FileVersionLS holds the minor version number (the .01 in version 2.01).
With that done, I was ready to create the functions to load the tables. Before issuing the Windows API calls to retrieve this information, you must set up the structure to hold the returned data and declare the API calls themselves (see Listing 1).
Listing 1. The code to declare the VERSION.DLL functions.
dwSignature As Long
dwStrucVersion As Long 'e.g. 0x00000042 = "0.42"
dwFileVersionMS As Long 'e.g. 0x00030075 = "3.75"
dwFileVersionLS As Long 'e.g. 0x00000031 = "0.31"
dwProductVersionMS As Long 'e.g. 0x00030010 = "3.10"
dwProductVersionLS As Long 'e.g. 0x00000031 = "0.31"
dwFileFlagsMask As Long '= 0x3F for version "0.42"
dwFileFlags As Long 'e.g. VFF_DEBUG
dwFileOS As Long 'e.g. VOS_DOS_WINDOWS16
dwFileType As Long 'e.g. VFT_DRIVER
dwFileSubtype As Long 'e.g. VFT2_DRV_KEYBOARD
dwFileDateMS As Long 'e.g. 0
dwFileDateLS As Long 'e.g. 0
' Returns size of the version info in Bytes
Declare Function GetFileVersionInfoSize Lib _
"Version.dll" Alias "GetFileVersionInfoSizeA" _
(ByVal lptstrFilename As String, _
lpdwHandle As Long) As Long
' Reads the version info resource for file
Declare Function GetFileVersionInfo Lib "Version.dll" _
Alias "GetFileVersionInfoA" _
(ByVal lptstrFilename As String, _
ByVal dwHandle As Long, _
ByVal dwLen As Long, lpData As Any) As Long
' Returns selected version information for resource
Declare Function VerQueryValue Lib "Version.dll" _
Alias "VerQueryValueA" _
(pBlock As Any, ByVal lpSubBlock As String, _
lplpBuffer As Any, puLen As Long) As Long
To actually capture the version information for my target files, I used a function that takes a folder name as a parameter and stores the version information for each file in the Current Version Table. I used the API calls to GetWindowsDirectory and GetSystemDirectory in the Kernel32 DLL to determine the location of the Windows and Windows\System directories.
The routine that captures the file version information uses the DIR function, provided by VBA, to cycle through each file in the passed folder (see the sidebar "DIRring Around"). The version information for each file found is obtained using the calls to Version.DLL and is formatted into a version string for easy display. The minor and major version numbers are used to compare file versions between the current and previous snapshots.
I won't go into the details of the API calls themselves, since that isn't really an Access issue. Basically, the filename retrieved by the DIR function is used to determine if the Version Resource for the file is greater than 0. If it is (indicating that the file has version information), that information is retrieved in the version record structure. This is a multiple-step process of retrieving the data into memory and then moving the data into the structure. Here's the routine:
Function SnapshotFolderFiles(strFolder As String) _
On Error GoTo SnapshotFolderFiles_Err
Dim dbs As Database
Set dbs = CurrentDb()
Dim lngResult As Long
Dim bytVerResource() As Byte
Dim lngResLen As Long
Dim typFI As VS_FIXEDFILEINFO
Dim lngFILen As Long
Dim lngFIPtr As Long
Dim lngDummy As Long
Dim strFileName As String, strFilePath As String
Dim rstF As Recordset
Set rstF = dbs.OpenRecordset("tblFileCurrent")
' Loop through the files in the folder
strFileName = Dir(strFolder & "*.*", vbNormal)
Do While strFileName <> ""
strFilePath = strFolder & strFileName
Application.Echo True, "Inspecting: " _
lngResLen = GetFileVersionInfoSize _
' version resource found
If lngResLen > 0 Then
' get versions resource
lngResult = GetFileVersionInfo(strFilePath, _
0&, lngResLen, bytVerResource(0))
' convert to file version information
lngResult = VerQueryValue(bytVerResource(0), _
"\", lngFIPtr, lngFILen)
MoveMemory typFI, lngFIPtr, _
' store file version info in tblCurrentVersion
rstF!FilePath = strFilePath
rstF!FileVersion = _
& "." & _
& "." & _
& "." & _
rstF!FileVersionMS = typFI.dwFileVersionMS
rstF!FileVersionLS = typFI.dwFileVersionLS
strFileName = Dir ' Get next entry.
Application.Echo True, ""
SnapshotFolderFiles = True
SnapshotFolderFiles = False
Comparing file versions
Having captured the file information, it was simply a matter of using the power of SQL to write the three queries to compare the current and previous file versions.
Here's the query that finds the files that have been replaced with an earlier version of the file:
SELECT tblFileCurrent.FilePath AS Path,
tblFileCurrent.FileVersion AS Current,
tblFilePrevious.FileVersion AS Previous
FROM tblFileCurrent INNER JOIN tblFilePrevious
ON tblFileCurrent.FilePath = tblFilePrevious.FilePath
The query to find the files that have been upgraded (replaced by a newer version) is identical to the previous query, except all the "<" are replaced with ">".
This query finds the files that have been added between the two snapshots:
SELECT tblFileCurrent.FilePath, tblFileCurrent.FileVersion
WHERE (((tblFileCurrent.FilePath) Not In
(Select FilePath from tblFilePrevious)));
While it's unlikely that an installation routine would delete a file, the query to find deleted files is identical to the previous query, except tblFileCurrent and tblFilePrevious are swapped.
In an ideal world, Microsoft would supply a table containing the latest version information for all system files. If such a file existed, you could use it to act as a standard for audit purposes, that is, to answer the question of which DLLs installed on your system aren't the most recent version. Such a table could even be replicable over the Internet and contain hyperlinks for downloading DLLs that need upgrading. Until that happens, you'll have to build your own audit table -- which is exactly what these routines will do for you.
Read about the download VERSION.ZIP on this page
Ashish Nanda is a Director of Calibre Computer Company, which specializes in custom software solutions. Ash@calibre
Sidebar: DIRring Around
The DIR function returns a single filename but, if you call it with the appropriate parameters, it will let you retrieve all of the filenames for a directory.
You can call DIR with two parameters. The first is a filespec and specifies what kind of filenames you want to retrieve. DIR(*.DOC) will retrieve all of the files with a DOC extension, for instance. Any filespec that you could use with the DIR command from the DOS prompt can be used as this parameter.
The second parameter specifies what kind of files will be retrieved. The predefined constants for this parameter are shown in Table 2.
Table 2. The DIR constants.
Volume label; if specified, all other attributes are ignored
Directory or folder
If you call DIR with parameters, it returns the first file in the current directory that meets the criteria set by the parameters. The neat trick comes when you call DIR without any parameters -- it returns the next file in the directory that matches the last specified criteria. If no records match the criteria specified, a zero-length string ("") is returned.
Typically, you'll see DIR used like this:
Dim strFileName as String
strFileName = Dir(strSpec, vbNormal)
While strFileName > ""
.code to process files
strFileName = Dir