Tutorials

Installing device drivers with Windows Installer for Visual Studio.NET

The idea for writing this article came to me while I was developing an installer for the software, which would include an installation of drivers for several different devices. While Microsoft is still working on a mechanism to simplify device driver installation which would be both easy to use and secure, there are several different ways to perform this task using existing software packages. My other article addresses this problem using InstallShield Custom Actions (CAs). This article, on the other hand, will demonstrate drivers installation by the means of standard Visual Studio.NET Setup and Deployment Project.

Unfortunately, VS.Net Setup Wizard is nowhere near as powerful as InstallShield, but still it provides some flexibility by introducing MSI standard for windows installation projects. To get the maximum power from our windows installer we need free utility Orca Database Editor by Microsoft, which is distributed as part of Microsoft SDK available for download at [http://www.microsoft.com/msdownload/platformsdk/sdkupdate/]. I would not go into great details describing msi standard. In short, it represents a database of all kinds of different options and events describing a particular installation. An additional executable file may be used as a wrapper for that database to run an intuitive installation wizard.

Creating a simple windows installer, which would include file copying and adding some registry keys, using deployment wizard in Visual Studio is a relatively straightforward task. However, drivers installation requires additional knowledge of SetupAPI functionality and, of course, usage of installer custom actions. The best way to install drivers using Setup Wizard is to create a dynamic link library (DLL) describing necessary setup actions and declare this DLL as a custom action in MSI database.

Creating Windows Installer Project

I shall start my tutorial with creating a simple installer project in Visual Studio.NET. In your VS environment select File-> New->Project… where you select Setup and Deployment Projects->Setup Wizard. This wizard should take care of some basic installer database events as well as it creates a project environment for editing and expanding that database. One thing to note when you run that wizard is to make sure to select files to be installed, including the device driver files. It is a good habit to put drivers into a separate folder, usually named "Drivers". When done with the wizard, build your installation and make sure it performs flawlessly, in order to install device drivers you need to have them copied to the destination folder first.

Now, when the installer skeleton is ready, we can move on to creating a driver installation script wrapped into a dll. Once again, Visual Studio allows us to create a sample compilable dll skeleton with an appropriate wizard. Create a new Win32 Project, make sure to select DLL in Application settings and unselect Empty project option in the project wizard. Your dll cpp file should now look something like this:

// setup.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"
BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{
    return TRUE;
}

Currently this code does pretty much nothing, while it needs to have some driver installation routine implemented. Before moving on to presenting the code, I shall spend some time explaining what has to be done in general.

Driver Installation Theory

A regular device driver usually consists of two or three files with different extensions: INF, SYS and optionally a CAT file. Sys file represents the driver itself, while inf file contains all the necessary installation information. The CAT file is a catalog file, which actually has an encrypted Microsoft driver signature used to protect OS user from installing unsigned and therefore not trusted drivers. If the installation does not have that CAT file, Windows will pop up a warning screen asking user to confirm the installation. On contrary, having the CAT file available allows to perform a silent installation.

But regardless of the CAT file availability, a number of steps should be performed to programmatically install drivers not having a user to experience the Windows Hardware Installation Wizard. First, the INF file has to be moved into a special folder and the information about it has to be placed into windows database, where it can be accessed by the windows plug and play device installer. Second, after the hardware is enumerated in the device manager, its driver information should be updated so that Windows Plug and Play Manager is able to automatically detect and install the appropriate device driver, as described in its INF file.

Microsoft Windows SDK provides a developer with the standard API for performing these basic steps, called SetupAPI. The functions we are going to use in our installation scripts are pretty much self-explanatory, but I shall present their prototypes anyway:

The SetupCopyOEMInf function copies a specified INF file to the %windir%/Inf directory. A caller of this function is required to have administrative privileges, otherwise the function fails.

BOOL WINAPI SetupCopyOEMInf(
  PCTSTR SourceInfFileName,
  PCTSTR OEMSourceMediaLocation,
  DWORD OEMSourceMediaType,
  DWORD CopyStyle,
  PTSTR DestinationInfFileName,
  DWORD DestinationInfFileNameSize,
  PDWORD RequiredSize,
  PTSTR DestinationInfFileNameComponent
);

Given an INF file and a hardware ID, the UpdateDriverForPlugAndPlayDevices function installs updated drivers for devices that match the hardware ID.

BOOL WINAPI
  UpdateDriverForPlugAndPlayDevices(
    HWND  hwndParent,
    LPCTSTR  HardwareId,
    LPCTSTR  FullInfPath,
    DWORD  InstallFlags,
    PBOOL  bRebootRequired OPTIONAL
    );

Properly applied, theses functions would install any plug and play device on Windows 2000/XP, provided that the user has administrative privileges. The code for the installation has to be put into a separate function, which is assigned an EXPORTS property to be able to call this function directly from the msi installer.

First, let us create that exportable function, I call it CustomFunction:

UINT __stdcall CustomFunction ( MSIHANDLE hModule )
{
    //TODO: Put your SetupAPI routine here
    return ERROR_SUCCESS;
}

In order for that function to be accessible by external programs, a DEF file should be added to your DLL project. In your VS.NET environment go to File->Add New Item and select a "Module-Definition File .def". Make sure that your new DEF file has at least the following code in it:

LIBRARY	CustomAction
EXPORTS
    CustomFunction

Where CustomAction is the name of your DLL project, and CustomFunction is the name of that function we just created.

Also, you will need to open up stdafx.h file and add the following definitions into it:

#include <windows.h>
#include <msi.h>
#include <msiquery.h>
#include <stdio.h>
#include <newdev.h> // for the API UpdateDriverForPlugAndPlayDevices().
#include <setupapi.h> // for SetupDiXxx functions.
#include <malloc.h> // for memory allocation routine

Finally, several libraries have to be correctly declared as well. Go to "Project->Properties->Configuration Properties->Linker->Command Line" and add the following line in the bottom box:

msi.lib Setupapi.lib newdev.lib

Make sure you also point your compiler to the location of these libraries, which is in Microsoft SDK lib/ and include/ folder. In my "Tools->Options->Projects->VC++ Directories" I have added the following three locations:

C:\Microsoft SDK\Lib
C:\Microsoft SDK\include
C:\WINDDK\3790\lib\wxp\i386

Now, you should be able to compile you DLL just fine.

Passing parameters into DLL

As you probably noticed, the first input parameter (SourceInfFileName) of SetupCopyOEMInf function requires the filename of the INF as well as the full path to it. The problem is that at this point we have no way to know what the user would choose as the installation path outside of the DLL. This parameter is generated according to user interaction in the Install Wizard. The simplest way to pass the path into the DLL is to create a Registry key by the wizard and have it read later by the DLL. I assumed that my Install Wizard would create a key named "TARGETDIR" in "HKEY_LOCAL_MACHINE/Software/Manufacturer/Software", also the driver files are located in "TARGETDIR/Drivers" folder. The following code demonstrates how to read a registry key:

HKEY hKey = NULL;
HKEY hSubKey = NULL;
unsigned char* install_directory; // the key value
unsigned long value_length; // the length of the key
unsigned char *target_file;
int ret_value;
ret_value = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
            "Software",0,KEY_QUERY_VALUE,&hKey);
