mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-26 23:25:56 -05:00
2269 lines
73 KiB
Objective-C
2269 lines
73 KiB
Objective-C
// 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
|