Server/ServerAppDelegate.m
/* |
File: ServerAppDelegate.m |
Abstract: Core server application logic. |
Version: 2.2 |
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. |
Copyright (C) 2013 Apple Inc. All Rights Reserved. |
*/ |
#import "ServerAppDelegate.h" |
#import "FileSendOperation.h" |
#include <sys/socket.h> |
#include <netinet/in.h> |
@interface ServerAppDelegate () <NSNetServiceDelegate, NSApplicationDelegate> |
// Actions |
- (IBAction)startStopAction:(id)sender; |
enum { |
kDebugMenuTag = 0x64626720 // == 'dbg ' == 1684170528 |
}; |
enum { |
kDebugOptionMaskStallSend = 0x01, |
kDebugOptionMaskSendBadChecksum = 0x02, |
kDebugOptionMaskForceIPv4 = 0x04, |
kDebugOptionMaskAutoAdvanceImage = 0x08 |
}; |
- (IBAction)toggleDebugOptionAction:(id)sender; |
// The user interface uses Cocoa bindings to set itself up based on the following |
// KVC/KVO compatible properties. |
@property (nonatomic, copy, readonly ) NSArray * pictureNames; |
@property (nonatomic, assign, readwrite) NSUInteger selectedPictureIndex; |
@property (nonatomic, copy, readonly ) NSString * selectedImagePath; |
@property (nonatomic, copy, readonly ) NSString * startStopButtonTitle; |
@property (nonatomic, copy, readonly ) NSString * shortStatus; |
@property (nonatomic, copy, readwrite) NSString * longStatus; |
@property (nonatomic, assign, readwrite, getter=isRunning) BOOL running; |
@property (nonatomic, copy, readwrite) NSString * serviceName; |
@property (nonatomic, assign, readonly, getter=isSending) BOOL sending; |
@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, strong, readwrite) NSNetService * netService; |
@property (nonatomic, strong, readonly ) NSOperationQueue * queue; |
@property (nonatomic, assign, readwrite) NSUInteger debugOptions; |
@end |
@implementation ServerAppDelegate { |
CFSocketRef _listeningSocket; |
} |
- (id)init |
{ |
self = [super init]; |
if (self != nil) { |
self->_queue = [[NSOperationQueue alloc] init]; |
} |
return self; |
} |
- (void)dealloc |
// The application delegate lives for the lifetime of the application, so we don't bother |
// implementing -dealloc. |
{ |
assert(NO); |
} |
#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 @[@"Dew Drop", @"Ladybug", @"Snowy Hills", @"Water"]; |
} |
+ (NSSet *)keyPathsForValuesAffectingSelectedImagePath |
{ |
return [NSSet setWithObject:@"selectedPictureIndex"]; |
} |
- (NSString *)selectedImagePath |
{ |
return [NSString stringWithFormat:@"/Library/Desktop Pictures/Nature/%@.jpg", self.pictureNames[self.selectedPictureIndex]]; |
} |
+ (NSSet *)keyPathsForValuesAffectingStartStopButtonTitle |
{ |
return [NSSet setWithObject:@"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; |
} |
- (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; |
} |
// We need to explicitly synthesise the ivar for _serviceName but, because we override both |
// the setter and getter. |
@synthesize serviceName = _serviceName; |
- (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 = [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 "Manfred Mann's Pictures" if |
// the user's full name changes from "Bob Dylan" to "Manfred Mann") 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"]; |
} |
} |
} |
+ (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 ^= (NSUInteger) [menuItem tag]; |
[menuItem setState: ! [menuItem state]]; |
} |
#pragma mark * Core networking code |
- (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 early versions of iOS). |
// 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, (__bridge void *)(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]; |
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 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."]; |
} |
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 = (__bridge 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:(__bridge 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. |
assert(self.queue != nil); |
[self.queue addOperation:op]; |
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]; |
} else { |
[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 |
Copyright © 2013 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2013-08-15