// // FireflyHelper.m // // 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 "FireflyHelper.h" #import "FireflyServer.h" #include "../FireflyCommon.h" #include #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