2006-06-29

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.

2006-xx-xx

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