ret_value = RegOpenKeyEx(hKey,"Manufacturer",0,KEY_QUERY_VALUE,&hSubKey);
hKey = hSubKey;
ret_value = RegOpenKeyEx(hKey,"Software",0,KEY_QUERY_VALUE,&hSubKey);
hKey = hSubKey;
if (ret_value == ERROR_SUCCESS) 
{
	// first query value to get size of value
	ret_value = RegQueryValueEx(hKey,"TARGETDIR",NULL,NULL,NULL,&value_length);
	install_directory = (unsigned char*)malloc(value_length);
	target_file = (unsigned char*)malloc(value_length+25); // add some for our filename
   ret_value = RegQueryValueEx(hKey,"TARGETDIR",NULL,NULL,install_directory,&value_length);
	RegCloseKey(hKey);
	RegCloseKey(hSubKey);
	if (ret_value == ERROR_SUCCESS)
	{
		target_file = install_directory; // convert to CString
		strcat((char*)target_file,"\\Drivers\\device.inf");
	}
}

Currently, the correct INF file path is saved into "target_file" variable. That would be the right time to introduce the driver installation routine, in my installation I have two devices defined. The code is to be added after Registry routine described above, right to the same custom function:

PCTSTR szInfFileName = (PCTSTR)target_file; //convert to PCTSTR
PCTSTR szHardwareId1 = "USB\\DEV_0001&PID_0001" ; // USB Device #1, look at your INF file for the correct value 
PCTSTR szHardwareId2 = "USB\\DEV_0001&PID_0002" ; // USB Device #2, look at your INF file for the correct value
PBOOL bRebootRequired = false;
	
