Mac OS X Reference Library Apple Developer
Search

Server/ServerAppDelegate.m

/*
    File:       AppDelegate.m
 
    Contains:   Core server application logic.
 
    Written by: DTS
 
    Copyright:  Copyright (c) 2010 Apple Inc. All Rights Reserved.
 
    Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc.
                ("Apple") in consideration of your agreement to the following
                terms, and your use, installation, modification or
                redistribution of this Apple software constitutes acceptance of
                these terms.  If you do not agree with these terms, please do
                not use, install, modify or redistribute this Apple software.
 
                In consideration of your agreement to abide by the following
                terms, and subject to these terms, Apple grants you a personal,
                non-exclusive license, under Apple's copyrights in this
                original Apple software (the "Apple Software"), to use,
                reproduce, modify and redistribute the Apple Software, with or
                without modifications, in source and/or binary forms; provided
                that if you redistribute the Apple Software in its entirety and
                without modifications, you must retain this notice and the
                following text and disclaimers in all such redistributions of
                the Apple Software. Neither the name, trademarks, service marks
                or logos of Apple Inc. may be used to endorse or promote
                products derived from the Apple Software without specific prior
                written permission from Apple.  Except as expressly stated in
                this notice, no other rights or licenses, express or implied,
                are granted by Apple herein, including but not limited to any
                patent rights that may be infringed by your derivative works or
                by other works in which the Apple Software may be incorporated.
 
                The Apple Software is provided by Apple on an "AS IS" basis. 
                APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
                WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT,
                MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING
                THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
                COMBINATION WITH YOUR PRODUCTS.
 
                IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
                INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
                TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
                DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY
                OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
                OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY
                OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR
                OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF
                SUCH DAMAGE.
 
*/
 
#import "ServerAppDelegate.h"
 
#import "FileSendOperation.h"
 
#include <sys/socket.h>
#include <netinet/in.h>
 
@interface ServerAppDelegate () <NSNetServiceDelegate, NSApplicationDelegate>
 
// read/write variants of public properties
 
@property (nonatomic, assign, readwrite, getter=isRunning) BOOL running;
@property (nonatomic, copy,   readwrite) NSString *         longStatus;
@property (nonatomic, copy,   readwrite) NSString *         serviceName;
@property (nonatomic, assign, readwrite) NSUInteger         inProgressSendCount;
@property (nonatomic, assign, readwrite) NSUInteger         successfulSendCount;
@property (nonatomic, assign, readwrite) NSUInteger         failedSendCount;
 
// private properties
 
@property (nonatomic, copy,   readonly ) NSString *         defaultServiceName;
@property (nonatomic, retain, readwrite) NSNetService *     netService;
@property (nonatomic, retain, readonly ) NSOperationQueue * queue;
 
// forward declarations
 
- (void)start;
- (void)stopWithStatus:(NSString *)newStatus;
 
- (void)connectionReceived:(int)fd;
 
@end
 
@implementation ServerAppDelegate
 
- (void)dealloc
    // The application delegate lives for the lifetime of the application, so we don't bother 
    // implementing -dealloc.
{
    assert(NO);
    [super dealloc];
}
 
#pragma mark * Application delegate callbacks
 
// This object is the delegate of the NSApplication instance so we can get notifications about 
// various state changes.
 
- (void)applicationDidFinishLaunching:(NSNotification *)notification
    // An application delegate callback called when the application has just started up.
{
    #pragma unused(notification)
    
    // Remove the debug menu if we're not the debug variant.
    
    #if defined(NDEBUG)
        {
            NSMenuItem *    debugMenuItem;
 
            debugMenuItem = nil;
            for (NSMenuItem * menuItem in [[NSApp mainMenu] itemArray]) {
                assert([menuItem isKindOfClass:[NSMenuItem class]]);
                if ([menuItem tag] == kDebugMenuTag) {
                    debugMenuItem = menuItem;
                    break;
                }
            }
            [[NSApp mainMenu] removeItem:debugMenuItem];
        }
    #endif
}
 
