blob: c8e10dd75f1e8ea8f478c48c1774fc31643cfa06 [file] [log] [blame]
/*
*
* Copyright 2016 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
#import "GRPCConnectivityMonitor.h"
#pragma mark Flags
@implementation GRPCReachabilityFlags {
SCNetworkReachabilityFlags _flags;
}
+ (instancetype)flagsWithFlags:(SCNetworkReachabilityFlags)flags {
return [[self alloc] initWithFlags:flags];
}
- (instancetype)initWithFlags:(SCNetworkReachabilityFlags)flags {
if ((self = [super init])) {
_flags = flags;
}
return self;
}
/*
* One accessor method implementation per flag. Example:
- (BOOL)isCell { \
return !!(_flags & kSCNetworkReachabilityFlagsIsWWAN); \
}
*/
#define GRPC_XMACRO_ITEM(methodName, FlagName) \
- (BOOL)methodName { \
return !!(_flags & kSCNetworkReachabilityFlags ## FlagName); \
}
#include "GRPCReachabilityFlagNames.xmacro.h"
#undef GRPC_XMACRO_ITEM
- (BOOL)isHostReachable {
// Note: connectionOnDemand means it'll be reachable only if using the CFSocketStream API or APIs
// on top of it.
// connectionRequired means we can't tell until a connection is attempted (e.g. for VPN on
// demand).
return self.reachable && !self.interventionRequired && !self.connectionOnDemand;
}
- (NSString *)description {
NSMutableArray *activeOptions = [NSMutableArray arrayWithCapacity:9];
/*
* For each flag, add its name to the array if it's ON. Example:
if (self.isCell) {
[activeOptions addObject:@"isCell"];
}
*/
#define GRPC_XMACRO_ITEM(methodName, FlagName) \
if (self.methodName) { \
[activeOptions addObject:@ #methodName]; \
}
#include "GRPCReachabilityFlagNames.xmacro.h"
#undef GRPC_XMACRO_ITEM
return activeOptions.count == 0 ? @"(none)" : [activeOptions componentsJoinedByString:@", "];
}
- (BOOL)isEqual:(id)object {
return [object isKindOfClass:[GRPCReachabilityFlags class]] &&
_flags == ((GRPCReachabilityFlags *)object)->_flags;
}
- (NSUInteger)hash {
return _flags;
}
@end
#pragma mark Connectivity Monitor
// Assumes the third argument is a block that accepts a GRPCReachabilityFlags object, and passes the
// received ones to it.
static void PassFlagsToContextInfoBlock(SCNetworkReachabilityRef target,
SCNetworkReachabilityFlags flags,
void *info) {
#pragma unused (target)
// This can be called many times with the same info. The info is retained by SCNetworkReachability
// while this function is being executed.
void (^handler)(GRPCReachabilityFlags *) = (__bridge void (^)(GRPCReachabilityFlags *))info;
handler([[GRPCReachabilityFlags alloc] initWithFlags:flags]);
}
@implementation GRPCConnectivityMonitor {
SCNetworkReachabilityRef _reachabilityRef;
GRPCReachabilityFlags *_previousReachabilityFlags;
}
- (nullable instancetype)initWithReachability:(nullable SCNetworkReachabilityRef)reachability {
if (!reachability) {
return nil;
}
if ((self = [super init])) {
_reachabilityRef = CFRetain(reachability);
_queue = dispatch_get_main_queue();
_previousReachabilityFlags = nil;
}
return self;
}
+ (nullable instancetype)monitorWithHost:(nonnull NSString *)host {
const char *hostName = host.UTF8String;
if (!hostName) {
[NSException raise:NSInvalidArgumentException
format:@"host.UTF8String returns NULL for %@", host];
}
SCNetworkReachabilityRef reachability =
SCNetworkReachabilityCreateWithName(NULL, hostName);
GRPCConnectivityMonitor *returnValue = [[self alloc] initWithReachability:reachability];
if (reachability) {
CFRelease(reachability);
}
return returnValue;
}
- (void)handleLossWithHandler:(nullable void (^)(void))lossHandler
wifiStatusChangeHandler:(nullable void (^)(void))wifiStatusChangeHandler {
__weak typeof(self) weakSelf = self;
[self startListeningWithHandler:^(GRPCReachabilityFlags *flags) {
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
if (lossHandler && !flags.reachable) {
lossHandler();
#if TARGET_OS_IPHONE
} else if (wifiStatusChangeHandler &&
strongSelf->_previousReachabilityFlags &&
(flags.isWWAN ^
strongSelf->_previousReachabilityFlags.isWWAN)) {
wifiStatusChangeHandler();
#endif
}
strongSelf->_previousReachabilityFlags = flags;
}
}];
}
- (void)startListeningWithHandler:(void (^)(GRPCReachabilityFlags *))handler {
// Copy to ensure the handler block is in the heap (and so can't be deallocated when this method
// returns).
void (^copiedHandler)(GRPCReachabilityFlags *) = [handler copy];
SCNetworkReachabilityContext context = {
.version = 0,
.info = (__bridge void *)copiedHandler,
.retain = CFRetain,
.release = CFRelease,
};
// The following will retain context.info, and release it when the callback is set to NULL.
SCNetworkReachabilitySetCallback(_reachabilityRef, PassFlagsToContextInfoBlock, &context);
SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, _queue);
}
- (void)stopListening {
// This releases the block on context.info.
SCNetworkReachabilitySetCallback(_reachabilityRef, NULL, NULL);
SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, NULL);
}
- (void)setQueue:(dispatch_queue_t)queue {
_queue = queue ?: dispatch_get_main_queue();
}
- (void)dealloc {
if (_reachabilityRef) {
[self stopListening];
CFRelease(_reachabilityRef);
}
}
@end