if (!SetupCopyOEMInf (szInfFileName,NULL,
        SPOST_PATH,
        SP_COPY_NOOVERWRITE,
        NULL,
        11,
        NULL,
        NULL)) 
	MessageBox(NULL, "Unable to install driver INF file", "Installer failure", MB_OK);
else
{
	if (!(UpdateDriverForPlugAndPlayDevices(0,
		szHardwareId1,
		szInfFileName,
		NULL,
		bRebootRequired))&&
		!(UpdateDriverForPlugAndPlayDevices(0,
		szHardwareId2,
		szInfFileName,
		NULL,
		bRebootRequired)))
	MessageBox(NULL, "Unable to install the device driver", "Installer failure", MB_OK);
}

Well, that should cover the driver installation script wrapped into a custom DLL. Now, before taking care of this DLL, we want to make sure, that the installer updates the Registry with the correct path to the application. To do so, in your Setup project open the Registry Editor and create the keys in the following sequence:

"HKEY_LOCAL_MACHINE/Software/[Manufacturer]/[ProductName]"

Note, that [Manufacturer] and [ProductName] are in square brackets and they are the actual keys, i.e. do not substitute them with your company name and the product title. Within the [ProductName] key create a new "String Value", name it "TARGETDIR" and assign a "[TARGETDIR]" value to it (including square brackets).

Hopefully, the setup project would compile fine now. When it does, it is time to edit the MSI file to call our custom DLL right after the files are copied into their destination folder and the complete path is put into the registry.

Customizing the Windows Installer MSI with Microsoft Orca

Open up the compiled MSI file with Orca. You will see that the MSI file is nothing more than a table containing all sorts of different data used during installation. Rather than spending time explaining that table, I shall show where exactly it should be customized in order to run the DLL. First, we need to change the values of [Manufacturer] and [ProductName] according to what we have defined in the DLL when working with the Registry. To do that, select "Property" in the table and locate "Manufacturer", "ARPCONTACT" and "ProductName" tabs. Their values should look exactly like the ones you have defined in your DLL. In my example I have the following data:

"Manufacturer"	Manufacturer
"ARPCONTACT"	Manufacturer
"ProductName"	Software

Now, select "Binary" table and add a new row to it. Put "CustomDLL" as a name and select the location of the DLL file. Then create a custom action by opening "CustomAction" table and adding a new row with the following information into it:

"Action"	CustomAction01
"Type"		1
"Source"	CustomDLL
"Target"	CustomFunction

Finally, you need to put that CustomAction01 in the correct order in your installation sequence. Open up the "InstallExecuteSequence" table and add a new row with the following information:

"Action"	CustomAction01
"Condition"	Not Installed
"Sequence"	6700

Here I picked the "Sequence" value to be 6700 for my installation, you should assign a number so that your custom action start right after the "InstallFinalize" action, which was at 6600 in my case.

By saving the changes made with Orca, the DLL file will be embedded into your installation , so you do not have to worry about it when installing the files. Your installation is now fully ready to be tested.

As a note, the method described above would install and update the drivers for a particular device, provided that the INF file is correct. Also, the silent installation is only possible if you have your drivers signed with Microsoft, otherwise the confirmaition window will pop up. Finally, if your drivers are not signed and you still want them to by installed automatically, the device should be plugged in BEFORE you call UpdateDriverForPlugAndPlayDevices function, as it only works with already enumerated hardware in the Device Manager.

 
Alexander Volynkin ICQ #: 490622
Last update: 09.18.2004