Detect Device Insertion

Introduction

The requirement: Know when a device (a USB stick, a CD etc.) has been inserted

Discussion

It is possible to do this with WMI (cf the Win32_DeviceChangeEvent extrinsic event) which wraps the WM_DEVICECHANGE event under the covers but, at least on XP, this doesn't include the useful information such as which device changed. There is shell functionality to do this kind of thing, but it also needs a top level window. Hopefully I get the chance to run up an example later. So you're left with handling the WM_DEVICECHANGE event yourself from a top-level window, or runing as a service and trapping certain events within the service control loop.

The advantage of the former approach is that you get relatively straightforward notification, through the standard Windows message mechanism, of a whole slew of different events with useful related data. The disadvantage is that you have to keep a top-level window hanging around. In my example the window's hidden even that may be too much for you depending on security policies, requirement to run when no users are logged on etc.

The latter approach can be used in a service as it sends notifications to the service control loop. The downside is that the docs pretty much exclude your being able to do anything which you can do anyway via the Windows message loop. So you just get a device id which you then have to work from. YM, as they say, MV.

FWIW, the second approach can work equally well with a top-level window. See the pywin32 demo referred to below for an example.

The Code

With a Top-level Window

import win32api, win32con, win32gui
from ctypes import *

#
# Device change events (WM_DEVICECHANGE wParam)
#
DBT_DEVICEARRIVAL = 0x8000
DBT_DEVICEQUERYREMOVE = 0x8001
DBT_DEVICEQUERYREMOVEFAILED = 0x8002
DBT_DEVICEMOVEPENDING = 0x8003
DBT_DEVICEREMOVECOMPLETE = 0x8004
DBT_DEVICETYPESSPECIFIC = 0x8005
DBT_CONFIGCHANGED = 0x0018

#
# type of device in DEV_BROADCAST_HDR
#
DBT_DEVTYP_OEM = 0x00000000
DBT_DEVTYP_DEVNODE = 0x00000001
DBT_DEVTYP_VOLUME = 0x00000002
DBT_DEVTYPE_PORT = 0x00000003
DBT_DEVTYPE_NET = 0x00000004

#
# media types in DBT_DEVTYP_VOLUME
#
DBTF_MEDIA = 0x0001
DBTF_NET = 0x0002

WORD = c_ushort
DWORD = c_ulong

class DEV_BROADCAST_HDR (Structure):
  _fields_ = [
    ("dbch_size", DWORD),
    ("dbch_devicetype", DWORD),
    ("dbch_reserved", DWORD)
  ]

class DEV_BROADCAST_VOLUME (Structure):
  _fields_ = [
    ("dbcv_size", DWORD),
    ("dbcv_devicetype", DWORD),
    ("dbcv_reserved", DWORD),
    ("dbcv_unitmask", DWORD),
    ("dbcv_flags", WORD)
  ]

def drive_from_mask (mask):
  n_drive = 0
  while 1:
    if (mask & (2 ** n_drive)): return n_drive
    else: n_drive += 1

class Notification:

  def __init__(self):
    message_map = {
      win32con.WM_DEVICECHANGE : self.onDeviceChange
    }

    wc = win32gui.WNDCLASS ()
    hinst = wc.hInstance = win32api.GetModuleHandle (None)
    wc.lpszClassName = "DeviceChangeDemo"
    wc.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW;
    wc.hCursor = win32gui.LoadCursor (0, win32con.IDC_ARROW)
    wc.hbrBackground = win32con.COLOR_WINDOW
    wc.lpfnWndProc = message_map
    classAtom = win32gui.RegisterClass (wc)
    style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU
    self.hwnd = win32gui.CreateWindow (
      classAtom,
      "Device Change Demo",
      style,
      0, 0,
      win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT,
      0, 0,
      hinst, None
    )

  def onDeviceChange (self, hwnd, msg, wparam, lparam):
    #
    # WM_DEVICECHANGE:
    #  wParam - type of change: arrival, removal etc.
    #  lParam - what's changed?
    #    if it's a volume then...
    #  lParam - what's changed more exactly
    #
    dev_broadcast_hdr = DEV_BROADCAST_HDR.from_address (lparam)

    if wparam == DBT_DEVICEARRIVAL:
      print "Something's arrived"

      if dev_broadcast_hdr.dbch_devicetype == DBT_DEVTYP_VOLUME:
        print "It's a volume!"

        dev_broadcast_volume = DEV_BROADCAST_VOLUME.from_address (lparam)
        if dev_broadcast_volume.dbcv_flags &; DBTF_MEDIA:
          print "with some media"
          drive_letter = drive_from_mask (dev_broadcast_volume.dbcv_unitmask)
          print "in drive", chr (ord ("A") + drive_letter)

    return 1

if __name__=='__main__':
  w = Notification ()
  win32gui.PumpMessages ()

Without a Top-level Window

If you don't have or want a top-level window you do have another option: catching certain events from with a service using the RegisterDeviceNotification API. This has certain caveats; the most notable is that the only events you can trap are DBT_DEVTYP_DEVICEINTERFACE or DBT_DEVTYP_HANDLE. You won't get a volume event this way. Also, although the good people behind the pywin32 modules have implemented all the interface stuff you need for this, and even provided two demos: with a window and with a service, some of the support code in win32gui_struct isn't quite right. Still, with a tiny bit of monkey-patching and judicious use of the ctypes library, we can have ourselves a working example.
I have linked to those examples on the pywin32 CVS browser, but they are installed as part of the pywin32 installation. From the site-packages directory, look for: win32/Demos/win32gui_devicenotify.py and win32/Demos/service/serviceEvents.py.