- (void)applicationWillTerminate:(NSNotification *)sender
    // An application delegate callback called when the application is about to quit.
    // At this point we stop our service so that it doesn't linger on the network.
{
    #pragma unused(sender)
    [self stopWithStatus:nil];
}
 
#pragma mark * Bound properties
 
// The user interface uses Cocoa bindings to set itself up based on these
// KVC/KVO compatible properties.
 
- (NSArray *)pictureNames
{
    return [NSArray arrayWithObjects:@"Dew Drop", @"Ladybug", @"Snowy Hills", @"Water", nil];
}
 
@synthesize selectedPictureIndex = _selectedPictureIndex;
 
+ (NSSet *)keyPathsForValuesAffectingSelectedImagePath
{
    return [NSSet setWithObject:@"selectedPictureIndex"];
}
 
- (NSString *)selectedImagePath
{
    return [NSString stringWithFormat:@"/Library/Desktop Pictures/Nature/%@.jpg", [self.pictureNames objectAtIndex:self.selectedPictureIndex]];
}
 
+ (NSSet *)keyPathsForValuesAffectingStartStopButtonTitle
{
    return [NSSet setWithObject:@"running"];
}
 
@synthesize running = _running;
 
- (NSString *)startStopButtonTitle
{
    NSString *  result;
    if (self.isRunning) {
        result = @"Stop";
    } else {
        result = @"Start";
    }
    return result;
}
 
+ (NSSet *)keyPathsForValuesAffectingShortStatus
{
    return [NSSet setWithObject:@"running"];
}
 
- (NSString *)shortStatus
{
    NSString *  result;
    if (self.isRunning) {
        result = @"Picture Sharing is on.";
    } else {
        result = @"Picture Sharing is off.";
    }
    return result;
}
 
- (NSString *)longStatus
{
    NSString *  result;
    if (self->_longStatus == nil) {
        result = @"Click Start to turn on Picture Sharing and allow other users to see a thumbnail of the picture below.";
    } else {
        result = self->_longStatus;
    }
    return result;
}
 
@synthesize longStatus = _longStatus;
 
- (NSString *)defaultServiceName
{
    NSString *  result;
    
    result = [[NSUserDefaults standardUserDefaults] stringForKey:@"defaultServiceName"];
    if (result == nil) {
        NSString *  str;
        
        str = NSFullUserName();
        if (str == nil) {
            result = @"Pictures";
            assert(result != nil);
        } else {
            result = [NSString stringWithFormat:@"%@'s Pictures", str];
            assert(result != nil);
        }
    }
    assert(result != nil);
    return result;
}
 
- (NSString *)serviceName
{
    if (self->_serviceName == nil) {
        self->_serviceName = [[self defaultServiceName] copy];
        assert(self->_serviceName != nil);
    }
    return self->_serviceName;
}
 
- (void)setServiceName:(NSString *)newValue
{
    if (newValue != self->_serviceName) {
        [self->_serviceName release];
        self->_serviceName = [newValue copy];
        
        // We write back the service name if it's ever set.  This means that the service 
        // name gets written back a) when the user modifies the field in the UI, and 
        // b) when the user clicks Start and the Bonjour service gets registered. 
        // The upshot is that the service name tracks changes to the user's name 
        // (that is, "Bob Dylan's Pictures" changes to "Mannfred Man's Pictures" if 
        // the user's full name changes from "Bob Dylan" to "Mannfred Man") unless the 
        // user explicitly changes it or they start the service.  After the service has 
        // started we want the name to stay fixed for the benefit of returning clients.
 
        if (self->_serviceName == nil) {
            [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"defaultServiceName"];
        } else {
            [[NSUserDefaults standardUserDefaults] setObject:self->_serviceName forKey:@"defaultServiceName"];
        }
    }
}
 
@synthesize inProgressSendCount = _inProgressSendCount;
@synthesize successfulSendCount = _successfulSendCount;
@synthesize failedSendCount     = _failedSendCount;
 
+ (NSSet *)keyPathsForValuesAffectingSending
{
    return [NSSet setWithObject:@"inProgressSendCount"];
}
 
- (BOOL)isSending
{
    return self.inProgressSendCount != 0;
}
 
#pragma mark * Actions
 
