Use FSEvents to detect when the user installs plug-ins in the plug-ins folder.
authorPeter Hosey <hg@boredzo.org>
Mon Oct 12 06:45:34 2009 -0700 (2009-10-12)
changeset 451249a403343bf0
parent 4511 23ffb0a5649f
child 4513 845941e3fa7f
Use FSEvents to detect when the user installs plug-ins in the plug-ins folder.

Currently has no visible effect in the prefpane, but does mean that you can drop a display plug-in into the Plugins folder, then open the prefpane, and be able to preview the display. Previously, the preview wouldn't work until you restarted Growl (or moved the plug-in back out of the folder and then double-clicked it).

We also detect when the user removes a plug-in from the folder, although we currently have no use for this knowledge.
Core/Source/GrowlPluginController.h
Core/Source/GrowlPluginController.m
     1.1 --- a/Core/Source/GrowlPluginController.h	Sun Oct 11 23:50:17 2009 -0700
     1.2 +++ b/Core/Source/GrowlPluginController.h	Mon Oct 12 06:45:34 2009 -0700
     1.3 @@ -184,6 +184,9 @@
     1.4  	NSArray *cache_allPluginInstances; //P
     1.5  	NSArray *cache_displayPlugins; //DP
     1.6  	//No cache for displayPluginNames; see -displayPluginNames for why.
     1.7 +
     1.8 +	struct FSEventStreamContext pluginsDirectoryEventStreamContext;
     1.9 +	FSEventStreamRef pluginsDirectoryEventStream;
    1.10  }
    1.11  
    1.12  + (GrowlPluginController *) sharedController;
     2.1 --- a/Core/Source/GrowlPluginController.m	Sun Oct 11 23:50:17 2009 -0700
     2.2 +++ b/Core/Source/GrowlPluginController.m	Mon Oct 12 06:45:34 2009 -0700
     2.3 @@ -39,6 +39,8 @@
     2.4  //for use on array of matching plug-in handlers in -openPluginAtPath:
     2.5  NSInteger comparePluginHandlerRegistrationOrder(id a, id b, void *context);
     2.6  
     2.7 +static void eventStreamCallback(ConstFSEventStreamRef eventStream, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIDs[]);
     2.8 +
     2.9  #pragma mark -
    2.10  
    2.11  NSString *GrowlPluginControllerWillAddPluginHandlerNotification = @"GrowlPluginControllerWillAddPluginHandlerNotification";
    2.12 @@ -77,6 +79,15 @@
    2.13   *	-	Better localize human-readable names
    2.14   */
    2.15  
    2.16 +@interface GrowlPluginController ()
    2.17 +
    2.18 +- (void) handleFileSystemEventFromStream:(ConstFSEventStreamRef)eventStream
    2.19 +							  eventPaths:(NSArray *)paths
    2.20 +							  eventFlags:(const FSEventStreamEventFlags [])eventFlags
    2.21 +								eventIDs:(const FSEventStreamEventId [])eventIDs;
    2.22 +
    2.23 +@end
    2.24 +
    2.25  @implementation GrowlPluginController
    2.26  
    2.27  + (GrowlPluginController *) sharedController {
    2.28 @@ -132,12 +143,34 @@
    2.29  		 * which are fairly common as some 3rd party plugins have been rolled into the Growl distribution.
    2.30  		 */
    2.31  		NSArray *libraries = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES);
    2.32 +		NSMutableArray *pluginsDirectoryPaths = [NSMutableArray arrayWithCapacity:[libraries count]];
    2.33 +		NSFileManager *mgr = [NSFileManager defaultManager];
    2.34  		NSEnumerator *enumerator = [libraries objectEnumerator];
    2.35  		NSString *dir;
    2.36  		while ((dir = [enumerator nextObject])) {
    2.37  			dir = [dir stringByAppendingPathComponent:@"Application Support/Growl/Plugins"];
    2.38 -			[self findPluginsInDirectory:dir];
    2.39 -		}		
    2.40 +			BOOL isDir = NO;
    2.41 +			if ([mgr fileExistsAtPath:dir isDirectory:&isDir] && isDir) {
    2.42 +				[self findPluginsInDirectory:dir];
    2.43 +				[pluginsDirectoryPaths addObject:dir];
    2.44 +			}
    2.45 +		}
    2.46 +
    2.47 +		pluginsDirectoryEventStreamContext = (struct FSEventStreamContext){
    2.48 +			.version = 0,
    2.49 +			.info = (void *)self,
    2.50 +			.copyDescription = (CFAllocatorCopyDescriptionCallBack)CFCopyDescription,
    2.51 +		};
    2.52 +		pluginsDirectoryEventStream = FSEventStreamCreate(kCFAllocatorDefault,
    2.53 +			eventStreamCallback,
    2.54 +			&pluginsDirectoryEventStreamContext,
    2.55 +			(CFArrayRef)pluginsDirectoryPaths,
    2.56 +			kFSEventStreamEventIdSinceNow,
    2.57 +			/*latency*/ 1.0,
    2.58 +			kFSEventStreamCreateFlagUseCFTypes);
    2.59 +
    2.60 +		FSEventStreamScheduleWithRunLoop(pluginsDirectoryEventStream, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
    2.61 +		FSEventStreamStart(pluginsDirectoryEventStream);
    2.62  	}
    2.63  
    2.64  	return self;
    2.65 @@ -175,6 +208,9 @@
    2.66  	[cache_allPluginInstances release];
    2.67  	[cache_displayPlugins release];
    2.68  
    2.69 +	FSEventStreamInvalidate(pluginsDirectoryEventStream);
    2.70 +	CFRelease(pluginsDirectoryEventStream);
    2.71 +
    2.72  	[super destroy];
    2.73  }
    2.74  
    2.75 @@ -904,6 +940,51 @@
    2.76  	return result;
    2.77  }
    2.78  
    2.79 +#pragma mark FSEvents
    2.80 +
    2.81 +- (void) handleFileSystemEventFromStream:(ConstFSEventStreamRef)eventStream
    2.82 +							  eventPaths:(NSArray *)paths
    2.83 +							  eventFlags:(const FSEventStreamEventFlags [])eventFlags
    2.84 +								eventIDs:(const FSEventStreamEventId [])eventIDs
    2.85 +{
    2.86 +#pragma unused(eventFlags)
    2.87 +#pragma unused(eventIDs)
    2.88 +	if (eventStream == pluginsDirectoryEventStream) {
    2.89 +		NSFileManager *mgr = [NSFileManager defaultManager];
    2.90 +
    2.91 +		//XXX We should make this properly support case-insensitive comparison, for events where the user renamed a plug-in and changed only the case of some letters.
    2.92 +		NSMutableSet *currentlyExistingPluginPaths = [NSMutableSet set];
    2.93 +
    2.94 +		for (NSString *dirPath in paths) {
    2.95 +			NSError *error = nil;
    2.96 +			NSArray *filenames = [mgr contentsOfDirectoryAtPath:dirPath error:&error];
    2.97 +			for (NSString *filename in filenames) {
    2.98 +				[currentlyExistingPluginPaths addObject:[dirPath stringByAppendingPathComponent:filename]];
    2.99 +			}
   2.100 +		}
   2.101 +
   2.102 +		NSSet *previouslyKnownPluginPaths = [[self allPluginDictionaries] valueForKey:GrowlPluginInfoKeyPath];
   2.103 +
   2.104 +		NSMutableSet *deletedPluginPaths = [[previouslyKnownPluginPaths mutableCopy] autorelease];
   2.105 +		[deletedPluginPaths minusSet:currentlyExistingPluginPaths];
   2.106 +
   2.107 +		NSMutableSet *newPluginPaths = [[currentlyExistingPluginPaths mutableCopy] autorelease];
   2.108 +		[newPluginPaths minusSet:previouslyKnownPluginPaths];
   2.109 +
   2.110 +		//XXX Handle deleted plug-ins.
   2.111 +
   2.112 +		NSWorkspace *wksp = [NSWorkspace sharedWorkspace];
   2.113 +		for (NSString *path in newPluginPaths) {
   2.114 +			NSString *pathExtension = [path pathExtension];
   2.115 +			NSString *fileType;
   2.116 +			[wksp getFileType:&fileType creatorCode:NULL forFile:path];
   2.117 +			if ([pluginHandlers objectForKey:pathExtension] || (fileType && [pluginHandlers objectForKey:fileType])) {
   2.118 +				[self dispatchPluginAtPath:path];
   2.119 +			}
   2.120 +		}
   2.121 +	}
   2.122 +}
   2.123 +
   2.124  @end
   2.125  
   2.126  static Boolean caseInsensitiveStringComparator(const void *value1, const void *value2) {
   2.127 @@ -977,3 +1058,13 @@
   2.128  	else
   2.129  		return NSOrderedSame;
   2.130  }
   2.131 +
   2.132 +static void eventStreamCallback(ConstFSEventStreamRef eventStream, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIDs[]) {
   2.133 +#pragma unused(numEvents)
   2.134 +#pragma unused(eventStream)
   2.135 +	GrowlPluginController *self = (GrowlPluginController *)clientCallBackInfo;
   2.136 +	[self handleFileSystemEventFromStream:eventStream
   2.137 +							   eventPaths:(NSArray *)eventPaths
   2.138 +							   eventFlags:eventFlags
   2.139 +								 eventIDs:eventIDs];
   2.140 +}