owntone-server/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.m
2006-07-26 07:48:06 +00:00

2269 lines
73 KiB
Objective-C
Raw Blame History

// NOTE: The canonical Cocoa way to do things is with a strict Model-View-Controller
// organization. However, that seems a bit silly for a simple case like a prefs
// pane, so the OrgFireflyMediaServerPrefs object is both model and controller.
#import <Foundation/NSPathUtilities.h>
#import <netinet/in.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <arpa/inet.h>
#import "OrgFireflyMediaServerPrefs.h"
#include "../FireflyCommon.h"
// Here we define some constants used when testing the existence of and accessing
// the components of our installation.
#define FIREFLY_HELPER_NAME "Firefly Helper.app"
#define FIREFLY_HELPER_PROC_N "Firefly Helper"
#define FIREFLY_PLUGIN_DIR "plugins"
#define FIREFLY_LOG_FILE "firefly.log"
#define FIREFLY_PLAYLIST_FILE "firefly.playlist"
@implementation OrgFireflyMediaServerPrefs
// ===========================================================================
// Initialization and deallocation
// ===========================================================================
- (id)initWithBundle:(NSBundle *)bundle
{
if( ( self = [super initWithBundle:bundle] ) != nil )
{
appID = CFSTR( "org.fireflymediaserver.prefs" );
// Init our instance variables
configFileStrings = [[NSMutableArray arrayWithCapacity:100] retain];
configError = [[NSMutableString stringWithCapacity:20] retain];
fireflyFolderPath = [[NSMutableString stringWithCapacity:20] retain];
fireflyHelperPath = [[NSMutableString stringWithCapacity:20] retain];
serverURL = [[NSMutableString stringWithCapacity:20] retain];
logFilePath = [[NSMutableString stringWithCapacity:20] retain];
playlistPath = [[NSMutableString stringWithCapacity:20] retain];
userName = nil;
configFilePath = [[NSMutableString stringWithCapacity:20] retain];
serverName = [[NSMutableString stringWithCapacity:20] retain];
serverPassword = [[NSMutableString stringWithCapacity:20] retain];
libraryPath = [[NSMutableString stringWithCapacity:20] retain];
serverProxy = nil;
protocolChecker = nil;
ipcTimer = nil;
logTimer = nil;
logDate = nil;
srand((unsigned int)time(NULL));
}
return self;
}
- (void)dealloc
{
[configFileStrings release];
[configError release];
[fireflyFolderPath release];
[fireflyHelperPath release];
[serverURL release];
[logFilePath release];
[playlistPath release];
[userName release];
[configFilePath release];
[serverName release];
[serverPassword release];
[libraryPath release];
[serverProxy release];
[protocolChecker release];
[ipcTimer release];
[logTimer release];
[logDate release];
[super dealloc];
}
// ===========================================================================
// NSPreferencePane methods for handling the installation and removal of
// the panel. We use these to read our prefs, set up our UI, and start
// and stop our scan for the server, as well as confirming whether a user
// wants to apply changes.
// ===========================================================================
// ---------------------------------------------------------------------------
// willSelect
//
// NSPreferencePane instance method. We're about to be put on screen.
// ---------------------------------------------------------------------------
- (void)willSelect
{
// NOTE: docs say default impl does nothing, so not necessary to call [super willSelect];
// Set up our user name (used for the library name as as IPC). Must do
// this early, because setDefaultValues will need it to make the library
// name. ("Copy" function name means no need to retain but we do need
// to release later. CSStringRef is toll-free bridged to NSString*)
[userName autorelease]; // in case we are being re-loaded within one Prefs session
userName = (NSString*)CSCopyUserName( false );
// We're about to be loaded. Set up everything
[self setDefaultValues];
// This is a bit of a hack. bConfigNeedsSaving will be set to YES upon
// exit from validateInstall if validateInstall had to create a new
// prefs file. We use this as a cue that it's a fresh install, and we
// need to get the startup item installed (by calling saveSettings)
bConfigNeedsSaving = NO;
if( ![self validateInstall] )
{
[self disableAllControls];
bConfigNeedsSaving = NO;
}
else
{
if(![self loadSettings])
{
[configError setString:NSLocalizedString( @"Unable to read configuration information",
@"Error message related to invalid config" ) ];
configAppearsValid = NO;
}
else
{
// If ValidateInstall told us it created a new file, then we are
// going to do some hacky things. First, set bStartServerOnLogin to
// false and save settings. This ensures that when we start firefly
// Helper in a few seconds, it does not launch the server before the
// user has a chance to set their settings. Then, we'll set it
// back to its original value, and leave bConfigNeedsSaving
// set. This way, when the user closes the panel or starts the server,
// their changes will be set. Ugh.
if( bConfigNeedsSaving )
{
BOOL priorVal = bStartServerOnLogin;
bStartServerOnLogin = NO;
[self saveSettings];
CFPreferencesAppSynchronize( CFSTR(FF_PREFS_DOMAIN) ); // flush changes
bConfigNeedsSaving = YES; // saveSettings sets to NO
bStartServerOnLogin = priorVal;
}
}
}
// Snag our current version
NSString *versionString = [[NSBundle bundleForClass:[self class]]
objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
[panelVersionText setStringValue:versionString];
if( configAppearsValid )
{
// GUI setup to initial state (note that although some of these are set in
// the nib, we may be closed and then re-opened, so we need to set them
// here.
[browseButton setEnabled:YES];
[nameField setEnabled:YES];
[passwordCheckbox setEnabled:YES];
[helperMenuCheckbox setEnabled:YES];
[serverStartOptions setEnabled:YES];
[mainTabView selectFirstTabViewItem:self];
[nameField setStringValue:serverName];
[libraryField setStringValue:libraryPath];
[self setIconForPath];
[passwordField setStringValue:serverPassword];
if( [serverPassword length] > 0 )
{
[passwordCheckbox setState:NSOnState];
[passwordField setEnabled:YES];
}
else
{
[passwordCheckbox setState:NSOffState];
[passwordField setEnabled:NO];
}
[portField setIntValue:serverPort];
if( 0 != serverPort )
{
[portField setEnabled:YES];
[portPopup selectItemAtIndex:1];
}
else
{
[portField setEnabled:NO];
[portPopup selectItemAtIndex:0];
}
if( bStartServerOnLogin )
[serverStartOptions selectItemAtIndex:1];
else
[serverStartOptions selectItemAtIndex:0];
// bConfigNeedsSaving is configured above
[applyNowButton setEnabled:bConfigNeedsSaving];
[helperMenuCheckbox setState:(bShowHelperMenu ? NSOnState : NSOffState)];
// Member setup to initial state (note, these are not our actual
// preferences, which are set above). Rather, these are members for running
// the prefs pane.
serverProxy = nil;
ipcTimer = nil;
logTimer = nil;
[logDate autorelease];
logDate = [[NSDate distantPast] retain];
// Start by assuming that the server is not running.
[self updateServerStatus:kFireflyStatusStopped];
// We always need the helper running when the panel is running,
// so launch it if it's not already running
[self launchHelperIfNeeded];
}
}
// ---------------------------------------------------------------------------
// didSelect
//
// NSPreferencePane instance method. We're now on screen.
// ---------------------------------------------------------------------------
- (void)didSelect
{
// NOTE: docs say default impl does nothing, so not necessary to call [super didSelect];
// We've been loaded and are on screen.
// Did we encounter any errors at startup that will prevent us from doing work? If so,
// here's where we put up a sheet to explain.
if( configAppearsValid )
{
// No errors. We could go ahead and try right now to establish
// Connection. BUT, since we may be being opened in response to the
// Helper application's menu choice, we avoid a possible (temporary)
// deadlock by doing our first proxy attempt in the timer function,
// which allows didSelect to return and let the Apple Event complete.
#if 0
//
if( [self makeProxyConnection] )
{
[self updateServerStatus:[self fireflyStatus]];
NSString *string = [self fireflyVersion];
if( nil != string )
[self versionChanged:string];
string = [self fireflyConfigURL];
if( nil != string )
[self configUrlChanged:string];
}
else
#endif
{
[startStopButton setEnabled:NO];
[statusText setStringValue:NSLocalizedString( @"Checking Firefly status<75>",
@"Status text for when Firefly state is not known" )];
[progressSpinner startAnimation:self];
ipcTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(proxyTimerFired:)
userInfo:nil
repeats:YES] retain];
}
}
else
{
NSString *errorIntro = NSLocalizedString( @"Firefly appears to be incorrectly installed or damaged. "
"Please consult the documentation.\n\n",
@"Explanatory text for the failure-to-apply alert" );
NSString *errorString = [errorIntro stringByAppendingString:configError];
NSBeginCriticalAlertSheet( NSLocalizedString( @"Configuration error",
@"Alert message notifying the user of config error" ),
@"OK",
NULL,
NULL,
[[self mainView] window],
nil,
NULL,
NULL,
NULL,
errorString );
}
}
// ---------------------------------------------------------------------------
// shouldUnselect
//
// NSPreferencePane delegate method
// ---------------------------------------------------------------------------
- (NSPreferencePaneUnselectReply)shouldUnselect
{
// We write our config when the user clicks "Apply Now". If they've made changes
// but not clicked the button, we need to ask them here if they want to save.
// if changes need saving, we want to put up a sheet asking if they want to apply
// NOTE: Sheets are complicated to deal with, because you have to handle their results
// in delegate methods, and it gets a bit wonky if handling the result in turn
// requires another modal dialog. Anyway, we post the sheet here. Look for sheetDidEnd to
// see the handling of the result.
if( bConfigNeedsSaving )
{
// Even more complicated than the average sheet (where we could call NSBeginAlertSheet),
// because we offer Cmd-D for "Don't apply"
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:NSLocalizedString( @"Apply configuration changes?",
@"Prompt to save changes when exiting prefs pane" )];
[alert addButtonWithTitle:NSLocalizedString( @"Apply", @"Label for apply button in save prompt dialog" )];
[alert addButtonWithTitle:NSLocalizedString( @"Cancel", @"Label for cancel button in save prompt dialog" )];
NSButton *button;
button = [alert addButtonWithTitle:NSLocalizedString( @"Don't Apply",
@"Label for dont' apply button in save prompt dialog" )];
[button setKeyEquivalent:@"d"];
[button setKeyEquivalentModifierMask:NSCommandKeyMask];
[alert beginSheetModalForWindow:[[self mainView] window]
modalDelegate:self
didEndSelector:@selector(applySheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
return NSUnselectLater;
}
else
{
return NSUnselectNow;
}
}
// ---------------------------------------------------------------------------
// willUnselect
//
// NSPreferencePane delegate method
// ---------------------------------------------------------------------------
- (void)willUnselect
{
// NOTE: docs say default impl does nothing, so not necessary to call
// [super willUnselect];
// We could be unselected, then reselected, so there are a few objects
// where we need to go ahead and disconnect them and then release
// them so we can re-create if we're reloaded.
[ipcTimer invalidate];
[ipcTimer autorelease];
ipcTimer = nil;
[logTimer invalidate];
[logTimer autorelease];
logTimer = nil;
if( nil != serverProxy )
{
@try
{
[serverProxy unregisterClientId:clientIdent];
}
@catch( NSException *exception )
{
NSLog(@"willUnselect caught %@: %@",
[exception name], [exception reason]);
}
@finally
{
[serverProxy autorelease];
serverProxy = nil;
}
}
// Flush our prefs
CFPreferencesAppSynchronize( CFSTR(FF_PREFS_DOMAIN) );
// Last, make sure the login item is set up appropriately, no matter how
// we are getting out of here.
[self updateLoginItem];
}
// ===========================================================================
// Functions to handle user input and interaction
// ===========================================================================
// ---------------------------------------------------------------------------
// browseButtonClicked:
//
// User wants to change the library location. Pop up an "Open" sheet
// ---------------------------------------------------------------------------
- (IBAction)browseButtonClicked:(id)sender
{
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setCanChooseDirectories:YES];
[panel setCanChooseFiles:NO];
[panel setResolvesAliases:YES];
[panel setPrompt:NSLocalizedString( @"Choose", @"The Choose button in the library browser dialog" )];
[panel setTitle:NSLocalizedString( @"Choose Library Location", @"Title of the library browser dialog" )];
[panel setMessage:NSLocalizedString(
@"Please select the folder containing your music library, then click Choose.",
@"Info text for the library browse dialog" )];
NSString *path = [@"~/" stringByExpandingTildeInPath]; // default
NSString *file = nil;
NSFileManager *mgr = [NSFileManager defaultManager];
BOOL bIsDir = NO;
if( [mgr fileExistsAtPath:libraryPath isDirectory:&bIsDir] && bIsDir )
{
file = [libraryPath lastPathComponent];
path = [libraryPath stringByDeletingLastPathComponent];
}
[panel beginSheetForDirectory:path
file:file
types:nil
modalForWindow:[[self mainView] window]
modalDelegate:self
didEndSelector:@selector(browsePanelEnded:returnCode:contextInfo:)
contextInfo:nil];
}
// ---------------------------------------------------------------------------
// browsePanelEnded:returnCode:contextInfo:
//
// Delegate method for the "Open" sheet. Handle the user's choice.
// ---------------------------------------------------------------------------
- (void)browsePanelEnded:(NSOpenPanel *)panel returnCode:(int)panelResult contextInfo:(void *)contextInfo
{
if( NSOKButton == panelResult )
{
NSArray *selectedDirArray = [panel filenames];
if( 0 < [selectedDirArray count] )
{
[libraryPath setString:[selectedDirArray objectAtIndex:0]];
[libraryField setStringValue:libraryPath];
[self setIconForPath];
[self setConfigNeedsSaving:YES];
}
}
}
// ---------------------------------------------------------------------------
// passwordChanged:
// ---------------------------------------------------------------------------
- (IBAction)passwordChanged:(id)sender
{
if( NSOrderedSame != [serverPassword compare:[passwordField stringValue]] )
{
[serverPassword setString:[passwordField stringValue]];
[self setConfigNeedsSaving:YES];
}
}
// ---------------------------------------------------------------------------
// shareNameChanged:
// ---------------------------------------------------------------------------
- (IBAction)shareNameChanged:(id)sender
{
if( NSOrderedSame != [serverName compare:[nameField stringValue]] )
{
[serverName setString:[nameField stringValue]];
[self setConfigNeedsSaving:YES];
}
}
// ---------------------------------------------------------------------------
// portPopupChanged:
// ---------------------------------------------------------------------------
- (IBAction)portPopupChanged:(id)sender
{
if( 0 == [portPopup indexOfSelectedItem] )
{
#if 0
[portField abortEditing];
[portField setIntValue:currentServerPort];
[portField setEnabled:false];
#endif
}
else
{
[portField setEnabled:true];
[[[self mainView] window] makeFirstResponder:portField];
}
[self setConfigNeedsSaving:YES];
}
// ---------------------------------------------------------------------------
// portChanged:
//
// The value of the port changed
// ---------------------------------------------------------------------------
- (IBAction)portChanged:(id)sender
{
if( serverPort != [portField intValue] )
{
serverPort = [portField intValue];
[self setConfigNeedsSaving:YES];
}
}
// ---------------------------------------------------------------------------
// pwCheckBoxChanged:
//
// User changed the state of the "Require Password" checkbox.
// ---------------------------------------------------------------------------
- (IBAction)pwCheckBoxChanged:(id)sender
{
if( NSOffState == [passwordCheckbox state] )
{
[passwordField validateEditing];
[passwordField setStringValue:@""];
[serverPassword setString:@""];
[passwordField setEnabled:false];
[self setConfigNeedsSaving:YES];
}
else
{
[passwordField setEnabled:true];
[[[self mainView] window] makeFirstResponder:passwordField];
if( 0 < [serverPassword length] )
[self setConfigNeedsSaving:YES]; // Only enable if there's a password
}
}
// ---------------------------------------------------------------------------
// serverStartOptionChanged:
//
// User changed the popup menu of server options.
// ---------------------------------------------------------------------------
- (IBAction)serverStartOptionChanged:(id)sender
{
bStartServerOnLogin = ( 1 == [serverStartOptions indexOfSelectedItem] );
[self setConfigNeedsSaving:YES];
}
// ---------------------------------------------------------------------------
// startStopButtonClicked:
//
// Start or stop the server.
// ---------------------------------------------------------------------------
- (IBAction)startStopButtonClicked:(id)sender
{
if( ![self fireflyIsRunning] )
{
// Server is not running, so we need to start it. First, let's see
// if we have unsaved changes
BOOL bOKToStart = !bConfigNeedsSaving;
if( bConfigNeedsSaving &&
[[[self mainView] window] makeFirstResponder:[[self mainView] window]] &&
[self currentTabIsValid] )
{
if( [self saveSettings] )
{
[applyNowButton setEnabled:NO];
bOKToStart = YES;
}
else
{
NSBeginCriticalAlertSheet( NSLocalizedString( @"Failed to save changes",
@"Alert message notifying the user of failure to save" ),
@"OK",
NULL,
NULL,
[[self mainView] window],
nil,
NULL,
NULL,
NULL,
NSLocalizedString( @"Firefly could not be started because your changes "
"could not be saved",
@"Explanatory text for the failure-to-save alert" ) );
}
}
if( bOKToStart )
{
[self updateServerStatus:kFireflyStatusStarting];
if( ![self startFirefly] )
{
[self updateServerStatus:kFireflyStatusInvalid];
NSBeginCriticalAlertSheet( NSLocalizedString( @"Unable to start Firefly",
@"Alert message notifying the user of failure to stop" ),
@"OK",
NULL,
NULL,
[[self mainView] window],
nil,
NULL,
NULL,
NULL,
NSLocalizedString( @"An unexpected error occurred when trying to start Firefly. "
"Please close and re-open this Preference pane, and try again.",
@"Explanatory text for the failure-to-stop alert" ) );
}
}
}
else
{
// Server is running, so stop it.
if( [self stopFirefly] )
{
[self updateServerStatus:kFireflyStatusStopping];
}
else
{
[self updateServerStatus:kFireflyStatusInvalid];
NSBeginCriticalAlertSheet( NSLocalizedString( @"Unable to stop Firefly",
@"Alert message notifying the user of failure to stop" ),
@"OK",
NULL,
NULL,
[[self mainView] window],
nil,
NULL,
NULL,
NULL,
NSLocalizedString( @"An unexpected error occurred when trying to stop Firefly. "
"Please close and re-open this Preference pane, and try again.",
@"Explanatory text for the failure-to-stop alert" ) );
}
}
}
// ---------------------------------------------------------------------------
// webPageButtonClicked:
//
// User clicked the Open Web Page button, so we want to open the server's
// config page.
// ---------------------------------------------------------------------------
- (IBAction)webPageButtonClicked:(id)sender
{
// User clicked the Show web page button. Open the firefly internal page.
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:serverURL]];
}
// ---------------------------------------------------------------------------
// applyNowButtonClicked:
//
// Time to save our settings!
// ---------------------------------------------------------------------------
- (IBAction)applyNowButtonClicked:(id)sender
{
if( [[[self mainView] window] makeFirstResponder:[[self mainView] window]] &&
[self currentTabIsValid] )
{
if( [self saveSettings] )
{
[applyNowButton setEnabled:NO];
if( [self fireflyIsRunning] )
[self restartFirefly];
}
else
NSBeginCriticalAlertSheet( NSLocalizedString( @"Failed to apply changes",
@"Alert message notifying the user of failure to apply" ),
@"OK",
NULL,
NULL,
[[self mainView] window],
nil,
NULL,
NULL,
NULL,
NSLocalizedString( @"Due to an unexpected error, your changes could not "
"be applied.",
@"Explanatory text for the failure-to-apply alert" ) );
}
}
// ---------------------------------------------------------------------------
// helperMenuCheckboxClicked:
//
// User clicked the checkbox to show or hide the firefly menu. This happens
// right away. The helper writes the pref for us.
// ---------------------------------------------------------------------------
- (IBAction)helperMenuCheckboxClicked:(id)sender
{
if( NSOffState == [helperMenuCheckbox state] )
[self showHelperMenu:NO];
else
[self showHelperMenu:YES];
}
// ---------------------------------------------------------------------------
// logoButtonClicked:
//
// User clicked the logo button, so we want to open the Firefly web site
// ---------------------------------------------------------------------------
- (IBAction)logoButtonClicked:(id)sender
{
// User clicked the Firefly logo in the prefs pane. Open the web page.
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://fireflymediaserver.org"]];
}
// ---------------------------------------------------------------------------
// controlTextDidChange:
//
// If the text in the control changes at all (the first time a user adds
// or removes a character), we want to mark the configuration as needing
// to be saved
// ---------------------------------------------------------------------------
- (void)controlTextDidChange:(NSNotification *)notification
{
// If any of our text fields have changed text, we need to enable the "Apply" button.
[self setConfigNeedsSaving:YES];
}
// ---------------------------------------------------------------------------
// validateInstall:
//
// Called when the prefs pane is first being loaded. Locates the pieces we
// need to do our work (specifically, the config file and the Firefly Helper
// application). Creates the Firefly directory and a default config file
// if none is present. Makes note of any errors encountered for reporting
// to the user when the panel finishes loading.
// ---------------------------------------------------------------------------
- (BOOL)validateInstall
{
configAppearsValid = NO;
do // while( false )
{
// First up, locate or create the Firefly directory in Application Support.
NSFileManager *mgr = [NSFileManager defaultManager];
NSArray * appSupportDirArray = nil;
NSString * appSupportPath = nil;
// If we were guaranteed to be on 10.4 or later, we could call this:
//appSupportDirArray = NSSearchPathForDirectoriesInDomains( NSApplicationSupportDirectory,
// NSUserDomainMask,
// YES );
// But, we're not on 10.4; we have to go back to 10.3. So, we look in
// the Library directory.
appSupportDirArray = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory,
NSUserDomainMask,
YES );
if( [appSupportDirArray count] > 0 )
{
appSupportPath = [[appSupportDirArray objectAtIndex:0]
stringByAppendingPathComponent:@"Application Support"];
}
else
{
[configError setString:NSLocalizedString( @"Library directory could not be found in user folder",
@"Error message displayed at panel load" )];
break;
}
BOOL isDir = YES;
if( ![mgr fileExistsAtPath:appSupportPath isDirectory:&isDir] || !isDir )
{
BOOL bFail = YES;
// If this is still true, it means that the directory is missing
// (Otherwise, there's a *file* called Application Support here!
if( isDir )
{
bFail = ( 0 != mkdir( [mgr fileSystemRepresentationWithPath:appSupportPath],
0755 ) );
}
if( bFail )
{
[configError setString:NSLocalizedString( @"Unable to find or create Application Support folder",
@"Error message displayed at panel load" )];
break;
}
}
[fireflyFolderPath setString:[appSupportPath stringByAppendingPathComponent:@FIREFLY_DIR_NAME]];
if( ![mgr fileExistsAtPath:fireflyFolderPath isDirectory:&isDir] || !isDir )
{
// As above, except that this is less unexpected
BOOL bFail = YES;
// If this is still true, it means that the directory is missing
// (Otherwise, there's a *file* called Application Support here!
if( isDir )
{
bFail = ( 0 != mkdir( [mgr fileSystemRepresentationWithPath:fireflyFolderPath],
0755 ) );
}
if( bFail )
{
// We're done. If we can't find the Firefly directory, notify the user and disable
// everything. Yes, maybe the server might be running and we could locate it, but somebody
// who has installed the server in a non-standard location doesn't need us.
NSString *formatString = NSLocalizedString( @"Firefly directory could not be found or created at: %@",
"Format string for error message" );
[configError setString:[NSString stringWithFormat:formatString, fireflyFolderPath]];
break;
}
}
// Check for the config file
[configFilePath setString:[fireflyFolderPath stringByAppendingPathComponent:@FIREFLY_CONF_NAME]];
if( [mgr fileExistsAtPath:configFilePath] )
{
// It exists. Can we write to it?
if( ![mgr isWritableFileAtPath:configFilePath] )
{
// This is bad. If we can't write to the config file, all we can do is open the web page and
// start/stop the server
NSString *formatString = NSLocalizedString( @"The configuration file is present, but is not writable: %@",
"Format string for error message" );
[configError setString:[NSString stringWithFormat:formatString, configFilePath]];
break;
}
}
else
{
// No config file, so let's create the default one
if( ![self createDefaultConfigFile] )
{
// Fatal error. Alert the user and disable everything.
NSString *formatString = NSLocalizedString( @"Unable to create a default configuration file at: %@",
"Format string for error message upon invalid install" );
[configError setString:[NSString stringWithFormat:formatString, configFilePath]];
break;
}
// This lets willSelect know that we wrote a new config file
bConfigNeedsSaving = YES;
}
// Check to make sure the helper app is present (also required)
[fireflyHelperPath setString:[[NSBundle bundleForClass:[self class]] pathForResource:@FIREFLY_HELPER_NAME
ofType:nil]];
if( ![mgr isExecutableFileAtPath:fireflyHelperPath] )
{
// As above, this is a fatal error
[configError setString:NSLocalizedString( @"The Firefly installation appears to be damaged. Unable to"
" locate Firefly Helper.",
@"Format string for error message upon invalid install" )];
break;
}
// Phew!
configAppearsValid = YES;
} while( false );
return configAppearsValid;
}
// ---------------------------------------------------------------------------
// readConfigFromPath:
//
// Reading and writing the config file
// ---------------------------------------------------------------------------
- (BOOL)readConfigFromPath:(NSString*)path
{
// I'm sure there's a nice Cocoa/Carbon/MacOS way to do this, but the docs
// are not forthcoming. So, we do it the Unix way.
FILE *configFile = fopen( [path UTF8String], "r" );
if( NULL == configFile )
return NO;
// Set up our members in case we've been run before
idxOfServerName = 0;
idxOfPassword = 0;
idxOfPort = 0;
idxOfLibraryPath = 0;
idxOfNextSection = 0;
idxOfDbPath = 0;
idxOfLogPath = 0;
// Now, read the file
BOOL bInGeneral = NO;
char buf[1024]; // yes, a hardcoded limit, but seriously, this is for one line.
[configFileStrings removeAllObjects];
while( NULL != fgets( buf, 1024, configFile ) )
{
buf[1023] = 0;
NSString *line = [NSString stringWithUTF8String:buf];
[configFileStrings addObject:line];
// Check to see if this is one of the lines we care about
if( 0 == idxOfNextSection )
{
if( bInGeneral )
{
if( 0 == idxOfServerName && 0 == strncasecmp( buf, "servername", 10 ) )
{
idxOfServerName = [configFileStrings count] - 1;
[serverName setString:[self readValueFromBuf:buf startingAt:10 unescapeCommas:NO]];
}
else if( 0 == idxOfPassword && 0 == strncasecmp( buf, "password", 8 ) )
{
idxOfPassword = [configFileStrings count] - 1;
[serverPassword setString:[self readValueFromBuf:buf startingAt:8 unescapeCommas:NO]];
}
else if( 0 == idxOfPort && 0 == strncasecmp( buf, "port", 4 ) )
{
idxOfPort = [configFileStrings count] - 1;
NSString *tmp = [self readValueFromBuf:buf startingAt:4 unescapeCommas:NO];
unsigned long num = atol( [tmp UTF8String] );
if( num < 65536 )
serverPort = num;
}
else if( 0 == idxOfLibraryPath && 0 == strncasecmp( buf, "mp3_dir", 7 ) )
{
idxOfLibraryPath = [configFileStrings count] - 1;
[libraryPath setString:[self readValueFromBuf:buf startingAt:7 unescapeCommas:YES]];
}
else if( 0 == idxOfDbPath && 0 == strncasecmp( buf, "db_parms", 8 ) )
{
// We only save the index of this if we're going to need to write the path
// out. We need to write it out if the string is empty (which means that
// it's coming from a default file).
NSString *string = [self readValueFromBuf:buf startingAt:8 unescapeCommas:NO];
if( 0 == [string length] )
idxOfDbPath = [configFileStrings count] - 1;
}
else if( 0 == idxOfLogPath && 0 == strncasecmp( buf, "logfile", 7 ) )
{
// as above
[logFilePath setString:[self readValueFromBuf:buf startingAt:7 unescapeCommas:NO]];
if( 0 == [logFilePath length] )
idxOfLogPath = [configFileStrings count] - 1;
}
else if( 0 == idxOfPlaylistPath && 0 == strncasecmp( buf, "playlist", 8 ) )
{
// as above
NSString *string = [self readValueFromBuf:buf startingAt:8 unescapeCommas:NO];
if( 0 == [string length] )
idxOfPlaylistPath = [configFileStrings count] - 1;
}
else if( buf[0] == '[' )
{
idxOfNextSection = [configFileStrings count] - 1;
}
}
else
{
if( 0 == strncasecmp( buf, "[general]", 9 ) )
bInGeneral = YES;
}
}
}
fclose( configFile );
return YES;
}
// ---------------------------------------------------------------------------
// WriteCommaEscapedStringToFile:
//
// Utility function. Takes a const char* as input and copies the string,
// escaping any commas as ",,". Writes the resulting string to the
// supplied FILE*, which is assumed to be open for writing
//
// NOTE: This function could be smarter. For example, if a string
// is passed in that is 1023 characters in length but contains commas,
// this function will truncate the string silently.
// ---------------------------------------------------------------------------
static void
WriteCommaEscapedStringToFile( FILE *inFile, const char *inStringToEscape )
{
if( NULL == inFile || NULL == inStringToEscape )
return;
char escapingBuf[1025]; // 1 extra in case of a final comma
int i = 0;
int j = 0;
while( '\0' != inStringToEscape[i] && j < 1023 )
{
// Extra comma for any comma we find
if( ',' == inStringToEscape[i] )
escapingBuf[j++] = ',';
escapingBuf[j++] = inStringToEscape[i++];
}
escapingBuf[j] = '\0';
fputs( escapingBuf, inFile );
}
// ---------------------------------------------------------------------------
// writeConfigToPath:
//
// Writes our configuration to the supplied path. readConfigFromPath
// MUST have been called first!
// ---------------------------------------------------------------------------
- (BOOL)writeConfigToPath:(NSString*)path
{
if( nil == configFileStrings || 0 == [configFileStrings count] )
return NO;
FILE *configFile = fopen( [path UTF8String], "w" );
if( NULL == configFile )
return NO;
char buf[1024];
unsigned i = 0;
for( ; i < [configFileStrings count]; i++ )
{
if( 0 == i )
{
// 0 is special-cased since it's also a special token for "this line wasn't
// in the original". Since we only use the new file format, though, we're guaranteed
// that none of our tokens is at the first (0th) line, because [general] has to
// come before any of our tokens
fputs( [[configFileStrings objectAtIndex:0] UTF8String], configFile );
}
else if( i == idxOfNextSection )
{
// We've reached the end of our general section, so it's now time to write
// out anything that the user has set, but that wasn't found in the config
// file before. Note that this is basically error recovery for somebody
// mucking with the file, since all lines should be present but with emtpy
// values if an optional setting isn't set. For example:
// password =
// If we didn't find it before, its index will be 0.
// servername is required
if( 0 == idxOfServerName )
{
sprintf( buf, "servername = %s\n", [serverName UTF8String] );
fputs( buf, configFile );
}
// so is the library path
if( 0 == idxOfLibraryPath )
{
sprintf( buf, "mp3_dir = %s\n", [libraryPath UTF8String] );
WriteCommaEscapedStringToFile( configFile, buf );
}
// password and port are optional, so don't write an empty entry
if( 0 == idxOfPassword && [serverPassword length] > 0 )
{
sprintf( buf, "password = %s\n", [serverPassword UTF8String] );
fputs( buf, configFile );
}
if( 0 == idxOfPort && 0 != serverPort )
{
sprintf( buf, "port = %u\n", serverPort );
fputs( buf, configFile );
}
// Don't forget the section header for that next section!
fputs( [[configFileStrings objectAtIndex:i] UTF8String], configFile );
}
else if( i == idxOfServerName )
{
sprintf( buf, "servername = %s\n", [serverName UTF8String] );
fputs( buf, configFile );
}
else if( i == idxOfPassword ) // NOTE: This will write an empty password if none is set
{
sprintf( buf, "password = %s\n", [serverPassword UTF8String] );
fputs( buf, configFile );
}
else if( i == idxOfPort )
{
if( 0 == serverPort )
fputs( "port =\n", configFile ); // no port is set, so write an empty value
else
{
sprintf( buf, "port = %u\n", serverPort );
fputs( buf, configFile );
}
}
else if( i == idxOfLibraryPath )
{
sprintf( buf, "mp3_dir = %s\n", [libraryPath UTF8String] );
WriteCommaEscapedStringToFile( configFile, buf );
}
else if ( i == idxOfDbPath )
{
sprintf( buf, "db_parms = %s\n", [fireflyFolderPath UTF8String] );
WriteCommaEscapedStringToFile( configFile, buf );
}
else if( i == idxOfLogPath )
{
sprintf( buf, "logfile = %s\n", [logFilePath UTF8String] );
WriteCommaEscapedStringToFile( configFile, buf );
}
else if( i == idxOfPlaylistPath )
{
sprintf( buf, "playlist = %s\n", [playlistPath UTF8String] );
WriteCommaEscapedStringToFile( configFile, buf );
}
else
{
// Just output our stored line
fputs( [[configFileStrings objectAtIndex:i] UTF8String], configFile );
}
}
fclose( configFile );
[self setConfigNeedsSaving:NO];
return YES;
}
// ---------------------------------------------------------------------------
// readValueFromBuf:startingAt:unescapeCommas:
//
// Read the value from a key/value pair string. idx is the start point, which
// is assumed to be the next character after the key. NOTE that this function
// may modify buf if it contains trailing whitespace.
// ---------------------------------------------------------------------------
- (NSString *)readValueFromBuf:(char*)buf startingAt:(int)idx unescapeCommas:(BOOL)bUnescapeCommas
{
char *retVal = NULL;
BOOL bFoundEquals = NO;
// skip over whitespace and = characters
while( buf[idx] )
{
if( buf[idx] == '=' )
bFoundEquals = YES;
else if( buf[idx] != ' ' && buf[idx] != '\t' )
break;
idx++;
}
// Okay, we found whitespace or the end of the line. If we didn't find an equals sign, then the
// value is empty. If there's nothing there but a newline, that's also considered empty.
if( !bFoundEquals || buf[idx] == '\0' || buf[idx] == '\n' || buf[idx] == '\r' )
return [NSString string];
// We found an equals, so retVal will point at our string to return. Now it's time
// to clip off any trailing whitespace. We work back from the end of the line, since
// whitespace is permitted in some places like the path and server name.
retVal = &buf[idx];
idx = strlen( retVal ); // this is at least 1, because we tested for empty above
while( idx-- && (retVal[idx] == ' ' || retVal[idx] == '\t' || retVal[idx] == '\r' || retVal[idx] == '\n') )
retVal[idx] = '\0';
// And, finally, if bUnescapeCommas is true, walk the string and
// convert ",," to "," in place
if( bUnescapeCommas)
{
int readIdx = 0;
int writeIdx = 0;
while( '\0' != retVal[readIdx] )
{
if( ',' == retVal[readIdx] && ',' == retVal[readIdx+1] )
readIdx++;
retVal[writeIdx++] = retVal[readIdx++];
}
retVal[writeIdx] = '\0';
}
return [NSString stringWithUTF8String:retVal];
}
// ---------------------------------------------------------------------------
// createDefaultConfigFile:
//
// Read the value from a key/value pair string. idx is the start point, which
// is assumed to be the next character after the key. NOTE that this function
// may modify buf if it contains trailing whitespace.
// ---------------------------------------------------------------------------
- (BOOL)createDefaultConfigFile
{
// Read the default file from our bundle
NSString *configPath = nil;
NSBundle *thisBundle = [NSBundle bundleForClass:[self class]];
if( nil == (configPath = [thisBundle pathForResource:@FIREFLY_CONF_NAME ofType:nil]) ||
![self readConfigFromPath:configPath] )
return NO;
// Set the default values. This takes the items that are deliberately
// left blank in the "starter" config file (because they're specific
// to the particular installation location) and fills them in.
[self setDefaultValues];
// Write it out
return [self writeConfigToPath:configFilePath];
}
// ---------------------------------------------------------------------------
// setDefaultValues:
//
// Model utility sets up the members representing server configuration to
// their proper initial defaults (which are host- and user-specific!)
// ---------------------------------------------------------------------------
- (void)setDefaultValues
{
// easy ones first
serverPort = 0;
[serverPassword setString:@""]; // FIXME: really no better way to clear a string?
bStartServerOnLogin = YES;
// Get the host name and make the default library name in a localization-friendly way
NSString *hostname = (NSString*)CSCopyMachineName();
NSString *format = NSLocalizedString( @"%@'s Firefly on %@",
@"Format string for default library name" );
[serverName setString:[NSString stringWithFormat:format, userName, hostname]];
[hostname release];
// Defaults for the log file and playlist paths. These get used only
// when we first write out our default config file.
[logFilePath setString:[fireflyFolderPath stringByAppendingPathComponent:
@FIREFLY_LOG_FILE]];
[playlistPath setString:[fireflyFolderPath stringByAppendingPathComponent:
@FIREFLY_PLAYLIST_FILE]];
// Finally, the default Music directory
[libraryPath setString:[@"~/Music" stringByExpandingTildeInPath]];
}
// ---------------------------------------------------------------------------
// setConfigNeedsSaving:
//
// Tracking the need to save the config.
// ---------------------------------------------------------------------------
-(void)setConfigNeedsSaving:(BOOL)needsSaving
{
[applyNowButton setEnabled:needsSaving];
bConfigNeedsSaving = needsSaving;
}
// ===========================================================================
// UI utility functions for setting the UI into certain common states
// (sets up control enabling, text, etc.)
// ===========================================================================
// ---------------------------------------------------------------------------
// disableAllControls:
//
// Disables all the controls in the prefs pane. Used when the configuration
// is invalid.
// ---------------------------------------------------------------------------
- (void)disableAllControls
{
[browseButton setEnabled:false];
[libraryField setEnabled:false];
[nameField setEnabled:false];
[passwordCheckbox setEnabled:false];
[passwordField setEnabled:false];
[portField setEnabled:false];
[serverStartOptions setEnabled:false];
[startStopButton setEnabled:false];
[webPageButton setEnabled:false];
[helperMenuCheckbox setEnabled:false];
}
// ---------------------------------------------------------------------------
// updateServerStatus:
//
// Handles updating all relevant UI elements according to the server status
// ---------------------------------------------------------------------------
- (void)updateServerStatus:(FireflyServerStatus) status
{
BOOL bAnimateProgress = NO;
BOOL bActivateStartStop = YES;
BOOL bButtonIsStart = YES;
BOOL bClearWebAndVersion = NO;
switch( status )
{
case kFireflyStatusStartFailed:
case kFireflyStatusCrashed:
case kFireflyStatusStopped:
bClearWebAndVersion = YES;
break;
case kFireflyStatusRestarting:
case kFireflyStatusStarting:
bAnimateProgress = YES;
bButtonIsStart = NO;
break;
case kFireflyStatusActive:
case kFireflyStatusScanning:
bButtonIsStart = NO;
break;
case kFireflyStatusStopping:
bActivateStartStop = NO;
bAnimateProgress = YES;
break;
case kFireflyStatusInvalid:
default:
bActivateStartStop = NO;
bClearWebAndVersion = YES;
break;
}
[startStopButton setEnabled:bActivateStartStop];
[statusText setStringValue:StringForFireflyStatus(status)];
if( bAnimateProgress )
[progressSpinner startAnimation:self];
else
[progressSpinner stopAnimation:self];
if( bButtonIsStart )
[startStopButton setTitle:NSLocalizedString( @"Start Firefly",
@"One of several titles for the start/stop button" )];
else
[startStopButton setTitle:NSLocalizedString( @"Stop Firefly",
@"One of several titles for the start/stop button" )];
if( bClearWebAndVersion )
{
[serverVersionText setStringValue:NSLocalizedString( @"(available when Firefly is running)",
@"Displayed in place of server version when server "
"is not running" )];
[webPageButton setEnabled:NO];
[webPageInfoText setStringValue:NSLocalizedString( @"Additional configuration options are "
"available from Firefly's built-in web page. "
"Available when Firefly is running.",
@"Info text for the web page button when server "
"is not running" )];
}
}
// ===========================================================================
// Alert delegate method(s)
// ===========================================================================
// ---------------------------------------------------------------------------
// alertDidEnd:returnCode:contextInfo:
//
// This is called for our "OK"-type alerts, and we don't need to do anything
// extra.
// ---------------------------------------------------------------------------
- (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
}
// ---------------------------------------------------------------------------
// applySheetDidEnd:returnCode:contextInfo:
//
// Sheet delegate method, specially for the "apply changes" sheet. Depending
// upon the user's answer, we may need to write out our config file and
// restart the server. We definitely have to send the replyToShouldUnselect
// message, since we deferred a reply in shouldUnselect, and that's what
// prompts this sheet.
// ---------------------------------------------------------------------------
- (void) applySheetDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
BOOL bResponse = YES; // what do we say to shouldUnselect?
// We want the sheet closed in case we have to post another alert
[[alert window] orderOut:self];
if( NSAlertSecondButtonReturn == returnCode ) // "Cancel" button
{
bResponse = NO;
}
else if( NSAlertThirdButtonReturn == returnCode ) // "Don't Apply" button
{
// bResponse is already YES
}
else if( NSAlertFirstButtonReturn == returnCode ) // "Apply" button
{
if( [self currentTabIsValid] )
{
bResponse = YES;
if( ![self saveSettings] )
{
bResponse = NO;
NSBeginCriticalAlertSheet( NSLocalizedString( @"Failed to apply changes",
@"Alert message notifying the user of failure to apply" ),
@"OK",
NULL,
NULL,
[[self mainView] window],
nil,
NULL,
NULL,
NULL,
NSLocalizedString( @"Due to an unexpected error, your changes could not "
"be applied.",
@"Explanatory text for the failure-to-apply alert" ) );
}
else
{
// If the server is running, we need to restart it. Happily,
// the Firefly Helper will take care of that, so we can
// go ahead and exit
if( [self fireflyIsRunning] )
[self restartFirefly];
}
}
else
{
// Our tab data wasn't valid, so now there's an alert on the
// screen. Cancel closing the sheet.
bResponse = NO;
}
}
[self replyToShouldUnselect:bResponse];
}
// ===========================================================================
// Tab view delegate method(s)
// ===========================================================================
// ---------------------------------------------------------------------------
// -tabView:shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem
//
// Our job is to return false if the tab view should not be able to switch
// panes. We shouldn't switch if there's invalid text in any of our
// fields. By trying to get the main window to become first responder, we
// make sure that any field with editing in process will call its delegate
// to see if the field value is valid. If it's not, then it won't be able
// to give up first responder status, and makeFirstResponder will return
// false.
// ---------------------------------------------------------------------------
- (BOOL)tabView:(NSTabView *)tabView shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem
{
BOOL bRetVal = [[[self mainView] window] makeFirstResponder:[[self mainView] window]];
if( bRetVal )
bRetVal = [self currentTabIsValid];
if( bRetVal )
{
// See whether we need to start or stop our log update timer
if( 3 == [tabView numberOfTabViewItems] )
{
NSTabViewItem *logTab = [tabView tabViewItemAtIndex:2];
if( [tabViewItem isEqual:logTab] )
{
// Update the view and start the timer
[self updateLogTextView];
logTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(logTimerFired:)
userInfo:nil
repeats:YES] retain];
}
else if( [[tabView selectedTabViewItem] isEqual:logTab] )
{
// stop timer
[logTimer invalidate];
[logTimer autorelease];
logTimer = nil;
}
}
}
return bRetVal;
}
// ---------------------------------------------------------------------------
// control:isValidObject
//
// NSControl delegate method, called when a control is about to commit its
// newly-edited value. We return NO if the new value is not allowed, so
// it will not allow editing to leave.
// ---------------------------------------------------------------------------
- (BOOL)control:(NSControl *)control isValidObject:(id) obj
{
BOOL bRetVal = YES;
if( [obj isKindOfClass:[NSString class]] )
{
NSString *string = (NSString *)obj;
if( control == portField )
{
bRetVal = ( 1023 < [string intValue] && 65536 > [string intValue] );
}
else if( control == nameField )
{
bRetVal = ( 0 < [string length] );
}
else if( control == passwordField )
{
bRetVal = ( NSOffState == [passwordCheckbox state] || 0 < [string length] );
}
}
if( NO == bRetVal )
[self alertForControl:control];
return bRetVal;
}
// ---------------------------------------------------------------------------
// currentTabIsValid
//
// Here's the deal. Annoyingly, Cocoa text fields don't call their delegate
// methods when they lose focus *if* they haven't changed. So, this method
// is our backstop, in case the other delegates don't catch this case. It
// figures out the current tab, and then checks that the fields are valid.
//
// NOTE: We assume that the window has already been set to the first
// responder, so that we can query the controls directly for their values.
// ---------------------------------------------------------------------------
- (BOOL)currentTabIsValid
{
BOOL bRetVal = YES;
NSTabViewItem *selectedTab = [mainTabView selectedTabViewItem];
int idx;
if( nil != selectedTab )
{
idx = [mainTabView indexOfTabViewItem:selectedTab];
if( 0 == idx )
{
// General
if( ! (bRetVal = [self control:nameField isValidObject:[nameField objectValue]]) )
[self alertForControl:nameField];
else if( ! (bRetVal = [self control:passwordField isValidObject:[passwordField objectValue]]) )
[self alertForControl:nameField];
}
else if( 1 == idx )
{
// Advanced
// If "Manual" is selected, but the value of the field is not kosher, we must say no
if( 1 == [portPopup indexOfSelectedItem] &&
!(bRetVal = [self control:portField isValidObject:[portField objectValue]]) )
[self alertForControl:portField];
}
}
return bRetVal;
}
// ========================================================================
// Private utilities
// ========================================================================
// ---------------------------------------------------------------------------
// alertForControl
//
// There are a couple of places where we may need to pop up a modal alert
// because a control's value is not valid. So, we have this utility. It
// displays a modal "OK" style alert sheet with text specific to the
// control, then returns.
// ---------------------------------------------------------------------------
- (void)alertForControl:(NSControl *)control
{
NSString *alertTitle;
NSString *alertMessage;
if( control == nameField )
{
alertTitle = NSLocalizedString( @"Missing library name", "@Alert title when library name is invalid" );
alertMessage = NSLocalizedString( @"Please enter a library name",
@"Error message if library name is invalid" );
}
else if( control == passwordField )
{
alertTitle = NSLocalizedString( @"Missing password", "@Alert title when password is invalid" );
alertMessage = NSLocalizedString( @"Please enter a password, or un-check the password checkbox",
@"Error message if password is empty" );
}
else if( control == portField )
{
alertTitle = NSLocalizedString( @"Invalid port number", "@Alert title when port number is invalid" );
alertMessage = NSLocalizedString( @"Please enter a port number between 1024 and 65535, or choose "
"\"Automatic\" from the pop-up menu",
@"Error message if invalid port entered" );
}
else
{
alertTitle = NSLocalizedString( @"Invalid value", @"Generic alert string for an invalid control" );
alertMessage = @"";
}
NSBeginAlertSheet( alertTitle,
@"OK", NULL, NULL, [[self mainView] window],
nil, NULL, NULL, NULL,
alertMessage );
}
// ---------------------------------------------------------------------------
// setIconForPath
//
// This function takes our library path and sets the icon in the Advanced tab
// to be that path's icon. It's complicated a bit by the need to have a
// special icon in case the path can't be found.
// ---------------------------------------------------------------------------
- (void)setIconForPath
{
NSFileManager *mgr = [NSFileManager defaultManager];
BOOL isDir = NO;
if( [mgr fileExistsAtPath:libraryPath isDirectory:&isDir] && isDir )
{
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
[libraryIcon setImage:[workspace iconForFile:libraryPath]];
}
else
{
// we want a default "?" image, and IconServices is kind enough to oblige
IconRef unknownIcon;
if( 0 == GetIconRef( kOnSystemDisk, kSystemIconsCreator, kUnknownFSObjectIcon, &unknownIcon ) )
{
NSImage* image = [[NSImage alloc] initWithSize:NSMakeSize(32,32)];
[image lockFocus];
CGRect iconRect = CGRectMake(0,0,32,32);
PlotIconRefInContext((CGContextRef)[[NSGraphicsContext currentContext] graphicsPort],
&iconRect,
kAlignNone,
kTransformNone,
NULL /*labelColor*/,
kPlotIconRefNormalFlags,
unknownIcon);
[image unlockFocus];
[libraryIcon setImage:image];
[image release];
ReleaseIconRef( unknownIcon );
}
else
{
[libraryIcon setImage:nil];
}
}
}
// ---------------------------------------------------------------------------
// loadSettings
//
// Read the config file and also fetch our server startup preference from
// MacOS's preference mechanism
// ---------------------------------------------------------------------------
- (BOOL)loadSettings
{
[self readSettingsForHelper:&bShowHelperMenu
andServer:&bStartServerOnLogin];
return [self readConfigFromPath:configFilePath];
}
// ---------------------------------------------------------------------------
// saveSettings
//
// Writes out the config file and sets our prefs for whether we need to
// launch the server at login
//
// Returns NO if this fails.
// ---------------------------------------------------------------------------
- (BOOL)saveSettings
{
BOOL bSuccess = NO;
CFPreferencesSetAppValue( CFSTR(FF_PREFS_LAUNCH_AT_LOGIN),
bStartServerOnLogin ? kCFBooleanTrue : kCFBooleanFalse,
CFSTR(FF_PREFS_DOMAIN) );
// Now the server config file
bSuccess = [self writeConfigToPath:configFilePath];
return bSuccess;
}
// ---------------------------------------------------------------------------
// updateLoginItem
//
// Based upon the current state of the persistent prefs (NOT our locals),
// either set or un-set the Helper as a login item.
//
// NOTE: If bStartOnLogin is true, or if bShowMenu is true, then
// we want the firefly helper to be in the startup items (because it handles
// both of those tasks). But, if bShowMenu is true and bStartOnLogin
// isn't, we'll start the helper but not the server.
// ---------------------------------------------------------------------------
- (BOOL)updateLoginItem
{
BOOL bSuccess = NO;
NSString *scriptSource = nil;
BOOL bStartOnLogin = NO;
BOOL bShowMenu = NO;
[self readSettingsForHelper:&bShowMenu andServer:&bStartOnLogin];
if( bStartOnLogin || bShowMenu )
{
scriptSource = [NSString stringWithFormat:
@"tell application \"System Events\"\n"
"if \"Firefly Helper\" is not in (name of every login item) then\n"
"make login item at end with properties {hidden:false, path:\"%@\"}\n"
"end if\n"
"end tell",
fireflyHelperPath];
}
else
{
scriptSource = [NSString stringWithFormat:
@"tell application \"System Events\"\n"
"if \"Firefly Helper\" is in (name of every login item) then\n"
"delete (every login item whose name is \"Firefly Helper\")\n"
"end if\n"
"end tell\n"];
}
NSDictionary *errorDict = nil;
NSAppleScript *myScript = [[NSAppleScript alloc] initWithSource:scriptSource];
if( nil != myScript )
{
bSuccess = (nil != [myScript executeAndReturnError:&errorDict]);
[myScript release];
}
return bSuccess;
}
// ---------------------------------------------------------------------------
// readSettingsForHelper:andServer:
//
// Utility to read the helper and server launch settings, since we need to
// do it in more than one place
// ---------------------------------------------------------------------------
- (void)readSettingsForHelper:(BOOL*)outHelper andServer:(BOOL*)outServer
{
if( NULL != outHelper )
{
CFBooleanRef showHelper =
CFPreferencesCopyAppValue( CFSTR(FF_PREFS_SHOW_MENU_EXTRA),
CFSTR(FF_PREFS_DOMAIN) );
if( nil != showHelper )
{
*outHelper = CFBooleanGetValue( showHelper );
CFRelease( showHelper );
}
else
{
// default value
*outHelper = NO;
}
}
if( NULL != outServer )
{
CFBooleanRef shouldLaunch =
CFPreferencesCopyAppValue( CFSTR(FF_PREFS_LAUNCH_AT_LOGIN),
CFSTR(FF_PREFS_DOMAIN) );
if( nil != shouldLaunch )
{
*outServer = CFBooleanGetValue( shouldLaunch );
CFRelease( shouldLaunch );
}
else
{
// default value
*outServer = YES;
}
}
}
// ------------------------------------------------------------------------
// helperIsRunning
//
// Returns YES if "Firefly Helper" is running under our UID.
// ------------------------------------------------------------------------
- (BOOL)helperIsRunning
{
bool bRetVal = NO;
kinfo_proc *result;
size_t length;
GetProcesses( &result, &length );
// Okay, now we have our list of processes. Let's find OUR copy of
// firefly. Note that Firefly runs as two processes, so we look
// for the higher-numbered one.
if( NULL != result )
{
int procCount = length / sizeof(kinfo_proc);
int i = 0;
uid_t ourUID = getuid();
for( ; i < procCount; i++ )
{
if( ourUID == result[i].kp_eproc.e_pcred.p_ruid &&
0 == strcasecmp( result[i].kp_proc.p_comm, FIREFLY_HELPER_PROC_N ) )
{
bRetVal = YES;
break;
}
}
free( result );
}
return bRetVal;
}
// ------------------------------------------------------------------------
// launchHelperIfNeeded
//
// Checks to see if our helper app is already running. If not, launch
// it using NSTask (which doesn't have the issue in NSWorkspace where
// launching another app, even background-only, causes us to lose our
// window focus!).
// ------------------------------------------------------------------------
- (void)launchHelperIfNeeded
{
if( ![self helperIsRunning] )
{
NSBundle *bundle = [NSBundle bundleWithPath:fireflyHelperPath];
NSString *path = [bundle executablePath];
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:path];
[task launch];
[task release];
}
}
// ------------------------------------------------------------------------
// makeProxyConnection
//
// Try to connect up our serverProxy object by looking it up by name.
// Returns a BOOL to indicate whether it has succeeded
// ------------------------------------------------------------------------
- (BOOL)makeProxyConnection
{
BOOL bRetVal = NO;
NSString *serviceName = [@"FireflyHelper" stringByAppendingString:(NSString*)userName];
serverProxy = [NSConnection rootProxyForConnectionWithRegisteredName:serviceName
host:nil];
if( nil != serverProxy )
{
// This will notify us if the helper quits out from under us
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(connectionDied:)
name:NSConnectionDidDieNotification
object:nil];
[serverProxy retain];
[serverProxy setProtocolForProxy:@protocol(FireflyPrefsServerProtocol)];
clientIdent = rand();
[protocolChecker autorelease]; // in case we're being re-run
protocolChecker = [[NSProtocolChecker
protocolCheckerWithTarget:self
protocol:@protocol(FireflyPrefsClientProtocol)] retain];
@try
{
bRetVal = [serverProxy registerClient:protocolChecker withIdentifier:clientIdent];
}
@catch( NSException *exception )
{
NSLog(@"makeProxyConnection caught %@: %@",
[exception name], [exception reason]);
}
// If we fail to register, we will ditch our server proxy and fail
if( !bRetVal )
{
[serverProxy autorelease];
serverProxy = nil;
}
}
return bRetVal;
}
// ------------------------------------------------------------------------
// checkProxyConnection
//
// Checks to see if we have a valid proxy connection. If we don't,
// this disables the controls in the panel, posts a dialog, and returns NO.
//
// Because of the dialog, this should only be called when a connection is
// believed to exist.
// ------------------------------------------------------------------------
- (BOOL)checkProxyConnection
{
BOOL bRetVal = NO;
if( nil != serverProxy )
{
@try
{
[serverProxy fireflyStatus];
bRetVal = YES;
}
@catch( NSException *exception )
{
NSLog(@"checkProxyConnection caught %@: %@",
[exception name], [exception reason]);
[serverProxy autorelease];
serverProxy = nil;
NSBeginCriticalAlertSheet( NSLocalizedString( @"Lost contact with Firefly Helper",
@"Alert message notifying the user of failure to get status" ),
@"OK",
NULL,
NULL,
[[self mainView] window],
nil,
NULL,
NULL,
NULL,
NSLocalizedString( @"Communication has been lost with the Firefly Helper. "
"Please close and re-open this Preference pane, and try again.",
@"Explanatory text for the connection-lost alert" ) );
[self disableAllControls];
[self updateServerStatus:kFireflyStatusInvalid];
}
}
return bRetVal;
}
// ------------------------------------------------------------------------
// connectionDied
//
// This notification fires if an NSConnection dies. We don't bother to
// save our server connection. Rather, if this notification comes in, we
// check our connection.
// ------------------------------------------------------------------------
- (void)connectionDied:(NSNotification *)notification
{
[self checkProxyConnection];
}
// ------------------------------------------------------------------------
// proxyTimerFired
//
// If the helper wasn't ready when we first checked, we try once a second
// for 10 seconds, using a timer
// ------------------------------------------------------------------------
- (void)proxyTimerFired:(NSTimer *) timer
{
#ifdef FIREFLY_DEBUG
NSBeep();
#endif
if( [self makeProxyConnection] )
{
[self updateServerStatus:[self fireflyStatus]];
NSString *string = [self fireflyVersion];
if( nil != string )
[self versionChanged:string];
string = [self fireflyConfigURL];
if( nil != string )
[self configUrlChanged:string];
[ipcTimer invalidate];
[ipcTimer autorelease];
ipcTimer = nil;
}
else if( 10 < ++ipcTries )
{
[ipcTimer invalidate];
[ipcTimer autorelease];
ipcTimer = nil;
[self updateServerStatus:kFireflyStatusInvalid];
NSBeginCriticalAlertSheet( NSLocalizedString( @"Unable to get server status",
@"Alert message notifying the user of failure to get status" ),
@"OK",
NULL,
NULL,
[[self mainView] window],
nil,
NULL,
NULL,
NULL,
NSLocalizedString( @"An unexpected error occurred when trying to get the "
"status of the Firefly server. "
"Please close and re-open this Preference pane, and try again.",
@"Explanatory text for the failure-to-get-status alert" ) );
}
// else we try again when the timer fires
}
// ------------------------------------------------------------------------
// updateLogTextView
// ------------------------------------------------------------------------
- (void)updateLogTextView
{
NSFileManager *mgr = [NSFileManager defaultManager];
if( [mgr isReadableFileAtPath:logFilePath] )
{
NSDictionary *dict = [mgr fileAttributesAtPath:logFilePath traverseLink:YES];
NSDate *modDate = [dict objectForKey:NSFileModificationDate];
if( nil != modDate && ![logDate isEqualTo:modDate] )
{
// log date is the last time we processed an update
[logDate autorelease];
logDate = [modDate retain];
NSString *newContents = nil;
NSData *data = [NSData dataWithContentsOfFile:logFilePath];
if( nil != data );
{
newContents = [[NSString alloc]
initWithData:data encoding:NSUTF8StringEncoding];
}
if( nil == newContents || 0 == [newContents length] )
{
[logTextView setString:NSLocalizedString( @"The log file is empty.",
@"Text for empty log file" )];
}
else
{
// We're going to figure out our current scroll position and
// the current selection (if any). After we set the text,
// we'll re-select the same range, and if we were at the bottom
// of the scroller, we'll make sure we stay there.
NSRange selection = [logTextView selectedRange];
float scrollPos = 0.0;
scrollPos = [[[logTextView enclosingScrollView] verticalScroller] floatValue];
// Actually set the new text
[logTextView setString:newContents];
// Restore selection (it's lost when setString is called)
[logTextView setSelectedRange:selection];
// If we were previously scrolled to the end, scroll to the end.
// Otherwise, leave the window as it is. (This makes a range
// of the very end of the view).
if( 1.0 == scrollPos )
[logTextView scrollRangeToVisible:NSMakeRange([[logTextView string] length], 0)];
}
if( nil != newContents )
[newContents autorelease];
}
}
else
{
[logTextView setString:NSLocalizedString( @"The log file has not been created.",
@"Text for missing log file" )];
}
}
// ------------------------------------------------------------------------
// logTimerFired
// ------------------------------------------------------------------------
- (void)logTimerFired:(NSTimer *) timer
{
[self updateLogTextView];
}
// ========================================================================
// These functions wrap our IPC calls, so we can catch the exception that
// will be thrown if we try to access the server with the connection
// broken
// ========================================================================
// ------------------------------------------------------------------------
// startFirefly
// ------------------------------------------------------------------------
- (FireflyStartResult)startFirefly
{
FireflyStartResult retVal = kFireflyStartFail;
if( nil != serverProxy )
{
@try
{
retVal = [serverProxy startFirefly];
}
@catch( NSException *exception )
{
NSLog(@"startFirefly caught %@: %@", [exception name], [exception reason]);
[self checkProxyConnection];
}
}
return retVal;
}
// ------------------------------------------------------------------------
// stopFirefly
// ------------------------------------------------------------------------
- (FireflyStopResult)stopFirefly
{
FireflyStopResult retVal = kFireflyStopFail;
if( nil != serverProxy )
{
@try
{
retVal = [serverProxy stopFirefly];
}
@catch( NSException *exception )
{
NSLog(@"stopFirefly caught %@: %@", [exception name], [exception reason]);
[self checkProxyConnection];
}
}
return retVal;
}
// ------------------------------------------------------------------------
// restartFirefly
// ------------------------------------------------------------------------
- (FireflyRestartResult)restartFirefly;
{
FireflyRestartResult retVal = kFireflyRestartFail;
if( nil != serverProxy )
{
@try
{
retVal = [serverProxy restartFirefly];
}
@catch( NSException *exception )
{
NSLog(@"restartFirefly caught %@: %@", [exception name], [exception reason]);
[self checkProxyConnection];
}
}
return retVal;
}
// ------------------------------------------------------------------------
// rescanLibrary
// ------------------------------------------------------------------------
- (FireflyRescanResult)rescanLibrary;
{
FireflyRescanResult retVal = kFireflyRescanFail;
if( nil != serverProxy )
{
@try
{
retVal = [serverProxy rescanLibrary];
}
@catch( NSException *exception )
{
NSLog(@"rescanLibrary caught %@: %@", [exception name], [exception reason]);
[self checkProxyConnection];
}
}
return retVal;
}
// ------------------------------------------------------------------------
// fireflyStatus
// ------------------------------------------------------------------------
- (FireflyServerStatus)fireflyStatus;
{
FireflyServerStatus retVal = kFireflyStatusInvalid;
if( nil != serverProxy )
{
@try
{
retVal = [serverProxy fireflyStatus];
}
@catch( NSException *exception )
{
NSLog(@"fireflyStatus caught %@: %@", [exception name], [exception reason]);
[self checkProxyConnection];
}
}
return retVal;
}
// ------------------------------------------------------------------------
// fireflyIsRunning
// ------------------------------------------------------------------------
- (BOOL)fireflyIsRunning;
{
BOOL retVal = NO;
if( nil != serverProxy )
{
@try
{
retVal = [serverProxy fireflyIsRunning];
}
@catch( NSException *exception )
{
NSLog(@"fireflyStatus caught %@: %@", [exception name], [exception reason]);
[self checkProxyConnection];
}
}
return retVal;
}
// ------------------------------------------------------------------------
// fireflyVersion
// ------------------------------------------------------------------------
- (NSString*)fireflyVersion;
{
NSString *retVal = nil;
if( nil != serverProxy )
{
@try
{
retVal = [serverProxy fireflyVersion];
}
@catch( NSException *exception )
{
NSLog(@"fireflyVersion caught %@: %@", [exception name], [exception reason]);
[self checkProxyConnection];
}
}
return retVal;
}
// ------------------------------------------------------------------------
// fireflyConfigURL
// ------------------------------------------------------------------------
- (NSString*)fireflyConfigURL;
{
NSString *retVal = nil;
if( nil != serverProxy )
{
@try
{
retVal = [serverProxy fireflyConfigURL];
}
@catch( NSException *exception )
{
NSLog(@"fireflyConfigURL caught %@: %@", [exception name], [exception reason]);
[self checkProxyConnection];
}
}
return retVal;
}
// ------------------------------------------------------------------------
// showHelperMenu
// ------------------------------------------------------------------------
- (void)showHelperMenu:(BOOL)bShowMenu
{
if( nil != serverProxy )
{
@try
{
[serverProxy showHelperMenu:bShowMenu];
bShowHelperMenu = bShowMenu;
CFPreferencesSetAppValue( CFSTR(FF_PREFS_SHOW_MENU_EXTRA),
bShowMenu ? kCFBooleanTrue : kCFBooleanFalse,
CFSTR(FF_PREFS_DOMAIN) );
}
@catch( NSException *exception )
{
NSLog(@"fireflyConfigURL caught %@: %@", [exception name], [exception reason]);
[self checkProxyConnection];
[helperMenuCheckbox setState:(bShowHelperMenu ? NSOnState : NSOffState)];
}
}
}
// ========================================================================
// Implementation of the FireflyPrefsClientProtocol
// ========================================================================
// ------------------------------------------------------------------------
// configUrlChanged
//
// We're being told that the server's configuration URL has changed
// ------------------------------------------------------------------------
- (void)configUrlChanged:(NSString *)newUrl
{
if( 0 != [newUrl length] )
{
[webPageButton setEnabled:YES];
[serverURL setString:newUrl];
[webPageInfoText setStringValue:NSLocalizedString( @"Additional configuration options are "
"available from Firefly's built-in web page. "
"Click to open the page in your browser.",
@"Info text for the web page button when server "
"is running" )];
}
}
// ------------------------------------------------------------------------
// versionChanged
//
// We're being told that the server's version has changed
// ------------------------------------------------------------------------
- (void)versionChanged:(NSString *)newVersion
{
if( 0 != [newVersion length] )
[serverVersionText setStringValue:newVersion];
}
// ------------------------------------------------------------------------
// newStatus
//
// We're being told that the server's status has changed
// ------------------------------------------------------------------------
- (void)statusChanged:(FireflyServerStatus)newStatus
{
[self updateServerStatus:newStatus];
}
// ------------------------------------------------------------------------
// stillThere
//
// A "ping" to test the connection. If we received it, we're here!
// ------------------------------------------------------------------------
- (BOOL)stillThere
{
return YES;
}
@end