Ok, so I was able to answer one of my own questions: Is there a way to make Windows notify me of both volume removals?
Yes - even though windows sends only one DBT_DEVTYP_VOLUME
WM_DEVICECHANGE
message, you actually do get notified of both volume removals - but, as always, the answer lies deep down buried in MSDN:
Although the dbcv_unitmask member may specify more than one volume in any message, this does not guarantee that only one message is generated for a specified event. Multiple system components may independently generate messages for logical volumes at the same time.
So, all I had to do was ignore the example function that Microsoft gives in one of their samples,
char FirstDriveFromMask (ULONG unitmask)
{
char i;
for (i = 0; i < 26; ++i)
{
if (unitmask & 0x1)
break;
unitmask = unitmask >> 1;
}
return (i + 'A');
}
And replace it with a function that interprets the mask for all drives affected. So the one message I was getting was indeed for both volumes, and both volume drive letters were available in the mask.
// [IN] ULONG unitmask
// [IN/OUT] char* outDriveLetters - an array of characters to be passed in
// that is filled out with the drive letters
// in the mask (this must be 26 bytes to be safe)
// RETURNS the number of drive letters in the mask
int MaskToDriveLetters (ULONG unitmask, char* outDriveLetters)
{
int cnt = 0;
for (i = 0; i < 26; ++i)
{
if (unitmask & 0x1)
{
outDriveLetters[cnt++] = 'A' + i;
cnt++;
}
unitmask = unitmask >> 1;
}
outDriveLetters[cnt] = 0; // set the last character to \0 (optional)
return cnt; // the number of drives that were set in the mask
}
I still have the other question to answer though - how can the two messages (DBT_DEVTYP_DEVICEINTERFACE
and DBT_DEVTYP_VOLUME
) be correlated?
Serial ports date from the stone age of computing. That's where you plugged in your ASR-33 teletype to start typing in your Fortran program. The electrical interface is very simple. So is the Windows API to use a serial port from your own code. Practically any runtime environment supports them.
USB has replaced serial port hardware completely. It has a much more advanced logical interface to the machine, supporting many different type of devices. And it supports Plug and Play, allowing the operating system to detect when a device is attached or removed as well as automatically installing the device driver, etcetera.
This flexibility comes at a price however, a USB device always needs a device driver to become usable. Device drivers are not created equal. Different drivers require different ways to talk to the device. Usually done through DeviceIoControl() or Read/WriteFile() but those are very opaque API functions. In the early days of USB, device manufacturers would supply a DLL that provided a rich API to hide the implementation details.
That did not work so well, manufacturers are not very good at writing good APIs and they sure don't like to support them. So a good solution would be to support a standard API, one that's available on any machine, supported by any runtime, documented and maintained by somebody else. Like the serial port API.
That did not work so well, manufacturers are not very good at writing device drivers that emulate serial ports. The biggest hang-up with the API is that it doesn't have any support for Plug and Play. The core support for it is missing, after all serial port hardware doesn't have the logical interface to support it. There is some support for detecting that a device is attached through the DTR hardware handshake line, but no support whatsoever for detecting that the port is no longer there.
Detaching the USB device is the problem. In an ideal world, the emulator built into the device driver would simply pretend that the serial port is still there until the last handle on the device is closed. That would be the logical implementation, given that there's no way to trigger a Plug and Play event. For some strange reason that seems to be difficult to implement. Most USB drivers take the crummy shortcut, they simply make the device disappear even while it is in use.
This plays havoc on any user mode code that uses the device. Which is typically written to assume it is a real serial port and real serial ports don't suddenly disappear. At least not without drawing a bright blue spark. What goes wrong is pretty unpredictable because it depends on how the driver responds to requests on a device that's no longer there. An uncatchable exception in a worker thread started by SerialPort was a common mishap. Sounds like your driver really gets it wrong, it generates an error return code on the MJ_CLOSE driver request. Which is kind of a logical thing to do for a driver, after all the device isn't there anymore, but quite unsolvable from your end. You have a handle and you can't close it. That's up a creek with no paddle.
Every major release of .NET had a small patch to the SerialPort classes to try to minimize the misery a bit. But there's a limited amount that Microsoft can do, catching all errors and pretending they didn't happen ultimately leads to class that provides no good diagnostic anymore, even with a good driver.
So practical approaches are:
- always use the Remove Hardware Safely tray icon in Windows
- use the latest version of .NET
- contact the vendor and ask for a driver update
- ditch vendors that supply lousy drivers
- tell your users that, just because it is the only thing you can do with a USB device, that unplugging it doesn't solve any problems
- make closing the port easy and accessible in your UI
- glue the USB connector to the port so it can't be removed
The 5th bullet is also what gets programmers in trouble. Writing serial port code isn't easy, it is heavily asynchronous and the threadpool thread that runs the DataReceived event is difficult to deal with. When you can't diagnose the software problem you tend to blame the hardware. There's very little you can do with the hardware but unplug it. Bad Idea. Now you have two problems.
Best Answer
I had the same problems with .NET 2.0 and I reverted to using the FTDI dll wrapper: http://www.ftdichip.com/Support/SoftwareExamples/CodeExamples/CSharp/FTD2XX_NET_1010.zip
Works very well, and you have access to control the real goodies of the driver, such as programmatically setting the latency timer.
I wrote my own class to detect unplug events, by catching the FT_IO_ERROR event, when trying to read the data. This is not entirely satisfying, by catching an error and claiming it is an unplug. But it works.
I have been talking to FTDI about this and recently they released a new application note: http://ftdichip.com/Support/Documents/AppNotes/AN_152_Detecting_USB_%20Device_Insertion_and_Removal.pdf
That uses the WM_DEVICECHANGE event to detect USB unplugs. Also works, but there is a small catch. Because it is a window message, it only works if you have a GUI and there are cases where the event will not arrive at your application, if another application is running and catches the event before you can process it.
The final option is to use WMI to do the detection.You can use the ManagementEventWatcher to create a listener on create and delete. Also got this to work, however there were some USB ports on my laptop where the FTDI driver would not give the correct COM port and location ID back (actually, just nothing at all) and that is the info which I could read from WMI, so I could not link a WMI event to the connected device in the DLL wrapper.
I reported this issue to FTDI in june/july 2010 and supposedly the GetCOMportNumber and Location ID issues are fixed in their 2.08.02 driver version (august), but I have not had the time to recheck this.
So far my experiences with the USB hell....