You can enhance the end user experience of your hardware by providing an application that starts whenever one of your devices exists. Microsoft provides a special-purpose mechanism for still-image cameras but hasn't provided a general-purpose mechanism that other devices can use. I'll describe just such a mechanism, named AutoLaunch, in this section.
Windows 98 and Windows 2000 both provide for notifications to applications when hardware events occur. Microsoft Windows 95 introduced the WM_DEVICECHANGE message. As originally conceived for Windows 95, the system broadcasts this message in user mode to all top-level windows for each of several possible device events.
Building on WM_DEVICECHANGE, Windows 2000 generates notifications to interested service applications whenever a device driver enables or disables a registered device interface. I wrote an AutoLaunch service to take advantage of these notifications. The service subscribes for notifications about a special interface GUID by calling a new user-mode API named RegisterDeviceNotification:
#include <dbt.h> DEV_BROADCAST_DEVICEINTERFACE filter = {0}; filter.dbcc_size = sizeof(filter); filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; filter.dbcc_classguid = GUID_AUTOLAUNCH_NOTIFY; HDEVNOTIFY hNotification = RegisterDeviceNotification(hService, (PVOID) &filter, DEVICE_NOTIFY_SERVICE_HANDLE); |
To receive the interface notifications, the service must initialize by calling RegisterServiceCtrlHandlerEx instead of RegisterServiceCtrlHandler in its ServiceMain function:
hService = RegisterServiceCtrlHandlerEx(<svcname>, HandlerEx, <context>); |
When you call RegisterServiceCtrlHandlerEx, you specify a HandlerEx event handler function that receives three more parameters than a standard service Handler function:
DWORD _ _stdcall HandlerEx(DWORD ctlcode, DWORD evtype, PVOID evdata, PVOID context) { } |
In the situation we're concerned with here, ctlcode will equal SERVICE_CONTROL_DEVICEEVENT, evtype will equal DBT_DEVICEARRIVAL, and evdata will be the address of a device interface broadcast structure. The context parameter will be whatever value you specified as the third argument to RegisterServiceCtrlHandlerEx.
The device interface broadcast structure looks like this:
struct _DEV_BROADCAST_DEVICEINTERFACE_W { DWORD dbcc_size; DWORD dbcc_devicetype; DWORD dbcc_reserved; GUID dbcc_classguid; WCHAR dbcc_name[1]; }; |
The dbcc_devicetype value will be DBT_DEVTYP_DEVICEINTERFACE. The dbcc_classguid will be the 128-bit interface GUID that some device driver enabled or disabled, and the dbcc_name will be the symbolic link name you can use to open a handle to the device. This particular structure comes in both ANSI and Unicode versions. The service notification always uses the Unicode version, even if your service happens to have been built, as AutoLaunch is, using ANSI.
To trigger a device interface arrival notification to AutoLaunch, a driver simply has to register and enable an interface by using the AutoLaunch GUID:
typedef struct _DEVICE_EXTENSION { ... UNICODE_STRING AutoLaunchInterfaceName; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; NTSTATUS AddDevice(...) { ... IoRegisterDeviceInterface(pdo, &GUID_AUTOLAUNCH_NOTIFY, NULL, &pdx->AutoLaunchInterfaceName); ... } NTSTATUS StartDevice(PDEVICE_OBJECT fdo, ...) { ... IoSetDeviceInterfaceState(&pdx->AutoLaunchInterfaceName, TRUE); ... } |
I discussed device interfaces in Chapter 2 as a method of giving a name to a device so that an application could find the device and open a handle to it. A single device can register as many interfaces as make sense. In this particular situation, you would register an AutoLaunch interface in addition to any interfaces that you might support. The only purpose of the AutoLaunch interface is to generate the notification for which the service is waiting.
When your driver enables its GUID_AUTOLAUNCH_NOTIFY interface, the system sends the AutoLaunch service a device arrival notification, which the service processes in this function:
1 2 |
DWORD CAutoLaunch::HandleDeviceChange(DWORD evtype, _DEV_BROADCAST_HEADER* dbhdr) { if (!dbhdr || evtype != DBT_DEVICEARRIVAL || dbhdr->dbcd_devicetype != DBT_DEVTYP_DEVICEINTERFACE) return 0; PDEV_BROADCAST_DEVICEINTERFACE_W p = (PDEV_BROADCAST_DEVICEINTERFACE_W) dbhdr; CString devname = p->dbcc_name; HDEVINFO info = SetupDiCreateDeviceInfoList(NULL, NULL); SP_DEVICE_INTERFACE_DATA ifdata = {sizeof(SP_DEVICE_INTERFACE_DATA)}; SP_DEVINFO_DATA devdata = {sizeof(SP_DEVINFO_DATA)}; SetupDiOpenDeviceInterface(info, devname, 0, &ifdata); SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, NULL, &devdata); OnNewDevice(devname, info, &devdata); SetupDiDestroyDeviceInfoList(info); return 0; } |
My OnNewDevice function is going to spawn a new process to perform whatever command line it finds in the registry. It was most convenient to use the device's hardware key as a repository for the command line. The code to do this is as follows:
1 2 3 4 5 6 7 8 |
void CAutoLaunch::OnNewDevice(const CString& devname, HDEVINFO info, PSP_DEVINFO_DATA devdata) { HKEY hkey = SetupDiOpenDevRegKey(info, devdata, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); DWORD junk; TCHAR buffer[_MAX_PATH]; DWORD size = sizeof(buffer); CString Command; RegQueryValueEx(hkey, "AutoLaunch", NULL, &junk, (LPBYTE) buffer, &size); Command = buffer; CString FriendlyName; SetupDiGetDeviceRegistryProperty(info, devdata, SPDRP_FRIENDLYNAME, NULL, (PBYTE) buffer, sizeof(buffer), NULL); FriendlyName.Format(_T("\"%s\""), buffer); RegCloseKey(hkey); ExpandEnvironmentStrings(Command, buffer, arraysize(buffer)); CString name; name.Format(_T("\"%s\""), (LPCTSTR) devname); Command.Format(buffer, (LPCTSTR) name, (LPCTSTR) FriendlyName); STARTUPINFO si = {sizeof(STARTUPINFO)}; si.lpDesktop = "WinSta0\\Default"; si.wShowWindow = SW_SHOW; PROCESS_INFORMATION pi; CreateProcess(NULL, (LPTSTR) (LPCTSTR) Command, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } |
The process I just described works great in a steady-state situation, where the AutoLaunch service is already up and running on a computer when a device comes along and tries to launch a special application. Two other situations need to be dealt with, though.
First, devices that are already plugged in when the system is bootstrapped will manage to register their GUID_AUTOLAUNCH_NOTIFY interfaces before the service manager starts up the AutoLaunch service. Yet, you still (presumably) want the AutoLaunch applications to start too.
AutoLaunch deals with this startup issue by enumerating all instances of the interface when it first starts:
VOID CAutoLaunch::EnumerateExistingDevices(const GUID* guid) { HDEVINFO info = SetupDiGetClassDevs(guid, NULL, NULL, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE); SP_INTERFACE_DEVICE_DATA ifdata; ifdata.cbSize = sizeof(ifdata); DWORD devindex; for (devindex = 0; SetupDiEnumDeviceInterfaces(info, NULL, guid, devindex, &ifdata); ++devindex) { DWORD needed; SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL); PSP_INTERFACE_DEVICE_DETAIL_DATA detail = (PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc(needed); detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA); SP_DEVINFO_DATA devdata = {sizeof(SP_DEVINFO_DATA)}; SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail, needed, NULL, &devdata); CString devname = detail->DevicePath; free((PVOID) detail); OnNewDevice(devname, guid); } } |
The only interesting lines of code in this whole function are the ones in bold face, where we obtain the necessary SP_DEVINFO_DATA structure and symbolic link name. We then call OnNewDevice (the function you've already seen) to deal with this pre-existing device.
The second startup situation you have to deal with is when your device is being installed for the first time onto a machine that's never seen the AutoLaunch service before. Your INF file needs to define the AutoLaunch service and copy the service binary file onto the end user computer. It can add a registry entry to the so-called RunOnce key to trigger the service. For example:
[DestinationDirs] AutoLaunchCopyFiles=10 [etc.] [DriverInstall.NT] CopyFiles=DriverCopyFiles,AutoLaunchCopyFiles AddReg=DriverAddReg.NT [DriverAddReg.NT] HKLM,%RUNONCEKEYNAME%,AutoLaunchStart,,\ "rundll32 StartService,StartService AutoLaunch" [DriverInstall.NT.Services] AddService=AutoLaunch,,AutoLaunchService [etc.] [AutoLaunchCopyFiles] AutoLaunch.exe,,,0x60 StartService.exe,,,0x60 [AutoLaunchService] ServiceType=16 StartType=2 DisplayName="AutoLaunch Service" ErrorControl=1 ServiceBinary=%10%\AutoLaunch.exe [Strings] RUNONCEKEYNAME="Software\Microsoft\Windows\CurrentVersion\RunOnce" |
Refer to the DEVICE.INF in the SYS subdirectory of the AUTOLAUNCH sample for the full picture.
After the installation of your device finishes, the system executes any commands that are within the RunOnce registry key. The command we put there starts the AutoLaunch service if it's not already running. Note that STARTSERVICE.DLL is a tiny DLL I wrote that starts a service without displaying any user interface or popping up a dialog box. You'll want to use RUNDLL32 as the command verb in the RunOnce value so that it will work correctly with a remote install of your driver package.
NOTE
Microsoft Knowledge Base article Q173039 suggests that the immediate-processing behavior of entries in the RunOnce key is essentially a side effect of a call to RUNDLL32. One of the Microsoft developers responsible for the device installer has assured me that the RunOnce values are always processed at the conclusion of installing a new device, regardless of what this article says.