From 4665559105f367e922f7490e04201cfa4ba05c9d Mon Sep 17 00:00:00 2001 From: Ron Pedde Date: Wed, 26 Jul 2006 07:48:06 +0000 Subject: [PATCH] Add OSX souces, courtesy of RokuMike --- .../English.lproj/InfoPlist.strings | Bin 0 -> 182 bytes .../English.lproj/MainMenu.nib/classes.nib | 18 + .../English.lproj/MainMenu.nib/info.nib | 23 + .../MainMenu.nib/keyedobjects.nib | Bin 0 -> 5383 bytes .../English.lproj/MainMenu.nib/objects.nib | Bin 0 -> 1940 bytes .../English.lproj/MainMenu~.nib/classes.nib | 18 + .../English.lproj/MainMenu~.nib/info.nib | 23 + .../MainMenu~.nib/keyedobjects.nib | Bin 0 -> 5383 bytes .../English.lproj/MainMenu~.nib/objects.nib | Bin 0 -> 1940 bytes .../Firefly Helper.xcodeproj/project.pbxproj | 348 +++ osx/Firefly Helper/Firefly Minder_Prefix.pch | 7 + osx/Firefly Helper/FireflyHelper.h | 57 + osx/Firefly Helper/FireflyHelper.icns | Bin 0 -> 31384 bytes osx/Firefly Helper/FireflyHelper.m | 622 +++++ osx/Firefly Helper/FireflyServer.h | 91 + osx/Firefly Helper/FireflyServer.m | 659 +++++ osx/Firefly Helper/Info.plist | 34 + .../Japanese.lproj/InfoPlist.strings | Bin 0 -> 208 bytes .../Japanese.lproj/MainMenu.nib/classes.nib | 4 + .../Japanese.lproj/MainMenu.nib/info.nib | 17 + .../MainMenu.nib/keyedobjects.nib | Bin 0 -> 10793 bytes osx/Firefly Helper/ff_logo_status_menu.tif | Bin 0 -> 20516 bytes osx/Firefly Helper/main.m | 14 + osx/Firefly Helper/version.plist | 16 + osx/FireflyCommon.h | 229 ++ .../FireflyPrefsPref.nib/classes.nib | 61 + .../FireflyPrefsPref.nib/info.nib | 20 + .../FireflyPrefsPref.nib/keyedobjects.nib | Bin 0 -> 20000 bytes .../FireflyPrefsPref~.nib/classes.nib | 61 + .../FireflyPrefsPref~.nib/info.nib | 20 + .../FireflyPrefsPref~.nib/keyedobjects.nib | Bin 0 -> 20000 bytes .../English.lproj/InfoPlist.strings | Bin 0 -> 184 bytes osx/FireflyPrefs/FireflyCommon.h | 229 ++ osx/FireflyPrefs/FireflyLogo.png | Bin 0 -> 1077 bytes .../FireflyPrefs.xcodeproj/project.pbxproj | 346 +++ osx/FireflyPrefs/FireflyPrefsPref.tiff | Bin 0 -> 23568 bytes osx/FireflyPrefs/FireflyPrefs_Prefix.pch | 8 + osx/FireflyPrefs/Info.plist | 36 + osx/FireflyPrefs/OrgFireflyMediaServerPrefs.h | 158 ++ osx/FireflyPrefs/OrgFireflyMediaServerPrefs.m | 2268 +++++++++++++++++ osx/FireflyPrefs/firefly.conf | 351 +++ osx/FireflyPrefs/version.plist | 16 + osx/FireflyPrefsProtocol.h | 43 + osx/Uninstall Firefly.app | Bin 0 -> 13780 bytes osx/makedist.sh.templ | 63 + 45 files changed, 5860 insertions(+) create mode 100644 osx/Firefly Helper/English.lproj/InfoPlist.strings create mode 100644 osx/Firefly Helper/English.lproj/MainMenu.nib/classes.nib create mode 100644 osx/Firefly Helper/English.lproj/MainMenu.nib/info.nib create mode 100644 osx/Firefly Helper/English.lproj/MainMenu.nib/keyedobjects.nib create mode 100644 osx/Firefly Helper/English.lproj/MainMenu.nib/objects.nib create mode 100644 osx/Firefly Helper/English.lproj/MainMenu~.nib/classes.nib create mode 100644 osx/Firefly Helper/English.lproj/MainMenu~.nib/info.nib create mode 100644 osx/Firefly Helper/English.lproj/MainMenu~.nib/keyedobjects.nib create mode 100644 osx/Firefly Helper/English.lproj/MainMenu~.nib/objects.nib create mode 100644 osx/Firefly Helper/Firefly Helper.xcodeproj/project.pbxproj create mode 100644 osx/Firefly Helper/Firefly Minder_Prefix.pch create mode 100644 osx/Firefly Helper/FireflyHelper.h create mode 100644 osx/Firefly Helper/FireflyHelper.icns create mode 100644 osx/Firefly Helper/FireflyHelper.m create mode 100644 osx/Firefly Helper/FireflyServer.h create mode 100644 osx/Firefly Helper/FireflyServer.m create mode 100644 osx/Firefly Helper/Info.plist create mode 100644 osx/Firefly Helper/Japanese.lproj/InfoPlist.strings create mode 100644 osx/Firefly Helper/Japanese.lproj/MainMenu.nib/classes.nib create mode 100644 osx/Firefly Helper/Japanese.lproj/MainMenu.nib/info.nib create mode 100644 osx/Firefly Helper/Japanese.lproj/MainMenu.nib/keyedobjects.nib create mode 100644 osx/Firefly Helper/ff_logo_status_menu.tif create mode 100644 osx/Firefly Helper/main.m create mode 100644 osx/Firefly Helper/version.plist create mode 100644 osx/FireflyCommon.h create mode 100644 osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/classes.nib create mode 100644 osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/info.nib create mode 100644 osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/keyedobjects.nib create mode 100644 osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/classes.nib create mode 100644 osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/info.nib create mode 100644 osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/keyedobjects.nib create mode 100644 osx/FireflyPrefs/English.lproj/InfoPlist.strings create mode 100644 osx/FireflyPrefs/FireflyCommon.h create mode 100644 osx/FireflyPrefs/FireflyLogo.png create mode 100644 osx/FireflyPrefs/FireflyPrefs.xcodeproj/project.pbxproj create mode 100644 osx/FireflyPrefs/FireflyPrefsPref.tiff create mode 100644 osx/FireflyPrefs/FireflyPrefs_Prefix.pch create mode 100644 osx/FireflyPrefs/Info.plist create mode 100644 osx/FireflyPrefs/OrgFireflyMediaServerPrefs.h create mode 100644 osx/FireflyPrefs/OrgFireflyMediaServerPrefs.m create mode 100644 osx/FireflyPrefs/firefly.conf create mode 100644 osx/FireflyPrefs/version.plist create mode 100644 osx/FireflyPrefsProtocol.h create mode 100755 osx/Uninstall Firefly.app create mode 100755 osx/makedist.sh.templ diff --git a/osx/Firefly Helper/English.lproj/InfoPlist.strings b/osx/Firefly Helper/English.lproj/InfoPlist.strings new file mode 100644 index 0000000000000000000000000000000000000000..1427aaaf2fa1f2bd2dd6d340bac2008b0920a565 GIT binary patch literal 182 zcmW-bF%E)25Jg|@DF`Kn8Veg^Wupn9z!R(rYEU4cCVC?;sXoahGqZpHf0@sh2?-G? z4SNC?&b%l%s6Ct=y-GvvjGU!?Wgn`))|Ik*y;BP(OenmbpyHM##mZeVAs hU?rh7(aAq@8W>ACF>m{fsWo@Y^;J^-H>Rdy7C%CEAGZJi literal 0 HcmV?d00001 diff --git a/osx/Firefly Helper/English.lproj/MainMenu.nib/classes.nib b/osx/Firefly Helper/English.lproj/MainMenu.nib/classes.nib new file mode 100644 index 00000000..e547306d --- /dev/null +++ b/osx/Firefly Helper/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,18 @@ +{ + IBClasses = ( + { + ACTIONS = {prefsMenuChosen = id; startStopMenuChosen = id; }; + CLASS = FireflyHelper; + LANGUAGE = ObjC; + OUTLETS = {statusMenu = NSMenu; }; + SUPERCLASS = NSObject; + }, + {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, + { + CLASS = MyASKNibObjectInfoManager; + LANGUAGE = ObjC; + SUPERCLASS = ASKNibObjectInfoManager; + } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/osx/Firefly Helper/English.lproj/MainMenu.nib/info.nib b/osx/Firefly Helper/English.lproj/MainMenu.nib/info.nib new file mode 100644 index 00000000..1459ec90 --- /dev/null +++ b/osx/Firefly Helper/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,23 @@ + + + + + IBDocumentLocation + 155 322 356 240 0 0 1440 878 + IBEditorPositions + + 209 + 283 632 160 106 0 0 1440 878 + 29 + 521 639 125 44 0 0 1440 878 + + IBFramework Version + 446.1 + IBOpenObjects + + 209 + + IBSystem Version + 8J135 + + diff --git a/osx/Firefly Helper/English.lproj/MainMenu.nib/keyedobjects.nib b/osx/Firefly Helper/English.lproj/MainMenu.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..7f747c518179e03cb3fa78437a442ff1a83a9206 GIT binary patch literal 5383 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 + + + + IBDocumentLocation + 155 322 356 240 0 0 1440 878 + IBEditorPositions + + 209 + 283 632 160 106 0 0 1440 878 + 29 + 521 639 125 44 0 0 1440 878 + + IBFramework Version + 446.1 + IBOpenObjects + + 209 + + IBSystem Version + 8J135 + + diff --git a/osx/Firefly Helper/English.lproj/MainMenu~.nib/keyedobjects.nib b/osx/Firefly Helper/English.lproj/MainMenu~.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..7f747c518179e03cb3fa78437a442ff1a83a9206 GIT binary patch literal 5383 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 +#endif diff --git a/osx/Firefly Helper/FireflyHelper.h b/osx/Firefly Helper/FireflyHelper.h new file mode 100644 index 00000000..f16b103a --- /dev/null +++ b/osx/Firefly Helper/FireflyHelper.h @@ -0,0 +1,57 @@ +// +// FireflyHelper.h +// +// The "controller" part of our Model-View-Controller trio, plus what +// little "view" there is for this mostly-faceless program. +// +// The Firefly Helper manages setup and startup of the server, as well +// as communication withe the Prefs pane via Distributed Objects. It +// also optionally handles the Status Item (the menu by the clock in +// the menu bar). +// +// Created by Mike Kobb on 7/12/06. +// Copyright 2006 Roku, LLC. All rights reserved. +// + +#import +#import "../FireflyPrefsProtocol.h" +#import "FireflyServer.h" + +@interface FireflyHelper : NSObject < FireflyPrefsServerProtocol > +{ + IBOutlet NSMenu *statusMenu; + NSStatusItem *statusItem; + + NSProtocolChecker *protocolChecker; + NSConnection *prefsConnection; + + FireflyServer *fireflyServer; + + // Our client + id client; + int clientIdent; +} + +- (IBAction)startStopMenuChosen:(id)sender; +- (IBAction)prefsMenuChosen:(id)sender; + +// NSApplication delegate methods +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification; +- (void)applicationWillTerminate:(NSNotification *)aNotification; + +// private utilities +- (void)serverNotification:(NSNotification *)theNotification; +- (BOOL)checkClient; + +// wrappers to client calls; trap exceptions +- (void)tellClientStatusChanged:(FireflyServerStatus)newStatus; +- (void)tellClientVersionChanged:(NSString *)newVersion; +- (void)tellClientURLChanged:(NSString *)newURL; + +// Methods for status item +- (void)displayStatusItem; +- (void)hideStatusItem; +- (int)numberOfItemsInMenu:(NSMenu *)menu; +- (void)menuNeedsUpdate:(NSMenu *)menu; + +@end diff --git a/osx/Firefly Helper/FireflyHelper.icns b/osx/Firefly Helper/FireflyHelper.icns new file mode 100644 index 0000000000000000000000000000000000000000..f473fc10660ee0be5b299d49646db23823262cbb GIT binary patch literal 31384 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 +#include +#include +#include + +@implementation FireflyHelper + +// --------------------------------------------------------------------------- +// init +// +// Note that we create this path in this way primarily because we cribbed +// the code from the prefs pane, and we eventually will probably +// change both, so for now it's best to be consistent. +// --------------------------------------------------------------------------- +- (id)init +{ + protocolChecker = nil; + prefsConnection = nil; + fireflyServer = nil; + statusItem = nil; + client = nil; + clientIdent = 0; + + return self; +} + +// --------------------------------------------------------------------------- +// dealloc +// --------------------------------------------------------------------------- +- (void)dealloc +{ + [protocolChecker release]; + [prefsConnection release]; + [statusItem release]; + [FireflyServer release]; + + [super dealloc]; +} + +// --------------------------------------------------------------------------- +// applicationDidFinishLaunching +// +// We implement this delegate method so that we will be called when the +// Firefly Helper app is launched. We check to see if the prefs say +// we should launch the server when we launch, and if they do, we launch. +// --------------------------------------------------------------------------- +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification +{ + // Perform any checking of the installation here. Probably just look for + // the config file and put up a dialog if not found. + + bool bSuccess = false; + NSString *errorString = @""; + do // while(false) + { + // Create and initialize our fireflyServer object + NSString *serverPath = + [[NSBundle bundleForClass:[self class]] pathForResource:@"firefly" + ofType:nil + inDirectory:@"Server"]; + fireflyServer = + [[[FireflyServer alloc] initWithServerPath:serverPath] retain]; + if( nil == fireflyServer ) + { + errorString = + NSLocalizedString( @"Failed to initialize Firefly server", + @"explanatory text for failure to launch Firefly helper" ); + break; + } + + // Register for notifications from our server. Go ahead and do this before we + // start it. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(serverNotification:) + name:@STATUS_CHANGE + object:fireflyServer ]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(serverNotification:) + name:@VERSION_CHANGE + object:fireflyServer ]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(serverNotification:) + name:@URL_CHANGE + object:fireflyServer ]; + + // Must call this or Bonjour stuff won't work + [fireflyServer setup]; + + CFBooleanRef shouldLaunch = CFPreferencesCopyAppValue( CFSTR(FF_PREFS_LAUNCH_AT_LOGIN), + CFSTR(FF_PREFS_DOMAIN) ); + if( NULL != shouldLaunch ) + { + if( CFBooleanGetValue( shouldLaunch ) ) + [fireflyServer start]; + CFRelease( shouldLaunch ); + } + + // Okay, we're open for business. Let's vend our interface for + // the prefs pane to use + CFStringRef userName = CSCopyUserName( false ); + NSString *serviceName = [@"FireflyHelper" stringByAppendingString:(NSString*)userName]; + CFRelease(userName); + protocolChecker = [NSProtocolChecker + protocolCheckerWithTarget:self + protocol:@protocol(FireflyPrefsServerProtocol)]; + prefsConnection = [NSConnection defaultConnection]; + [prefsConnection setRootObject:protocolChecker]; + if( ![prefsConnection registerName:serviceName] ) + { + errorString = + NSLocalizedString( @"Unable to open communication channel for Preference pane", + @"explanatory text for failure to luanch Firefly helper" ); + break; + } + + // Made it through! + bSuccess = true; + + // If we're supposed to put up a Menu Extra (NSStatusItem), do so + CFBooleanRef showMenu = CFPreferencesCopyAppValue( CFSTR(FF_PREFS_SHOW_MENU_EXTRA), + CFSTR(FF_PREFS_DOMAIN) ); + if( NULL != showMenu ) + { + if( CFBooleanGetValue( showMenu ) ) + [self displayStatusItem]; + CFRelease( showMenu ); + } + + } while( false ); + + // If we encountered a critical failure, we need to display an alert and + // then quit. + if( !bSuccess ) + { + NSString *alertString = NSLocalizedString( @"Firefly cannot start", + @"Alert message when firefly helper can't start" ); + NSString *quitString = NSLocalizedString( @"Quit", + @"Label for quit button in failure alert" ); + + NSAlert *alert = [NSAlert alertWithMessageText:alertString + defaultButton:quitString + alternateButton:nil + otherButton:nil + informativeTextWithFormat:errorString]; + + [alert setAlertStyle:NSCriticalAlertStyle]; + [NSApp activateIgnoringOtherApps:YES]; + [alert runModal]; + [NSApp terminate:self]; + } +} + +// --------------------------------------------------------------------------- +// applicationWillTerminate +// +// We implement this delegate method so that we will be called when the +// Firefly Helper app is quitting. When the user logs out, we quit their +// Firefly server process. +// --------------------------------------------------------------------------- +- (void)applicationWillTerminate:(NSNotification *)aNotification +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + // Notify the prefs pane we're bailing? + [fireflyServer shutdown]; // also stops the server + [prefsConnection invalidate]; +} + +// --------------------------------------------------------------------------- +// serverNotification +// +// Our handler for notifications from the server +// --------------------------------------------------------------------------- +- (void)serverNotification:(NSNotification *)theNotification +{ + if( [[theNotification name] isEqualToString:@STATUS_CHANGE] ) + { + [self tellClientStatusChanged:[fireflyServer status]]; + } + else if( [[theNotification name] isEqualToString:@VERSION_CHANGE] ) + { + [self tellClientVersionChanged:[fireflyServer version]]; + } + else if( [[theNotification name] isEqualToString:@URL_CHANGE] ) + { + [self tellClientURLChanged:[fireflyServer configURL]]; + } +} + +// --------------------------------------------------------------------------- +// checkClient +// +// Call this function to see if a client is really connected. If our client +// ivar is nil, returns NO, but if it's not nil, we try to "ping" the client. +// If that ping fails, the client ivar is released and set to nil, and we +// also return NO. +// --------------------------------------------------------------------------- +- (BOOL)checkClient +{ + BOOL bRetVal = NO; + @try + { + bRetVal = [client stillThere]; + } + @catch( NSException *exception ) + { + [client autorelease]; + client = nil; + NSLog( @"Client disappeared; setting to nil" ); + } + + return bRetVal; +} + +// --------------------------------------------------------------------------- +// tellClientStatusChanged +// +// A wrapper for the call to statusChanged, that tests client for nil and +// traps any IPC-related exception. If an exception fires, this function +// will call our checkClient function to see if the client is still valid. +// --------------------------------------------------------------------------- +- (void)tellClientStatusChanged:(FireflyServerStatus)newStatus +{ + if( nil != client ) + { + @try + { + [client statusChanged:newStatus]; + } + @catch( NSException *exception ) + { + [self checkClient]; + } + } +} + +// --------------------------------------------------------------------------- +// tellClientVersionChanged +// +// A wrapper for the call to versionChanged. See tellClientStatusChanged +// for details. +// --------------------------------------------------------------------------- +- (void)tellClientVersionChanged:(NSString *)newVersion +{ + if( nil != client ) + { + @try + { + [client versionChanged:newVersion]; + } + @catch( NSException *exception ) + { + [self checkClient]; + } + } +} + +// --------------------------------------------------------------------------- +// tellClientURLChanged +// +// A wrapper for the call to configUrlChanged. See tellClientStatusChanged +// for details. +// --------------------------------------------------------------------------- +- (void)tellClientURLChanged:(NSString *)newURL +{ + if( nil != client ) + { + @try + { + [client configUrlChanged:newURL]; + } + @catch( NSException * ) + { + [self checkClient]; + } + } +} + +// =========================================================================== +// Items pertaining to the Status Item (our little menu bar item) +// =========================================================================== + +// --------------------------------------------------------------------------- +// displayStatusItem +// +// Adds our Firefly menu to the menu bar +// --------------------------------------------------------------------------- +- (void)displayStatusItem +{ + if( nil == statusItem ) + { + statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength: + NSSquareStatusItemLength] retain]; + if( nil != statusItem ) + { + [statusMenu setDelegate:self]; + [statusItem setMenu:statusMenu]; + NSImage *statusImage = + [[NSImage alloc] initWithContentsOfFile: + [[NSBundle mainBundle] pathForResource:@"ff_logo_status_menu" + ofType:@"tif"]]; + [statusItem setImage:statusImage]; + [statusItem setHighlightMode:YES]; + } + } +} + +// --------------------------------------------------------------------------- +// hideStatusItem +// +// Takes our Firefly menu out of the menu bar +// --------------------------------------------------------------------------- +- (void)hideStatusItem +{ + if( nil != statusItem ) + { + [[statusItem statusBar] removeStatusItem:statusItem]; + [statusItem autorelease]; + statusItem = nil; + } +} + +// --------------------------------------------------------------------------- +// startStopMenuChosen +// +// Somebody chose "Start Firefly" or "Stop Firefly" from the menu +// --------------------------------------------------------------------------- +- (IBAction)startStopMenuChosen:(id)sender +{ + if( nil != fireflyServer ) + { + if( [fireflyServer isRunning] ) + [fireflyServer stop]; + else + [fireflyServer start]; + } +} + +// --------------------------------------------------------------------------- +// prefsMenuChosen +// +// Somebody chose "Firefly PreferencesÉ" +// --------------------------------------------------------------------------- +- (IBAction)prefsMenuChosen:(id)sender +{ + NSDictionary *errorDict = nil; + NSString *scriptSource = @"tell application \"System Preferences\"\n" + "activate\n" + "set current pane to pane \"org.fireflymediaserver.prefpanel\"\n" + "end tell\n"; + NSAppleScript *myScript = [[NSAppleScript alloc] initWithSource:scriptSource]; + + if( nil != myScript ) + { + [myScript executeAndReturnError:&errorDict]; + [myScript release]; + } +} + +// --------------------------------------------------------------------------- +// numberOfItemsInMenu +// +// NSMenu delegate method. We always return -1 because we don't change the +// number of items in the menu. +// --------------------------------------------------------------------------- +- (int)numberOfItemsInMenu:(NSMenu *)menu +{ + return -1; +} + +// --------------------------------------------------------------------------- +// menuNeedsUpdate +// +// NSMenu delegate method. If our status has changed, we update the menu +// item text. +// --------------------------------------------------------------------------- +- (void)menuNeedsUpdate:(NSMenu *)menu +{ + if( menu == statusMenu && 3 == [menu numberOfItems] ) + { + // Just need to update the status of the server and the start/stop + // menu + id item = [menu itemAtIndex:0]; + [item setTitle:StringForFireflyStatus( [self fireflyStatus] )]; + + item = [menu itemAtIndex:1]; + if( [self fireflyIsRunning] ) + { + [item setTitle:NSLocalizedString( @"Stop Firefly", + @"Text for status menu" )]; + } + else + { + [item setTitle:NSLocalizedString( @"Start Firefly", + @"Text for status menu" )]; + } + } +} + +// =========================================================================== +// Implementation of FireflyPrefsProtocol +// =========================================================================== + +// --------------------------------------------------------------------------- +// registerClient +// +// When the Prefs pane starts up and connects to us, it will register itself +// with us so that we may notify it of changes in the server status while +// it's open. This registration process also gives us the ability to detect +// when a +// --------------------------------------------------------------------------- +- (BOOL)registerClient:(id) newClient withIdentifier:(int) ident +{ + BOOL bRetVal = NO; + if( nil != client ) + { + // Hm. We already have a client connected. Let's see if it's really + // still there. This will set client to nil if the client has died. + if( [self checkClient] ) + NSLog(@"registerClient called, but valid client already connected!\n"); + } + + if( nil == client ) + { + client = [newClient retain]; + clientIdent = ident; + bRetVal = YES; + } + + return bRetVal; +} + +// --------------------------------------------------------------------------- +// unregisterClientId +// +// When the Prefs pane is closing, it unregisters, so we know not to try to +// send it more udpates +// --------------------------------------------------------------------------- +- (void)unregisterClientId:(int) ident +{ + if( ident == clientIdent ) + { + clientIdent = 0; + [client autorelease]; + client = nil; + } +} + +// --------------------------------------------------------------------------- +// startFirefly +// +// Starts the server. Return value indicates that the server process was +// started. If a client has registered for status updates, it will see the +// status kFireflyStatusStarting. The server could possibly quit before +// coming online, so clients really should register for status updates. +// --------------------------------------------------------------------------- +- (FireflyStartResult)startFirefly +{ + FireflyStartResult retVal = kFireflyStartFail; + if( nil != fireflyServer && [fireflyServer start] ) + retVal = kFireflyStartSuccess; + return retVal; +} + +// --------------------------------------------------------------------------- +// stopFirefly +// +// Signals the server to stop. A successful return value indicates that the +// server was not running, or has been successfully signaled to stop. +// Clients should register for status updates and look for the actual change +// to kFireflyStatusStopped to confirm shutdown. +// --------------------------------------------------------------------------- +- (FireflyStopResult)stopFirefly +{ + FireflyStopResult retVal = kFireflyStopFail; + if( nil != fireflyServer && [fireflyServer stop] ) + retVal = kFireflyStopSuccess; + return retVal; +} + +// --------------------------------------------------------------------------- +// rescanLibrary +// +// Tells the server to re-scan the library. Returns a failure result if +// the server is not running. +// --------------------------------------------------------------------------- +- (FireflyRescanResult)rescanLibrary +{ + FireflyRescanResult retVal = kFireflyRescanInvalid; + if( nil != fireflyServer ) + retVal = [fireflyServer status]; + return retVal; +} + +// --------------------------------------------------------------------------- +// fireflyStatus +// +// Replies with the state of the server +// --------------------------------------------------------------------------- +- (FireflyServerStatus)fireflyStatus +{ + FireflyServerStatus retVal = kFireflyStatusInvalid; + if( nil != fireflyServer ) + retVal = [fireflyServer status]; + return retVal; +} + +// --------------------------------------------------------------------------- +// stopFirefly +// +// Signals the server to restart. A successful return value indicates that +// the server has been successfully signaled to restart. +// Clients should register for status updates and look for the actual changes +// in server status to verify that the signal is handled. +// --------------------------------------------------------------------------- +- (FireflyRestartResult)restartFirefly +{ + FireflyRestartResult retVal = kFireflyRestartFail; + if( nil != fireflyServer && [fireflyServer restart] ) + retVal = kFireflyRestartSuccess; + return retVal; +} + +// --------------------------------------------------------------------------- +// fireflyVersion +// +// Replies with the version of the server. Returns nil if the +// server is not running or we are unable to ascertain the version. Note +// that a method invocation on nil returns nil, so we don't need to check +// fireflyServer for nil. +// --------------------------------------------------------------------------- +- (NSString *)fireflyVersion +{ + return [fireflyServer version]; +} + +// --------------------------------------------------------------------------- +// fireflyConfigURL +// +// Replies with the URL to the advanced configuration page for the server. +// Returns nil if the server is not running or we are unable to +// ascertain the URL. See note about nil above. +// --------------------------------------------------------------------------- +- (NSString *)fireflyConfigURL +{ + return [fireflyServer configURL]; +} + +// --------------------------------------------------------------------------- +// showHelperMenu +// +// Allows the prefs pane to specify whether to show the item. This setting +// is persistent +// --------------------------------------------------------------------------- +- (void)showHelperMenu:(BOOL)bShowMenu +{ + if( bShowMenu ) + [self displayStatusItem]; + else + [self hideStatusItem]; +} + +// --------------------------------------------------------------------------- +// fireflyVersion +// +// Returns YES if the server is running, NO if not +// --------------------------------------------------------------------------- +- (BOOL)fireflyIsRunning +{ + BOOL retVal = NO; + if( nil != fireflyServer ) + retVal = [fireflyServer isRunning]; + + return retVal; +} + +// =========================================================================== +// Implementation of NSMenuValidation +// =========================================================================== + +// --------------------------------------------------------------------------- +// validateMenuItem +// +// Our first item is always disabled. Our last is always enabled. The +// one in the middle depends upon our status +// --------------------------------------------------------------------------- +- (BOOL)validateMenuItem:(id )menuItem +{ + BOOL bRetVal = NO; + if( nil != statusMenu && 3 == [statusMenu numberOfItems] ) + { + if( menuItem == [statusMenu itemAtIndex:2] ) + bRetVal = YES; + else if( menuItem == [statusMenu itemAtIndex:1] ) + { + FireflyServerStatus status = [self fireflyStatus]; + if( status != kFireflyStatusStopping && status != kFireflyStatusInvalid ) + bRetVal = YES; + } + } + return bRetVal; +} + + +@end diff --git a/osx/Firefly Helper/FireflyServer.h b/osx/Firefly Helper/FireflyServer.h new file mode 100644 index 00000000..b764a0d8 --- /dev/null +++ b/osx/Firefly Helper/FireflyServer.h @@ -0,0 +1,91 @@ +// +// FireflyServer.h +// +// The "model" part of our Model-View-Controller trio. Represents the +// firefly server itself, and encapsulates launching, quitting, status, +// etc. +// +// Created by Mike Kobb on 7/12/06. +// Copyright 2006 Roku, LLC. All rights reserved. +// + +#import +#include "../FireflyCommon.h" + +#define STATUS_CHANGE "org.fireflymediaserver.status-change" +#define VERSION_CHANGE "org.fireflymediaserver.version-change" +#define URL_CHANGE "org.fireflymediaserver.url-change" + +@interface FireflyServer : NSObject +{ + NSString *fireflyServerPath; + NSTask *serverTask; + + NSString *serverVersion; + NSString *serverURL; + unsigned short serverPort; + + id delegate; + FireflyServerStatus status; + + // We use Bonjour to find our server port and help notice if the server + // stops + NSNetServiceBrowser *netBrowser; + BOOL bScanIsActive; + NSMutableArray *pendingNetServices; + NSNetService *fireflyService; + char ffid[9]; + +} + +// public methods for managing the lifecycle of the server object +- (void)setup; +- (void)shutdown; + +// public methods for controlling the server process +- (id)initWithServerPath:(NSString *) serverPath; +- (BOOL)start; +- (BOOL)stop; +- (BOOL)restart; +- (void)setDelegate:(id) delegate; + +// public methods for querying server status & properties +- (BOOL)isRunning; +- (FireflyServerStatus)status; +- (NSString *)version; +- (NSString *)configURL; + +// private utilities +- (void)setStatus:(FireflyServerStatus)newStatus; +- (void)setURL:(NSString *)newURL; +- (void)setVersion:(NSString *)newVersion; +- (void)taskEnded:(NSNotification *)notification; +- (NSString*)fireflyConfigFilePath; +- (BOOL)startAndUpdateStatus:(BOOL)bUpdate; +- (void)killRunningFireflies; + +// Bonjour delegate methods (NSNetServiceBrowser) +- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)netServiceBrowser; +- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)netServiceBrowser; +- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser + didRemoveService:(NSNetService *)netService + moreComing:(BOOL)moreServicesComing; +- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser + didRemoveDomain:(NSString *)domainName + moreComing:(BOOL)moreDomainsComing; +- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser + didNotSearch:(NSDictionary *)errorInfo; +- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser + didFindService:(NSNetService *)netService + moreComing:(BOOL)moreServicesComing; +- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser + didFindDomain:(NSString *)domainName + moreComing:(BOOL)moreDomainsComing; + +// Bonjour delegate methods (NSNetService) +- (void)netServiceDidResolveAddress:(NSNetService *)service; +- (void)netService:(NSNetService *)service didNotResolve:(NSDictionary *)errorInfo; +- (void)netServiceWillResolve:(NSNetService *)service; + + +@end diff --git a/osx/Firefly Helper/FireflyServer.m b/osx/Firefly Helper/FireflyServer.m new file mode 100644 index 00000000..8c9bfe37 --- /dev/null +++ b/osx/Firefly Helper/FireflyServer.m @@ -0,0 +1,659 @@ +// +// FireflyServer.m +// +// The "model" part of our Model-View-Controller trio. Represents the +// firefly server itself, and encapsulates launching, quitting, status, +// etc. +// +// Created by Mike Kobb on 7/12/06. +// Copyright 2006 Roku, LLC. All rights reserved. +// +#import +#import // for sockaddr_in +#include // for inet_ntoa and so forth +#include // AF_INET6 +#import "FireflyServer.h" + +@implementation FireflyServer + +// --------------------------------------------------------------------------- +// initWithServerPath +// +// Initialize the server object to manage the server at the given path. +// --------------------------------------------------------------------------- +- (id)initWithServerPath:(NSString *) path +{ + if( ( self = [super init] ) != nil ) + { + fireflyServerPath = [[NSString stringWithString:path] retain]; + serverTask = nil; + delegate = nil; + serverVersion = nil; + serverURL = nil; + status = kFireflyStatusStopped; + + // Bonjour stuff below + netBrowser = [[NSNetServiceBrowser alloc] init]; + pendingNetServices = [[NSMutableArray arrayWithCapacity:5] retain]; + fireflyService = nil; + + // Pick a random ffid that we'll be able to use to identify our + // server easily + srand((unsigned int)time(NULL)); + sprintf( ffid, "%08x", rand() ); + + // Register for task ending notifications, so we can find out + // if our server process quits. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(taskEnded:) + name:NSTaskDidTerminateNotification + object:nil]; + } + + return self; +} + +// --------------------------------------------------------------------------- +// dealloc +// +// +// --------------------------------------------------------------------------- +- (void)dealloc +{ + // First, kill the server! + + + [fireflyServerPath release]; + [serverTask release]; + [serverVersion release]; + [serverURL release]; + [netBrowser release]; + [pendingNetServices release]; + [fireflyService release]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +// --------------------------------------------------------------------------- +// setup +// +// Not to be confused with 'start', this function is to be called before +// starting to use the object. It handles starting our Bonjour stuff and +// so on. +// --------------------------------------------------------------------------- +- (void)setup +{ + // It's time to start our scan, limiting to the local host. + [netBrowser setDelegate:self]; + [netBrowser searchForServicesOfType:@"_http._tcp." inDomain:@"local."]; +} + +// --------------------------------------------------------------------------- +// shutdown +// +// Not to be confused with 'stop', this function is to be called when +// preparing to dispose the object. It handles shutting down our Bonjour +// stuff and so on. But, it does also stop the server if it's running. +// --------------------------------------------------------------------------- +- (void)shutdown +{ + // shut down the firefly server + [self setStatus:kFireflyStatusStopping]; + [self stop]; + + // Now shut down the Bonjour scan. + [netBrowser stop]; + + // FIXME: Is it safe to do this right after calling stop, or should we + // put these in didStop? + NSEnumerator *enumerator = [pendingNetServices objectEnumerator]; + NSNetService *service; + while( service = [enumerator nextObject] ) + [service stop]; + [pendingNetServices removeAllObjects]; +} + +// --------------------------------------------------------------------------- +// setDelegate +// +// We will message our delegate when important things happen, like the server +// starting or stopping, etc. +// --------------------------------------------------------------------------- +- (void)setDelegate:(id) delegateToSet +{ + delegate = delegateToSet; +} + +// --------------------------------------------------------------------------- +// isRunning +// +// Is the server running? +// --------------------------------------------------------------------------- +- (BOOL)isRunning +{ + BOOL retVal = NO; + if( nil != serverTask ) + retVal = [serverTask isRunning]; + return retVal; +} + +// --------------------------------------------------------------------------- +// start +// +// Starts the server. Note that this function may fail if the server is +// already running. +// --------------------------------------------------------------------------- +- (BOOL)start +{ + BOOL retVal = NO; + + FireflyServerStatus curStatus = [self status]; + if( curStatus == kFireflyStatusStopped || + curStatus == kFireflyStatusStartFailed || + curStatus == kFireflyStatusCrashed ) + { + retVal = [self startAndUpdateStatus:YES]; + } + + return retVal; +} + +// --------------------------------------------------------------------------- +// stop +// +// Signals the server to stop. Returns YES if the signal was sent successfully +// --------------------------------------------------------------------------- +- (BOOL)stop +{ + BOOL retVal = NO; + if( nil != serverTask ) + { + [self setStatus:kFireflyStatusStopping]; + [serverTask terminate]; + retVal = YES; + } + return retVal; +} + +// --------------------------------------------------------------------------- +// restart +// +// restarts the server. Tells the server to shut down after setting our +// status to "restarting". When the server shuts down, the taskEnded +// method will see that status, and restart the server. +// --------------------------------------------------------------------------- +- (BOOL)restart +{ + BOOL retVal = NO; + if( nil != serverTask ) + { + [self setStatus:kFireflyStatusRestarting]; + [serverTask terminate]; + retVal = YES; + } + return retVal; +} + +// --------------------------------------------------------------------------- +// status +// +// Returns the current server status +// --------------------------------------------------------------------------- +- (FireflyServerStatus)status +{ + return status; +} + +// --------------------------------------------------------------------------- +// version +// +// Returns the current server version, or nil if it's not yet known +// --------------------------------------------------------------------------- +- (NSString *)version +{ + return serverVersion; +} + +// --------------------------------------------------------------------------- +// configURL +// +// Returns the server's advanced user configuration URL, or nil if it's not +// yet known +// --------------------------------------------------------------------------- +- (NSString *)configURL +{ + return serverURL; +} + +// =========================================================================== +// Private utilities. +// =========================================================================== + +// --------------------------------------------------------------------------- +// setStatus +// +// Sets the status and notifies interested parties of the change. +// --------------------------------------------------------------------------- +- (void)setStatus:(FireflyServerStatus) newStatus +{ + status = newStatus; + + [[NSNotificationCenter defaultCenter] postNotificationName:@STATUS_CHANGE + object:self]; +} + +// --------------------------------------------------------------------------- +// setURL +// +// Sets the server config URL and notifies interested parties of the change. +// --------------------------------------------------------------------------- +- (void)setURL:(NSString *) newUrl +{ + [serverURL autorelease]; + serverURL = [[NSString stringWithString:newUrl] retain]; + [[NSNotificationCenter defaultCenter] postNotificationName:@URL_CHANGE + object:self]; +} + +// --------------------------------------------------------------------------- +// setVersion +// +// Sets the server version and notifies interested parties of the change. +// --------------------------------------------------------------------------- +- (void)setVersion:(NSString *)newVersion +{ + [serverVersion autorelease]; + serverVersion = [[NSString stringWithString:newVersion] retain]; + [[NSNotificationCenter defaultCenter] postNotificationName:@VERSION_CHANGE + object:self]; +} + +// --------------------------------------------------------------------------- +// taskEnded +// +// We register this function to be called when tasks end. If the task is +// our server task, then we dispose the (now useless) object and notify +// interested parties of the change. We check for normal versus abnormal +// termination and set status accordingly. +// --------------------------------------------------------------------------- +- (void)taskEnded:(NSNotification *)notification +{ + if( serverTask == [notification object] ) + { + int termStatus = [[notification object] terminationStatus]; + [serverTask autorelease]; + serverTask = nil; + if( kFireflyStatusRestarting == status ) + { + // Don't post the message saying that the server stopped; + // just start up and let the success or failure of that startup + // handle the status update. + [self startAndUpdateStatus:NO]; + } + else + { + if( 0 == termStatus ) + [self setStatus:kFireflyStatusStopped]; + else if( kFireflyStatusStarting == status ) + [self setStatus:kFireflyStatusStartFailed]; + else + [self setStatus:kFireflyStatusCrashed]; + NSLog(@"Server Task ended with status %d\n", termStatus); + } + } +} + +// --------------------------------------------------------------------------- +// fireflyConfigFilePath +// +// Build the path to the config file, test that it's valid and return it. If +// we can't find a file at the expected location, we return nil +// --------------------------------------------------------------------------- +- (NSString*)fireflyConfigFilePath +{ + NSString *retVal = nil; + NSArray * appSupportDirArray = + NSSearchPathForDirectoriesInDomains( NSLibraryDirectory, + NSUserDomainMask, + YES ); + if( [appSupportDirArray count] > 0 ) + { + BOOL bIsDir = NO; + NSFileManager *mgr = [NSFileManager defaultManager]; + NSString *configFilePath = + [[appSupportDirArray objectAtIndex:0] + stringByAppendingPathComponent:@"Application Support/Firefly/" + FIREFLY_CONF_NAME]; + if( [mgr fileExistsAtPath:configFilePath isDirectory:&bIsDir] && !bIsDir ) + retVal = configFilePath; + } + + return retVal; +} + +// --------------------------------------------------------------------------- +// startAndUpdateStatus +// +// Private utility that actually starts the server. If the bUpdate flag is +// NO, then this utility will leave the current status in place, even though +// the server is starting. This is intended for use by the restart function, +// so that the status will remain in "restarting" until the server actually +// comes online (or fails) +// --------------------------------------------------------------------------- +- (BOOL)startAndUpdateStatus:(BOOL)bUpdate +{ + BOOL retVal = NO; + + [self killRunningFireflies]; + + NSString *configFilePath = [self fireflyConfigFilePath]; + if( nil != configFilePath ) + { + NSArray *array = [NSArray arrayWithObjects: + @"-y", @"-f", @"-c", + configFilePath, + @"-b", + [NSString stringWithUTF8String:ffid], // best 10.4<->10.3 compromise method... + nil]; + @try + { + serverTask = [[[NSTask alloc] init] retain]; + [serverTask setLaunchPath:fireflyServerPath]; + [serverTask setCurrentDirectoryPath:[fireflyServerPath stringByDeletingLastPathComponent]]; + [serverTask setArguments:array]; + + if( bUpdate ) + [self setStatus:kFireflyStatusStarting]; + [serverTask launch]; + retVal = YES; + } + @catch( NSException *exception ) + { + if( [[exception name] isEqual:NSInvalidArgumentException] ) + ; + NSLog(@"FireflyServer: Caught %@: %@", [exception name], [exception reason]); + [self setStatus:kFireflyStatusStartFailed]; + } + } + else + { + NSLog(@"couldn't find config file at %@\n", configFilePath); + } + return retVal; +} + +// --------------------------------------------------------------------------- +// killRunningFireflies +// +// This may seem like paranoia, but things really go badly if there is more +// than one copy of Firefly running (e.g. started from the command line, or +// failing to quit when signaled). So, we enforce some preconditions here. +// --------------------------------------------------------------------------- +- (void)killRunningFireflies +{ + 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 multiple processes, so we look + // through all processes, not stopping when we find one. We *are* + // careful to only kill processes owned by the current user! + 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_SERVER_NAME ) ) + { + NSLog(@"Killing rogue firefly, pid %d\n", result[i].kp_proc.p_pid); + kill( result[i].kp_proc.p_pid, SIGKILL ); + } + } + free( result ); + } +} + +// =========================================================================== +// Below are the delegate methods for Bonjour discovery of the server. +// =========================================================================== + +// --------------------------------------------------------------------------- +// netServiceBrowserWillSearch: +// +// Lets us know that the Bonjour search has started +// --------------------------------------------------------------------------- +- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)netServiceBrowser +{ +#ifdef FIREFLY_DEBUG + NSLog(@"NSNetServiceBrowser started\n"); +#endif + bScanIsActive = YES; +} + +// --------------------------------------------------------------------------- +// netServiceBrowserDidStopSearch: +// +// Should only stop if we ask it to, as when we're quitting the app +// --------------------------------------------------------------------------- +- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)netServiceBrowser +{ +#ifdef FIREFLY_DEBUG + NSLog(@"NSNetServiceBrowser stopped\n"); +#endif + bScanIsActive = NO; +} + +// --------------------------------------------------------------------------- +// netServiceBrowser:didRemoveService:moreComing: +// +// Called when a Bonjour service goes away. +// --------------------------------------------------------------------------- +- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser + didRemoveService:(NSNetService *)netService + moreComing:(BOOL)moreServicesComing +{ + // Is it our service? If so, we need to switch the status text and change + // the start/stop button, and also update the web page button and text. + if( [netService isEqual:fireflyService] ) + { + [fireflyService autorelease]; + fireflyService = nil; + // FIXME: AND? Theoretically, we should be notified that our NSTask + // went away, so this notification isn't needed to detect that the + // server stopped. But, what if due to some error, the Bonjour + // service croaked but left the server itself running? + } +} + +// --------------------------------------------------------------------------- +// netServiceBrowser:didRemoveDomain:moreComing: +// +// unless our local host goes away, we really don't care. +// --------------------------------------------------------------------------- +- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser + didRemoveDomain:(NSString *)domainName + moreComing:(BOOL)moreDomainsComing +{ +} + +// --------------------------------------------------------------------------- +// netServiceBrowser:didNotSearch: +// +// Called if the search failed to start. We need to alert the user. +// --------------------------------------------------------------------------- +- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser + didNotSearch:(NSDictionary *)errorInfo +{ + // FIXME: display error info? Try again? Quit? +} + +// --------------------------------------------------------------------------- +// netServiceBrowser:didFindService:moreComing: +// +// A Bonjour service has been discovered. It might be our server. We need +// to ask the service to resolve, so we can see if it is. +// --------------------------------------------------------------------------- +- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser + didFindService:(NSNetService *)netService + moreComing:(BOOL)moreServicesComing +{ + // We need to ask this object to resolve itself so we can figure out if it's the server + // we want. In case the user closes the panel, we need to have a list of all the ones + // pending so we can stop them. + [pendingNetServices addObject:netService]; + [netService setDelegate:self]; + [netService resolve]; + + if( !moreServicesComing ) + bScanIsActive = NO; +} + +// --------------------------------------------------------------------------- +// netServiceBrowser:didFindDomain:moreComing: +// +// Don't think we care about this one, but I'm pretty sure we're supposed to +// implement it (why?) +// --------------------------------------------------------------------------- +- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser + didFindDomain:(NSString *)domainName + moreComing:(BOOL)moreDomainsComing +{ +} + +// --------------------------------------------------------------------------- +// netServiceDidResolveAddress: +// +// We asked a service to resolve, and it has. Time to check to see if it's +// our server. +// --------------------------------------------------------------------------- +- (void)netServiceDidResolveAddress:(NSNetService *)service +{ + // Is it a firefly service, and if so, is it ours? + + // NOTE: EXTREMELY IMPORTANT! + // protocolSpecificInformation is a deprecated API, and the Tiger-on approved way is TXTRecordData. + // protocolSpecificInformation, though, is available back to 10.2. These return the data in + // DIFFERENT FORMATS. TXTRecordData returns the data in the mDNS-standard format of a packed + // array of Pascal strings, while protocolSpecificInformation returns the strings separated by 0x01 + // characters. The TXTRecordContainsKey function and related utilities assume (at least on Tiger) + // the packed-Pascal format, so we have to use strstr instead. Happily, all we really care + // about here is that the ffid tag exists, so we don't need to do much parsing. + const char *version = NULL; + const char* txtRecordBytes = NULL; + NSData *data = nil; + + NSString *txtRecord = [service protocolSpecificInformation]; + if( nil != txtRecord ) + data = [txtRecord dataUsingEncoding:NSUTF8StringEncoding]; + txtRecordBytes = (const char*)[data bytes]; + if( NULL != txtRecordBytes && + NULL != ( version = strnstr( txtRecordBytes, "\001mtd-version=", [data length] ) ) ) + { + // Okay, this is a firefly server. Let's see if it's *our* server + int i = 0; + char buf[256]; // max allowed size, but we'll still be careful not to overrun. + strncpy( buf, txtRecordBytes, 255 ); + buf[255] = '\0'; +#ifdef FIREFLY_DEBUG + NSLog( @"Text record is: %s\n", buf ); +#endif + const char *ffidptr = strnstr( txtRecordBytes, "\001ffid=", [data length] ); + if( NULL != ffidptr ) + { + // This is a bit of a pain due to the stuff described in the big + // comment above. + + // advance over the key + ffidptr += 6; + while( '\0' != ffidptr[i] && + '\001' != ffidptr[i] && + ((ffidptr-txtRecordBytes)+i) < [data length] ) + { +#ifdef FIREFLY_DEBUG + NSLog(@"Adding %c (%d)to ffidptr\n", ffidptr[i], ffidptr[i]); +#endif + buf[i] = ffidptr[i++]; + } + buf[i] = '\0'; + NSLog(@"Comparing buf %s against our ffid %s\n", buf, ffid); + if( 0 == strcmp( buf, ffid ) ) + { + // WOOT! This is us. Get the version and port + i = 0; + version += 13; + while( '\0' != version[i] && + '\001' != version[i] && + ((version-txtRecordBytes)+i) < [data length] ) + buf[i] = version[i++]; + buf[i] = '\0'; + [self setVersion:[NSString stringWithUTF8String:buf]]; + + // Time to get the port. + NSArray *svcAddresses = [service addresses]; + if( 0 != [svcAddresses count] ) + { + NSData *addrData = [svcAddresses objectAtIndex:0]; + struct sockaddr_in *addrPtr = (struct sockaddr_in*)[addrData bytes]; + if( NULL != addrPtr ) + { + serverPort = ntohs(addrPtr->sin_port); + [self setURL:[NSString stringWithFormat:@"http://localhost:%u", serverPort]]; + } + } + + // Okay, it's the one we want, so let's remember it and update + // our status + [fireflyService autorelease]; + fireflyService = [service retain]; + [self setStatus:kFireflyStatusActive]; +#ifdef FIREFLY_DEBUG + NSBeep(); + sleep(1); + NSBeep(); + sleep(1); + NSBeep(); +#endif + } + } + } + + // It's no longer pending, so remove from array. If it was ours, we've + // retained it. + [pendingNetServices removeObject:service]; + + // If we're no longer scanning and we've exhausted all the + // services that we found without identifying the correct server, + // we need to ...? + if( !bScanIsActive && 0 == [pendingNetServices count] && nil == fireflyService ) + ; //FIXME +} + +// --------------------------------------------------------------------------- +// netService:didNotResolve: +// +// We tried to resolve a service, and failed. It's probably not really +// running. We could always try again, but it doesn't seem to be necessary +// --------------------------------------------------------------------------- +- (void)netService:(NSNetService *)service didNotResolve:(NSDictionary *)errorInfo +{ + [pendingNetServices removeObject:service]; +#ifdef FIREFLY_DEBUG + if( nil == fireflyService ) + NSLog(@"Failed to resolve service: %@\n", [errorInfo valueForKey:NSNetServicesErrorCode] ); +#endif +} + +// --------------------------------------------------------------------------- +// netServiceWillResolve: +// +// Just lets us know resolution has started. +// --------------------------------------------------------------------------- +- (void)netServiceWillResolve:(NSNetService *)service +{ +} + +@end diff --git a/osx/Firefly Helper/Info.plist b/osx/Firefly Helper/Info.plist new file mode 100644 index 00000000..20bd6da0 --- /dev/null +++ b/osx/Firefly Helper/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + FireflyHelper + CFBundleIdentifier + org.fireflymediaserver.fireflyhelper + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleVersion + 102 + CFBundleShortVersionString + 1.0b2 + NSHumanReadableCopyright + Š 2006 Roku LLC + LSUIElement + 1 + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/osx/Firefly Helper/Japanese.lproj/InfoPlist.strings b/osx/Firefly Helper/Japanese.lproj/InfoPlist.strings new file mode 100644 index 0000000000000000000000000000000000000000..8a78ff4d929fd583412178e9bed76c3a85237e8f GIT binary patch literal 208 zcmW-ZO%B0e6h+V2D#Wl679a+V2u=JXm@q3^#9z}QVMk&K-5XAtFM0RA_uYHGKiUM; z8F6Jp%!vap61LtRYEI$h%0;cKsWWm*^)0e0UvTq!H2thnfy|n3!ki&@&T=oz{Ey|v wtZgOqBxE*n`8)O%qbsMu+xcN?V?Zb+Vx=zBsoO+9rcX(;$}RPw+0tg`59Kl?G5`Po literal 0 HcmV?d00001 diff --git a/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/classes.nib b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/classes.nib new file mode 100644 index 00000000..b9b4b09f --- /dev/null +++ b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,4 @@ +{ + IBClasses = ({CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }); + IBVersion = 1; +} \ No newline at end of file diff --git a/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/info.nib b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/info.nib new file mode 100644 index 00000000..4c74210b --- /dev/null +++ b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/info.nib @@ -0,0 +1,17 @@ + + + + + IBDocumentLocation + 94 103 356 240 0 0 1280 1002 + IBEditorPositions + + 29 + 93 343 318 44 0 0 1280 1002 + + IBFramework Version + 403.0 + IBSystem Version + 8A278 + + diff --git a/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/keyedobjects.nib b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..ee1187e80a541b541ad487a8210fa08154011d9c GIT binary patch literal 10793 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 literal 0 HcmV?d00001 diff --git a/osx/Firefly Helper/ff_logo_status_menu.tif b/osx/Firefly Helper/ff_logo_status_menu.tif new file mode 100644 index 0000000000000000000000000000000000000000..7dde9746655a73c61b92d40440796b4b18fd7e5e GIT binary patch literal 20516 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 + +int main(int argc, char *argv[]) +{ + return NSApplicationMain(argc, (const char **) argv); +} diff --git a/osx/Firefly Helper/version.plist b/osx/Firefly Helper/version.plist new file mode 100644 index 00000000..6f3c68ca --- /dev/null +++ b/osx/Firefly Helper/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 1 + CFBundleShortVersionString + 0.1 + CFBundleVersion + 0.1 + ProjectName + NibPBTemplates + SourceVersion + 1160200 + + diff --git a/osx/FireflyCommon.h b/osx/FireflyCommon.h new file mode 100644 index 00000000..817ae0db --- /dev/null +++ b/osx/FireflyCommon.h @@ -0,0 +1,229 @@ +/* + * FireflyCommonConstants.h + * + * Created by Mike Kobb on 7/12/06. + * Copyright 2006 Roku LLC. All rights reserved. + * + * This file contains common constants and types needed by both the + * prefs pane and helper apps. + */ + +#ifndef __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H +#define __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H + +#include // used by GetProcesses + +#define FIREFLY_SERVER_NAME "firefly" +#define FIREFLY_DIR_NAME "Firefly" +#define FIREFLY_CONF_NAME "firefly.conf" + +#define FF_PREFS_DOMAIN "org.fireflymediaserver.firefly" +#define FF_PREFS_LAUNCH_AT_LOGIN "org.fireflymediaserver.launchAtLogin" +#define FF_PREFS_SHOW_MENU_EXTRA "org.fireflymediaserver.showMenuExtra" + +// Define this to enable certain debug output +//#define FIREFLY_DEBUG + + +typedef enum +{ + kFireflyStartInvalid = 0, + kFireflyStartSuccess = 1, + kFireflyStartFail = 2 +} FireflyStartResult; + +typedef enum +{ + kFireflyStopInvalid = 0, + kFireflyStopSuccess = 1, + kFireflyStopFail = 2 +} FireflyStopResult; + +typedef enum +{ + kFireflyRestartInvalid = 0, + kFireflyRestartSuccess = 1, + kFireflyRestartFail = 2 +} FireflyRestartResult; + +typedef enum +{ + kFireflyRescanInvalid = 0, + kFireflyRescanSuccess = 1, + kFireflyRescanFail = 2 +} FireflyRescanResult; + + +typedef enum +{ + kFireflyStatusInvalid, + kFireflyStatusStopped, + kFireflyStatusStarting, + kFireflyStatusActive, + kFireflyStatusScanning, + kFireflyStatusStopping, + kFireflyStatusRestarting, + kFireflyStatusStartFailed, + kFireflyStatusCrashed +} FireflyServerStatus; + +static NSString* +StringForFireflyStatus( FireflyServerStatus inStatus ) +{ + NSString *retVal = nil; + switch( inStatus ) + { + case kFireflyStatusStopped: + retVal = NSLocalizedString( @"Firefly is not running", + @"Status message for Firefly" ); + break; + + case kFireflyStatusStarting: + retVal = NSLocalizedString( @"Firefly is starting", + @"Status message for Firefly" ); + break; + + case kFireflyStatusActive: + retVal = NSLocalizedString( @"Firefly is running", + @"Status message for Firefly" ); + break; + + case kFireflyStatusScanning: + retVal = NSLocalizedString( @"Firefly is scanning the library", + @"Status message for Firefly" ); + break; + + case kFireflyStatusStopping: + retVal = NSLocalizedString( @"Firefly is stopping", + @"Status message for Firefly" ); + break; + + case kFireflyStatusRestarting: + retVal = NSLocalizedString( @"Firefly is restarting", + @"Status message for Firefly" ); + break; + + case kFireflyStatusStartFailed: + retVal = NSLocalizedString( @"Firefly failed to start", + @"Status message for Firefly" ); + break; + + case kFireflyStatusCrashed: + retVal = NSLocalizedString( @"Firefly stopped unexpectedly", + @"Status message for Firefly" ); + break; + + case kFireflyStatusInvalid: + default: + retVal = NSLocalizedString( @"Firefly status is unknown", + @"Status message for Firefly" ); + break; + } + + return retVal; +} + +// =========================================================================== +// Process management the Unix way -- Finding if the server is already +// running, or finding a specific process +// =========================================================================== + +// This just makes syntax more convenient (don't have to say 'struct' everyplace) +typedef struct kinfo_proc kinfo_proc; + +// ------------------------------------------------------------------------ +// GetProcesses +// +// Static utility function allocates and returns an array of kinfo_proc +// structures representing the currently-running processes on the machine. +// The calling function is responsible for disposing the returned pointer +// with free() +// +// Because Firefly runs as a BSD daemon, the Process Manager is not useful +// in finding it. Instead, we have to talk to the BSD layer. This code +// was provided by Apple in a tech note. +// ------------------------------------------------------------------------ +static void +GetProcesses( kinfo_proc **outResult, size_t *outLength) +{ + int err; + kinfo_proc * result; + BOOL done; + static const int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 }; + // Declaring name as const requires us to cast it when passing it to + // sysctl because the prototype doesn't include the const modifier. + // (That's the Apple comment, but they don't say *why* they made it const...) + size_t length; + + // We call sysctl with result == NULL and length == 0. + // That will succeed, and set length to the appropriate length. + // We then allocate a buffer of that size and call sysctl again + // with that buffer. If that succeeds, we're done. If that fails + // with ENOMEM, we have to throw away our buffer and loop. Note + // that the loop causes use to call sysctl with NULL again; this + // is necessary because the ENOMEM failure case sets length to + // the amount of data returned, not the amount of data that + // could have been returned. + + result = NULL; + done = NO; + do + { + // Call sysctl with a NULL buffer. + length = 0; + err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1, + NULL, &length, + NULL, 0); + if (err == -1) + err = errno; + + // Allocate an appropriately sized buffer based on the results + // from the previous call. + if (err == 0) + { + result = malloc(length); + if (result == NULL) + err = ENOMEM; + } + + // Call sysctl again with the new buffer. If we get an ENOMEM + // error, toss away our buffer and start again. + if (err == 0) + { + err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1, + result, &length, + NULL, 0); + if (err == -1) + err = errno; + if (err == 0) + done = YES; + else if (err == ENOMEM) + { + free(result); + result = NULL; + err = 0; + } + } + } while (err == 0 && !done); + + // Clean up and establish post conditions. + if( err != 0 ) + { + if( result != NULL) + free(result); + *outResult = NULL; + *outLength = 0; + } + + if( err == 0 ) + { + *outResult = result; + *outLength = length; + } +} + + + +// __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H +#endif + diff --git a/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/classes.nib b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/classes.nib new file mode 100644 index 00000000..c65366f9 --- /dev/null +++ b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/classes.nib @@ -0,0 +1,61 @@ +{ + IBClasses = ( + {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, + { + CLASS = NSPreferencePane; + LANGUAGE = ObjC; + OUTLETS = { + "_firstKeyView" = id; + "_initialKeyView" = id; + "_lastKeyView" = id; + "_window" = id; + }; + SUPERCLASS = NSObject; + }, + { + ACTIONS = { + applyNowButtonClicked = id; + browseButtonClicked = id; + helperMenuCheckboxClicked = id; + logoButtonClicked = id; + myAction = id; + passwordChanged = id; + portChanged = id; + portPopupChanged = id; + pwCheckBoxChanged = id; + serverStartOptionChanged = id; + shareNameChanged = id; + startStopButtonClicked = id; + webPageButtonClicked = id; + }; + CLASS = OrgFireflyMediaServerPrefs; + LANGUAGE = ObjC; + OUTLETS = { + applyNowButton = NSButton; + browseButton = NSButton; + helperMenuCheckbox = NSButton; + libraryField = NSTextField; + libraryIcon = NSImageView; + logTextView = NSTextView; + mainTabView = NSTabView; + myOutlet = id; + nameField = NSTextField; + panelVersionText = NSTextField; + passwordCheckbox = NSButton; + passwordField = NSTextField; + portField = NSTextField; + portPopup = NSPopUpButton; + progressSpinner = NSProgressIndicator; + serverStartOptions = NSPopUpButton; + serverVersionText = NSTextField; + startStopButton = NSButton; + statusText = NSTextField; + webPageButton = NSButton; + webPageInfoText = NSTextField; + }; + SUPERCLASS = NSPreferencePane; + }, + {CLASS = TextFormatter; LANGUAGE = ObjC; SUPERCLASS = NSFormatter; } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/info.nib b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/info.nib new file mode 100644 index 00000000..c40710df --- /dev/null +++ b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/info.nib @@ -0,0 +1,20 @@ + + + + + IBDocumentLocation + 41 85 481 349 0 0 1440 878 + IBFramework Version + 446.1 + IBLockedTabItems + + 99 + + IBOpenObjects + + 12 + + IBSystem Version + 8J135 + + diff --git a/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/keyedobjects.nib b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..8b2f3e894251abf2c7d18f830b9b4b470f030ca9 GIT binary patch literal 20000 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 + + + + IBDocumentLocation + 41 85 481 349 0 0 1440 878 + IBFramework Version + 446.1 + IBLockedTabItems + + 99 + + IBOpenObjects + + 12 + + IBSystem Version + 8J135 + + diff --git a/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/keyedobjects.nib b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..ce074d1f75a46e21ab54eff2daffaddbfb4cd434 GIT binary patch literal 20000 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 KvGGr{(EkUIkmi{H literal 0 HcmV?d00001 diff --git a/osx/FireflyPrefs/English.lproj/InfoPlist.strings b/osx/FireflyPrefs/English.lproj/InfoPlist.strings new file mode 100644 index 0000000000000000000000000000000000000000..e48cbef578394fc62120f911e1f2295118883175 GIT binary patch literal 184 zcmW-bF%E)25Jg|jDF`Kn8Veg^WupnPz!R(rYEU4cCVC?;sXoahGqZpHf0@shF)1+_ z4SNC?&b%l%s6Ct=y-GvvjGd)@?W=Sj(GLEen`))|Ik-}pk+osViaS@iHM##mZeVAs iU@4(B(aAq@8W;;X32*z9i8XV}^;J^-H)f_1=0rb%?jOMb literal 0 HcmV?d00001 diff --git a/osx/FireflyPrefs/FireflyCommon.h b/osx/FireflyPrefs/FireflyCommon.h new file mode 100644 index 00000000..817ae0db --- /dev/null +++ b/osx/FireflyPrefs/FireflyCommon.h @@ -0,0 +1,229 @@ +/* + * FireflyCommonConstants.h + * + * Created by Mike Kobb on 7/12/06. + * Copyright 2006 Roku LLC. All rights reserved. + * + * This file contains common constants and types needed by both the + * prefs pane and helper apps. + */ + +#ifndef __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H +#define __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H + +#include // used by GetProcesses + +#define FIREFLY_SERVER_NAME "firefly" +#define FIREFLY_DIR_NAME "Firefly" +#define FIREFLY_CONF_NAME "firefly.conf" + +#define FF_PREFS_DOMAIN "org.fireflymediaserver.firefly" +#define FF_PREFS_LAUNCH_AT_LOGIN "org.fireflymediaserver.launchAtLogin" +#define FF_PREFS_SHOW_MENU_EXTRA "org.fireflymediaserver.showMenuExtra" + +// Define this to enable certain debug output +//#define FIREFLY_DEBUG + + +typedef enum +{ + kFireflyStartInvalid = 0, + kFireflyStartSuccess = 1, + kFireflyStartFail = 2 +} FireflyStartResult; + +typedef enum +{ + kFireflyStopInvalid = 0, + kFireflyStopSuccess = 1, + kFireflyStopFail = 2 +} FireflyStopResult; + +typedef enum +{ + kFireflyRestartInvalid = 0, + kFireflyRestartSuccess = 1, + kFireflyRestartFail = 2 +} FireflyRestartResult; + +typedef enum +{ + kFireflyRescanInvalid = 0, + kFireflyRescanSuccess = 1, + kFireflyRescanFail = 2 +} FireflyRescanResult; + + +typedef enum +{ + kFireflyStatusInvalid, + kFireflyStatusStopped, + kFireflyStatusStarting, + kFireflyStatusActive, + kFireflyStatusScanning, + kFireflyStatusStopping, + kFireflyStatusRestarting, + kFireflyStatusStartFailed, + kFireflyStatusCrashed +} FireflyServerStatus; + +static NSString* +StringForFireflyStatus( FireflyServerStatus inStatus ) +{ + NSString *retVal = nil; + switch( inStatus ) + { + case kFireflyStatusStopped: + retVal = NSLocalizedString( @"Firefly is not running", + @"Status message for Firefly" ); + break; + + case kFireflyStatusStarting: + retVal = NSLocalizedString( @"Firefly is starting", + @"Status message for Firefly" ); + break; + + case kFireflyStatusActive: + retVal = NSLocalizedString( @"Firefly is running", + @"Status message for Firefly" ); + break; + + case kFireflyStatusScanning: + retVal = NSLocalizedString( @"Firefly is scanning the library", + @"Status message for Firefly" ); + break; + + case kFireflyStatusStopping: + retVal = NSLocalizedString( @"Firefly is stopping", + @"Status message for Firefly" ); + break; + + case kFireflyStatusRestarting: + retVal = NSLocalizedString( @"Firefly is restarting", + @"Status message for Firefly" ); + break; + + case kFireflyStatusStartFailed: + retVal = NSLocalizedString( @"Firefly failed to start", + @"Status message for Firefly" ); + break; + + case kFireflyStatusCrashed: + retVal = NSLocalizedString( @"Firefly stopped unexpectedly", + @"Status message for Firefly" ); + break; + + case kFireflyStatusInvalid: + default: + retVal = NSLocalizedString( @"Firefly status is unknown", + @"Status message for Firefly" ); + break; + } + + return retVal; +} + +// =========================================================================== +// Process management the Unix way -- Finding if the server is already +// running, or finding a specific process +// =========================================================================== + +// This just makes syntax more convenient (don't have to say 'struct' everyplace) +typedef struct kinfo_proc kinfo_proc; + +// ------------------------------------------------------------------------ +// GetProcesses +// +// Static utility function allocates and returns an array of kinfo_proc +// structures representing the currently-running processes on the machine. +// The calling function is responsible for disposing the returned pointer +// with free() +// +// Because Firefly runs as a BSD daemon, the Process Manager is not useful +// in finding it. Instead, we have to talk to the BSD layer. This code +// was provided by Apple in a tech note. +// ------------------------------------------------------------------------ +static void +GetProcesses( kinfo_proc **outResult, size_t *outLength) +{ + int err; + kinfo_proc * result; + BOOL done; + static const int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 }; + // Declaring name as const requires us to cast it when passing it to + // sysctl because the prototype doesn't include the const modifier. + // (That's the Apple comment, but they don't say *why* they made it const...) + size_t length; + + // We call sysctl with result == NULL and length == 0. + // That will succeed, and set length to the appropriate length. + // We then allocate a buffer of that size and call sysctl again + // with that buffer. If that succeeds, we're done. If that fails + // with ENOMEM, we have to throw away our buffer and loop. Note + // that the loop causes use to call sysctl with NULL again; this + // is necessary because the ENOMEM failure case sets length to + // the amount of data returned, not the amount of data that + // could have been returned. + + result = NULL; + done = NO; + do + { + // Call sysctl with a NULL buffer. + length = 0; + err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1, + NULL, &length, + NULL, 0); + if (err == -1) + err = errno; + + // Allocate an appropriately sized buffer based on the results + // from the previous call. + if (err == 0) + { + result = malloc(length); + if (result == NULL) + err = ENOMEM; + } + + // Call sysctl again with the new buffer. If we get an ENOMEM + // error, toss away our buffer and start again. + if (err == 0) + { + err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1, + result, &length, + NULL, 0); + if (err == -1) + err = errno; + if (err == 0) + done = YES; + else if (err == ENOMEM) + { + free(result); + result = NULL; + err = 0; + } + } + } while (err == 0 && !done); + + // Clean up and establish post conditions. + if( err != 0 ) + { + if( result != NULL) + free(result); + *outResult = NULL; + *outLength = 0; + } + + if( err == 0 ) + { + *outResult = result; + *outLength = length; + } +} + + + +// __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H +#endif + diff --git a/osx/FireflyPrefs/FireflyLogo.png b/osx/FireflyPrefs/FireflyLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..fcc645cbb5dccdb637233f1ca57ff37ca999e61c GIT binary patch literal 1077 zcmV-51j_q~P)mHE^6%Zue&mEkp08_y^9k>C6PZU6?fTRLU1#CJ<-jVQi5G3AVHu`+B z7VqxtA3J8GRZf@0@$d7@x3g>CBs0eE_phJROOCP9d`!^jo_+lBILA4qGeEzjmzqWa zaBg%Eqa`uE{dWQo_}c+^Xh<=3G`1whuSE+$;CJ-WL;_diERRhhd|sRYsP6_>ed;*U zC}a3U7O~F7Oz;W3Q zD}Cpb2B!US0q|B4056m*m4;e`lt8;6co|z{BnQ4Pp(gXZ`k$^)^Bhmu0oVrr^qtJJ z?(1Zkk@x4ufWJ(qsfPgH9h)36@QQq<%*kPRxuusVQb+hZ0I_Nkt?T26dCG>VnoxkV z#h)(lvaP;xJa7~0$Pi>hLI_-?*s+5_w>EgEMhI7~2!U6G*iVoJJ38(LtH;g;@7f8L zvEUs<=#b8Acx9S4!p9cC37#Q>1bRi|1*23{cvXN=c&ApFO5l%Y7j_HpSmBD+V<&>1 zz6X_?An%m-zZitt7|vM>&&~k2zc0K%_7ngR!)8U33EPqPHzS##g&!DVVcJCDmcS|i zel;>j+eD}};e)0^}YwZ(1d4ML?#M0MLn0G2koCTE%xU#4)^@A*xV% z_hFq^1`+lQ_{lazLg)E2{$Icp<<8CrwAkwBwIalT_Yoj;oyX zk~ws6r2~AQ3H`c!?d$FV@T@z`858~jggY@Dc=pUP;XL1RK9n-yxm!&kyEwJ3Z)a|B zI;a1W08IF5aI#ZG2d`-R6UQ!8zc_Lc{-5QvRsb%1-v`j=e`;!aNg-#D1Gf)ox$sIR zEH!pq_+EguOZ$t1w+n?oCyqC5G6Rfh;Zt#%j4MRt`Up)1n4`<84?j{S{2kjlqVai{ zDpgul=>>vYL&`?xwd>xis|{H(SlfD`iTw=m9M1~hW6CT&zJ3IFJJrL} vX);=}k}&gad2T<8FbIYhIz47)yx8^2OL|G?rKs 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 + #import +#endif diff --git a/osx/FireflyPrefs/Info.plist b/osx/FireflyPrefs/Info.plist new file mode 100644 index 00000000..be466185 --- /dev/null +++ b/osx/FireflyPrefs/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + org.fireflymediaserver.prefpanel + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + BNDL + CFBundleSignature + ???? + CFBundleVersion + 102 + CFBundleShortVersionString + 1.0b3 + NSHumanReadableCopyright + Š 2006 Roku LLC + NSMainNibFile + FireflyPrefsPref + NSPrefPaneIconFile + FireflyPrefsPref.tiff + NSPrefPaneIconLabel + Firefly + NSPrincipalClass + OrgFireflyMediaServerPrefs + + diff --git a/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.h b/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.h new file mode 100644 index 00000000..b660e472 --- /dev/null +++ b/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.h @@ -0,0 +1,158 @@ +/* OrgFireflyMediaServerPrefs */ + +#import +#import +#import +#import "../FireflyPrefsProtocol.h" + +@interface OrgFireflyMediaServerPrefs : NSPreferencePane < FireflyPrefsClientProtocol > +{ + IBOutlet NSButton *browseButton; + IBOutlet NSTextField *libraryField; + IBOutlet NSImageView *libraryIcon; + IBOutlet NSTextField *nameField; + IBOutlet NSButton *passwordCheckbox; + IBOutlet NSButton *helperMenuCheckbox; + IBOutlet NSTextField *passwordField; + IBOutlet NSTextField *portField; + IBOutlet NSPopUpButton *portPopup; + IBOutlet NSPopUpButton *serverStartOptions; + IBOutlet NSTextField *panelVersionText; + IBOutlet NSTextField *serverVersionText; + IBOutlet NSButton *startStopButton; + IBOutlet NSTextField *statusText; + IBOutlet NSButton *webPageButton; + IBOutlet NSTextField *webPageInfoText; + IBOutlet NSTabView *mainTabView; + IBOutlet NSButton *applyNowButton; + IBOutlet NSProgressIndicator *progressSpinner; + IBOutlet NSTextView *logTextView; + IBOutlet NSScrollView *logTextScroller; + + CFStringRef appID; + NSMutableString *ourHostName; + NSMutableString *fireflyFolderPath; + NSMutableString *fireflyHelperPath; + NSMutableString *serverURL; + NSMutableString *logFilePath; + NSMutableString *playlistPath; + NSString *userName; + + // Handling of the config file + NSMutableString *configFilePath; + BOOL configAppearsValid; + NSMutableString *configError; + NSMutableArray *configFileStrings; + unsigned long idxOfServerName; + unsigned long idxOfPassword; + unsigned long idxOfPort; + unsigned long idxOfLibraryPath; + unsigned long idxOfNextSection; + unsigned long idxOfDbPath; + unsigned long idxOfLogPath; + unsigned long idxOfPlaylistPath; + + // Track whether we need to save + BOOL bConfigNeedsSaving; + + // The actual preferences we manage with this GUI + NSMutableString *serverName; + NSMutableString *serverPassword; + NSMutableString *libraryPath; + unsigned short serverPort; // 0 means automatic + BOOL bStartServerOnLogin; + BOOL bShowHelperMenu; + + // Timer mechanism for setting up IPC + int ipcTries; + NSTimer *ipcTimer; + + // Interprocess communication with Firefly Helper + id serverProxy; + NSProtocolChecker *protocolChecker; + int clientIdent; + + // Log view updating + NSTimer *logTimer; + NSDate *logDate; +} + +- (IBAction)browseButtonClicked:(id)sender; +- (IBAction)passwordChanged:(id)sender; +- (IBAction)shareNameChanged:(id)sender; +- (IBAction)portPopupChanged:(id)sender; +- (IBAction)portChanged:(id)sender; +- (IBAction)pwCheckBoxChanged:(id)sender; +- (IBAction)serverStartOptionChanged:(id)sender; +- (IBAction)startStopButtonClicked:(id)sender; +- (IBAction)webPageButtonClicked:(id)sender; +- (IBAction)applyNowButtonClicked:(id)sender; +- (IBAction)helperMenuCheckboxClicked:(id)sender; + +// Overrides of NSPreferencePane methods +- (void)willSelect; +- (void)didSelect; +- (NSPreferencePaneUnselectReply)shouldUnselect; +- (void)willUnselect; + +// Checking the validity of the Firefly installation. +- (BOOL)validateInstall; + +// Tracking the need to save the config +- (void)setConfigNeedsSaving:(BOOL)needsSaving; + +// UI utility functions +- (void)disableAllControls; +- (void)updateServerStatus:(FireflyServerStatus) status; +- (void)setIconForPath; + +// Functions for loading and saving our configuration, as well as +// reading and writing the config file. +- (BOOL)loadSettings; +- (BOOL)saveSettings; +- (BOOL)updateLoginItem; +- (void)readSettingsForHelper:(BOOL*)outHelper andServer:(BOOL*)outServer; +- (BOOL)readConfigFromPath:(NSString*)path; +- (BOOL)writeConfigToPath:(NSString*)path; +- (BOOL)createDefaultConfigFile; +- (NSString *)readValueFromBuf:(char*)buf startingAt:(int)idx unescapeCommas:(BOOL) bUnescapeCommas; +- (void)setDefaultValues; + +// Finding or launching the helper +- (BOOL)helperIsRunning; +- (void)launchHelperIfNeeded; + +// Validation of user entries +- (BOOL)control:(NSControl *)control isValidObject:(id) obj; +- (BOOL)currentTabIsValid; +- (void)alertForControl:(NSControl *)control; + +// Alert delegate method(s) +- (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo; +- (void)applySheetDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo; + +// Tab view delegate method(s) +- (BOOL)tabView:(NSTabView *)tabView shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem; + +// Browse panel delegate method(s) +- (void)browsePanelEnded:(NSOpenPanel *)panel returnCode:(int)panelResult contextInfo:(void *)contextInfo; + +// Methods for dealing with the IPC proxy +- (BOOL)makeProxyConnection; +- (BOOL)checkProxyConnection; +- (void)proxyTimerFired:(NSTimer *) timer; +- (FireflyStartResult)startFirefly; +- (FireflyStopResult)stopFirefly; +- (FireflyRestartResult)restartFirefly; +- (FireflyRescanResult)rescanLibrary; +- (FireflyServerStatus)fireflyStatus; +- (BOOL)fireflyIsRunning; +- (NSString*)fireflyVersion; +- (NSString*)fireflyConfigURL; +- (void)showHelperMenu:(BOOL)bShowMenu; + +// Log view stuff +- (void)updateLogTextView; +- (void)logTimerFired:(NSTimer *) timer; + +@end diff --git a/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.m b/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.m new file mode 100644 index 00000000..b4678b40 --- /dev/null +++ b/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.m @@ -0,0 +1,2268 @@ +// 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 +#import +#include +#include +#include +#include +#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É", + @"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 diff --git a/osx/FireflyPrefs/firefly.conf b/osx/FireflyPrefs/firefly.conf new file mode 100644 index 00000000..6752aac6 --- /dev/null +++ b/osx/FireflyPrefs/firefly.conf @@ -0,0 +1,351 @@ +# $Id: mt-daapd.conf.templ 1000 2006-05-01 08:07:56Z rpedde $ +# +# This is the mt-daapd config file. +# +# If you have problems or questions with the format of this file, +# direct your questions to rpedde@users.sourceforge.net. +# +# You can also check the website at http://mt-daapd.sourceforge.net, +# as there is a growing documentation library there, peer-supported +# forums and possibly more. +# + +[general] + +# +# web_root (required) +# +# Location of the admin web pages. +# +# If you installed from .RPM, .deb, or tarball with --prefix=/usr, then +# this is correct. +# +# If you installed from tarball without --prefix=/usr, then the correct +# path is probably /usr/local/share/mt-daapd/admin-root. +# +# In the default Mac install, this is a relative path, compared to the server + +web_root = admin-root + +# +# port (required) +# +# What port to listen on. Leave blank to auto-assign. If the port is +# specified, and that port is already taken, the server will not start. +# + +port = + +# +# admin_pw (required) +# +# This is the password to the administrative pages. If blank, access +# will only be possible from the local host. +# +# In the default Mac install, this is left blank so that only local +# host control is allowed + +admin_pw = + + +# +# db_type (required) +# +# This is what kind of backend database to store the song +# info in. Valid choices are "sqlite" and "sqlite3". +# + +db_type = sqlite + +# +# db_parms +# +# This is any extra information the db needs to connect. +# in the case of sqlite and sqlite3, this is the name +# of the directory to store the database in +# +# If you installed from RPM or .deb, this path likely already +# exists. If not, then you must create it. The directory itself +# must be writable by the "runas" user. +# +# On the Mac, this lives in the Application Support folder, in our +# Firefly folder. +# + +db_parms = + +# +# mp3_dir (required) +# +# Location of the mp3 files to share. Note that because the +# files are stored in the database by inode, these must be +# in the same physical filesystem. +# + +mp3_dir = + +# +# servername (required) +# +# This is both the name of the server as advertised +# via rendezvous, and the name of the database +# exported via DAAP. Also know as "What shows up in iTunes". +# + +servername = + +# +# runas (required) +# +# This is the user to drop privs to if running as +# root. If mt-daapd is not started as root, this +# configuration option is ignored. Notice that this +# must be specified whether the server is running +# as root or not. +# +# This is also ignored on Windows. +# + +runas = nobody + +# +# playlist (optional) +# +# This is the location of a playlist file. +# This is for Apple-style "Smart Playlists" +# See the mt-daapd.playlist file in the +# contrib directory for syntax and examples +# +# This doesn't control static playlists... these +# are controlled with the "process_m3u" directive +# below. +# +# +# On the Mac, this lives in our Firefly folder in Application Support + +playlist = + +# +# password (optional) +# +# This is the password required to listen to MP3 files +# i.e. the password that iTunes prompts for +# + +password = + +# +# extensions (optional) +# +# These are the file extensions that the daap server will +# try to index and serve. By default, it only indexes and +# serves .mp3 files. It can also server .m4a and .m4p files, +# and just about any other files, really. Unfortunately, while +# it can *attempt* to serve other files (.ogg?), iTunes won't +# play them. Perhaps this would be useful on Linux with +# Rhythmbox, once it understands daap. (hurry up!) +# +# Failing that, one can use server-side conversion to transcode +# non-standard (.ogg, .flac) music to wav on the server side. +# See the ssc_* options below. +# +# To be able to index .ogg files, you'll need to have configured +# with --enable-oggvorbis. For .flac, --enable-flac, for .mpc, +# --enable-musepack. +# + +extensions = .mp3,.m4a,.m4p,.aiff + +# +# ssc_codectypes (optional) +# +# List of codectypes for files that the daap server should +# perform internal format conversion and present to clients +# as WAV files. The file extensions that these codectypes correspond +# to must also be present in 'extensions' +# configuration value, or files are not probed in the first +# place. +# +# Valid codectypes: +# +# mp4a - for AAC (.aac, .mp4, .m4a, .m4p) +# mpeg - for mp3 +# wav - for wav +# wma - for wma +# ogg - for ogg +# flac - for flac (.flac, .fla) +# mpc for musepack (.mpc, .mpp, .mp+) +# alac for alac (.m4a) +# +# NOTE: 1.0b3 of the Mac server does not have support for transcoding +# Ogg and FLAC. Stay tuned + +ssc_codectypes = alac + +# +# ssc_prog (optional) +# +# Program that is used in server side format conversion. +# Program must accept following command line syntax: +# ssc_prog filename offset length ... +# Parameter filename is the real name of the file that is +# to be converted and streamed, offset is number of bytes +# that are skipped from the beginning of the _output_ file +# before streaming is started, length is length of the song +# in seconds (or zero). All other possible arguments must +# be ignored. The resulting wav file (or the rest of +# the file after initial seek) is written to the standard +# output by the ssc_prog program. This is typically +# a script that is a front end for different conversion tools +# handling different formats. +# +# On the Mac, this is a relative path by default + +ssc_prog = ./mt-daapd-ssc.sh + +# +# logfile (optional) +# +# This is the file to log to. If this is not configured, +# then it will log to the syslog. +# +# Not that the -d switch will control the log verbosity. +# By default, it runs at log level 1. Log level 9 will churn +# out scads of useless debugging information. Values in between +# will vary the amount of logging you get. +# +# On the Mac, this lives in our Firefly folder in Application Support + +logfile = + +# +# truncate (optional) +# +# If logging is configured and this flag is enabled, the +# server will truncate the log file each time it starts. +# This is a good idea for both disk space and readability. +# + +truncate = 1 + +# +# art_filename (optional) +# +# There is experimental support thanks to Hiren Joshi +# (hirenj@mooh.org) for dynamically adding art to the id3v2 +# header as it is streamed (!!). If you were using a music system +# like zina or andromeda, for example, with cover art called +# "_folderOpenImage.jpg", you could use the parameter +# art_file _folderOpenImage.jpg and if the file _folderOpenImage.jpg +# was located in the same folder as the .mp3 file, it would appear +# in iTunes. Cool, eh? +# + +#art_filename = _folderOpenImage.jpg + +# +# rescan_interval +# +# How often to check the file system to see if any mp3 files +# have been added or removed. +# +# if not specified, the default is 0, which disables background scanning. +# +# If background rescanning is disabled, a scan can still be forced from the +# "status" page of the administrative web interface +# +# Setting a rescan_interval lower than the time it takes to rescan +# won't hurt anything, it will just waste CPU, and make connect times +# to the daap server longer. +# +# + +rescan_interval = 300 + +# always_scan +# +# The default behavior is not not do background rescans of the +# filesystem unless there are clients connected. The thought is to +# allow the drives to spin down unless they are in use. This might be +# of more importance in IDE drives that aren't designed to be run +# 24x7. Forcing a scan through the web interface will always work +# though, even if no users are connected. + +# always_scan = 0 + +# +# process_m3u +# +# By default m3u processing is turned off, since most m3u files +# sitting around in peoples mp3 directories have bad paths, and +# I hear about it. :) +# +# If you are sure your m3u files have good paths (i.e. unixly pathed, +# with relative paths relative to the directory the m3u is in), then +# you can turn on m3u processing by setting this directive to 1. +# +# I'm not sure "unixly" is a word, but you get the idea. +# +# On the Mac, process_m3u needs to be on in order to scan the iTunes +# database for playlists, so this is on in the default Mac install. + +process_m3u = 1 + +# +# scan_type +# +# +# This sets how aggressively mp3 files should be scanned to determine +# file length. There are three values: +# +# 0 (Normal) +# Just scan the first mp3 frame to try and calculate size. This will +# be accurate for most files, but VBR files without an Xing tag will +# probably have wildly inaccurate file times. This is the default. +# +# 1 (Aggressive) +# This checks the bitrates of 10 frames in the middle of the song. +# This will still be inaccurate for VBR files without an Xing tag, +# but they probably won't be quite as inaccurate as 0. This takes +# more time, obviously, although the time hit will only happen the +# first time you scan a particular file. +# +# 2 (Painfully aggressive) +# This walks through the entire song, counting the number of frames. +# This should result in accurate song times, but will take the most +# time. Again, this will only have to be incurred the first time +# the file is indexed. +# + +scan_type = 2 + +# +# compress +# +# Whether to use gzip content-encoding when transferring playlists etc. +# This was contributed as a patch by Ciamac Moallemi just prior to the 0.2.1 +# release, and as such, hasn't gotten as much testing as other features. +# +# This feature should substantially speed up transfers of large databases +# and playlists. +# +# It will eventually default to 1, but currently it defaults to 0. +# + +#compress = 0 + +[scan] + +# +# correct_order +# +# When set to 1, ensures that items in a playlist are returned in the +# order in which they are set in the playlist. On platforms with +# limited memory, this may impose an unacceptable performance penalty, +# but on a PC or Mac, it's fine. +# +correct_order = 1 + +[plugins] +plugin_dir = plugins +plugins = ssc-script.so,rsp.so diff --git a/osx/FireflyPrefs/version.plist b/osx/FireflyPrefs/version.plist new file mode 100644 index 00000000..c12b7080 --- /dev/null +++ b/osx/FireflyPrefs/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 54 + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + ProjectName + PreferencePaneTemplate + SourceVersion + 120000 + + diff --git a/osx/FireflyPrefsProtocol.h b/osx/FireflyPrefsProtocol.h new file mode 100644 index 00000000..f8b70c69 --- /dev/null +++ b/osx/FireflyPrefsProtocol.h @@ -0,0 +1,43 @@ +/* + * FireflyPrefsProtocol.h + * Firefly Helper + * + * Created by Mike Kobb on 7/10/06. + * Copyright 2006 Roku LLC. All rights reserved. + * + * This file houses the declaration of the FireflyPrefsServerProtocol + * and FireflyPrefsClientProtocol, which are used on the Macintosh + * for communication between the Firefly prefs pane and the Firefly + * Helper background app. + * + */ + +#include "FireflyCommon.h" + +// The protocol for functions exported by the server (the Firefly Helper) +@protocol FireflyPrefsServerProtocol + +- (BOOL)registerClient:(byref id)client withIdentifier:(int)ident; +- (oneway void)unregisterClientId:(int)ident; +- (FireflyStartResult)startFirefly; +- (FireflyStopResult)stopFirefly; +- (FireflyRestartResult)restartFirefly; +- (FireflyRescanResult)rescanLibrary; +- (FireflyServerStatus)fireflyStatus; +- (BOOL)fireflyIsRunning; +- (bycopy NSString*)fireflyVersion; +- (bycopy NSString*)fireflyConfigURL; +- (oneway void)showHelperMenu:(BOOL)bShowMenu; + +@end + + +// The protocol for functions exported by the client (the prefs pane) +@protocol FireflyPrefsClientProtocol + +- (BOOL)stillThere; +- (oneway void)statusChanged:(FireflyServerStatus)newStatus; +- (oneway void)versionChanged:(bycopy NSString*)newVersion; +- (oneway void)configUrlChanged:(bycopy NSString*)newUrl; + +@end \ No newline at end of file diff --git a/osx/Uninstall Firefly.app b/osx/Uninstall Firefly.app new file mode 100755 index 0000000000000000000000000000000000000000..d07649a6788e877876d1ed742279bc4692e47a9e GIT binary patch literal 13780 zcmeHOe{56N6+VtlJ+pE2uYUmhf@M`N?-x9kd16*8`JZP{gOQ8+0T1^ z0f#x{g&$Qb(#lmqz{F^nXarKUp{<&>c0W=HRhxviPGgg{^$(^>)3j0vsiR`FYxbS@ zt{o>hZl|$-rgzEVe(&Dzo^$TG=h?~e^M~);`85$OgK#T@R1qz%M1K( zRp@PBZK6eN(rJi*v?(MV4tz%seK;R!$HV&nXMosN)oZS-NBkXro>wD{_Y$jGV?io&*O1!aC z?KNa$xN*B7$CU%RF_>!HsvF84#n`V#l~hAtZoKg;ijh+FM5?iE4g-qO*v3XkRqL&b zowNyvZRC7`KaRPECmP_F+KYK7xdzZLhj3m+?y*l$X*W*ZX$b6iUqs(j%CCymj|OOs z^=ug}^H+o^cCqcBJ_?fO{60TfSPW%rd;#k)QQOf+mcG(@=QmZ>4_^N2hcnzpkB&YN zx;gVNvM+BgeQMfMeft&O+s)=a&kY|IqJn1yUtHPxL-QgtXws z{%8>Phuw^Qgg@K$p#)xVkbeu9fdjJ~D13hu|2d6qU52o&JCJ*}_uYF+-zKy3u=?>E z`?thI zH`dF)E3n=xki8}J?opychH4HMSDl-Ml8@2Yt_fkgk3qCjcI{W2t$LzQ?H@8q+>Fj^ z{Tw1x=NsD$LD(kaIj_eD8$OGFkL6#(FrRDuzFP%4zh|r_YW^i|4}RZS4C#gxHvT=4 z<-x+ixwx6v)Xa`GG34JVRzl!cGwbH{{QJ&j2)}gI;fK%3ZEghI2)Ge&Bj85Bjer{g zHv(=1+z7Z4a3k=47Jg`KfOL?i0g z)Ajg^9Gtx^m~RQAfQ4(}LtUn#`+LBR;qQ`v-BznePzf` z9{B0E@K-w9$yStij!wRvwjR#w3mmK}AldprkD=~ zLp2n}SUYUC=kh6R=Lw~GY-}y(H2Ey}nSno;>ns@OGtP+djXcge*amWnb#~x7kVPH| z%=uJ@9IJ-^4>F(ihnQ1C?Ym}X5GPM=e=98L5&6fuSy!dFuFhRshp1!D+2gu{%ATJS z8^mLw*yQeUtZ8xX3GYu~#}9w_Y{CWh!V@^B9`bFeLtcc(_+FxFUSp|VeqH#zz$WlZ z3)yc8+OI`^&M$tA$;#9o!u@&KLr;#k~RRxzEj{xE{j?qDEZm8%jh|1>^Fd zBE=L<9#&#Z8qyR=O{k_SYwAJ8kfO4tF`aq_l;EIZBor-}8cHU0!wmN6M$p7?Y&apu z)o4&t6JP|@R4}2N!K9(2l!O^H^`J4701@L!L+{6DRSoyUU+j&~V`QLga`PZxdnd<^ zdv1(8mf+M2PJIbZrQlR9 z%%K|&hi;VMTos(FB{)-pGgX2!E;!>QIGW&S%;7wlZT#7I(^0$&j1#R-60N(Dr5(X6 zow}Z-G03S%mJWxrbbKO9>#c10%DOC_1U??g(y>VP)7p4;&A}@Z^v3iA1@=KsPf$%} zR(~N{cct*k#cl-L2)Ge&Bj85Bjer{gHv(=1+z7Z4a3kPGz>UEFE&`XR3FWeNSJ1B! zeNgl$pV{N>q7RF{RrDxf+4CDk-z0jJ9r%{|`1vo;@LaRNKH%j7R{{r7 zCIbgQc2Z1Mm-xc@*N^wgBz6jhP z@Dy;jz?XqdfnNqbDg5*UpA+Lh2LApM@_4xJ0sOj+Q3nBja|xAsxNZVG158dWq%7dz zz&?~IKjUx?hkEPoNz=s6p@e2+<$>UdTj2aB^@0{^d*Ma}+;A6~x3mA6a zmb>Hlsf(`u+IsfW*~HR5}&l_<*FO@rb9XNCS!-Q}DfrsBA`gG^!-|5opPf z)fA{jhw)7i*(^ONsWDVf@dPPUXnJ%|k~JCSTSG}|7>emqT!uLmTGhT`6maF3IsCbT}>tVC`j6!R1)nrG`ez literal 0 HcmV?d00001 diff --git a/osx/makedist.sh.templ b/osx/makedist.sh.templ new file mode 100755 index 00000000..3cacafcb --- /dev/null +++ b/osx/makedist.sh.templ @@ -0,0 +1,63 @@ +#!/bin/sh + +if [ $# -eq 1 ]; then + # any argument leaves the server folder as-is + AR="Firefly Helper/Server/admin-root" + + mkdir "Firefly Helper/Server/plugins" + mkdir "${AR}" + mkdir "${AR}/lib-js" + mkdir "${AR}/lib-js/script.aculo.us" + + cp ../.build/ppc/mt-daapd FireflyHelper/Server/firefly + cp ../.build/ppc/*so FireflyHelper/Server/Plugins + cp ../.build/wavstreamer FireflyHelper/Server + cp ../.build/alac FireflyHelper/Server + + cp ../admin-root/*\(html|xml|txt|jar|gif|js|png|jpg\) "${AR}" + cp ../admin-root/CREDITS "${AR}" + cp ../admin-root/lib-js/*js "${AR}/lib-js" + cp ../admin-root/lib-js/script.aculo.us/*js "${AR}/li-js/script.aculo.us" +fi + +pushd "Firefly Helper" +xcodebuild +if [ "$?" -ne "0" ]; then + echo "Could not build Firefly Helper Project" + exit 1 +fi + +popd +rm -rf "FireflyPrefs/Firefly Helper.app" +mv "Firefly Helper/build/Release/Firefly Helper.app" FireflyPrefs + +pushd "FireflyPrefs" +xcodebuild +if [ "$?" -ne "0" ]; then + echo "Could not build FireflyPrefs panel" + exit 2 +fi + +# Now, build the image + +mkdir staging +cp Install/root_DS_Store staging/.DS_Store +mkdir staging/.background +cp Install/background.png staging/.background/background.png +cp Install/_background_DS_Store staging/.background/.DS_Store + +if [ ! -x FireflyPrefs/build/Release/Firefly.prefPane ]; then + echo "Wait... I can't find the pref pane" + exit 1 +fi + +mv FireflyPrefs/build/Release/Firefly.prefPane staging +cp "Uninstall Firefly.app" staging +cp -r "Install/Read Me First!.rtfd" staging + +hdiutil makehybrid -hfs -hfs-volume-name "Install Firefly" -hfs-openfolder staging staging -o tmp.dmg +hdiutil convert -format UDZO tmp.dmg -o Firefly.dmg + +rm -rf staging +rm tmp.dmg +