AAAAaaarrrrggghhh! Months ago I gave up on receiving notifications from the IOKit when devices changed. The initial matching step would work; I could list all current devices at any instant of time. I must have spent ten hours over two weeks trying to get the notification code to work and got nothing. I wrote Cocoa programs: nothing. I wrote command line programs using CFRunLoop: nothing. I tried different run loop modes: nothing. I was convinced Apple had changed something at a low level and forgot to fix the documentation. I finally implemented UDF Media Reader with timer-based polling.
Today I happened to notice a line in the documentation I had always missed before: "In the case of IOServiceAddMatchingNotification, make sure you release the iterator only if you’re also ready to stop receiving notifications: When you release the iterator you receive from IOServiceAddMatchingNotification, you also disable the notification." That was it. Every test rig I ever made conscientiously cleaned up after itself by releasing the initial iterator, stopping notifications before they ever began.
Here is some example code showing how I used IOKit notifications in UDF Media Reader circa 2006:
// Application method which will respond to a CD or DVD being mounted.
-(void) ioDeviceMatched: (io_iterator_t) i {
int oldDeviceCount = [diskMenu count];
BOOL reloadFlag = NO;
io_object_t device;
while (device = IOIteratorNext(i)) {
CFMutableDictionaryRef cfProperties;
IOReturn err = IORegistryEntryCreateCFProperties( device, &cfProperties, kCFAllocatorDefault, 0 );
if (err == kIOReturnSuccess) {
NSMutableDictionary *props = (NSMutableDictionary *) cfProperties;
if ([[props objectForKey: @"Whole"] boolValue] == YES && [[props objectForKey: @"Ejectable"] boolValue] == YES) {
// When a new removable disk is mounted, start a new thread to analyze the media.
[diskMenu addObject: props];
reloadFlag = YES;
[props setObject: @"Scanning disk" forKey: @"Attributes"];
diskImage *scannerImage = [[diskImage alloc] init];
[NSThread detachNewThreadSelector: @selector(scanThread:) toTarget: scannerImage withObject: props];
} else {
[props release];
}
}
}
if (reloadFlag) {
[deviceTreeView reloadData];
if (oldDeviceCount == 0) [deviceTreeView selectRowIndexes: [NSIndexSet indexSetWithIndex: 0] byExtendingSelection: NO];
}
}
// Application method which will respond to a CD or DVD being dismounted.
-(void) ioDeviceEjected: (io_iterator_t) i {
BOOL reloadFlag = NO;
io_object_t device;
while (device = IOIteratorNext(i)) {
NSString *name = (NSString *) IORegistryEntryCreateCFProperty( device, (CFStringRef) @"BSD Name", kCFAllocatorDefault, 0 );
if (name == nil) continue;
NSDictionary *p;
int j;
for (j = 0; j < [diskMenu count]; j++) {
p = [diskMenu objectAtIndex: j];
if ([[p objectForKey: @"BSD Name"] isEqualToString: name]) {
[diskMenu removeObjectAtIndex: j];
reloadFlag = YES;
break;
}
}
}
if (reloadFlag) [deviceTreeView reloadData];
}
// C-language callback functions convert IOKit notifications to Objective-C method calls above.
static void cb_device_matched( void *p, io_iterator_t i ) { [((x36application *)p) ioDeviceMatched: i]; }
static void cb_device_ejected( void *p, io_iterator_t i ) { [((x36application *)p) ioDeviceEjected: i]; }
// Use IOServiceAddMatchingNotification() to choose the appropriate signals.
- (void) setupIOKitNotifications {
IOReturn err = IOMasterPort(MACH_PORT_NULL, &masterPort );
if (err != kIOReturnSuccess) return;
IONotificationPortRef portref = IONotificationPortCreate( masterPort );
if (!portref) return;
CFRunLoopSourceRef loopref = IONotificationPortGetRunLoopSource( portref );
if (!loopref) return;
IOServiceAddMatchingNotification( portref, kIOTerminatedNotification,
IOServiceMatching(kIOMediaClass), cb_device_ejected, self, &mediaEjectIterator);
while (IOIteratorNext(mediaEjectIterator)) ; // should always be empty
IOServiceAddMatchingNotification( portref, kIOMatchedNotification,
IOServiceMatching(kIOMediaClass), cb_device_matched, self, &mediaMatchIterator );
[self ioDeviceMatched: mediaMatchIterator]; // populate initial disk list
// CFRunLoopAddSource(CFRunLoopGetCurrent(), loopref, kCFRunLoopDefaultMode);
CFRunLoopAddSource([[NSRunLoop currentRunLoop] getCFRunLoop], loopref, kCFRunLoopDefaultMode);
}