diff --git a/src/Network/WebSockets/TSSocketManager.m b/src/Network/WebSockets/TSSocketManager.m index b1397789f..8f5f139cd 100644 --- a/src/Network/WebSockets/TSSocketManager.m +++ b/src/Network/WebSockets/TSSocketManager.m @@ -1,9 +1,5 @@ // -// TSSocketManager.m -// TextSecureiOS -// -// Created by Frederic Jacobs on 17/05/14. -// Copyright (c) 2014 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "SubProtocol.pb.h" @@ -26,28 +22,31 @@ NSString *const SocketOpenedNotification = @"SocketOpenedNotification"; NSString *const SocketClosedNotification = @"SocketClosedNotification"; NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; +// TSSocketManager's properties should only be accessed from the main thread. @interface TSSocketManager () -@property (nonatomic, readonly, strong) OWSSignalService *signalService; +@property (nonatomic, readonly) OWSSignalService *signalService; -@property (nonatomic, retain) NSTimer *pingTimer; -@property (nonatomic, retain) NSTimer *reconnectTimer; +@property (nonatomic) NSTimer *pingTimer; +@property (nonatomic) NSTimer *reconnectTimer; -@property (nonatomic, retain) SRWebSocket *websocket; +@property (nonatomic) SRWebSocket *websocket; @property (nonatomic) SocketStatus status; @property (nonatomic) UIBackgroundTaskIdentifier fetchingTaskIdentifier; -@property BOOL didConnectBg; -@property BOOL didRetreiveMessageBg; -@property BOOL shouldDownloadMessage; +@property (nonatomic) BOOL didConnectBg; +@property (nonatomic) BOOL didRetreiveMessageBg; +@property (nonatomic) BOOL shouldDownloadMessage; -@property (nonatomic, retain) NSTimer *backgroundKeepAliveTimer; -@property (nonatomic, retain) NSTimer *backgroundConnectTimer; +@property (nonatomic) NSTimer *backgroundKeepAliveTimer; +@property (nonatomic) NSTimer *backgroundConnectTimer; @end +#pragma mark - + @implementation TSSocketManager - (instancetype)init @@ -57,9 +56,11 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; if (!self) { return self; } + + OWSAssert([NSThread isMainThread]); _signalService = [OWSSignalService new]; - _websocket = nil; + [self addObserver:self forKeyPath:@"status" options:0 context:kSocketStatusObservationContext]; return self; @@ -87,6 +88,21 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; - (void)becomeActive { + OWSAssert([NSThread isMainThread]); + + if ([NSThread isMainThread]) { + [self ensureWebsocket]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [self ensureWebsocket]; + }); + } +} + +- (void)ensureWebsocket +{ + OWSAssert([NSThread isMainThread]); + if (self.signalService.isCensored) { DDLogWarn(@"%@ Refusing to start websocket in `becomeActive`.", self.tag); return; @@ -98,10 +114,12 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; switch ([socket readyState]) { case SR_OPEN: DDLogVerbose(@"WebSocket already open on connection request"); + OWSAssert(self.status == kSocketStatusOpen); self.status = kSocketStatusOpen; return; case SR_CONNECTING: DDLogVerbose(@"WebSocket is already connecting"); + OWSAssert(self.status == kSocketStatusConnecting); self.status = kSocketStatusConnecting; return; default: @@ -110,9 +128,10 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; } // Discard the old socket which is already closed or is closing. - [socket close]; - self.status = kSocketStatusClosed; - socket.delegate = nil; + [self closeWebSocket]; + + OWSAssert(self.status == kSocketStatusClosed); + self.status = kSocketStatusConnecting; // Create a new web socket. NSString *webSocketConnect = [textSecureWebSocketAPI stringByAppendingString:[self webSocketAuthenticationString]]; @@ -127,13 +146,32 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; } + (void)resignActivity { - SRWebSocket *socket = [[self sharedManager] websocket]; - [socket close]; + OWSAssert([NSThread isMainThread]); + + [[self sharedManager] closeWebSocket]; +} + +- (void)closeWebSocket +{ + OWSAssert([NSThread isMainThread]); + + if (self.websocket) { + DDLogWarn(@"%@ closeWebSocket.", self.tag); + } + + [self.websocket close]; + self.websocket.delegate = nil; + self.websocket = nil; + [self.pingTimer invalidate]; + self.pingTimer = nil; + self.status = kSocketStatusClosed; } #pragma mark - Delegate methods - (void)webSocketDidOpen:(SRWebSocket *)webSocket { + OWSAssert([NSThread isMainThread]); + self.pingTimer = [NSTimer timerWithTimeInterval:kWebSocketHeartBeat target:self selector:@selector(webSocketHeartBeat) @@ -143,6 +181,7 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; // Additionally, we want the ping timer to work in the background too. [[NSRunLoop mainRunLoop] addTimer:self.pingTimer forMode:NSDefaultRunLoopMode]; + OWSAssert(self.status == kSocketStatusConnecting); self.status = kSocketStatusOpen; [self.reconnectTimer invalidate]; self.reconnectTimer = nil; @@ -150,10 +189,12 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; } - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error { + OWSAssert([NSThread isMainThread]); + DDLogError(@"Error connecting to socket %@", error); - [self.pingTimer invalidate]; - self.status = kSocketStatusClosed; + [self closeWebSocket]; + [self scheduleRetry]; } @@ -194,6 +235,8 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; } - (void)keepAliveBackground { + OWSAssert([NSThread isMainThread]); + [self.backgroundConnectTimer invalidate]; if (self.fetchingTaskIdentifier) { @@ -214,6 +257,8 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; } - (void)sendWebSocketMessageAcknowledgement:(WebSocketRequestMessage *)request { + OWSAssert([NSThread isMainThread]); + WebSocketResponseMessageBuilder *response = [WebSocketResponseMessage builder]; [response setStatus:200]; [response setMessage:@"OK"]; @@ -227,6 +272,7 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; [self.websocket sendDataNoCopy:message.build.data error:&error]; if (error) { DDLogWarn(@"Error while trying to write on websocket %@", error); + [self scheduleRetry]; } } @@ -234,20 +280,27 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean { - [self.pingTimer invalidate]; - self.status = kSocketStatusClosed; + OWSAssert([NSThread isMainThread]); + + [self closeWebSocket]; - if (!wasClean && [UIApplication sharedApplication].applicationState == UIApplicationStateActive) { + if (!wasClean && [self shouldKeepWebSocketAlive]) { [self scheduleRetry]; } } - (void)webSocketHeartBeat { - NSError *error; - - [self.websocket sendPing:nil error:&error]; - if (error) { - DDLogWarn(@"Error in websocket heartbeat: %@", error.localizedDescription); + OWSAssert([NSThread isMainThread]); + + if ([self shouldKeepWebSocketAlive]) { + NSError *error; + [self.websocket sendPing:nil error:&error]; + if (error) { + DDLogWarn(@"Error in websocket heartbeat: %@", error.localizedDescription); + [self scheduleRetry]; + } + } else { + [self closeWebSocket]; } } @@ -258,8 +311,29 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; [TSStorageManager serverAuthToken]]; } +- (BOOL)shouldKeepWebSocketAlive { + if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { + // If app is active, keep web socket alive. + return YES; + } else if (self.backgroundKeepAliveTimer || + self.backgroundConnectTimer || + self.fetchingTaskIdentifier != UIBackgroundTaskInvalid) { + // If app is doing any work in the background, keep web socket alive. + return YES; + } else { + return NO; + } +} + - (void)scheduleRetry { - if (![self.reconnectTimer isValid]) { + OWSAssert([NSThread isMainThread]); + + if (![self shouldKeepWebSocketAlive]) { + // Don't bother retrying if app is inactive and not doing any background activity. + [self.reconnectTimer invalidate]; + self.reconnectTimer = nil; + } else if (![self.reconnectTimer isValid]) { + // TODO: It'd be nice to do exponential backoff. self.reconnectTimer = [NSTimer timerWithTimeInterval:kWebSocketReconnectTry target:[self class] selector:@selector(becomeActive) @@ -275,37 +349,53 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; #pragma mark - Background Connect + (void)becomeActiveFromForeground { - TSSocketManager *sharedInstance = [self sharedManager]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[self sharedManager] becomeActiveFromForeground]; + }); +} - if (sharedInstance.fetchingTaskIdentifier != UIBackgroundTaskInvalid) { - [sharedInstance closeBackgroundTask]; +- (void)becomeActiveFromForeground { + OWSAssert([NSThread isMainThread]); + + if (self.fetchingTaskIdentifier != UIBackgroundTaskInvalid) { + [self closeBackgroundTask]; } - + [self becomeActive]; } + (void)becomeActiveFromBackgroundExpectMessage:(BOOL)expected { - TSSocketManager *sharedInstance = [TSSocketManager sharedManager]; - - if (sharedInstance.fetchingTaskIdentifier == UIBackgroundTaskInvalid) { - sharedInstance.backgroundConnectTimer = [NSTimer timerWithTimeInterval:kBackgroundConnectTimer - target:sharedInstance - selector:@selector(backgroundConnectTimerExpired) - userInfo:nil - repeats:NO]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[TSSocketManager sharedManager] becomeActiveFromBackgroundExpectMessage:expected]; + }); +} + +- (void)becomeActiveFromBackgroundExpectMessage:(BOOL)expected { + OWSAssert([NSThread isMainThread]); + + if (self.fetchingTaskIdentifier == UIBackgroundTaskInvalid) { + [self.backgroundConnectTimer invalidate]; + self.backgroundConnectTimer = [NSTimer timerWithTimeInterval:kBackgroundConnectTimer + target:self + selector:@selector(backgroundConnectTimerExpired) + userInfo:nil + repeats:NO]; NSRunLoop *loop = [NSRunLoop mainRunLoop]; [loop addTimer:[TSSocketManager sharedManager].backgroundConnectTimer forMode:NSDefaultRunLoopMode]; - - [sharedInstance.backgroundKeepAliveTimer invalidate]; - sharedInstance.didConnectBg = NO; - sharedInstance.didRetreiveMessageBg = NO; - sharedInstance.shouldDownloadMessage = expected; - sharedInstance.fetchingTaskIdentifier = - [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ - [TSSocketManager resignActivity]; - [[TSSocketManager sharedManager] closeBackgroundTask]; - }]; - + + [self.backgroundKeepAliveTimer invalidate]; + self.backgroundKeepAliveTimer = nil; + self.didConnectBg = NO; + self.didRetreiveMessageBg = NO; + self.shouldDownloadMessage = expected; + self.fetchingTaskIdentifier = + [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + OWSAssert([NSThread isMainThread]); + + [TSSocketManager resignActivity]; + [[TSSocketManager sharedManager] closeBackgroundTask]; + }]; + [self becomeActive]; } else { DDLogWarn(@"Got called to become active in the background but there was already a background task running."); @@ -313,17 +403,28 @@ NSString *const SocketConnectingNotification = @"SocketConnectingNotification"; } - (void)backgroundConnectTimerExpired { + [self.backgroundConnectTimer invalidate]; + self.backgroundConnectTimer = nil; + [self backgroundTimeExpired]; } - (void)backgroundTimeExpired { - [[self class] resignActivity]; + OWSAssert([NSThread isMainThread]); + + if (![self shouldKeepWebSocketAlive]) { + [[self class] resignActivity]; + } [self closeBackgroundTask]; } - (void)closeBackgroundTask { + OWSAssert([NSThread isMainThread]); + [self.backgroundKeepAliveTimer invalidate]; + self.backgroundKeepAliveTimer = nil; [self.backgroundConnectTimer invalidate]; + self.backgroundConnectTimer = nil; /* If VOIP Push worked, we should just have to check if message was retreived and if not, alert the user.