Added support for background mode on iOS

This commit is contained in:
Pierre-Olivier Latour
2014-04-16 23:49:35 -03:00
parent 14acb7b323
commit 0a21059d25
2 changed files with 177 additions and 46 deletions

View File

@@ -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;

View File

@@ -26,8 +26,10 @@
*/
#import <TargetConditionals.h>
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
#else
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
#if !TARGET_OS_IPHONE
#import <AppKit/AppKit.h>
#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();
}
}