- (IBAction)startStopAction:(id)sender
    // Called when user clicks the Start/Stop button.  This either starts or 
    // stops the picture sharing service.
{
    #pragma unused(sender)
    if (self.isRunning) {
        [self stopWithStatus:nil];
    } else {
        [self start];
    }
}
 
- (IBAction)toggleDebugOptionAction:(id)sender
    // Called when the user selects an item from the Debug menu.  We use the 
    // menu item's tag to determine which debug option to toggle.
{
    NSMenuItem *    menuItem;
    
    menuItem = (NSMenuItem *) sender;
    assert([menuItem isKindOfClass:[NSMenuItem class]]);
    assert([menuItem tag] != 0);
    self->_debugOptions ^= [menuItem tag];
    [menuItem setState: ! [menuItem state]];
}
 
#pragma mark * Core networking code
 
@synthesize netService = _netService;
 
static void ListeningSocketCallback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info);
    // forward declaration
 
- (void)start
    // Called to start the network service.
{
    int                 err;
    int                 junk;
    int                 fdForListening;
    int                 chosenPort;
    socklen_t           namelen;
 
    assert( ! self.isRunning );
    assert(_listeningSocket == NULL);
    assert(self.netService == nil);
    
    chosenPort = -1;        // quieten assert
 
    // Here, create the socket from traditional BSD socket calls, and then set up a CFSocket with that to listen for 
    // incoming connections.
 
    // Start by trying to do everything with IPv6.  This will work for both IPv4 and IPv6 clients 
    // via the miracle of mapped IPv4 addresses.
    
    if (self->_debugOptions & kDebugOptionMaskForceIPv4) {
        // This allows us to test IPv4 support on an IPv6-capable kernel.
        fdForListening = -1;
        err = EAFNOSUPPORT;
    } else {
        err = 0;
        fdForListening = socket(AF_INET6, SOCK_STREAM, 0);
        if (fdForListening < 0) {
            err = errno;
        }
    }
    if (err == 0) {
        struct sockaddr_in6 serverAddress6;
 
        // If we created an IPv6 socket, bind it to a kernel-assigned port and then use 
        // getsockname to determine what port we got.
        
        memset(&serverAddress6, 0, sizeof(serverAddress6));
        serverAddress6.sin6_family = AF_INET6;
        serverAddress6.sin6_len    = sizeof(serverAddress6);
 
        err = bind(fdForListening, (const struct sockaddr *) &serverAddress6, sizeof(serverAddress6));
        if (err < 0) {
            err = errno;
        }
        if (err == 0) {
            namelen = sizeof(serverAddress6);
            err = getsockname(fdForListening, (struct sockaddr *) &serverAddress6, &namelen);
            if (err < 0) {
                err = errno;
                assert(err != 0);       // quietens static analyser
            } else {
                chosenPort = ntohs(serverAddress6.sin6_port);
            }
        }
    } else if (err == EAFNOSUPPORT) {
        struct sockaddr_in  serverAddress;
 
        // IPv6 is not available (this can happen, for example, on the iPhone OS 3 kerne).  
        // Let's fall back to IPv4.  Create an IPv4 socket, bind it to a kernel-assigned port 
        // and then use getsockname to determine what port we got.
        
        err = 0;
        fdForListening = socket(AF_INET, SOCK_STREAM, 0);
        if (fdForListening < 0) {
            err = errno;
        }
 
        if (err == 0) {
            memset(&serverAddress, 0, sizeof(serverAddress));
            serverAddress.sin_family = AF_INET;
            serverAddress.sin_len    = sizeof(serverAddress);
 
            err = bind(fdForListening, (const struct sockaddr *) &serverAddress, sizeof(serverAddress));
            if (err < 0) {
                err = errno;
            }
        }
        if (err == 0) {
            namelen = sizeof(serverAddress);
            err = getsockname(fdForListening, (struct sockaddr *) &serverAddress, &namelen);
            if (err < 0) {
                err = errno;
                assert(err != 0);       // quietens static analyser
            } else {
                chosenPort = ntohs(serverAddress.sin_port);
            }
        }
    }
    
    // Listen for connections on our socket, then create a CFSocket to route any connections 
    // to a run loop based callback.
    
    if (err == 0) {
        err = listen(fdForListening, 5);
        if (err < 0) {
            err = errno;
        } else {
            CFSocketContext     context = {0, self, NULL, NULL, NULL};
            CFRunLoopSourceRef  rls;
            
            self->_listeningSocket = CFSocketCreateWithNative(NULL, fdForListening, kCFSocketAcceptCallBack, ListeningSocketCallback, &context);
            if (self->_listeningSocket != NULL) {
                assert( CFSocketGetSocketFlags(self->_listeningSocket) & kCFSocketCloseOnInvalidate );
                fdForListening = -1;        // so that the clean up code doesn't close it
                
                rls = CFSocketCreateRunLoopSource(NULL, self->_listeningSocket, 0);
                assert(rls != NULL);
                
                CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
                
                CFRelease(rls);
            }
        }
    }
 
    // Register our service with Bonjour.
 
    if (err == 0) {
        NSLog(@"chosenPort = %d", chosenPort);
 
        self.netService = [[[NSNetService alloc] initWithDomain:@"" type:@"_wwdcpic2._tcp." name:self.serviceName port:chosenPort] autorelease];
        if (self.netService != nil) {
            [self.netService setDelegate:self];
            [self.netService publishWithOptions:0];
        }
    }
 
    // Clean up.
    
    if ( (self->_listeningSocket != NULL) && (self.netService != nil) ) {
        self.longStatus = @"Click Stop to turn off Picture Sharing.";
        self.running = YES;
    } else {
        [self stopWithStatus:@"Failed to start up."];
    }
    if (fdForListening >= 0) {
        junk = close(fdForListening);
        assert(junk == 0);
    }
}
 