This code below is ripped almost straight out of the service example above, so I claim even less than the usual credit. Standing on the shoulders, etc., etc... All I've done is to trim it down a little to cope with device insertion / removal and to pick up the device name correctly. I've also edited the layout to match my house style. (Which was a bit of a liberty but there you go).

import win32serviceutil
import win32service
import win32event
import servicemanager

import win32gui
import win32gui_struct
struct = win32gui_struct.struct
pywintypes = win32gui_struct.pywintypes
import win32con

GUID_DEVINTERFACE_USB_DEVICE = "{A5DCBF10-6530-11D2-901F-00C04FB951ED}"
DBT_DEVICEARRIVAL = 0x8000
DBT_DEVICEREMOVECOMPLETE = 0x8004

import ctypes

#
# Cut-down clone of UnpackDEV_BROADCAST from win32gui_struct, to be
# used for monkey-patching said module with correct handling
# of the "name" param of DBT_DEVTYPE_DEVICEINTERFACE
#
def _UnpackDEV_BROADCAST (lparam):
  if lparam == 0: return None
  hdr_format = "iii"
  hdr_size = struct.calcsize (hdr_format)
  hdr_buf = win32gui.PyGetMemory (lparam, hdr_size)
  size, devtype, reserved = struct.unpack ("iii", hdr_buf)
  # Due to x64 alignment issues, we need to use the full format string over
  # the entire buffer.  ie, on x64:
  # calcsize('iiiP') != calcsize('iii')+calcsize('P')
  buf = win32gui.PyGetMemory (lparam, size)

  extra = {}
  if devtype == win32con.DBT_DEVTYP_DEVICEINTERFACE:
    fmt = hdr_format + "16s"
    _, _, _, guid_bytes = struct.unpack (fmt, buf[:struct.calcsize(fmt)])
    extra['classguid'] = pywintypes.IID (guid_bytes, True)
    extra['name'] = ctypes.wstring_at (lparam + struct.calcsize(fmt))
  else:
    raise NotImplementedError("unknown device type %d" % (devtype,))
  return win32gui_struct.DEV_BROADCAST_INFO(devtype, **extra)
win32gui_struct.UnpackDEV_BROADCAST = _UnpackDEV_BROADCAST

class DeviceEventService (win32serviceutil.ServiceFramework):

  _svc_name_ = "DevEventHandler"
  _svc_display_name_ = "Device Event Handler"
  _svc_description_ = "Handle device notification events"

  def __init__(self, args):
    win32serviceutil.ServiceFramework.__init__ (self, args)
    self.hWaitStop = win32event.CreateEvent (None, 0, 0, None)
    #
    # Specify that we're interested in device interface
    # events for USB devices
    #
    filter = win32gui_struct.PackDEV_BROADCAST_DEVICEINTERFACE (
      GUID_DEVINTERFACE_USB_DEVICE
    )
    self.hDevNotify = win32gui.RegisterDeviceNotification (
      self.ssh, # copy of the service status handle
      filter,
      win32con.DEVICE_NOTIFY_SERVICE_HANDLE
    )

  #
  # Add to the list of controls already handled by the underlying
  # ServiceFramework class. We're only interested in device events
  #
  def GetAcceptedControls(self):
    rc = win32serviceutil.ServiceFramework.GetAcceptedControls (self)
    rc |= win32service.SERVICE_CONTROL_DEVICEEVENT
    return rc

  #
  # Handle non-standard service events (including our device broadcasts)
  # by logging to the Application event log
  #
  def SvcOtherEx(self, control, event_type, data):
    if control == win32service.SERVICE_CONTROL_DEVICEEVENT:
      info = win32gui_struct.UnpackDEV_BROADCAST(data)
      #
      # This is the key bit here where you'll presumably
      # do something other than log the event. Perhaps pulse
      # a named event or write to a secure pipe etc. etc.
      #
      if event_type == DBT_DEVICEARRIVAL:
        servicemanager.LogMsg (
          servicemanager.EVENTLOG_INFORMATION_TYPE,
          0xF000,
          ("Device %s arrived" % info.name, '')
        )
      elif event_type == DBT_DEVICEREMOVECOMPLETE:
        servicemanager.LogMsg (
          servicemanager.EVENTLOG_INFORMATION_TYPE,
          0xF000,
          ("Device %s removed" % info.name, '')
        )

  #
  # Standard stuff for stopping and running service; nothing
  # specific to device notifications
  #
  def SvcStop(self):
    self.ReportServiceStatus (win32service.SERVICE_STOP_PENDING)
    win32event.SetEvent (self.hWaitStop)

  def SvcDoRun(self):
    win32event.WaitForSingleObject (self.hWaitStop, win32event.INFINITE)
    servicemanager.LogMsg (
      servicemanager.EVENTLOG_INFORMATION_TYPE,
      servicemanager.PYS_SERVICE_STOPPED,
      (self._svc_name_, '')
    )

if __name__=='__main__':
  win32serviceutil.HandleCommandLine (DeviceEventService)

Unfortunately, the complications don't end there. If you've got one of those 4-way card readers which you put, eg, SD cards into, it will "pre-register" the four devices it's going to expose so inserting and removing media doesn't fire a device change event. If that happens, you're down to monitoring disks to watch when media is inserted. This can be done via WMI and is fairly convenient, but when all's said and done it's just polling on your behalf.