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:
#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 )
{
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>
#include <setupapi.h>
#include <malloc.h>
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;
unsigned long value_length;
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)
{
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);
ret_value = RegQueryValueEx(hKey,"TARGETDIR",NULL,NULL,install_directory,&value_length);
RegCloseKey(hKey);
RegCloseKey(hSubKey);
if (ret_value == ERROR_SUCCESS)
{
target_file = install_directory;
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;
PCTSTR szHardwareId1 = "USB\\DEV_0001&PID_0001" ;
PCTSTR szHardwareId2 = "USB\\DEV_0001&PID_0002" ;
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.