- (void)stopWithStatus:(NSString *)newStatus
    // Called to stop the network service.
{
    // assert(self.isRunning);          -- can be called when we're not running, for example, when we fail to start up
    // newStatus may be nil, which results in a generic 'Click Start to turn on...' message.
 
    self.longStatus = newStatus;
 
    if (self.netService != nil) {
        [self.netService setDelegate:nil];
        [self.netService stop];
        self.netService = nil;
    }
    if (self->_listeningSocket != NULL) {
        CFSocketInvalidate(self->_listeningSocket);
        CFRelease(self->_listeningSocket);
        self->_listeningSocket = NULL;
    }
 
    [self.queue cancelAllOperations];
    // We don't do a -waitUntilAllOperationsAreFinished because the operations may require 
    // the main thread's run loop to be running in the default mode in order to complete 
    // the cancellation.  This isn't actually the case right now (due to implementation 
    // details of NSOperationQueue and the FileSendOperation), but I don't want to rely 
    // on those implementation details staying the same forever.
 
    self.running = NO;
}
 
- (void)netServiceDidPublish:(NSNetService *)sender
    // An NSNetService delegate callback that's called when the service is successfully 
    // registered on the network.  We set our service name to the name of the service 
    // because the service might be been automatically renamed by Bonjour to avoid 
    // conflicts.
{
    assert(sender == self.netService);
    self.serviceName = [sender name];
}
 
- (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary *)errorDict
    // An NSNetService delegate callback that's called when the service fails to 
    // register on the network.  We respond by shutting down our entire network 
    // service.
{
    assert(sender == self.netService);
    #pragma unused(sender)
    #pragma unused(errorDict)
    [self stopWithStatus:@"Failed to registered service."];
}
 
- (void)netServiceDidStop:(NSNetService *)sender
    // An NSNetService delegate callback that's called when the service spontaneously 
    // stops.  This rarely happens on Mac OS X but, regardless, we respond by shutting 
    // down our entire network service.
{
    assert(sender == self.netService);
    #pragma unused(sender)
    [self stopWithStatus:@"Network service stopped."];
}
 
- (NSOperationQueue *)queue
{
    if (self->_queue == nil) {
        self->_queue = [[NSOperationQueue alloc] init];
        assert(self->_queue != nil);
    }
    return self->_queue;
}
 
