diff --git a/GCDWebServer/Core/GCDWebServer.h b/GCDWebServer/Core/GCDWebServer.h index 0b51839..dcf5bc5 100644 --- a/GCDWebServer/Core/GCDWebServer.h +++ b/GCDWebServer/Core/GCDWebServer.h @@ -49,6 +49,9 @@ extern NSString* const GCDWebServerOption_ServerName; // NSString (default is s extern NSString* const GCDWebServerOption_ConnectionClass; // Subclass of GCDWebServerConnection (default is GCDWebServerConnection class) extern NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET; // NSNumber / BOOL (default is YES) extern NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval; // NSNumber / double (default is 1.0 seconds - set to <=0.0 to disable coaslescing of -webServerDidConnect: / -webServerDidDisconnect:) +#if TARGET_OS_IPHONE +extern NSString* const GCDWebServerOption_AutomaticallySuspendInBackground; // NSNumber / BOOL (default is YES) +#endif @class GCDWebServer; diff --git a/GCDWebServer/Core/GCDWebServer.m b/GCDWebServer/Core/GCDWebServer.m index 9a71ee9..bafb352 100644 --- a/GCDWebServer/Core/GCDWebServer.m +++ b/GCDWebServer/Core/GCDWebServer.m @@ -26,8 +26,10 @@ */ #import +#if TARGET_OS_IPHONE +#import +#else #ifdef __GCDWEBSERVER_ENABLE_TESTING__ -#if !TARGET_OS_IPHONE #import #endif #endif @@ -50,6 +52,7 @@ BOOL _connected; CFRunLoopTimerRef _connectedTimer; + NSDictionary* _options; NSString* _serverName; Class _connectionClass; BOOL _mapHEADToGET; @@ -57,6 +60,10 @@ NSUInteger _port; dispatch_source_t _source; CFNetServiceRef _service; +#if TARGET_OS_IPHONE + BOOL _suspendInBackground; + UIBackgroundTaskIdentifier _backgroundTask; +#endif #ifdef __GCDWEBSERVER_ENABLE_TESTING__ BOOL _recording; #endif @@ -77,6 +84,9 @@ NSString* const GCDWebServerOption_ServerName = @"ServerName"; NSString* const GCDWebServerOption_ConnectionClass = @"ConnectionClass"; NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET = @"AutomaticallyMapHEADToGET"; NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval = @"ConnectedStateCoalescingInterval"; +#if TARGET_OS_IPHONE +NSString* const GCDWebServerOption_AutomaticallySuspendInBackground = @"AutomaticallySuspendInBackground"; +#endif #ifndef __GCDWEBSERVER_LOGGING_HEADER__ #ifdef NDEBUG @@ -166,6 +176,9 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) { CFRunLoopTimerContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL}; _connectedTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, HUGE_VAL, HUGE_VAL, 0, 0, _ConnectedTimerCallBack, &context); CFRunLoopAddTimer(CFRunLoopGetMain(), _connectedTimer, kCFRunLoopCommonModes); +#if TARGET_OS_IPHONE + _backgroundTask = UIBackgroundTaskInvalid; +#endif } return self; } @@ -175,7 +188,7 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) { DCHECK(_activeConnections == 0); _delegate = nil; - if (_source) { + if (_options) { [self stop]; } @@ -187,16 +200,42 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) { ARC_DEALLOC(super); } +#if TARGET_OS_IPHONE + +// Always called on main thread +- (void)_startBackgroundTask { + DCHECK([NSThread isMainThread]); + if (_backgroundTask == UIBackgroundTaskInvalid) { + LOG_DEBUG(@"Did start background task"); + _backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + + LOG_WARNING(@"Application is being suspended while %@ is still connected", [self class]); + [self _endBackgroundTask]; + + }]; + } else { + DNOT_REACHED(); + } +} + +#endif + +// Always called on main thread - (void)_didConnect { + DCHECK([NSThread isMainThread]); DCHECK(_connected == NO); _connected = YES; LOG_DEBUG(@"Did connect"); + +#if TARGET_OS_IPHONE + [self _startBackgroundTask]; +#endif + if ([_delegate respondsToSelector:@selector(webServerDidConnect:)]) { [_delegate webServerDidConnect:self]; } } -// Called from any thread - (void)willStartConnection:(GCDWebServerConnection*)connection { dispatch_sync(_syncQueue, ^{ @@ -216,16 +255,41 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) { }); } +#if TARGET_OS_IPHONE + +// Always called on main thread +- (void)_endBackgroundTask { + DCHECK([NSThread isMainThread]); + if (_backgroundTask != UIBackgroundTaskInvalid) { + if (_suspendInBackground && ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) && _source) { + [self _stop]; + } + [[UIApplication sharedApplication] endBackgroundTask:_backgroundTask]; + _backgroundTask = UIBackgroundTaskInvalid; + LOG_DEBUG(@"Did end background task"); + } else { + DNOT_REACHED(); + } +} + +#endif + +// Always called on main thread - (void)_didDisconnect { + DCHECK([NSThread isMainThread]); DCHECK(_connected == YES); _connected = NO; LOG_DEBUG(@"Did disconnect"); + +#if TARGET_OS_IPHONE + [self _endBackgroundTask]; +#endif + if ([_delegate respondsToSelector:@selector(webServerDidDisconnect:)]) { [_delegate webServerDidDisconnect:self]; } } -// Called from any thread - (void)didEndConnection:(GCDWebServerConnection*)connection { dispatch_sync(_syncQueue, ^{ DCHECK(_activeConnections > 0); @@ -248,21 +312,17 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) { } - (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock { - DCHECK(_source == NULL); + DCHECK(_options == nil); GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock]; [_handlers insertObject:handler atIndex:0]; ARC_RELEASE(handler); } - (void)removeAllHandlers { - DCHECK(_source == NULL); + DCHECK(_options == nil); [_handlers removeAllObjects]; } -- (BOOL)start { - return [self startWithPort:kDefaultPort bonjourName:@""]; -} - static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) { @autoreleasepool { if (error->error) { @@ -274,23 +334,16 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er } } -- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name { - NSMutableDictionary* options = [NSMutableDictionary dictionary]; - [options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port]; - [options setValue:name forKey:GCDWebServerOption_BonjourName]; - return [self startWithOptions:options]; -} - static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValue) { id value = [options objectForKey:key]; return value ? value : defaultValue; } -- (BOOL)startWithOptions:(NSDictionary*)options { +- (BOOL)_start { DCHECK(_source == NULL); - NSUInteger port = [_GetOption(options, GCDWebServerOption_Port, [NSNumber numberWithUnsignedInteger:0]) unsignedIntegerValue]; - NSString* name = _GetOption(options, GCDWebServerOption_BonjourName, @""); - NSUInteger maxPendingConnections = [_GetOption(options, GCDWebServerOption_MaxPendingConnections, [NSNumber numberWithUnsignedInteger:16]) unsignedIntegerValue]; + NSUInteger port = [_GetOption(_options, GCDWebServerOption_Port, @0) unsignedIntegerValue]; + NSString* name = _GetOption(_options, GCDWebServerOption_BonjourName, @""); + NSUInteger maxPendingConnections = [_GetOption(_options, GCDWebServerOption_MaxPendingConnections, @16) unsignedIntegerValue]; int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if (listeningSocket > 0) { int yes = 1; @@ -305,10 +358,10 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu if (bind(listeningSocket, (void*)&addr4, sizeof(addr4)) == 0) { if (listen(listeningSocket, (int)maxPendingConnections) == 0) { LOG_DEBUG(@"Did open listening socket %i", listeningSocket); - _serverName = [_GetOption(options, GCDWebServerOption_ServerName, NSStringFromClass([self class])) copy]; - _connectionClass = _GetOption(options, GCDWebServerOption_ConnectionClass, [GCDWebServerConnection class]); - _mapHEADToGET = [_GetOption(options, GCDWebServerOption_AutomaticallyMapHEADToGET, [NSNumber numberWithBool:YES]) boolValue]; - _disconnectDelay = [_GetOption(options, GCDWebServerOption_ConnectedStateCoalescingInterval, [NSNumber numberWithDouble:1.0]) doubleValue]; + _serverName = [_GetOption(_options, GCDWebServerOption_ServerName, NSStringFromClass([self class])) copy]; + _connectionClass = _GetOption(_options, GCDWebServerOption_ConnectionClass, [GCDWebServerConnection class]); + _mapHEADToGET = [_GetOption(_options, GCDWebServerOption_AutomaticallyMapHEADToGET, @YES) boolValue]; + _disconnectDelay = [_GetOption(_options, GCDWebServerOption_ConnectedStateCoalescingInterval, @1.0) doubleValue]; _source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listeningSocket, 0, kGCDWebServerGCDQueue); dispatch_source_set_cancel_handler(_source, ^{ @@ -403,34 +456,109 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu return (_source ? YES : NO); } +- (void)_stop { + DCHECK(_source != NULL); + + if (_service) { + CFNetServiceUnscheduleFromRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes); + CFNetServiceSetClient(_service, NULL, NULL); + CFRelease(_service); + _service = NULL; + } + + dispatch_source_cancel(_source); // This will close the socket + ARC_DISPATCH_RELEASE(_source); + _source = NULL; + _port = 0; + + ARC_RELEASE(_serverName); + _serverName = nil; + + LOG_INFO(@"%@ stopped", [self class]); + if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [_delegate webServerDidStop:self]; + }); + } +} + +- (BOOL)start { + return [self startWithPort:kDefaultPort bonjourName:@""]; +} + +- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name { + NSMutableDictionary* options = [NSMutableDictionary dictionary]; + [options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port]; + [options setValue:name forKey:GCDWebServerOption_BonjourName]; + return [self startWithOptions:options]; +} + +#if TARGET_OS_IPHONE + +- (void)_didEnterBackground:(NSNotification*)notification { + DCHECK([NSThread isMainThread]); + LOG_DEBUG(@"Did enter background"); + if ((_backgroundTask == UIBackgroundTaskInvalid) && _source) { + [self _stop]; + } +} + +- (void)_willEnterForeground:(NSNotification*)notification { + DCHECK([NSThread isMainThread]); + LOG_DEBUG(@"Will enter foreground"); + if (!_source) { + [self _start]; // TODO: There's probably nothing we can do on failure + } +} + +#endif + +- (BOOL)startWithOptions:(NSDictionary*)options { + if (_options == nil) { + _options = [options copy]; +#if TARGET_OS_IPHONE + _suspendInBackground = [_GetOption(_options, GCDWebServerOption_AutomaticallySuspendInBackground, @YES) boolValue]; + if (((_suspendInBackground == NO) || ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground)) && ![self _start]) +#else + if (![self _start]) +#endif + { + ARC_RELEASE(_options); + _options = nil; + return NO; + } +#if TARGET_OS_IPHONE + if (_suspendInBackground) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; + } +#endif + return YES; + } else { + DNOT_REACHED(); + } + return NO; +} + - (BOOL)isRunning { return (_source ? YES : NO); } - (void)stop { - DCHECK(_source != NULL); - if (_source) { - if (_service) { - CFNetServiceUnscheduleFromRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes); - CFNetServiceSetClient(_service, NULL, NULL); - CFRelease(_service); - _service = NULL; + if (_options) { +#if TARGET_OS_IPHONE + if (_suspendInBackground) { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil]; } - - dispatch_source_cancel(_source); // This will close the socket - ARC_DISPATCH_RELEASE(_source); - _source = NULL; - _port = 0; - - ARC_RELEASE(_serverName); - _serverName = nil; - - LOG_INFO(@"%@ stopped", [self class]); - if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_delegate webServerDidStop:self]; - }); +#endif + if (_source) { + [self _stop]; } + ARC_RELEASE(_options); + _options = nil; + } else { + DNOT_REACHED(); } }