381 lines
14 KiB
Objective-C
381 lines
14 KiB
Objective-C
//
|
|
// GrowlPathUtil.m
|
|
// Growl
|
|
//
|
|
// Created by Ingmar Stein on 17.04.05.
|
|
// Copyright 2005-2006 The Growl Project. All rights reserved.
|
|
//
|
|
// This file is under the BSD License, refer to License.txt for details
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
|
|
#import "GrowlPathUtilities.h"
|
|
#import "GrowlDefinesInternal.h"
|
|
|
|
static NSBundle *helperAppBundle;
|
|
static NSBundle *prefPaneBundle;
|
|
|
|
#define NAME_OF_SCREENSHOTS_DIRECTORY @"Screenshots"
|
|
#define NAME_OF_TICKETS_DIRECTORY @"Tickets"
|
|
#define NAME_OF_PLUGINS_DIRECTORY @"Plugins"
|
|
|
|
@implementation GrowlPathUtilities
|
|
|
|
#pragma mark Bundles
|
|
|
|
//Searches the process list (as yielded by GetNextProcess) for a process with the given bundle identifier.
|
|
//Returns the oldest matching process.
|
|
+ (NSBundle *) bundleForProcessWithBundleIdentifier:(NSString *)identifier
|
|
{
|
|
|
|
restart:;
|
|
OSStatus err;
|
|
NSBundle *bundle = nil;
|
|
struct ProcessSerialNumber psn = { 0, 0 };
|
|
UInt32 oldestProcessLaunchDate = UINT_MAX;
|
|
|
|
while ((err = GetNextProcess(&psn)) == noErr) {
|
|
struct ProcessInfoRec info = { .processInfoLength = (UInt32)sizeof(struct ProcessInfoRec) };
|
|
err = GetProcessInformation(&psn, &info);
|
|
if (err == noErr) {
|
|
//Compare the launch dates first, since it's cheaper than comparing bundle IDs.
|
|
if (info.processLaunchDate < oldestProcessLaunchDate) {
|
|
//This one is older (fewer ticks since startup), so this is our current prospect to be the result.
|
|
NSDictionary *dict = (NSDictionary *)ProcessInformationCopyDictionary(&psn, kProcessDictionaryIncludeAllInformationMask);
|
|
|
|
if (dict) {
|
|
CFMakeCollectable(dict);
|
|
pid_t pid = 0;
|
|
GetProcessPID(&psn, &pid);
|
|
if ([[dict objectForKey:(NSString *)kCFBundleIdentifierKey] isEqualToString:identifier]) {
|
|
NSString *bundlePath = [dict objectForKey:@"BundlePath"];
|
|
if (bundlePath) {
|
|
bundle = [NSBundle bundleWithPath:bundlePath];
|
|
oldestProcessLaunchDate = info.processLaunchDate;
|
|
}
|
|
}
|
|
|
|
[dict release];
|
|
} else {
|
|
//ProcessInformationCopyDictionary returning NULL probably means that the process disappeared out from under us (i.e., exited) in between GetProcessInformation and ProcessInformationCopyDictionary. Start over.
|
|
goto restart;
|
|
}
|
|
}
|
|
} else {
|
|
if (err != noErr) {
|
|
//Unexpected failure of GetProcessInformation (Process Manager got confused?). Assume severe breakage and bail.
|
|
NSLog(@"Couldn't get information about process %lu,%lu: GetProcessInformation returned %i/%s", psn.highLongOfPSN, psn.lowLongOfPSN, err, GetMacOSStatusCommentString(err));
|
|
err = noErr; //So our NSLog for GetNextProcess doesn't complain. (I wish I had Python's while..else block.)
|
|
break;
|
|
} else {
|
|
//Process disappeared out from under us (i.e., exited) in between GetNextProcess and GetProcessInformation. Start over.
|
|
goto restart;
|
|
}
|
|
}
|
|
}
|
|
if (err != procNotFound) {
|
|
NSLog(@"%s: GetNextProcess returned %i/%s", __PRETTY_FUNCTION__, err, GetMacOSStatusCommentString(err));
|
|
}
|
|
|
|
return bundle;
|
|
}
|
|
|
|
//Obtains the bundle for the active GrowlHelperApp process. Returns nil if there is no such process.
|
|
+ (NSBundle *) runningHelperAppBundle {
|
|
return [self bundleForProcessWithBundleIdentifier:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
|
|
}
|
|
|
|
+ (NSBundle *) growlPrefPaneBundle {
|
|
NSArray *librarySearchPaths;
|
|
NSString *path;
|
|
NSString *bundleIdentifier;
|
|
NSEnumerator *searchPathEnumerator;
|
|
NSBundle *bundle;
|
|
|
|
if (prefPaneBundle)
|
|
return prefPaneBundle;
|
|
|
|
prefPaneBundle = [NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER];
|
|
if (prefPaneBundle)
|
|
return prefPaneBundle;
|
|
|
|
//If GHA is running, the prefpane bundle is the bundle that contains it.
|
|
NSBundle *runningHelperAppBundle = [self runningHelperAppBundle];
|
|
NSString *runningHelperAppBundlePath = [runningHelperAppBundle bundlePath];
|
|
//GHA in Growl.prefPane/Contents/Resources/
|
|
NSString *possiblePrefPaneBundlePath1 = [runningHelperAppBundlePath stringByDeletingLastPathComponent];
|
|
//GHA in Growl.prefPane/ (hypothetical)
|
|
NSString *possiblePrefPaneBundlePath2 = [[possiblePrefPaneBundlePath1 stringByDeletingLastPathComponent] stringByDeletingLastPathComponent];
|
|
if ([[[possiblePrefPaneBundlePath1 pathExtension] lowercaseString] isEqualToString:@"prefpane"]) {
|
|
prefPaneBundle = [NSBundle bundleWithPath:possiblePrefPaneBundlePath1];
|
|
if (prefPaneBundle)
|
|
return prefPaneBundle;
|
|
}
|
|
if ([[[possiblePrefPaneBundlePath2 pathExtension] lowercaseString] isEqualToString:@"prefpane"]) {
|
|
prefPaneBundle = [NSBundle bundleWithPath:possiblePrefPaneBundlePath2];
|
|
if (prefPaneBundle)
|
|
return prefPaneBundle;
|
|
}
|
|
|
|
static const unsigned bundleIDComparisonFlags = NSCaseInsensitiveSearch | NSBackwardsSearch;
|
|
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
|
//Find Library directories in all domains except /System (as of Panther, that's ~/Library, /Library, and /Network/Library)
|
|
librarySearchPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask & ~NSSystemDomainMask, YES);
|
|
|
|
/*First up, we'll look for Growl.prefPane, and if it exists, check whether
|
|
* it is our prefPane.
|
|
*This is much faster than having to enumerate all preference panes, and
|
|
* can drop a significant amount of time off this code.
|
|
*/
|
|
searchPathEnumerator = [librarySearchPaths objectEnumerator];
|
|
while ((path = [searchPathEnumerator nextObject])) {
|
|
path = [path stringByAppendingPathComponent:PREFERENCE_PANES_SUBFOLDER_OF_LIBRARY];
|
|
path = [path stringByAppendingPathComponent:GROWL_PREFPANE_NAME];
|
|
|
|
if ([fileManager fileExistsAtPath:path]) {
|
|
bundle = [NSBundle bundleWithPath:path];
|
|
|
|
if (bundle) {
|
|
bundleIdentifier = [bundle bundleIdentifier];
|
|
|
|
if (bundleIdentifier && ([bundleIdentifier compare:GROWL_PREFPANE_BUNDLE_IDENTIFIER options:bundleIDComparisonFlags] == NSOrderedSame)) {
|
|
prefPaneBundle = bundle;
|
|
return prefPaneBundle;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*Enumerate all installed preference panes, looking for the Growl prefpane
|
|
* bundle identifier and stopping when we find it.
|
|
*Note that we check the bundle identifier because we should not insist
|
|
* that the user not rename his preference pane files, although most users
|
|
* of course will not. If the user wants to mutilate the Info.plist file
|
|
* inside the bundle, he/she deserves to not have a working Growl
|
|
* installation.
|
|
*/
|
|
searchPathEnumerator = [librarySearchPaths objectEnumerator];
|
|
while ((path = [searchPathEnumerator nextObject])) {
|
|
NSString *bundlePath;
|
|
NSDirectoryEnumerator *bundleEnum;
|
|
|
|
path = [path stringByAppendingPathComponent:PREFERENCE_PANES_SUBFOLDER_OF_LIBRARY];
|
|
bundleEnum = [fileManager enumeratorAtPath:path];
|
|
|
|
while ((bundlePath = [bundleEnum nextObject])) {
|
|
if ([[bundlePath pathExtension] isEqualToString:PREFERENCE_PANE_EXTENSION]) {
|
|
bundle = [NSBundle bundleWithPath:[path stringByAppendingPathComponent:bundlePath]];
|
|
|
|
if (bundle) {
|
|
bundleIdentifier = [bundle bundleIdentifier];
|
|
|
|
if (bundleIdentifier && ([bundleIdentifier compare:GROWL_PREFPANE_BUNDLE_IDENTIFIER options:bundleIDComparisonFlags] == NSOrderedSame)) {
|
|
prefPaneBundle = bundle;
|
|
return prefPaneBundle;
|
|
}
|
|
}
|
|
|
|
[bundleEnum skipDescendents];
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
+ (NSBundle *) helperAppBundle {
|
|
if (!helperAppBundle) {
|
|
helperAppBundle = [self runningHelperAppBundle];
|
|
if (!helperAppBundle) {
|
|
//look in the prefpane bundle.
|
|
NSBundle *bundle = [GrowlPathUtilities growlPrefPaneBundle];
|
|
NSString *helperAppPath = [bundle pathForResource:@"GrowlHelperApp" ofType:@"app"];
|
|
helperAppBundle = [NSBundle bundleWithPath:helperAppPath];
|
|
}
|
|
}
|
|
return helperAppBundle;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Directories
|
|
|
|
+ (NSArray *) searchPathForDirectory:(GrowlSearchPathDirectory) directory inDomains:(GrowlSearchPathDomainMask) domainMask mustBeWritable:(BOOL)flag {
|
|
if (directory < GrowlSupportDirectory) {
|
|
NSArray *searchPath = NSSearchPathForDirectoriesInDomains(directory, domainMask, /*expandTilde*/ YES);
|
|
if (!flag)
|
|
return searchPath;
|
|
else {
|
|
//flag is not NO: exclude non-writable directories.
|
|
NSMutableArray *result = [NSMutableArray arrayWithCapacity:[searchPath count]];
|
|
NSFileManager *mgr = [NSFileManager defaultManager];
|
|
|
|
NSEnumerator *searchPathEnum = [searchPath objectEnumerator];
|
|
NSString *dir;
|
|
while ((dir = [searchPathEnum nextObject])) {
|
|
if ([mgr isWritableFileAtPath:dir])
|
|
[result addObject:dir];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
} else {
|
|
//determine what to append to each Application Support folder.
|
|
NSString *subpath = nil;
|
|
switch (directory) {
|
|
case GrowlSupportDirectory:
|
|
//do nothing.
|
|
break;
|
|
|
|
case GrowlScreenshotsDirectory:
|
|
subpath = NAME_OF_SCREENSHOTS_DIRECTORY;
|
|
break;
|
|
|
|
case GrowlTicketsDirectory:
|
|
subpath = NAME_OF_TICKETS_DIRECTORY;
|
|
break;
|
|
|
|
case GrowlPluginsDirectory:
|
|
subpath = NAME_OF_PLUGINS_DIRECTORY;
|
|
break;
|
|
|
|
default:
|
|
NSLog(@"ERROR: GrowlPathUtil was asked for directory 0x%x, but it doesn't know what directory that is. Please tell the Growl developers.", directory);
|
|
return nil;
|
|
}
|
|
if (subpath)
|
|
subpath = [@"Application Support/Growl" stringByAppendingPathComponent:subpath];
|
|
else
|
|
subpath = @"Application Support/Growl";
|
|
|
|
/*get the search path, and append the subpath to all the items therein.
|
|
*exclude results that don't exist.
|
|
*/
|
|
NSFileManager *mgr = [NSFileManager defaultManager];
|
|
BOOL isDir = NO;
|
|
|
|
NSArray *searchPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, domainMask, /*expandTilde*/ YES);
|
|
NSMutableArray *mSearchPath = [NSMutableArray arrayWithCapacity:[searchPath count]];
|
|
NSEnumerator *searchPathEnum = [searchPath objectEnumerator];
|
|
NSString *path;
|
|
while ((path = [searchPathEnum nextObject])) {
|
|
path = [path stringByAppendingPathComponent:subpath];
|
|
if ([mgr fileExistsAtPath:path isDirectory:&isDir] && isDir)
|
|
[mSearchPath addObject:path];
|
|
}
|
|
|
|
return mSearchPath;
|
|
}
|
|
}
|
|
|
|
+ (NSArray *) searchPathForDirectory:(GrowlSearchPathDirectory) directory inDomains:(GrowlSearchPathDomainMask) domainMask {
|
|
//NO to emulate the default NSSearchPathForDirectoriesInDomains behaviour.
|
|
return [self searchPathForDirectory:directory inDomains:domainMask mustBeWritable:NO];
|
|
}
|
|
|
|
+ (NSString *) growlSupportDirectory {
|
|
NSArray *searchPath = [self searchPathForDirectory:GrowlSupportDirectory inDomains:NSUserDomainMask mustBeWritable:YES];
|
|
if ([searchPath count])
|
|
return [searchPath objectAtIndex:0U];
|
|
else {
|
|
NSString *path = nil;
|
|
|
|
//if this doesn't return any writable directories, path will still be nil.
|
|
searchPath = [self searchPathForDirectory:NSLibraryDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
|
|
if ([searchPath count]) {
|
|
path = [[searchPath objectAtIndex:0U] stringByAppendingPathComponent:@"Application Support/Growl"];
|
|
//try to create it. if that doesn't work, don't return it. return nil instead.
|
|
if (![[NSFileManager defaultManager] createDirectoryAtPath:path attributes:nil])
|
|
path = nil;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
}
|
|
|
|
+ (NSString *) screenshotsDirectory {
|
|
NSArray *searchPath = [self searchPathForDirectory:GrowlScreenshotsDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
|
|
if ([searchPath count])
|
|
return [searchPath objectAtIndex:0U];
|
|
else {
|
|
NSString *path = nil;
|
|
|
|
//if this doesn't return any writable directories, path will still be nil.
|
|
path = [self growlSupportDirectory];
|
|
if (path) {
|
|
path = [path stringByAppendingPathComponent:NAME_OF_SCREENSHOTS_DIRECTORY];
|
|
//try to create it. if that doesn't work, don't return it. return nil instead.
|
|
if (![[NSFileManager defaultManager] createDirectoryAtPath:path attributes:nil])
|
|
path = nil;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
}
|
|
|
|
+ (NSString *) ticketsDirectory {
|
|
NSArray *searchPath = [self searchPathForDirectory:GrowlTicketsDirectory inDomains:NSAllDomainsMask mustBeWritable:YES];
|
|
if ([searchPath count])
|
|
return [searchPath objectAtIndex:0U];
|
|
else {
|
|
NSString *path = nil;
|
|
|
|
//if this doesn't return any writable directories, path will still be nil.
|
|
path = [self growlSupportDirectory];
|
|
if (path) {
|
|
path = [path stringByAppendingPathComponent:NAME_OF_TICKETS_DIRECTORY];
|
|
//try to create it. if that doesn't work, don't return it. return nil instead.
|
|
if (![[NSFileManager defaultManager] createDirectoryAtPath:path attributes:nil])
|
|
path = nil;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Screenshot names
|
|
|
|
+ (NSString *) nextScreenshotName {
|
|
return [self nextScreenshotNameInDirectory:nil];
|
|
}
|
|
|
|
+ (NSString *) nextScreenshotNameInDirectory:(NSString *) directory {
|
|
NSFileManager *mgr = [NSFileManager defaultManager];
|
|
|
|
if (!directory)
|
|
directory = [GrowlPathUtilities screenshotsDirectory];
|
|
|
|
//build a set of all the files in the directory, without their filename extensions.
|
|
NSArray *origContents = [mgr directoryContentsAtPath:directory];
|
|
NSMutableSet *directoryContents = [[NSMutableSet alloc] initWithCapacity:[origContents count]];
|
|
|
|
NSEnumerator *filesEnum = [origContents objectEnumerator];
|
|
NSString *existingFilename;
|
|
while ((existingFilename = [filesEnum nextObject]))
|
|
[directoryContents addObject:[existingFilename stringByDeletingPathExtension]];
|
|
|
|
//look for a filename that doesn't exist (with any extension) in the directory.
|
|
NSString *filename = nil;
|
|
unsigned long long i;
|
|
for (i = 1ULL; i < ULLONG_MAX; ++i) {
|
|
[filename release];
|
|
filename = [[NSString alloc] initWithFormat:@"Screenshot %llu", i];
|
|
if (![directoryContents containsObject:filename])
|
|
break;
|
|
}
|
|
[directoryContents release];
|
|
|
|
return [filename autorelease];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Tickets
|
|
|
|
+ (NSString *) defaultSavePathForTicketWithApplicationName:(NSString *) appName {
|
|
return [[self ticketsDirectory] stringByAppendingPathComponent:[appName stringByAppendingPathExtension:GROWL_PATHEXTENSION_TICKET]];
|
|
}
|
|
|
|
@end
|