2016-02-24 16:56:39 +01:00

652 lines
18 KiB
Objective-C

#import "XMPPReconnect.h"
#import "XMPPStream.h"
#import "XMPPLogging.h"
#import "NSXMLElement+XMPP.h"
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
#define IMPOSSIBLE_REACHABILITY_FLAGS 0xFFFFFFFF
// Log levels: off, error, warn, info, verbose
#if DEBUG
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#else
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#endif
enum XMPPReconnectFlags
{
kShouldReconnect = 1 << 0, // If set, disconnection was accidental, and autoReconnect may be used
kMultipleChanges = 1 << 1, // If set, there have been reachability changes during a connection attempt
kManuallyStarted = 1 << 2, // If set, we were started manually via manualStart method
kQueryingDelegates = 1 << 3, // If set, we are awaiting response(s) from the delegate(s)
};
enum XMPPReconnectConfig
{
kAutoReconnect = 1 << 0, // If set, automatically attempts to reconnect after a disconnection
};
#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_5 && !TARGET_OS_IPHONE
// SCNetworkConnectionFlags was renamed to SCNetworkReachabilityFlags in 10.6
typedef SCNetworkConnectionFlags SCNetworkReachabilityFlags;
#endif
@interface XMPPReconnect (PrivateAPI)
- (void)setupReconnectTimer;
- (void)teardownReconnectTimer;
- (void)setupNetworkMonitoring;
- (void)teardownNetworkMonitoring;
- (void)maybeAttemptReconnect;
- (void)maybeAttemptReconnectWithTicket:(int)ticket;
- (void)maybeAttemptReconnectWithReachabilityFlags:(SCNetworkReachabilityFlags)reachabilityFlags;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation XMPPReconnect
@dynamic autoReconnect;
@synthesize reconnectDelay;
@synthesize reconnectTimerInterval;
- (id)init
{
return [self initWithDispatchQueue:NULL];
}
- (id)initWithDispatchQueue:(dispatch_queue_t)queue
{
if ((self = [super initWithDispatchQueue:queue]))
{
flags = 0;
config = kAutoReconnect;
reconnectDelay = DEFAULT_XMPP_RECONNECT_DELAY;
reconnectTimerInterval = DEFAULT_XMPP_RECONNECT_TIMER_INTERVAL;
reconnectTicket = 0;
previousReachabilityFlags = IMPOSSIBLE_REACHABILITY_FLAGS;
}
return self;
}
- (void)dealloc
{
dispatch_block_t block = ^{
[self teardownReconnectTimer];
[self teardownNetworkMonitoring];
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_sync(moduleQueue, block);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Configuration and Flags
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (BOOL)autoReconnect
{
__block BOOL result = NO;
dispatch_block_t block = ^{
result = (config & kAutoReconnect) ? YES : NO;
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_sync(moduleQueue, block);
return result;
}
- (void)setAutoReconnect:(BOOL)flag
{
dispatch_block_t block = ^{
if (flag)
config |= kAutoReconnect;
else
config &= ~kAutoReconnect;
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
- (BOOL)shouldReconnect
{
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
return (flags & kShouldReconnect) ? YES : NO;
}
- (void)setShouldReconnect:(BOOL)flag
{
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
if (flag)
flags |= kShouldReconnect;
else
flags &= ~kShouldReconnect;
}
- (BOOL)multipleReachabilityChanges
{
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
return (flags & kMultipleChanges) ? YES : NO;
}
- (void)setMultipleReachabilityChanges:(BOOL)flag
{
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
if (flag)
flags |= kMultipleChanges;
else
flags &= ~kMultipleChanges;
}
- (BOOL)manuallyStarted
{
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
return (flags & kManuallyStarted) ? YES : NO;
}
- (void)setManuallyStarted:(BOOL)flag
{
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
if (flag)
flags |= kManuallyStarted;
else
flags &= ~kManuallyStarted;
}
- (BOOL)queryingDelegates
{
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
return (flags & kQueryingDelegates) ? YES : NO;
}
- (void)setQueryingDelegates:(BOOL)flag
{
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
if (flag)
flags |= kQueryingDelegates;
else
flags &= ~kQueryingDelegates;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Manual Manipulation
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)manualStart
{
dispatch_block_t block = ^{ @autoreleasepool {
if ([xmppStream isDisconnected] && [self manuallyStarted] == NO)
{
[self setManuallyStarted:YES];
[self setupReconnectTimer];
[self setupNetworkMonitoring];
}
}};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
- (void)stop
{
dispatch_block_t block = ^{ @autoreleasepool {
// Clear all flags to disable any further reconnect attemts regardless of the state we're in.
flags = 0;
// Stop any planned reconnect attempts and stop monitoring the network.
reconnectTicket++;
[self teardownReconnectTimer];
[self teardownNetworkMonitoring];
}};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark XMPPStream Delegate
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
// This method is executed on our moduleQueue.
// The stream is up so we can stop our reconnect attempts now.
//
// We essentially want to do the same thing as the stop method with one exception:
// We do not want to clear the shouldReconnect flag.
//
// Remember the shouldReconnect flag gets set upon authentication.
// A combination of this flag and the autoReconnect flag controls the auto reconnect mechanism.
//
// It is possible for us to get accidentally disconnected after
// the stream opens but prior to authentication completing.
// If this happens we still want to abide by the previous shouldReconnect setting.
[self setMultipleReachabilityChanges:NO];
[self setManuallyStarted:NO];
reconnectTicket++;
[self teardownReconnectTimer];
[self teardownNetworkMonitoring];
}
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
// This method is executed on our moduleQueue.
// We're now connected and properly authenticated.
// Should we get accidentally disconnected we should automatically reconnect (if autoReconnect is set).
[self setShouldReconnect:YES];
}
- (void)xmppStream:(XMPPStream *)sender didReceiveError:(NSXMLElement *)element
{
// This method is executed on our moduleQueue.
// <stream:error>
// <conflict xmlns="urn:ietf:params:xml:ns:xmpp-streams"/>
// <text xmlns="urn:ietf:params:xml:ns:xmpp-streams" xml:lang="">Replaced by new connection</text>
// </stream:error>
//
// If our connection ever gets replaced, we shouldn't attempt a reconnect,
// because the user has logged in on another device.
// If we still applied the reconnect logic,
// the two devices may get into an infinite loop of kicking each other off the system.
NSString *elementName = [element name];
if ([elementName isEqualToString:@"stream:error"] || [elementName isEqualToString:@"error"])
{
NSXMLElement *conflict = [element elementForName:@"conflict" xmlns:@"urn:ietf:params:xml:ns:xmpp-streams"];
if (conflict)
{
[self setShouldReconnect:NO];
}
}
}
- (void)xmppStreamWasToldToDisconnect:(XMPPStream *)sender
{
// This method is executed on our moduleQueue.
// We should not automatically attempt to reconnect when the connection closes.
[self stop];
}
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
{
// This method is executed on our moduleQueue.
if ([self autoReconnect] && [self shouldReconnect])
{
[self setupReconnectTimer];
[self setupNetworkMonitoring];
SCNetworkReachabilityFlags reachabilityFlags = 0;
SCNetworkReachabilityGetFlags(reachability, &reachabilityFlags);
[multicastDelegate xmppReconnect:self didDetectAccidentalDisconnect:reachabilityFlags];
}
if ([self multipleReachabilityChanges])
{
// While the previous connection attempt was in progress, the reachability of the xmpp host changed.
// This means that while the previous attempt failed, an attempt now might succeed.
int ticket = ++reconnectTicket;
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (0.1 * NSEC_PER_SEC));
dispatch_after(tt, moduleQueue, ^{ @autoreleasepool {
[self maybeAttemptReconnectWithTicket:ticket];
}});
// Note: We delay the method call.
// This allows the other delegates to be notified of the closed stream prior to our reconnect attempt.
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Reachability
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static void XMPPReconnectReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info)
{
@autoreleasepool {
XMPPReconnect *instance = (__bridge XMPPReconnect *)info;
[instance maybeAttemptReconnectWithReachabilityFlags:flags];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Logic
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)setupReconnectTimer
{
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
if (reconnectTimer == NULL)
{
if ((reconnectDelay <= 0.0) && (reconnectTimerInterval <= 0.0))
{
// All timed reconnect attempts are disabled
return;
}
reconnectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);
dispatch_source_set_event_handler(reconnectTimer, ^{ @autoreleasepool {
[self maybeAttemptReconnect];
}});
#if !OS_OBJECT_USE_OBJC
dispatch_source_t theReconnectTimer = reconnectTimer;
dispatch_source_set_cancel_handler(reconnectTimer, ^{
XMPPLogVerbose(@"dispatch_release(reconnectTimer)");
dispatch_release(theReconnectTimer);
});
#endif
dispatch_time_t startTime;
if (reconnectDelay > 0.0)
startTime = dispatch_time(DISPATCH_TIME_NOW, (reconnectDelay * NSEC_PER_SEC));
else
startTime = dispatch_time(DISPATCH_TIME_NOW, (reconnectTimerInterval * NSEC_PER_SEC));
uint64_t intervalTime;
if (reconnectTimerInterval > 0.0)
intervalTime = reconnectTimerInterval * NSEC_PER_SEC;
else
intervalTime = 0.0;
dispatch_source_set_timer(reconnectTimer, startTime, intervalTime, 0.25);
dispatch_resume(reconnectTimer);
}
}
- (void)teardownReconnectTimer
{
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
if (reconnectTimer)
{
dispatch_source_cancel(reconnectTimer);
reconnectTimer = NULL;
}
}
- (void)setupNetworkMonitoring
{
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
if (reachability == NULL)
{
NSString *domain = xmppStream.hostName;
if (domain == nil)
{
domain = @"apple.com";
}
reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, [domain UTF8String]);
if (reachability)
{
SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
SCNetworkReachabilitySetCallback(reachability, XMPPReconnectReachabilityCallback, &context);
if (moduleQueue)
{
SCNetworkReachabilitySetDispatchQueue(reachability,moduleQueue);
}
else
{
XMPPLogWarn(@"%@: %@ - No xmpp moduleQueue!", THIS_FILE, THIS_METHOD);
}
}
}
}
- (void)teardownNetworkMonitoring
{
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
if (reachability)
{
if (moduleQueue)
{
SCNetworkReachabilitySetDispatchQueue(reachability,NULL);
}
else
{
XMPPLogWarn(@"%@: %@ - No xmpp moduleQueue!", THIS_FILE, THIS_METHOD);
}
SCNetworkReachabilitySetCallback(reachability, NULL, NULL);
CFRelease(reachability);
reachability = NULL;
}
}
/**
* This method may be invoked by the reconnectTimer.
*
* During auto reconnection it is invoked reconnectDelay seconds after an accidental disconnection.
* After that, it is then invoked every reconnectTimerInterval seconds.
*
* This handles disconnections that were not the result of an internet connectivity issue.
**/
- (void)maybeAttemptReconnect
{
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
if (reachability)
{
SCNetworkReachabilityFlags reachabilityFlags;
if (SCNetworkReachabilityGetFlags(reachability, &reachabilityFlags))
{
[self maybeAttemptReconnectWithReachabilityFlags:reachabilityFlags];
}
}
}
/**
* This method is invoked (after a short delay) if the reachability changed while
* a reconnection attempt was in progress.
**/
- (void)maybeAttemptReconnectWithTicket:(int)ticket
{
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
if (ticket != reconnectTicket)
{
// The dispatched task was cancelled.
return;
}
if (reachability)
{
SCNetworkReachabilityFlags reachabilityFlags;
if (SCNetworkReachabilityGetFlags(reachability, &reachabilityFlags))
{
[self maybeAttemptReconnectWithReachabilityFlags:reachabilityFlags];
}
}
}
- (void)maybeAttemptReconnectWithReachabilityFlags:(SCNetworkReachabilityFlags)reachabilityFlags
{
if (!dispatch_get_specific(moduleQueueTag))
{
dispatch_async(moduleQueue, ^{ @autoreleasepool {
[self maybeAttemptReconnectWithReachabilityFlags:reachabilityFlags];
}});
return;
}
if (([self manuallyStarted]) || ([self autoReconnect] && [self shouldReconnect]))
{
if ([xmppStream isDisconnected] && ([self queryingDelegates] == NO))
{
// The xmpp stream is disconnected, and is not attempting reconnection
// Delegate rules:
//
// If ALL of the delegates return YES, then the result is YES.
// If ANY of the delegates return NO, then the result is NO.
// If there are no delegates, the default answer is YES.
GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator];
id del;
dispatch_queue_t dq;
SEL selector = @selector(xmppReconnect:shouldAttemptAutoReconnect:);
NSUInteger delegateCount = [delegateEnumerator countForSelector:selector];
dispatch_semaphore_t delSemaphore = dispatch_semaphore_create(0);
dispatch_group_t delGroup = dispatch_group_create();
while ([delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector])
{
dispatch_group_async(delGroup, dq, ^{ @autoreleasepool {
if (![del xmppReconnect:self shouldAttemptAutoReconnect:reachabilityFlags])
{
dispatch_semaphore_signal(delSemaphore);
}
}});
}
[self setQueryingDelegates:YES];
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{ @autoreleasepool {
dispatch_group_wait(delGroup, DISPATCH_TIME_FOREVER);
// What was the delegate response?
BOOL shouldAttemptReconnect;
if (delegateCount == 0)
{
shouldAttemptReconnect = YES;
}
else
{
shouldAttemptReconnect = (dispatch_semaphore_wait(delSemaphore, DISPATCH_TIME_NOW) != 0);
}
dispatch_async(moduleQueue, ^{ @autoreleasepool {
[self setQueryingDelegates:NO];
if (shouldAttemptReconnect)
{
[self setMultipleReachabilityChanges:NO];
previousReachabilityFlags = reachabilityFlags;
if (self.usesOldSchoolSecureConnect)
{
[xmppStream oldSchoolSecureConnectWithTimeout:XMPPStreamTimeoutNone error:nil];
}
else
{
[xmppStream connectWithTimeout:XMPPStreamTimeoutNone error:nil];
}
}
else if ([self multipleReachabilityChanges])
{
[self setMultipleReachabilityChanges:NO];
previousReachabilityFlags = IMPOSSIBLE_REACHABILITY_FLAGS;
[self maybeAttemptReconnect];
}
else
{
previousReachabilityFlags = IMPOSSIBLE_REACHABILITY_FLAGS;
}
}});
#if !OS_OBJECT_USE_OBJC
dispatch_release(delSemaphore);
dispatch_release(delGroup);
#endif
}});
}
else
{
// The xmpp stream is already attempting a connection.
if (reachabilityFlags != previousReachabilityFlags)
{
// It seems that the reachability of our xmpp host has changed in the middle of either
// a reconnection attempt or while querying our delegates for permission to attempt reconnect.
//
// This may mean that the current attempt will fail,
// but an another attempt after the failure will succeed.
//
// We make a note of the multiple changes,
// and if the current attempt fails, we'll try again after a short delay.
[self setMultipleReachabilityChanges:YES];
}
}
}
}
@end