User Mode USB Device Arbitration
Last Updated: (6-Feb-2002) for MacOS X 10.2 and later.

Introduction

This document describes a new feature in Mac OS X 10.1: USB Device Arbitration.  It is aimed at developers writing user-mode USB drivers/applications (of which Classic is a notable example).  The basic principles described herein also apply to kernel drivers although some of the details of the implementation differ (e.g., use of the ::message() method instead of General Interest Notification.)

Background / The Problem

The Mac OS 8.x/9.x USB stack was implemented with the expectation that it had exclusive access to the USB bus and the devices connected to it.  As a result, any application that wanted to communicate with any USB device would have to go through that API.  This same code runs inside Classic on top of Mac OS X, but that assumption is no longer true: the Mac OS X kernel now "owns" the hardware and non-Classic applications can communicate with USB devices without going through the Classic USB API.

One consequence of this is that Classic will "acquire" all USB devices for which it might have a driver as soon as it sees them.  It acquires them by calling USBDeviceOpen() and USBInterfaceOpen() which in turn prevents other user-mode applications from opening them.  If some other application tries to open a device/interface that Classic has opened, it will get a kIOReturnExclusiveAccess error and will not be allowed to communicate with the device.

This causes problems for developers trying to develop Mac OS X versions of their drivers and applications.

What devices will be acquired by Classic?  Any device that fits any of the following criteria (or has an interface that fits any of the following criteria):

deviceClass == 255 (vendor-specific)
deviceClass == 0 && interfaceClass == 16 (composite device with an interface of class 16)
interfaceClass == 255 (vendor-specific)
interfaceClass == 7 (printer)
interfaceClass == 0 && interfaceSubclass == 0 (deprecated vendor-specific)

Workarounds

The only reasonable workarounds in Mac OS X 10.0.x are to not run Classic if you are attempting to access one of these kinds of devices from a non-Classic application, or to launch your non-Classic application before launching Classic.  Shutting down Classic will also cause it to release its hold on these devices.

USB Device Arbitration Mechanisms

There are two mechanisms for USB device arbitration that will be available starting in the 10.1 release of Mac OS X.

USB Device Arbitration Mechanism #1: Static

The first mechanism is more static and is intended for devices where you would like to prevent Classic from ever opening the device at all, or for forcing Classic to open it even though it might not do so otherwise.  This mechanism works via a "skeleton" kext that contains no code but has a personality to match each USB device you're interested in.

All it does is cause a Boolean property "ClassicMustNotSeize" or "ClassicMustSeize" to be set to "True" ("Yes") on the IOUSBDevice or IOUSBInterface node in the IORegistry inside the kernel.  Classic looks for these properties when iterating through the devices on the system.

You might be tempted to set the property programmatically, but you need to set it via a kext if you want to be sure to avoid the race condition that will occur between Classic and your code -- Classic might open the device before your code gets a chance to set the property.  Another thing to keep in mind is that the IORegistry is never stored persistently and thus any properties you add to a node will not exist after a reboot.  An example of this technique is included in the Mac OS X USB DDK.

You can set the property on either the device or, starting with MacOS X 10.2, one of the interfaces on the device that you are interested in.  If you set the property on an interface, the entire device will be affected; there is no per-interface granularity; it is simply another way to communicate your intentions for the entire device.

Classic considers interfaces as well as devices in order to make it easier to write your "skeleton" kext.  If your device is a Composite device (deviceClass == 0), Mac OS X IOUSBFamily's Composite Driver will match against your device "first" and prevent your 'Seize property from being considered, unless you also set the "OSBundleRequired" property to "Root" so that your kext loads earlier (see the Inside Macintosh document "Loading Kernel Extensions at Boot Time").  This is probably not a good use of the OSBundleRequired mechanism.  If the property is set on an interface, your 'Seize property will be added to the registry even if it refers to a Composite device.

The following is an example of the kind of Info.plist text file you might place in your "skeleton" kext.  You can edit it manually or use the PropertyListEditor tool.  The values for idProduct, idVendor, bConfigurationValue and bInterfaceNumber are used for matching against an interface; only idProduct and idVendor are used for matching against a device.  You may wish to use the IORegistryExplorer tool, observing the "IOService" service plane with your device connected, for help in determining these values.

