When an error occurs while loading a plug-in, log it. Bug found by the analyzer.
2 // GrowlPluginController.m
5 // Created by Nelson Elhage on 8/25/04.
6 // Copyright 2004-2006 The Growl Project. All rights reserved.
8 // This file is under the BSD License, refer to License.txt for details
10 #import "GrowlPluginController.h"
11 #import "GrowlPreferencesController.h"
12 #import "GrowlDisplayPlugin.h"
13 #import "GrowlDefinesInternal.h"
14 #include "CFDictionaryAdditions.h"
15 #include "CFMutableDictionaryAdditions.h"
17 #import "GrowlPathUtilities.h"
19 #import "NSSetAdditions.h"
20 #import "NSWorkspaceAdditions.h"
21 #import "GrowlWebKitPluginHandler.h"
23 @interface GrowlPluginController (PRIVATE)
24 - (void) registerDefaultPluginHandlers;
25 - (void) findPluginsInDirectory:(NSString *)dir;
26 - (void) pluginInstalledSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
27 - (void) pluginExistsSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
28 - (BOOL) hasNativeArchitecture:(NSString *)filename;
31 @interface WebCoreCache
35 //for use as CFSetCallBacks.equal
36 static Boolean caseInsensitiveStringComparator(const void *value1, const void *value2);
37 //for use as CFSetCallBacks.hash
38 static CFHashCode passthroughStringHash(const void *value);
39 //for use on array of matching plug-in handlers in -openPluginAtPath:
40 NSInteger comparePluginHandlerRegistrationOrder(id a, id b, void *context);
44 NSString *GrowlPluginControllerWillAddPluginHandlerNotification = @"GrowlPluginControllerWillAddPluginHandlerNotification";
45 NSString *GrowlPluginControllerDidAddPluginHandlerNotification = @"GrowlPluginControllerDidAddPluginHandlerNotification";
46 NSString *GrowlPluginControllerWillRemovePluginHandlerNotification = @"GrowlPluginControllerWillRemovePluginHandlerNotification";
47 NSString *GrowlPluginControllerDidRemovePluginHandlerNotification = @"GrowlPluginControllerDidRemovePluginHandlerNotification";
49 //Info.plist keys for plug-in bundles.
50 NSString *GrowlPluginInfoKeyName = @"CFBundleName";
51 NSString *GrowlPluginInfoKeyAuthor = @"GrowlPluginAuthor";
52 NSString *GrowlPluginInfoKeyVersion = @"CFBundleVersion";
53 NSString *GrowlPluginInfoKeyDescription = @"GrowlPluginDescription";
54 //keys in plug-in description dictionaries (also includes the above).
55 NSString *GrowlPluginInfoKeyBundle = @"GrowlPluginBundle";
56 NSString *GrowlPluginInfoKeyTypes = @"GrowlPluginType";
57 NSString *GrowlPluginInfoKeyPath = @"GrowlPluginPath";
58 NSString *GrowlPluginInfoKeyHumanReadableName = @"GrowlPluginHumanReadableName";
59 NSString *GrowlPluginInfoKeyIdentifier = @"GrowlPluginIdentifier";
60 NSString *GrowlPluginInfoKeyInstance = @"GrowlPluginInstance";
62 /*******************************************************************************
64 * |_ _/ _ \| _ \ / _ \
65 * | || | | | | | | | | |
66 * | || |_| | |_| | |_| |
67 * |_| \___/|____/ \___/
69 *******************************************************************************
71 * - Use identifier strings for all loaded plug-ins (simpler than using
72 * human-readable names) (DONE though this will probably only be used for
73 * storage of plug-in prefs)
74 * - Use plug-in dictionaries (DONE)
75 * - Write the built-in plug-in handler (DONE)
76 * - Add a WebKit plug-in handler (jkp)
77 * - Better localize human-readable names
80 @implementation GrowlPluginController
82 + (GrowlPluginController *) sharedController {
83 return [self sharedInstance];
86 - (id) initSingleton {
87 if ((self = [super initSingleton])) {
88 bundlesToLazilyInstantiateAnInstanceFrom = [[NSMutableSet alloc] init];
90 pluginsByIdentifier = [[NSMutableDictionary alloc] init];
91 pluginIdentifiersByPath = [[NSMutableDictionary alloc] init];
92 pluginIdentifiersByBundle = [[NSMapTable mapTableWithStrongToStrongObjects] retain];
93 pluginIdentifiersByInstance = [[NSMapTable mapTableWithStrongToStrongObjects] retain];
95 pluginsByName = [[NSMutableDictionary alloc] init];
96 pluginsByAuthor = [[NSMutableDictionary alloc] init];
97 pluginsByVersion = [[NSMutableDictionary alloc] init];
98 pluginsByFilename = [[NSMutableDictionary alloc] init];
99 pluginsByType = [[NSMutableDictionary alloc] init];
100 pluginHumanReadableNames = [[NSCountedSet alloc] init];
102 loadedBundleIdentifiers = [[NSMutableSet alloc] init];
104 allPluginHandlers = [[NSMutableArray alloc] init];
105 pluginHandlers = [[NSMutableDictionary alloc] init];
106 handlersForPlugins = [[NSMapTable mapTableWithStrongToStrongObjects] retain];
108 displayPlugins = [[NSMutableArray alloc] init];
109 disabledPlugins = [[NSMutableArray alloc] init];
111 [self registerDefaultPluginHandlers];
113 enum { builtInTypesCount = 4U };
114 NSString *builtInTypesArray[builtInTypesCount] = {
115 GROWL_STYLE_EXTENSION,
116 GROWL_VIEW_EXTENSION,
117 GROWL_PATHWAY_EXTENSION,
118 GROWL_PLUGIN_EXTENSION,
120 CFSetCallBacks callbacks = kCFCopyStringSetCallBacks;
121 callbacks.equal = caseInsensitiveStringComparator;
122 callbacks.hash = passthroughStringHash;
123 builtInTypes = (NSSet *)CFSetCreate(kCFAllocatorDefault,
124 (const void **)builtInTypesArray,
128 //Find plugins inside GHA itself first
129 [self findPluginsInDirectory:[[GrowlPathUtilities helperAppBundle] builtInPlugInsPath]];
131 /* Then find plug-ins in Library/Application Support/Growl/Plugins directories. This allows GHA to override externally installed plugins,
132 * which are fairly common as some 3rd party plugins have been rolled into the Growl distribution.
134 NSArray *libraries = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES);
135 NSEnumerator *enumerator = [libraries objectEnumerator];
137 while ((dir = [enumerator nextObject])) {
138 dir = [dir stringByAppendingPathComponent:@"Application Support/Growl/Plugins"];
139 [self findPluginsInDirectory:dir];
147 [pluginsByIdentifier release];
148 [pluginIdentifiersByPath release];
149 [pluginIdentifiersByBundle release];
150 [pluginIdentifiersByInstance release];
152 [pluginsByName release];
153 [pluginsByAuthor release];
154 [pluginsByVersion release];
155 [pluginsByFilename release];
156 [pluginsByType release];
157 [pluginHumanReadableNames release];
159 [loadedBundleIdentifiers release];
161 [bundlesToLazilyInstantiateAnInstanceFrom release];
162 [displayPlugins release];
163 [disabledPlugins release];
165 [allPluginHandlers release];
166 [pluginHandlers release];
167 [handlersForPlugins release];
169 [builtInTypes release];
171 [cache_allPlugins release];
172 [cache_allPluginsArray release];
173 [cache_registeredPluginTypes release];
174 [cache_registeredPluginNames release];
175 [cache_registeredPluginNamesArray release];
176 [cache_allPluginInstances release];
177 [cache_displayPlugins release];
184 - (void) registerDefaultPluginHandlers {
185 //register ourselves for display plug-ins (non-WebKit), pathway plug-ins, and functional plug-ins.
186 NSSet *types = [[NSSet alloc] initWithObjects:
188 GROWL_VIEW_EXTENSION,
189 NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('DISP')),
191 GROWL_PATHWAY_EXTENSION,
192 NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('PWAY')),
193 //generic functional plug-ins
194 GROWL_PLUGIN_EXTENSION,
195 NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('GEXT')),
198 [self addPluginHandler:self forPluginTypes:types];
202 [GrowlWebKitPluginHandler sharedInstance]; // Calling this here will cause the handler to register
206 #pragma mark GrowlPluginHandler protocol conformance
208 //the method that dispatches incoming plug-ins to plug-in handlers is -dispatchPluginAtPath:.
209 //this is for handling plug-ins of the built-in types.
210 - (BOOL)loadPluginWithBundle:(NSBundle *)bundle {
211 [self addPluginInstance:nil fromBundle:bundle];
217 #pragma mark Plugin-handler handling
219 - (void) addPluginHandler:(id <GrowlPluginHandler>)handler forPluginTypes:(NSSet *)types {
220 NSParameterAssert(handler != nil);
221 NSParameterAssert(types != nil);
223 if (![types count]) {
224 NSLog(@"Warning: -[%@ addPluginHandler:forPluginTypes:] called with an empty set of file types. This may be indicative of a bug in Growl or a plug-in. The handler was %@.", [self class], handler);
226 //make sure nobody tries to register a plug-in handler for a built-in type.
228 NSMutableSet *builtInMutable = [builtInTypes mutableCopy];
229 [builtInMutable intersectSet:types];
231 //the intersection must be empty; if it isn't, at least one of the types for which this handler is attempting to register is a built-in type.
232 NSAssert2([builtInMutable count] == 0U, @"Something attempted to register a plug-in handler for one or more reserved file types (%@). The handler was %@.", builtInMutable, handler);
234 [builtInMutable release];
237 NSEnumerator *typeEnum = [types objectEnumerator];
239 while ((type = [typeEnum nextObject])) {
240 //normalise: strip leading ., if any.
241 NSUInteger i = 0U, len = [type length];
242 for (; i < len; ++i) {
243 if ([type characterAtIndex:i] != '.')
246 NSAssert2(i < len, @"Something tried to register a plug-in handler for a file type consisting entirely of %u full stops ('.'). The handler was %@.", len, handler);
248 type = [type substringFromIndex:i];
250 NSMutableArray *handlers = [pluginHandlers objectForKey:type];
252 handlers = [[NSMutableArray alloc] initWithCapacity:1U];
253 [pluginHandlers setObject:handlers forKey:type];
256 [handlers addObject:handler];
259 [allPluginHandlers addObject:handler];
261 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
262 NSDictionary *notificationUserInfo = [[[NSDictionary alloc] initWithObjectsAndKeys:
263 handler, @"GrowlPluginHandler",
266 [nc postNotificationName:GrowlPluginControllerWillAddPluginHandlerNotification
268 userInfo:notificationUserInfo];
270 [allPluginHandlers addObject:handler];
272 //add the handler as an observer for various notifications.
273 if ([handler respondsToSelector:@selector(growlPluginControllerWillAddPluginHandler:)]) {
274 [nc addObserver:handler
275 selector:@selector(growlPluginControllerWillAddPluginHandler:)
276 name:GrowlPluginControllerWillAddPluginHandlerNotification
279 if ([handler respondsToSelector:@selector(growlPluginControllerDidAddPluginHandler:)]) {
280 [nc addObserver:handler
281 selector:@selector(growlPluginControllerDidAddPluginHandler:)
282 name:GrowlPluginControllerDidAddPluginHandlerNotification
285 if ([handler respondsToSelector:@selector(growlPluginControllerWillRemovePluginHandler:)]) {
286 [nc addObserver:handler
287 selector:@selector(growlPluginControllerWillRemovePluginHandler:)
288 name:GrowlPluginControllerWillRemovePluginHandlerNotification
291 if ([handler respondsToSelector:@selector(growlPluginControllerDidRemovePluginHandler:)]) {
292 [nc addObserver:handler
293 selector:@selector(growlPluginControllerDidRemovePluginHandler:)
294 name:GrowlPluginControllerDidRemovePluginHandlerNotification
298 [nc postNotificationName:GrowlPluginControllerDidAddPluginHandlerNotification
300 userInfo:notificationUserInfo];
304 - (void) removePluginHandler:(id <GrowlPluginHandler>)handler forPluginTypes:(NSSet *)extensions {
305 NSParameterAssert(handler != nil);
307 [allPluginHandlers removeObjectIdenticalTo:handler];
310 extensions = (NSSet *)[pluginHandlers allKeysForObject:handler];
312 NSEnumerator *extEnum = [extensions objectEnumerator];
314 while ((ext = [extEnum nextObject])) {
315 NSMutableArray *handlers = [pluginHandlers objectForKey:ext];
317 NSUInteger idx = [handlers indexOfObjectIdenticalTo:handler];
318 if (idx != NSNotFound)
319 [handlers removeObjectAtIndex:idx];
324 - (NSArray *) allPluginHandlers {
325 return [[allPluginHandlers copy] autorelease];
331 - (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromPath:(NSString *)path bundle:(NSBundle *)bundle {
332 //If we're passed a bundle, refuse to load it if we've already loaded a bundle with the same identifier, instead returning early.
333 NSString *bundleIdentifier = (bundle ? [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey] : nil);
334 if (bundleIdentifier && [loadedBundleIdentifiers containsObject:[bundle objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey]]) {
338 //Look up the identifier for the plugin. We try to look up the identifier by the instance, by the bundle; and by the pathname, in that order.
339 NSString *identifier = nil;
341 identifier = [pluginIdentifiersByInstance objectForKey:plugin];
343 identifier = [pluginIdentifiersByBundle objectForKey:bundle];
345 identifier = [pluginIdentifiersByPath objectForKey:path];
347 /* If we have an identifier, look up the plug-in dictionary.
348 * If we have a plug-in dictionary but no instance (the identifier was retrieved by bundle or by path), attempt to retrieve the instance from the dictionary.
350 NSMutableDictionary *pluginDict = identifier ? [pluginsByIdentifier objectForKey:identifier] : nil;
351 if (pluginDict && !plugin)
352 plugin = [pluginDict pluginInstance];
354 //Assert that we have an instance OR a bundle. We need at least one to proceed.
355 NSAssert1(plugin || bundle, @"Cannot load plug-ins lazily without a bundle (path: %@)", path);
357 //Get the plug-in's name, author, and version. All three come from the plug-in instance if it exists and responds to -name/-author/-version; if both requirements are not satisfied, the information is retrieved from the bundle's Info.plist.
358 NSString *name = plugin ? ([plugin respondsToSelector:@selector(name)] ? [plugin name] : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey])
359 : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey];
360 NSString *author = plugin ? ([plugin respondsToSelector:@selector(author)] ? [plugin author] : [bundle objectForInfoDictionaryKey:GrowlPluginInfoKeyAuthor])
361 : [bundle objectForInfoDictionaryKey:GrowlPluginInfoKeyAuthor];
362 NSString *version = plugin ? ([plugin respondsToSelector:@selector(version)] ? [plugin version] : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey])
363 : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
365 //If we don't have a pathname, get it as the bundle's pathname.
367 path = [bundle bundlePath];
368 NSString *extension = [path pathExtension];
369 NSString *fileType = nil;
371 //Assert that we have a name, author, and version. (We got the path first so we can use it in the assertion message.)
372 NSAssert5((name != nil) && (author != nil) && (version != nil),
373 @"Cannot load plug-in at path %@ (plug-in instance's class: %@). One of these is (null), but they must all not be:\n"
375 @"\t"@" author: %@\n"
376 @"\t"@"version: %@\n",
377 path, [plugin class], name, author, version);
379 //In case we got the names from the plug-in instance and it gave us a mutable string for some reason, make copies for ourselves.
380 //Note: This isn't a performance hit when the strings are immutable. -copy = -retain in that situation. Thanks, Apple!
382 author = [author copy];
383 version = [version copy];
385 //If we don't have an identifier yet, forge it.
387 identifier = [NSString stringWithFormat:@"Name: %@ Author: %@ Path: %@", name, author, path];
389 //If we don't have an instance but we do have a bundle, see if we've previously queued the bundle for lazy instantiation.
390 if (!plugin && bundle) {
391 if (![bundlesToLazilyInstantiateAnInstanceFrom containsObject:bundle]) {
392 //We haven't previously queued it: Queue it.
393 [bundlesToLazilyInstantiateAnInstanceFrom addObject:bundle];
395 //We have: This is our cue to instantiate it.
396 plugin = [[[[bundle principalClass] alloc] init] autorelease];
397 //Dequeue it, because we don't want to hit this branch again for this plug-in.
398 [bundlesToLazilyInstantiateAnInstanceFrom removeObject:bundle];
399 //Stash the plug-in instance in the plug-in dictionary. This retains the instance and means that we'll never hit the lazy-instantiation machinery again (because plugin will be non-nil).
400 [pluginDict setObject:plugin forKey:GrowlPluginInfoKeyInstance];
404 if (!plugin && bundle) {
405 //*Still* no plug-in! Again we check whether it's queued for instantiation (bug?).
406 if (![bundlesToLazilyInstantiateAnInstanceFrom containsObject:bundle])
407 [bundlesToLazilyInstantiateAnInstanceFrom addObject:bundle];
409 //Apparently it is. Instantiate it, but don't stash the plug-in instance in the plug-in dictionary (why not?).
410 plugin = [[[[bundle principalClass] alloc] init] autorelease];
411 [bundlesToLazilyInstantiateAnInstanceFrom removeObject:bundle];
415 /*If we don't actually have a plug-in dictionary, create it.
416 *Elements of a plug-in dictionary:
422 * Instance (later; see above lazy-instantiation code)
423 * Plug-in's HFS type and filename extension (combined in a set)
425 BOOL pluginDictIsNew = !pluginDict;
427 pluginDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
428 name, GrowlPluginInfoKeyName,
429 author, GrowlPluginInfoKeyAuthor,
430 version, GrowlPluginInfoKeyVersion,
431 path, GrowlPluginInfoKeyPath,
432 identifier, GrowlPluginInfoKeyIdentifier,
434 NSString *description = ([plugin respondsToSelector:@selector(pluginDescription)] ? [plugin pluginDescription] : nil);
437 [pluginDict setObject:description forKey:GrowlPluginInfoKeyDescription];
439 [[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:path];
441 //Record the file types (HFS and filename extension) that the plug-in possessed at this time. These help determine what kind of plug-in it is (e.g. .growlView = custom view; .growlStyle = WebKit display).
444 #warning problem here...
445 ///XXX when there is no file type it is coming back as \'\'...im guessing this means no type, but it still tests as true so each plugin is registered against that type...wrong???
447 types = [NSSet setWithObjects:extension, fileType, nil];
449 types = [NSSet setWithObject:extension];
451 types = [NSSet setWithObject:fileType];
454 [pluginDict setObject:types forKey:GrowlPluginInfoKeyTypes];
457 //We have a bundle. If no previous bundle was stored in the plug-in dictionary (why wouldn't there be?), store this bundle there. Also register the identifier as being the one for this bundle.
459 if (![pluginDict objectForKey:GrowlPluginInfoKeyBundle])
460 [pluginDict setObject:bundle forKey:GrowlPluginInfoKeyBundle];
461 [pluginIdentifiersByBundle setObject:identifier forKey:bundle];
463 //We have an instance. If no previous instance was stored in the plug-in dictionary (why wouldn't there be?), store this instance there. Also register the identifier as being the one for this instance.
465 if (![pluginDict objectForKey:GrowlPluginInfoKeyInstance])
466 [pluginDict setObject:plugin forKey:GrowlPluginInfoKeyInstance];
467 [pluginIdentifiersByInstance setObject:identifier forKey:plugin];
470 //If we just created the dictionary (and got done filling it out), start storing it in places.
471 if (pluginDictIsNew) {
472 [pluginsByIdentifier setObject:pluginDict forKey:identifier];
473 [pluginIdentifiersByPath setObject:identifier forKey:path];
475 #define ADD_TO_DICT(dictName, key, value) \
477 NSMutableSet *plugins = [dictName objectForKey:key]; \
479 [plugins addObject:value]; \
481 [dictName setObject:[NSMutableSet setWithObject:value] forKey:key]; \
483 ADD_TO_DICT(pluginsByName, name, pluginDict);
484 ADD_TO_DICT(pluginsByAuthor, author, pluginDict);
485 ADD_TO_DICT(pluginsByVersion, version, pluginDict);
486 ADD_TO_DICT(pluginsByFilename, [path lastPathComponent], pluginDict);
488 ADD_TO_DICT(pluginsByType, extension, pluginDict);
489 ADD_TO_DICT(pluginsByType, fileType, pluginDict);
493 //Release our copies.
498 //Invalidate non-display plug-in caches.
499 [cache_allPlugins release];
500 cache_allPlugins = nil;
501 [cache_allPluginsArray release];
502 cache_allPluginsArray = nil;
503 [cache_registeredPluginTypes release];
504 cache_registeredPluginTypes = nil;
505 [cache_registeredPluginNames release];
506 cache_registeredPluginNames = nil;
507 [cache_registeredPluginNamesArray release];
508 cache_registeredPluginNamesArray = nil;
510 //Special handling if this plug-in is a display.
511 if ([self pluginWithDictionaryIsDisplayPlugin:pluginDict]) {
512 //If it doesn't respond to -requiresPositioning, it's old. Add it as a disabled plug-in.
513 if(![[pluginDict valueForKey:GrowlPluginInfoKeyInstance] respondsToSelector:@selector(requiresPositioning)]) {
514 [disabledPlugins addObject:[pluginDict valueForKey:GrowlPluginInfoKeyName]];
517 //It responds to -requiresPositioning, so add it as a(n enabled) display plug-in.
518 [displayPlugins addObject:pluginDict];
521 //Invalidate display plug-in cache.
522 [cache_displayPlugins release];
523 cache_displayPlugins = nil;
526 //Store the bundle identifier so we know we've loaded it.
527 if (bundleIdentifier) {
528 [loadedBundleIdentifiers addObject:bundleIdentifier];
534 - (NSArray *) disabledPlugins {
535 return disabledPlugins;
538 - (BOOL) disabledPluginsPresent {
539 return ([disabledPlugins count] > 0);
542 - (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromBundle:(NSBundle *)bundle {
543 return [self addPluginInstance:plugin fromPath:nil bundle:bundle];
545 - (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromPath:(NSString *)path {
546 return [self addPluginInstance:plugin fromPath:path bundle:nil];
551 - (NSSet *) registeredPluginTypes {
552 if (!cache_registeredPluginTypes)
553 cache_registeredPluginTypes = [[NSSet alloc] initWithArray:[pluginHandlers allKeys]];
555 return cache_registeredPluginTypes;
558 - (NSSet *) registeredPluginNames {
559 if (!cache_registeredPluginNames)
560 cache_registeredPluginNames = [[NSSet alloc] initWithArray:[self registeredPluginNamesArray]];
561 return cache_registeredPluginNames;
564 - (NSArray *) registeredPluginNamesArray {
565 if (!cache_registeredPluginNamesArray) {
566 cache_registeredPluginNamesArray = [[[pluginsByIdentifier allValues] valueForKey:GrowlPluginInfoKeyName] retain];
568 return cache_registeredPluginNamesArray;
571 - (NSArray *) registeredPluginNamesArrayForType:(NSString *)type {
572 #warning this should be cached per type
573 return [[[pluginsByType valueForKey:type] allObjects] valueForKey:GrowlPluginInfoKeyName];
579 - (NSArray *) allPluginDictionariesArray {
580 if (!cache_allPluginsArray)
581 cache_allPluginsArray = [[pluginsByIdentifier allValues] copy];
582 return cache_allPluginsArray;
584 - (NSSet *) allPluginDictionaries {
585 if (!cache_allPlugins)
586 cache_allPlugins = [[NSMutableSet alloc] initWithArray:[self allPluginDictionariesArray]];
587 return cache_allPlugins;
589 - (NSArray *) allPluginInstances {
590 if (!cache_allPluginInstances)
591 cache_allPluginInstances = [[[self allPluginDictionaries] valueForKey:GrowlPluginInfoKeyInstance] retain];
592 return cache_allPluginInstances;
595 - (NSSet *) pluginDictionariesWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
596 NSMutableSet *matches = [[[self allPluginDictionaries] mutableCopy] autorelease];
598 if ([matches count]) {
600 [matches intersectSet:[pluginsByName objectForKey:name]];
602 [matches intersectSet:[pluginsByAuthor objectForKey:author]];
604 [matches intersectSet:[pluginsByVersion objectForKey:version]];
606 [matches intersectSet:[pluginsByType objectForKey:type]];
611 - (NSDictionary *) pluginDictionaryWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
612 NSSet *matches = [self pluginDictionariesWithName:name author:author version:version type:type];
613 if ([matches count] == 1U)
614 return [matches anyObject];
618 - (NSDictionary *) pluginDictionaryWithName:(NSString *)name {
619 return [self pluginDictionaryWithName:name author:nil version:nil type:nil];
621 - (GrowlPlugin *) pluginInstanceWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
622 NSDictionary *pluginDict = [self pluginDictionaryWithName:name author:author version:version type:type];
623 GrowlPlugin *instance = [pluginDict pluginInstance];
625 NSBundle *bundle = [pluginDict pluginBundle];
627 [self addPluginInstance:nil fromBundle:bundle]; //causes instantiation
628 instance = [pluginDict pluginInstance];
633 - (GrowlPlugin *) pluginInstanceWithName:(NSString *)name {
634 return [self pluginInstanceWithName:name author:nil version:nil type:nil];
639 - (NSString *) humanReadableNameForPluginWithDictionary:(NSDictionary *)pluginDict {
640 NSString *humanReadableName = [pluginDict pluginHumanReadableName];
642 if (!humanReadableName) {
643 NSString *name = [pluginDict pluginName];
644 if ([[pluginsByName objectForKey:name] count] == 1U)
645 humanReadableName = name;
647 NSString *author = [pluginDict pluginAuthor];
648 if ([[pluginsByAuthor objectForKey:author] count] == 1U)
649 humanReadableName = [NSString stringWithFormat:@"%@ (by %@)", name, author]; //XXX LOCALIZEME
651 NSString *filename = [[pluginDict pluginPath] lastPathComponent];
652 if ([[pluginsByFilename objectForKey:filename] count] == 1U)
653 humanReadableName = [NSString stringWithFormat:@"%@ (filename %@)", name, filename]; //XXX LOCALIZEME
655 humanReadableName = [NSString stringWithFormat:@"%@ (by %@, filename %@)", name, author, filename]; //XXX LOCALIZEME
656 NSUInteger count = [pluginHumanReadableNames countForObject:humanReadableName];
657 [pluginHumanReadableNames addObject:humanReadableName];
659 humanReadableName = [NSString stringWithFormat:@"%@ %u", humanReadableName, count];
664 if ([pluginDict isKindOfClass:[NSMutableDictionary class]]) {
665 //save the name for later retrieval.
666 [(NSMutableDictionary *)pluginDict setObject:humanReadableName forKey:GrowlPluginInfoKeyHumanReadableName];
670 return humanReadableName;
673 - (BOOL) pluginWithDictionaryIsDisplayPlugin:(NSDictionary *)pluginDict {
674 GrowlPlugin *instance = [pluginDict pluginInstance];
676 return [instance isKindOfClass:[GrowlDisplayPlugin class]];
678 NSBundle *bundle = [pluginDict pluginBundle];
679 NSAssert1(bundle, @"no instance or bundle in plug-in dictionary! description of dictionary follows\n%@", pluginDict);
680 Class principalClass = [bundle principalClass];
681 NSAssert1(bundle, @"bundle in plug-in dictionary has no principal class! description of dictionary follows\n%@", pluginDict);
682 return [principalClass isSubclassOfClass:[GrowlDisplayPlugin class]];
687 #warning XXX all of this could potentially go bye-bye if it is not needed
689 - (NSArray *) displayPlugins {
690 if (!cache_displayPlugins)
691 cache_displayPlugins = [[NSArray alloc] initWithArray:displayPlugins];
692 return cache_displayPlugins;
697 - (NSSet *) displayPluginDictionariesWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
698 NSMutableSet *matches = (NSMutableSet *)[self pluginDictionariesWithName:name
703 NSSet *copyForIteration = [matches copy];
704 NSEnumerator *matchesEnum = [copyForIteration objectEnumerator];
705 NSDictionary *pluginDict;
706 while ((pluginDict = [matchesEnum nextObject])) {
707 if (![self pluginWithDictionaryIsDisplayPlugin:pluginDict])
708 [matches removeObject:pluginDict];
710 [copyForIteration release];
715 - (NSDictionary *) displayPluginDictionaryWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
716 NSSet *matches = [self displayPluginDictionariesWithName:name
720 if ([matches count] == 1U)
721 return [matches anyObject];
726 - (GrowlDisplayPlugin *) displayPluginInstanceWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
727 GrowlPlugin *plugin = [self pluginInstanceWithName:name author:author version:version type:type];
728 if (plugin && [plugin isKindOfClass:[GrowlDisplayPlugin class]])
729 return (GrowlDisplayPlugin *)plugin;
735 #pragma mark Finding and using installed plug-ins
737 - (void) findPluginsInDirectory:(NSString *)dir {
738 NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:dir];
740 while ((file = [enumerator nextObject])) {
741 NSString *fullPath = [dir stringByAppendingPathComponent:file];
743 NSString *pathExtension = [file pathExtension];
745 [[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:fullPath];
746 if ([pluginHandlers objectForKey:pathExtension] || (fileType && [pluginHandlers objectForKey:fileType])) {
747 [self dispatchPluginAtPath:fullPath];
748 [enumerator skipDescendents];
753 - (void) dispatchPluginAtPath:(NSString *)path {
754 //get all the relevant handlers, by extension and by type.
755 NSArray *handlersByExtension = [pluginHandlers objectForKey:[path pathExtension]];
757 [[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:path];
758 NSArray *handlersByType = [pluginHandlers objectForKey:fileType];
760 //strip duplicates by making a set from both arrays.
761 NSMutableSet *allMatchingHandlersSet = handlersByExtension ? [NSMutableSet setWithArray:handlersByExtension] : [NSMutableSet set];
763 [allMatchingHandlersSet unionSet:[NSSet setWithArray:handlersByType]];
765 NSMutableArray *allMatchingHandlers = [[[allMatchingHandlersSet allObjects] mutableCopy] autorelease];
766 [allMatchingHandlers sortUsingFunction:comparePluginHandlerRegistrationOrder context:self];
768 NSBundle *pluginBundle = [[NSBundle alloc] initWithPath:path];
770 NSEnumerator *handlersEnum = [allMatchingHandlers objectEnumerator];
771 id <GrowlPluginHandler> handler;
772 while ((handler = [handlersEnum nextObject])) {
774 if (pluginBundle && [handler respondsToSelector:@selector(loadPluginWithBundle:)]) {
775 success = (NSUInteger)[handler performSelector:@selector(loadPluginWithBundle:) withObject:pluginBundle];
777 NSLog(@"%@: Handler %@ could not load plug-in with bundle %@", [self class], handler, pluginBundle);
778 } else if ([handler respondsToSelector:@selector(loadPluginAtPath:)]) {
779 success = (NSUInteger)[handler performSelector:@selector(loadPluginAtPath:) withObject:path];
781 NSLog(@"%@: Handler %@ could not load plug-in at path %@", [self class], handler, path);
782 } else if ([handler respondsToSelector:@selector(loadPluginAtURL:)]) {
783 success = (NSUInteger)[handler performSelector:@selector(loadPluginAtURL:) withObject:[NSURL fileURLWithPath:path]];
785 NSLog(@"%@: Handler %@ could not load plug-in at URL for path %@", [self class], handler, path);
787 NSLog(@"warning: while loading plug-in at %@, tried to use plug-in handler %@, but it appears incapable of handling a plug-in", path, handler); //XXX should do this diagnostic when adding the handler
790 [pluginBundle release];
794 #pragma mark Installing plug-ins
796 - (void) pluginInstalledSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
797 #pragma unused(sheet, contextInfo)
798 if (returnCode == NSAlertAlternateReturn) {
799 NSBundle *prefPane = [GrowlPathUtilities growlPrefPaneBundle];
801 if (prefPane && ![[NSWorkspace sharedWorkspace] openFile:[prefPane bundlePath]])
802 NSLog(@"Could not open Growl PrefPane");
806 - (void) pluginExistsSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
807 #pragma unused(sheet)
808 NSString *filename = (NSString *)contextInfo;
810 if (returnCode == NSAlertAlternateReturn) {
811 //'Yes' to 'Do you want to overwrite [the installed plug-in with the version you double-clicked]?'
812 NSString *pluginFile = [filename lastPathComponent];
813 NSString *destination = [[NSHomeDirectory()
814 stringByAppendingPathComponent:@"Library/Application Support/Growl/Plugins"]
815 stringByAppendingPathComponent:pluginFile];
816 NSFileManager *fileManager = [NSFileManager defaultManager];
818 // first remove old copy if present
819 [fileManager removeFileAtPath:destination handler:nil];
821 // copy new version to destination
822 if ([fileManager copyPath:filename toPath:destination handler:nil]) {
823 [self dispatchPluginAtPath:destination];
824 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
825 if([self hasNativeArchitecture:destination])
826 NSBeginInformationalAlertSheet( NSLocalizedString( @"Plugin installed", @"" ),
827 NSLocalizedString( @"No", @"" ),
828 NSLocalizedString( @"Yes", @"" ),
830 @selector(pluginInstalledSelector:returnCode:contextInfo:),
832 NSLocalizedString( @"Plugin '%@' has been installed successfully. Do you want to configure it now?", @"" ),
833 [pluginFile stringByDeletingPathExtension] );
835 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
836 NSBeginCriticalAlertSheet( NSLocalizedString( @"Plugin not installed", @"" ),
837 NSLocalizedString( @"OK", @"" ),
838 nil, nil, nil, self, NULL, NULL, NULL,
839 NSLocalizedString( @"There was an error while installing the plugin '%@'.", @"" ),
840 [pluginFile stringByDeletingPathExtension] );
847 - (void) installPluginFromPath:(NSString *)filename {
848 NSString *pluginFile = [filename lastPathComponent];
849 NSString *destination = [[NSHomeDirectory()
850 stringByAppendingPathComponent:@"Library/Application Support/Growl/Plugins"]
851 stringByAppendingPathComponent:pluginFile];
852 // retain a copy of the filename because it is passed as context to the sheetDidEnd selectors
853 NSString *filenameCopy = [[NSString alloc] initWithString:filename];
855 //Check to see if we've got valid architectures in this plugin for our use, if not, bail.
856 if(![self hasNativeArchitecture:filenameCopy]) {
857 NSBeginAlertSheet( NSLocalizedString( @"Plugin missing native architecture", @"" ),
858 NSLocalizedString( @"No", @"" ),
859 NSLocalizedString( @"Yes", @"" ), nil, nil, self,
860 NULL, @selector(pluginExistsSelector:returnCode:contextInfo:),
862 NSLocalizedString( @"Plugin '%@' will not work on this Mac running this version of Mac OS X. Install it anyway?", @"" ),
863 [pluginFile stringByDeletingPathExtension] );
866 if ([[NSFileManager defaultManager] fileExistsAtPath:destination]) {
867 // plugin already exists at destination
868 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
869 NSBeginAlertSheet( NSLocalizedString( @"Plugin already exists", @"" ),
870 NSLocalizedString( @"No", @"" ),
871 NSLocalizedString( @"Yes", @"" ), nil, nil, self,
872 NULL, @selector(pluginExistsSelector:returnCode:contextInfo:),
874 NSLocalizedString( @"Plugin '%@' is already installed, do you want to overwrite it?", @"" ),
875 [pluginFile stringByDeletingPathExtension] );
877 [self pluginExistsSelector:nil returnCode:NSAlertAlternateReturn contextInfo:filenameCopy];
882 - (BOOL)hasNativeArchitecture:(NSString*)filename {
884 NSInteger currentArchitecture = 0;
885 #if defined(__ppc__) && __ppc__
886 currentArchitecture = NSBundleExecutableArchitecturePPC;
887 #elif defined(__i386__) && __i386__
888 currentArchitecture = NSBundleExecutableArchitectureI386;
889 #elif defined(__x86_64__) && __x86_64__
890 currentArchitecture = NSBundleExecutableArchitectureX86_64;
892 #error unsupported architecture
894 NSBundle *pluginBundle = [NSBundle bundleWithPath:filename];
895 NSString *executablePath = [pluginBundle executablePath];
896 //we check to see if there is actually an executable in this plugin, it could be a growlStyle, under which we accept it as valid.
897 if(executablePath && [[NSFileManager defaultManager] fileExistsAtPath:executablePath]) {
898 NSArray *pluginArchitectures = [pluginBundle executableArchitectures];
899 if([pluginArchitectures containsObject:[NSNumber numberWithInteger:currentArchitecture]])
911 static Boolean caseInsensitiveStringComparator(const void *value1, const void *value2) {
912 Class NSStringClass = [NSString class];
913 return [(id)value1 isKindOfClass:NSStringClass] \
914 && [(id)value2 isKindOfClass:NSStringClass] \
915 && ([(NSString *)value1 caseInsensitiveCompare:(NSString *)value2] == NSOrderedSame);
918 static CFHashCode passthroughStringHash(const void *value) {
919 return [[(NSString *)value lowercaseString] hash];
924 @implementation NSDictionary (GrowlPluginKeys)
926 - (NSString *) pluginName {
927 return [self objectForKey:GrowlPluginInfoKeyName];
929 - (NSString *) pluginAuthor {
930 return [self objectForKey:GrowlPluginInfoKeyAuthor];
932 - (NSString *) pluginDescription {
933 return [self objectForKey:GrowlPluginInfoKeyDescription];
935 - (NSString *) pluginVersion {
936 return [self objectForKey:GrowlPluginInfoKeyVersion];
938 - (NSBundle *) pluginBundle {
939 return [self objectForKey:GrowlPluginInfoKeyBundle];
941 - (NSString *) pluginPath {
942 return [self objectForKey:GrowlPluginInfoKeyPath];
944 - (NSSet *) pluginTypes {
945 return [self objectForKey:GrowlPluginInfoKeyTypes];
947 - (NSString *) pluginHumanReadableName {
948 return [self objectForKey:GrowlPluginInfoKeyHumanReadableName];
950 - (NSString *) pluginIdentifier {
951 return [self objectForKey:GrowlPluginInfoKeyIdentifier];
953 - (GrowlPlugin *) pluginInstance {
954 return [self objectForKey:GrowlPluginInfoKeyInstance];
959 #define ASSERT_IN_FUNCTION(condition, desc, ...) \
960 [[NSAssertionHandler currentHandler] handleFailureInFunction:[NSString stringWithCString:__func__] \
961 file:[NSString stringWithCString:__FILE__] \
962 lineNumber:__LINE__ \
963 description:desc, __VA_ARGS__];
965 NSInteger comparePluginHandlerRegistrationOrder(id a, id b, void *context) {
966 GrowlPluginController *self = (GrowlPluginController *)context;
967 NSArray *allPluginHandlers = [self allPluginHandlers];
969 NSUInteger aIndex = [allPluginHandlers indexOfObjectIdenticalTo:a];
970 NSUInteger bIndex = [allPluginHandlers indexOfObjectIdenticalTo:b];
972 ASSERT_IN_FUNCTION(aIndex != NSNotFound, @"Attempted to compare two plug-in handlers, but the first object was not a (registered) plug-in handler! Description of object: %@", a);
973 ASSERT_IN_FUNCTION(bIndex != NSNotFound, @"Attempted to compare two plug-in handlers, but the second object was not a (registered) plug-in handler! Description of object: %@", b);
976 return NSOrderedAscending;
977 else if (aIndex > bIndex)
978 return NSOrderedDescending;
980 return NSOrderedSame;