[Previous] [Next]

Launching an Application

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.

The AutoLaunch Service

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.

Triggering AutoLaunch

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;
  }

  1. There are other notifications besides the ones we're interested in. Some of them are queries. Returning 0 is how we indicate success or acquiescence to some query we don't specifically process. In fact, the real AUTOLAUNCH sample on the disc handles the DBT_DEVICEREMOVECOMPLETE notification too so that it can keep track of which arrival notifications it's already processed and avoid duplication during system startup. I left that detail out here to avoid clutter.
  2. I built the AutoLaunch sample without UNICODE. This statement therefore converts the UNICODE linkname in the notification structure to ANSI.

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);
  }

  1. This statement opens the Device Parameters subkey of the device's hardware registry key.
  2. The INF file put an AutoLaunch value in the registry. We read that value here.
  3. Here we fetch the FriendlyName of the device for use as a command line argument. There might be blanks in the name, so we want to put quotes around it before submitting the command.
  4. I wanted to allow the command line template in the registry to include environment variables surrounded by % characters. This statement expands the environment strings.
  5. I also wanted the command line template to use a %s escape to indicate where the device name and friendly name belong. This statement produces a command line with the substitution taken care of.
  6. We're about to call CreateProcess to execute the command. Unless we're careful, the command will use the same hidden desktop as our own service process, which is not going to be very useful to the end user! So we create a STARTUPINFO structure that specifies the interactive session desktop.
  7. Here's where we actually launch the application whose name we found in the registry. CreateProcess returns right away; the application lives on until someone closes it.
  8. CreateProcess also gives us handles to the process and its initial thread. We need to close those handles, or else the process and thread will never go away.

Chickens and Eggs

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.

Getting the Service Running

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.