// // 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