Navigation:  Articles > Mar-1998 >

Track Your System File Versions

Previous pageReturn to chapter overviewNext page

Ashish Nanda          
 
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.

199803_an1 Figure 1
 
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.

Field name

Field type

FilePath

Text (255)

FileVersion

Text (50)

FileVersionMS

Long

FileVersionLS

Long

 
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.

 

Type VS_FIXEDFILEINFO

 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

End Type

 

'  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) _

As Boolean

 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: " _

                                 & strFilePath

   lngResLen = GetFileVersionInfoSize _

                (strFilePath, lngDummy)

   ' version resource found

   If lngResLen > 0 Then

     ReDim bytVerResource(lngResLen)

     ' get versions resource

     lngResult = GetFileVersionInfo(strFilePath, _

                  0&, lngResLen, bytVerResource(0))

     ' convert to file version information

     lngResult = VerQueryValue(bytVerResource(0), _

               "\", lngFIPtr, lngFILen)

     MoveMemory typFI, lngFIPtr, _

                       Len(typFI)

 

     ' store file version info in tblCurrentVersion

     rstF.AddNew

     rstF!FilePath = strFilePath

     rstF!FileVersion = _

      Format$(HighWord(typFI.dwFileVersionMS)) _

      & "." & _

      Format$(LowWord(typFI.dwFileVersionMS)) _

      & "." & _

      Format$(HighWord(typFI.dwFileVersionLS)) _

      & "." & _

       Format$(LowWord(typFI.dwFileVersionLS))

     rstF!FileVersionMS = typFI.dwFileVersionMS

     rstF!FileVersionLS = typFI.dwFileVersionLS

     rstF.Update

 

   End If

   strFileName = Dir    ' Get next entry.

 Loop

 Application.Echo True, ""

 

SnapshotFolderFiles_Exit:

 SnapshotFolderFiles = True

 Exit Function

SnapshotFolderFiles_Err:

 SnapshotFolderFiles = False

End Function

 
 
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

 WHERE (((tblFileCurrent.FileVersionMS)<

   [tblFilePrevious].[FileVersionMS])) OR

(((tblFileCurrent.FileVersionMS)=

       [tblFilePrevious].[FileVersionMS]) AND

((tblFileCurrent.FileVersionLS)<

   [tblFilePrevious].[FileVersionLS]));

 
 
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

FROM tblFileCurrent

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.
 
Conclusion
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.

Constant

Value

Description

vbNormal

0

Normal

vbHidden

2

Hidden

vbSystem

4

System file

vbVolume

8

Volume label; if specified, all other attributes are ignored

vbDirectory

16

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:
 

Sub ProcessFiles(strSpec)        

Dim strFileName as String

 

 strFileName = Dir(strSpec, vbNormal)

 While strFileName > ""

         .code to process files

         strFileName = Dir

 Loop

End Sub