static void ListeningSocketCallback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
    // The CFSocket callback associated with _listeningSocket.  This is called when 
    // a new connection arrives.  It routes the connection to the -connectionReceived: 
    // method.
{
    ServerAppDelegate *   obj;
    int             fd;
    
    obj = (ServerAppDelegate *) info;
    assert([obj isKindOfClass:[ServerAppDelegate class]]);
    
    assert(s == obj->_listeningSocket);
    #pragma unused(s)
    assert(type == kCFSocketAcceptCallBack);
    #pragma unused(type)
    assert(address != NULL);
    #pragma unused(address)
    assert(data != nil);
    
    fd = * (const int *) data;
    assert(fd >= 0);
    [obj connectionReceived:fd];
}
 
- (void)connectionReceived:(int)fd
    // Called when a connection is received.  We respond by creating and running a 
    // FileSendOperation that sends the current picture down the connection.
{
    CFWriteStreamRef    writeStream;
    FileSendOperation * op;
    Boolean             success;
    
    assert(fd >= 0);
 
    // Create a CFStream from the connection socket.
    
    CFStreamCreatePairWithSocket(NULL, fd, NULL, &writeStream);
    assert(writeStream != nil);
    
    success = CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    assert(success);
 
    // Create a FileSendOperation to run the connection.
    
    op = [[FileSendOperation alloc] initWithFilePath:self.selectedImagePath outputStream:(NSOutputStream *) writeStream];
    assert(op != nil);
 
    // Configure that operation.
    
    #if ! defined(NDEBUG)
        if (self->_debugOptions & kDebugOptionMaskStallSend) {
            op.debugStallSend = YES;
        }
        if (self->_debugOptions & kDebugOptionMaskSendBadChecksum) {
            op.debugSendBadChecksum = YES;
        }
    #endif
    
    // Watch for the operation finishing.  In a real application I'd probably use something more 
    // sophisticated (like the QWatchedOperationQueue class from the LinkedImageFetcher sample code), 
    // but in this small sample I just use KVO directly.
    
    [op addObserver:self forKeyPath:@"isFinished" options:0 context:&self->_queue];
    self.inProgressSendCount += 1;
    
    // Enqueue the operation and then clean up.
    
    [self.queue addOperation:op];
    
    [op release];
    CFRelease(writeStream);
}
 
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == &self->_queue) {
        assert([keyPath isEqual:@"isFinished"]);
        assert([object isKindOfClass:[FileSendOperation class]]);
        
        // This notification is delivered when a FileSendOperation's "isFinished" property 
        // changes.  We respond by calling the -didFinishOperation: operation on the main 
        // thread to clean up that operation.
        
        assert( [(FileSendOperation *) object isFinished] );
        
        // IMPORTANT
        // ---------
        // KVO notifications arrive on the thread that sets the property.  In this case that's 
        // always going to be the main thread (because FileSendOperation is a concurrent operation 
        // that runs off the main thread run loop), but I take no chances and force us to the 
        // main thread.  There's no worries about race conditions here (one of the things that 
        // QWatchedOperationQueue solves nicely) because AppDelegate lives for the lifetime of 
        // the application.
        
        [self performSelectorOnMainThread:@selector(didFinishOperation:) withObject:object waitUntilDone:NO];
    }
    if (NO) {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
 
- (void)didFinishOperation:(FileSendOperation *)op
    // Called when a FileSendOperation finishes.  It simply updates our statistics. 
{
    assert([op isKindOfClass:[FileSendOperation class]]);
    
    [op removeObserver:self forKeyPath:@"isFinished"];
    
    if (op.error == nil) {
        self.successfulSendCount += 1;
    } else {
        self.failedSendCount += 1;
    }
    assert(self.inProgressSendCount != 0);
    self.inProgressSendCount -= 1;
    
    if (self->_debugOptions & kDebugOptionMaskAutoAdvanceImage) {
        NSUInteger  newPictureIndex;
 
        newPictureIndex = self.selectedPictureIndex + 1;
        if (newPictureIndex == [self.pictureNames count]) {
            newPictureIndex = 0;
        }
        self.selectedPictureIndex = newPictureIndex;
    }
}
 
@end



Last updated: 2010-08-06

Did this document help you? Yes It's good, but... Not helpful...