Common/Source/GrowlPathUtilities.m
author Rudy Richter
Sat Aug 01 20:51:35 2009 -0400 (2009-08-01)
changeset 4263 680abae744d7
parent 4246 4f52d1d98978
child 4459 fac59330ff0c
permissions -rw-r--r--
Common: clang warning
     1 //
     2 //  GrowlPathUtil.m
     3 //  Growl
     4 //
     5 //  Created by Ingmar Stein on 17.04.05.
     6 //  Copyright 2005-2006 The Growl Project. All rights reserved.
     7 //
     8 // This file is under the BSD License, refer to License.txt for details
     9 
    10 #import "GrowlPathUtilities.h"
    11 #import "GrowlPreferencesController.h"
    12 #import "GrowlTicketController.h"
    13 #import "GrowlDefinesInternal.h"
    14 
    15 static NSBundle *helperAppBundle;
    16 static NSBundle *prefPaneBundle;
    17 
    18 #define NAME_OF_SCREENSHOTS_DIRECTORY           @"Screenshots"
    19 #define NAME_OF_TICKETS_DIRECTORY               @"Tickets"
    20 #define NAME_OF_PLUGINS_DIRECTORY               @"Plugins"
    21 
    22 @implementation GrowlPathUtilities
    23 
    24 #pragma mark Bundles
    25 
    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
    29 {
    30 
    31 restart:;
    32 	OSStatus err;
    33 	NSBundle *bundle = nil;
    34 	struct ProcessSerialNumber psn = { 0, 0 };
    35 	UInt32 oldestProcessLaunchDate = UINT_MAX;
    36 
    37 	while ((err = GetNextProcess(&psn)) == noErr) {
    38 		struct ProcessInfoRec info = { .processInfoLength = (UInt32)sizeof(struct ProcessInfoRec) };
    39 		err = GetProcessInformation(&psn, &info);
    40 		if (err == noErr) {
    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);
    45 				
    46 				if (dict) {
    47 					CFMakeCollectable(dict);
    48 					pid_t pid = 0;
    49 					GetProcessPID(&psn, &pid);
    50 					if ([[dict objectForKey:(NSString *)kCFBundleIdentifierKey] isEqualToString:identifier]) {
    51 						NSString *bundlePath = [dict objectForKey:@"BundlePath"];
    52 						if (bundlePath) {
    53 							bundle = [NSBundle bundleWithPath:bundlePath];
    54 							oldestProcessLaunchDate = info.processLaunchDate;
    55 						}
    56 					}
    57 
    58 					[dict release];
    59 				} else {
    60 					//ProcessInformationCopyDictionary returning NULL probably means that the process disappeared out from under us (i.e., exited) in between GetProcessInformation and ProcessInformationCopyDictionary. Start over.
    61 					goto restart;
    62 				}
    63 			}
    64 		} else {
    65 			if (err != noErr) {
    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.)
    69 				break;
    70 			} else {
    71 				//Process disappeared out from under us (i.e., exited) in between GetNextProcess and GetProcessInformation. Start over.
    72 				goto restart;
    73 			}
    74 		}
    75 	}
    76 	if (err != procNotFound) {
    77 		NSLog(@"%s: GetNextProcess returned %i/%s", __PRETTY_FUNCTION__, err, GetMacOSStatusCommentString(err));
    78 	}
    79 
    80 	return bundle;
    81 }
    82 
    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];
    86 }
    87 
    88 + (NSBundle *) growlPrefPaneBundle {
    89 	NSArray			*librarySearchPaths;
    90 	NSString		*path;
    91 	NSString		*bundleIdentifier;
    92 	NSEnumerator	*searchPathEnumerator;
    93 	NSBundle		*bundle;
    94 
    95 	if (prefPaneBundle)
    96 		return prefPaneBundle;
    97 
    98 	prefPaneBundle = [NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER];
    99  	if (prefPaneBundle)
   100 		return prefPaneBundle;
   101 
   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];
   111 		if (prefPaneBundle)
   112 			return prefPaneBundle;
   113 	}
   114 	if ([[[possiblePrefPaneBundlePath2 pathExtension] lowercaseString] isEqualToString:@"prefpane"]) {
   115 		prefPaneBundle = [NSBundle bundleWithPath:possiblePrefPaneBundlePath2];
   116 		if (prefPaneBundle)
   117 			return prefPaneBundle;
   118 	}
   119 	
   120 	static const unsigned bundleIDComparisonFlags = NSCaseInsensitiveSearch | NSBackwardsSearch;
   121 
   122 	NSFileManager *fileManager = [NSFileManager defaultManager];
   123 
   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);
   126 
   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.
   131 	 */
   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];
   136 
   137 		if ([fileManager fileExistsAtPath:path]) {
   138 			bundle = [NSBundle bundleWithPath:path];
   139 
   140 			if (bundle) {
   141 				bundleIdentifier = [bundle bundleIdentifier];
   142 
   143 				if (bundleIdentifier && ([bundleIdentifier compare:GROWL_PREFPANE_BUNDLE_IDENTIFIER options:bundleIDComparisonFlags] == NSOrderedSame)) {
   144 					prefPaneBundle = bundle;
   145 					return prefPaneBundle;
   146 				}
   147 			}
   148 		}
   149 	}
   150 
   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
   157 	 *	installation.
   158 	 */
   159 	searchPathEnumerator = [librarySearchPaths objectEnumerator];
   160 	while ((path = [searchPathEnumerator nextObject])) {
   161 		NSString				*bundlePath;
   162 		NSDirectoryEnumerator   *bundleEnum;
   163 
   164 		path = [path stringByAppendingPathComponent:PREFERENCE_PANES_SUBFOLDER_OF_LIBRARY];
   165 		bundleEnum = [fileManager enumeratorAtPath:path];
   166 
   167 		while ((bundlePath = [bundleEnum nextObject])) {
   168 			if ([[bundlePath pathExtension] isEqualToString:PREFERENCE_PANE_EXTENSION]) {
   169 				bundle = [NSBundle bundleWithPath:[path stringByAppendingPathComponent:bundlePath]];
   170 
   171 				if (bundle) {
   172 					bundleIdentifier = [bundle bundleIdentifier];
   173 
   174 					if (bundleIdentifier && ([bundleIdentifier compare:GROWL_PREFPANE_BUNDLE_IDENTIFIER options:bundleIDComparisonFlags] == NSOrderedSame)) {
   175 						prefPaneBundle = bundle;
   176 						return prefPaneBundle;
   177 					}
   178 				}
   179 
   180 				[bundleEnum skipDescendents];
   181 			}
   182 		}
   183 	}
   184 
   185 	return nil;
   186 }
   187 
   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];
   196 		}
   197 	}
   198 	return helperAppBundle;
   199 }
   200 
   201 #pragma mark -
   202 #pragma mark Directories
   203 
   204 + (NSArray *) searchPathForDirectory:(GrowlSearchPathDirectory) directory inDomains:(GrowlSearchPathDomainMask) domainMask mustBeWritable:(BOOL)flag {
   205 	if (directory < GrowlSupportDirectory) {
   206 		NSArray *searchPath = NSSearchPathForDirectoriesInDomains(directory, domainMask, /*expandTilde*/ YES);
   207 		if (!flag)
   208 			return searchPath;
   209 		else {
   210 			//flag is not NO: exclude non-writable directories.
   211 			NSMutableArray *result = [NSMutableArray arrayWithCapacity:[searchPath count]];
   212 			NSFileManager *mgr = [NSFileManager defaultManager];
   213 
   214 			NSEnumerator *searchPathEnum = [searchPath objectEnumerator];
   215 			NSString *dir;
   216 			while ((dir = [searchPathEnum nextObject])) {
   217 				if ([mgr isWritableFileAtPath:dir])
   218 					[result addObject:dir];
   219 			}
   220 
   221 			return result;
   222 		}
   223 	} else {
   224 		//determine what to append to each Application Support folder.
   225 		NSString *subpath = nil;
   226 		switch (directory) {
   227 			case GrowlSupportDirectory:
   228 				//do nothing.
   229 				break;
   230 
   231 			case GrowlScreenshotsDirectory:
   232 				subpath = NAME_OF_SCREENSHOTS_DIRECTORY;
   233 				break;
   234 
   235 			case GrowlTicketsDirectory:
   236 				subpath = NAME_OF_TICKETS_DIRECTORY;
   237 				break;
   238 
   239 			case GrowlPluginsDirectory:
   240 				subpath = NAME_OF_PLUGINS_DIRECTORY;
   241 				break;
   242 
   243 			default:
   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);
   245 				return nil;
   246 		}
   247 		if (subpath)
   248 			subpath = [@"Application Support/Growl" stringByAppendingPathComponent:subpath];
   249 		else
   250 			subpath =  @"Application Support/Growl";
   251 
   252 		/*get the search path, and append the subpath to all the items therein.
   253 		 *exclude results that don't exist.
   254 		 */
   255 		NSFileManager *mgr = [NSFileManager defaultManager];
   256 		BOOL isDir = NO;
   257 
   258 		NSArray *searchPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, domainMask, /*expandTilde*/ YES);
   259 		NSMutableArray *mSearchPath = [NSMutableArray arrayWithCapacity:[searchPath count]];
   260 		NSEnumerator *searchPathEnum = [searchPath objectEnumerator];
   261 		NSString *path;
   262 		while ((path = [searchPathEnum nextObject])) {
   263 			path = [path stringByAppendingPathComponent:subpath];
   264 			if ([mgr fileExistsAtPath:path isDirectory:&isDir] && isDir)
   265 				[mSearchPath addObject:path];
   266 		}
   267 
   268 		return mSearchPath;
   269 	}
   270 }
   271 
   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];
   275 }
   276 
   277 + (NSString *) growlSupportDirectory {
   278 	NSArray *searchPath = [self searchPathForDirectory:GrowlSupportDirectory inDomains:NSUserDomainMask mustBeWritable:YES];
   279 	if ([searchPath count])
   280 		return [searchPath objectAtIndex:0U];
   281 	else {
   282 		NSString *path = nil;
   283 
   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])
   290 				path = nil;
   291 		}
   292 
   293 		return path;
   294 	}
   295 }
   296 
   297 + (NSString *) screenshotsDirectory {
   298 	NSArray *searchPath = [self searchPathForDirectory:GrowlScreenshotsDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
   299 	if ([searchPath count])
   300 		return [searchPath objectAtIndex:0U];
   301 	else {
   302 		NSString *path = nil;
   303 
   304 		//if this doesn't return any writable directories, path will still be nil.
   305 		path = [self growlSupportDirectory];
   306 		if (path) {
   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])
   310 				path = nil;
   311 		}
   312 
   313 		return path;
   314 	}
   315 }
   316 
   317 + (NSString *) ticketsDirectory {
   318 	NSArray *searchPath = [self searchPathForDirectory:GrowlTicketsDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
   319 	if ([searchPath count])
   320 		return [searchPath objectAtIndex:0U];
   321 	else {
   322 		NSString *path = nil;
   323 
   324 		//if this doesn't return any writable directories, path will still be nil.
   325 		path = [self growlSupportDirectory];
   326 		if (path) {
   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])
   330 				path = nil;
   331 		}
   332 
   333 		return path;
   334 	}
   335 }
   336 
   337 #pragma mark -
   338 #pragma mark Screenshot names
   339 
   340 + (NSString *) nextScreenshotName {
   341 	return [self nextScreenshotNameInDirectory:nil];
   342 }
   343 
   344 + (NSString *) nextScreenshotNameInDirectory:(NSString *) directory {
   345 	NSFileManager *mgr = [NSFileManager defaultManager];
   346 
   347 	if (!directory)
   348 		directory = [GrowlPathUtilities screenshotsDirectory];
   349 
   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]];
   353 
   354 	NSEnumerator *filesEnum = [origContents objectEnumerator];
   355 	NSString *existingFilename;
   356 	while ((existingFilename = [filesEnum nextObject]))
   357 		[directoryContents addObject:[existingFilename stringByDeletingPathExtension]];
   358 
   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) {
   363 		[filename release];
   364 		filename = [[NSString alloc] initWithFormat:@"Screenshot %llu", i];
   365 		if (![directoryContents containsObject:filename])
   366 			break;
   367 	}
   368 	[directoryContents release];
   369 
   370 	return [filename autorelease];
   371 }
   372 
   373 #pragma mark -
   374 #pragma mark Tickets
   375 
   376 + (NSString *) defaultSavePathForTicketWithApplicationName:(NSString *) appName {
   377 	return [[self ticketsDirectory] stringByAppendingPathComponent:[appName stringByAppendingPathExtension:GROWL_PATHEXTENSION_TICKET]];
   378 }
   379 
   380 @end