To test your "skeleton" kext, be sure to delete the cache file /System/Library/Extensions.mkext and then reboot.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
<dict>
	   <key>CFBundleDevelopmentRegion</key>
   <string>English</string>
	   <key>CFBundleIdentifier</key>
   <string>com.apple.driver.test</string>
	   <key>CFBundleInfoDictionaryVersion</key>
	   <string>6.0</string>
	   <key>CFBundlePackageType</key>
	   <string>KEXT</string>
	   <key>CFBundleSignature</key>
	   <string>????</string>
	   <key>CFBundleVersion</key>
	   <string>1.5.1</string>
	   <key>IOKitPersonalities</key>
   <dict>
	      <key>TestSeizeOrUnseizeAnInterface</key>
	      <dict>
	         <key>CFBundleIdentifier</key>
	         <string>com.apple.driver.AppleUSBMergeNub</string>
	         <key>IOClass</key>
	         <string>AppleUSBMergeNub</string>
	         <key>IOProviderClass</key>
	         <string>IOUSBInterface</string>
	         <key>IOProviderMergeProperties</key>
	         <dict>
	            <key>ClassicMustNotSeize or ClassicMustSeize</key>
            <true/>	         </dict>	         <key>idProduct</key>
         <integer>NumberInDecimal</integer>	         <key>idVendor</key>
	         <integer>NumberInDecimal</integer>	         <key>bConfigurationValue</key>	         <integer>NumberInDecimal, often 1</integer>
	         <key>bInterfaceNumber</key>
	         <integer>NumberInDecimal, often 0</integer>
      </dict>
   </dict>
	   <key>OSBundleLibraries</key>
	   <dict>
	      <key>com.apple.driver.AppleUSBMergeNub</key>
	      <string>1.8.3b1</string>
	      <key>com.apple.iokit.IOUSBFamily</key>	      <string>1.8</string>
	   </dict>
</dict>
</plist>

USB Device Arbitration Mechanism #2: Dynamic

The second mechanism is more dynamic and is intended for devices where you would like Classic and non-Classic applications to be able to “take turns” accessing the device.  This mechanism is more complex than the first mechanism but is also more flexible.

A note about Devices vs. Interfaces: everything in this section applies symmetrically to Devices and Interfaces.  I will write Open() to stand for any of USBDeviceOpen(), USBInterfaceOpen(), USBDeviceOpenSeize(), or USBInterfaceOpenSeize().  Similarly, if I write Close() then it could stand for USBDeviceClose() or USBInterfaceClose().

Registering for General Interest Notifications is very similar for registering for Service Matching notifications, which you are already doing in order to find Devices or Interfaces of interest.  Here is the prototype for the function you use to do this:

#include <IOKit/IOKitLib.h>
#include <IOKit/IOKitKeys.h>

kern_return_t
IOServiceAddInterestNotification(
	IONotificationPortRef	notifyPort,	io_service_t	service,
	const io_name_t	interestType,
	IOServiceInterestCallback 	callback,
	void *	refCon,
	io_object_t *	notification );

The notifyPort is the same one you used to register for Service Matching notifications.  The service is the io_service_t  representing the IOUSBDevice or IOUSBInterface.  We’ll be using interestType = kIOGeneralInterest, which is defined in IOKitKeys.h.  The callback is your callback function whose prototype matches the one below.  The refCon will be passed to your callback function each time it is invoked.  (One use for this, for example, might be a pointer to your record describing the device/interface.  If you do this, be careful to validate the pointer somehow before using it!)  The notification is an output parameter that represents the notification.  If you want to dispose of the notification (i.e., because you no longer wish to receive these notifications) then invoke IOObjectRelease() on it.

typedef void
(*IOServiceInterestCallback)(
	void *	refcon,
	io_service_t	service,
	natural_t	messageType,
	void *	messageArgument );

The refcon and the service are just the ones you supplied when you registered for notifications.  The messageType and messageArgument is one of the ones listed below, and described in detail in what follows.

#include <IOKit/Message.h>

kIOMessageServiceIsAttemptingOpen
kIOMessageServiceIsRequestingClose (kernel only)
kIOMessageServiceWasClosed

USBDeviceOpen()/USBInterfaceOpen() -> AttemptingOpen(0)

