5 // Created by Ingmar Stein on 17.04.05.
6 // Copyright 2005-2006 The Growl Project. All rights reserved.
8 // This file is under the BSD License, refer to License.txt for details
10 #import "GrowlPathUtilities.h"
11 #import "GrowlPreferencesController.h"
12 #import "GrowlTicketController.h"
13 #import "GrowlDefinesInternal.h"
15 static NSBundle *helperAppBundle;
16 static NSBundle *prefPaneBundle;
18 #define NAME_OF_SCREENSHOTS_DIRECTORY @"Screenshots"
19 #define NAME_OF_TICKETS_DIRECTORY @"Tickets"
20 #define NAME_OF_PLUGINS_DIRECTORY @"Plugins"
22 @implementation GrowlPathUtilities
26 //Searches the process list (as yielded by GetNextProcess) for a process with the given bundle identifier.
27 //Returns the oldest matching process.
28 + (NSBundle *) bundleForProcessWithBundleIdentifier:(NSString *)identifier
33 NSBundle *bundle = nil;
34 struct ProcessSerialNumber psn = { 0, 0 };
35 UInt32 oldestProcessLaunchDate = UINT_MAX;
37 while ((err = GetNextProcess(&psn)) == noErr) {
38 struct ProcessInfoRec info = { .processInfoLength = (UInt32)sizeof(struct ProcessInfoRec) };
39 err = GetProcessInformation(&psn, &info);
41 //Compare the launch dates first, since it's cheaper than comparing bundle IDs.
42 if (info.processLaunchDate < oldestProcessLaunchDate) {
43 //This one is older (fewer ticks since startup), so this is our current prospect to be the result.
44 NSDictionary *dict = (NSDictionary *)ProcessInformationCopyDictionary(&psn, kProcessDictionaryIncludeAllInformationMask);
47 CFMakeCollectable(dict);
49 GetProcessPID(&psn, &pid);
50 if ([[dict objectForKey:(NSString *)kCFBundleIdentifierKey] isEqualToString:identifier]) {
51 NSString *bundlePath = [dict objectForKey:@"BundlePath"];
53 bundle = [NSBundle bundleWithPath:bundlePath];
54 oldestProcessLaunchDate = info.processLaunchDate;
60 //ProcessInformationCopyDictionary returning NULL probably means that the process disappeared out from under us (i.e., exited) in between GetProcessInformation and ProcessInformationCopyDictionary. Start over.
66 //Unexpected failure of GetProcessInformation (Process Manager got confused?). Assume severe breakage and bail.
67 NSLog(@"Couldn't get information about process %lu,%lu: GetProcessInformation returned %i/%s", psn.highLongOfPSN, psn.lowLongOfPSN, err, GetMacOSStatusCommentString(err));
68 err = noErr; //So our NSLog for GetNextProcess doesn't complain. (I wish I had Python's while..else block.)
71 //Process disappeared out from under us (i.e., exited) in between GetNextProcess and GetProcessInformation. Start over.
76 if (err != procNotFound) {
77 NSLog(@"%s: GetNextProcess returned %i/%s", __PRETTY_FUNCTION__, err, GetMacOSStatusCommentString(err));
83 //Obtains the bundle for the active GrowlHelperApp process. Returns nil if there is no such process.
84 + (NSBundle *) runningHelperAppBundle {
85 return [self bundleForProcessWithBundleIdentifier:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
88 + (NSBundle *) growlPrefPaneBundle {
89 NSArray *librarySearchPaths;
91 NSString *bundleIdentifier;
92 NSEnumerator *searchPathEnumerator;
96 return prefPaneBundle;
98 prefPaneBundle = [NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER];
100 return prefPaneBundle;
102 //If GHA is running, the prefpane bundle is the bundle that contains it.
103 NSBundle *runningHelperAppBundle = [self runningHelperAppBundle];
104 NSString *runningHelperAppBundlePath = [runningHelperAppBundle bundlePath];
105 //GHA in Growl.prefPane/Contents/Resources/
106 NSString *possiblePrefPaneBundlePath1 = [runningHelperAppBundlePath stringByDeletingLastPathComponent];
107 //GHA in Growl.prefPane/ (hypothetical)
108 NSString *possiblePrefPaneBundlePath2 = [[possiblePrefPaneBundlePath1 stringByDeletingLastPathComponent] stringByDeletingLastPathComponent];
109 if ([[[possiblePrefPaneBundlePath1 pathExtension] lowercaseString] isEqualToString:@"prefpane"]) {
110 prefPaneBundle = [NSBundle bundleWithPath:possiblePrefPaneBundlePath1];
112 return prefPaneBundle;
114 if ([[[possiblePrefPaneBundlePath2 pathExtension] lowercaseString] isEqualToString:@"prefpane"]) {
115 prefPaneBundle = [NSBundle bundleWithPath:possiblePrefPaneBundlePath2];
117 return prefPaneBundle;
120 static const unsigned bundleIDComparisonFlags = NSCaseInsensitiveSearch | NSBackwardsSearch;
122 NSFileManager *fileManager = [NSFileManager defaultManager];
124 //Find Library directories in all domains except /System (as of Panther, that's ~/Library, /Library, and /Network/Library)
125 librarySearchPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask & ~NSSystemDomainMask, YES);
127 /*First up, we'll look for Growl.prefPane, and if it exists, check whether
128 * it is our prefPane.
129 *This is much faster than having to enumerate all preference panes, and
130 * can drop a significant amount of time off this code.
132 searchPathEnumerator = [librarySearchPaths objectEnumerator];
133 while ((path = [searchPathEnumerator nextObject])) {
134 path = [path stringByAppendingPathComponent:PREFERENCE_PANES_SUBFOLDER_OF_LIBRARY];
135 path = [path stringByAppendingPathComponent:GROWL_PREFPANE_NAME];
137 if ([fileManager fileExistsAtPath:path]) {
138 bundle = [NSBundle bundleWithPath:path];
141 bundleIdentifier = [bundle bundleIdentifier];
143 if (bundleIdentifier && ([bundleIdentifier compare:GROWL_PREFPANE_BUNDLE_IDENTIFIER options:bundleIDComparisonFlags] == NSOrderedSame)) {
144 prefPaneBundle = bundle;
145 return prefPaneBundle;
151 /*Enumerate all installed preference panes, looking for the Growl prefpane
152 * bundle identifier and stopping when we find it.
153 *Note that we check the bundle identifier because we should not insist
154 * that the user not rename his preference pane files, although most users
155 * of course will not. If the user wants to mutilate the Info.plist file
156 * inside the bundle, he/she deserves to not have a working Growl
159 searchPathEnumerator = [librarySearchPaths objectEnumerator];
160 while ((path = [searchPathEnumerator nextObject])) {
161 NSString *bundlePath;
162 NSDirectoryEnumerator *bundleEnum;
164 path = [path stringByAppendingPathComponent:PREFERENCE_PANES_SUBFOLDER_OF_LIBRARY];
165 bundleEnum = [fileManager enumeratorAtPath:path];
167 while ((bundlePath = [bundleEnum nextObject])) {
168 if ([[bundlePath pathExtension] isEqualToString:PREFERENCE_PANE_EXTENSION]) {
169 bundle = [NSBundle bundleWithPath:[path stringByAppendingPathComponent:bundlePath]];
172 bundleIdentifier = [bundle bundleIdentifier];
174 if (bundleIdentifier && ([bundleIdentifier compare:GROWL_PREFPANE_BUNDLE_IDENTIFIER options:bundleIDComparisonFlags] == NSOrderedSame)) {
175 prefPaneBundle = bundle;
176 return prefPaneBundle;
180 [bundleEnum skipDescendents];
188 + (NSBundle *) helperAppBundle {
189 if (!helperAppBundle) {
190 helperAppBundle = [self runningHelperAppBundle];
191 if (!helperAppBundle) {
192 //look in the prefpane bundle.
193 NSBundle *bundle = [GrowlPathUtilities growlPrefPaneBundle];
194 NSString *helperAppPath = [bundle pathForResource:@"GrowlHelperApp" ofType:@"app"];
195 helperAppBundle = [NSBundle bundleWithPath:helperAppPath];
198 return helperAppBundle;
202 #pragma mark Directories
204 + (NSArray *) searchPathForDirectory:(GrowlSearchPathDirectory) directory inDomains:(GrowlSearchPathDomainMask) domainMask mustBeWritable:(BOOL)flag {
205 if (directory < GrowlSupportDirectory) {
206 NSArray *searchPath = NSSearchPathForDirectoriesInDomains(directory, domainMask, /*expandTilde*/ YES);
210 //flag is not NO: exclude non-writable directories.
211 NSMutableArray *result = [NSMutableArray arrayWithCapacity:[searchPath count]];
212 NSFileManager *mgr = [NSFileManager defaultManager];
214 NSEnumerator *searchPathEnum = [searchPath objectEnumerator];
216 while ((dir = [searchPathEnum nextObject])) {
217 if ([mgr isWritableFileAtPath:dir])
218 [result addObject:dir];
224 //determine what to append to each Application Support folder.
225 NSString *subpath = nil;
227 case GrowlSupportDirectory:
231 case GrowlScreenshotsDirectory:
232 subpath = NAME_OF_SCREENSHOTS_DIRECTORY;
235 case GrowlTicketsDirectory:
236 subpath = NAME_OF_TICKETS_DIRECTORY;
239 case GrowlPluginsDirectory:
240 subpath = NAME_OF_PLUGINS_DIRECTORY;
244 NSLog(@"ERROR: GrowlPathUtil was asked for directory 0x%x, but it doesn't know what directory that is. Please tell the Growl developers.", directory);
248 subpath = [@"Application Support/Growl" stringByAppendingPathComponent:subpath];
250 subpath = @"Application Support/Growl";
252 /*get the search path, and append the subpath to all the items therein.
253 *exclude results that don't exist.
255 NSFileManager *mgr = [NSFileManager defaultManager];
258 NSArray *searchPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, domainMask, /*expandTilde*/ YES);
259 NSMutableArray *mSearchPath = [NSMutableArray arrayWithCapacity:[searchPath count]];
260 NSEnumerator *searchPathEnum = [searchPath objectEnumerator];
262 while ((path = [searchPathEnum nextObject])) {
263 path = [path stringByAppendingPathComponent:subpath];
264 if ([mgr fileExistsAtPath:path isDirectory:&isDir] && isDir)
265 [mSearchPath addObject:path];
272 + (NSArray *) searchPathForDirectory:(GrowlSearchPathDirectory) directory inDomains:(GrowlSearchPathDomainMask) domainMask {
273 //NO to emulate the default NSSearchPathForDirectoriesInDomains behaviour.
274 return [self searchPathForDirectory:directory inDomains:domainMask mustBeWritable:NO];
277 + (NSString *) growlSupportDirectory {
278 NSArray *searchPath = [self searchPathForDirectory:GrowlSupportDirectory inDomains:NSUserDomainMask mustBeWritable:YES];
279 if ([searchPath count])
280 return [searchPath objectAtIndex:0U];
282 NSString *path = nil;
284 //if this doesn't return any writable directories, path will still be nil.
285 searchPath = [self searchPathForDirectory:NSLibraryDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
286 if ([searchPath count]) {
287 path = [[searchPath objectAtIndex:0U] stringByAppendingPathComponent:@"Application Support/Growl"];
288 //try to create it. if that doesn't work, don't return it. return nil instead.
289 if (![[NSFileManager defaultManager] createDirectoryAtPath:path attributes:nil])
297 + (NSString *) screenshotsDirectory {
298 NSArray *searchPath = [self searchPathForDirectory:GrowlScreenshotsDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
299 if ([searchPath count])
300 return [searchPath objectAtIndex:0U];
302 NSString *path = nil;
304 //if this doesn't return any writable directories, path will still be nil.
305 path = [self growlSupportDirectory];
307 path = [path stringByAppendingPathComponent:NAME_OF_SCREENSHOTS_DIRECTORY];
308 //try to create it. if that doesn't work, don't return it. return nil instead.
309 if (![[NSFileManager defaultManager] createDirectoryAtPath:path attributes:nil])
317 + (NSString *) ticketsDirectory {
318 NSArray *searchPath = [self searchPathForDirectory:GrowlTicketsDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
319 if ([searchPath count])
320 return [searchPath objectAtIndex:0U];
322 NSString *path = nil;
324 //if this doesn't return any writable directories, path will still be nil.
325 path = [self growlSupportDirectory];
327 path = [path stringByAppendingPathComponent:NAME_OF_TICKETS_DIRECTORY];
328 //try to create it. if that doesn't work, don't return it. return nil instead.
329 if (![[NSFileManager defaultManager] createDirectoryAtPath:path attributes:nil])
338 #pragma mark Screenshot names
340 + (NSString *) nextScreenshotName {
341 return [self nextScreenshotNameInDirectory:nil];
344 + (NSString *) nextScreenshotNameInDirectory:(NSString *) directory {
345 NSFileManager *mgr = [NSFileManager defaultManager];
348 directory = [GrowlPathUtilities screenshotsDirectory];
350 //build a set of all the files in the directory, without their filename extensions.
351 NSArray *origContents = [mgr directoryContentsAtPath:directory];
352 NSMutableSet *directoryContents = [[NSMutableSet alloc] initWithCapacity:[origContents count]];
354 NSEnumerator *filesEnum = [origContents objectEnumerator];
355 NSString *existingFilename;
356 while ((existingFilename = [filesEnum nextObject]))
357 [directoryContents addObject:[existingFilename stringByDeletingPathExtension]];
359 //look for a filename that doesn't exist (with any extension) in the directory.
360 NSString *filename = nil;
361 unsigned long long i;
362 for (i = 1ULL; i < ULLONG_MAX; ++i) {
364 filename = [[NSString alloc] initWithFormat:@"Screenshot %llu", i];
365 if (![directoryContents containsObject:filename])
368 [directoryContents release];
370 return [filename autorelease];
376 + (NSString *) defaultSavePathForTicketWithApplicationName:(NSString *) appName {
377 return [[self ticketsDirectory] stringByAppendingPathComponent:[appName stringByAppendingPathExtension:GROWL_PATHEXTENSION_TICKET]];