Browse Source

Added support for background mode on iOS

Pierre-Olivier Latour 11 years ago
parent
commit
0a21059d25
2 changed files with 177 additions and 46 deletions
  1. 3 0
      GCDWebServer/Core/GCDWebServer.h
  2. 174 46
      GCDWebServer/Core/GCDWebServer.m

+ 3 - 0
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;
 

+ 174 - 46
GCDWebServer/Core/GCDWebServer.m

@@ -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();
   }
 }