When someone (including your own code) calls Open(), your callback will be called with messageType == kIOMessageServiceIsAttemptingOpen and messageArgument == 0.  Classic listens for this message in order to be a "good citizen" but other applications may want to release devices only if the extra argument is non-zero, in response to the more serious USBDeviceOpenSeize() or USBInterfaceOpenSeize().  Before releasing a device in response to this message Classic first checks an "activity timer" which it keeps for each device.  If no I/O has been done to/from the device in the last five (5) seconds, then Classic will release the device.  From inside Classic it will appear as though the device had been unplugged.

USBDeviceOpenSeize()/USBInterfaceOpenSeize() -> AttemptingOpen(1)

This works just the same as Open(), except the kIOMessageServiceIsRequestingClose message gets sent to whatever kernel driver is holding the IOUSBDevice/Interface object open instead of the kIOMessageServiceIsAttemptingOpen message.  Outside the kernel, a user-mode application which has registered for General Interest Notifications on this IOUSBDevice/Interface will only receive a callback with messageType == kIOMessageServiceIsAttemptingOpen and messageArgument != 0.  The way to distinguish whether Open() or OpenSeize() was called is to check the messageArgument.    We use this distinction in order to distinguish between two levels of "seriousness".  A casual request to open a device or interface should be treated differently than a "seize" request.  You should use the ...Seize() variation of these calls only when you're sure you want the application that's holding the device/interface open to release it as soon as possible, without regard for any ongoing I/O.  If your kernel driver receives a kIOMessageServiceIsRequestingClose message then it should close the IOUSBDevice/Interface as soon as possible (if possible).  The same is true for a user-mode driver that receives a General Interest Notification callback with messageType == kIOMessageServiceIsAttemptingOpen and messageArgument != 0.

USBDeviceClose()/USBInterfaceClose() -> WasClosed

When the IOUSBDevice/Interface is closed, your callback will be called with the kIOMessageServiceWasClosed message.  If you recently got an kIOReturnExclusiveAccess error this is your signal to re-try the Open() that failed.  For example, Classic listens for this message after releasing a device (in respond to an kIOMessageServiceIsAttemptingOpen message) to know when to attempt to reacquire the device.  When it reacquires the device it simulates a hot plug so that from inside Classic it appears that the device has been plugged in.

Warning: be careful to ignore messages that you caused to be sent.  Set a flag or increment a counter when you Open or Close.  That way you'll know when the next AttemptingOpen or WasClosed message arrives that you were the one who caused it to be sent.  Otherwise you could go into an infinite loop arbitrating access to the device between yourself and yourself.

Scenarios

Consider a vendor-specific USB device that has been noticed by Classic.  And imagine that you have a user-mode application or driver that would like to access the device from outside Classic.  The first time this entity attempts to USBDeviceOpen() (similarly, USBDeviceOpenSeize(), USBInterfaceOpen(), or USBInterfaceOpenSeize()) it will get a kIOReturnExclusiveAccess error.  Shortly thereafter, Classic will USBDeviceClose() the device.  To applications running inside Classic, the device will appear to have been unplugged.  At this point, anyone can attempt the USBDeviceOpen() again and it ought to succeed.  And when you are done using the device and you USBDeviceClose() it, Classic will reopen it.  It will then re-appear to applications running inside Classic, just as if it had been hot-plugged.

This should provide a workaround for existing Mac OS X USB software: the user can simply retry the failed operation without needing to quit Classic.

The most complete way to handle arbitration is to register for general interest notifications and handle kIOMessageServiceIsAttemptingOpen, and kIOMessageServiceWasClosed messages as described above.

In some situations, the following simplified mechanism might suffice:

for (sleepCount = 5; sleepCount > 0; sleepCount--)
{
	// Attempt to open the device.
	err = (*device)->DeviceOpen(device);
	if ( kIOReturnExclusiveAccess == err )
	{
		// Someone else has the device open.
		// But they might release it in a few seconds.
		// So sleep for a second and try again.
	     	// Update user interface to warn the user that
		// you're waiting for the device.
	     	sleep(1);
		continue;
	}
	else
	if ( kIOReturnSuccess != err )
	{
          		// Unexpected error.  Break out of the loop.
          		break;
     	}
	else
	{
		// Success.  Break out of the loop.
		break;
	}
}
// Check <err> here to see whether you got access or not.