From e1185761d3b27e1aeaf1a4b72600169967ec448e Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Thu, 28 Feb 2019 21:12:45 -0800 Subject: [PATCH 1/7] Add ClientNetworkMonitor for tracking network changes The SwiftGRPC implementation that is backed by [gRPC-Core](https://github.com/grpc/grpc) (and not SwiftNIO) is known to have some connectivity issues on iOS clients - namely, silently disconnecting (making it seem like active calls/connections are hanging) when switching between wifi <> cellular. The root cause of these problems is that the backing gRPC-Core doesn't get the optimizations made by iOS' networking stack when these types of changes occur, and isn't able to handle them itself. To aid in this problem, we're adding a [`ClientNetworkMonitor`](./Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift) that monitors the device for events that can cause gRPC to disconnect silently. We recommend utilizing this component to call `shutdown()` (or destroy) any active `Channel` instances, and start new ones when the network is reachable. Details: - **Switching between wifi <> cellular:** Channels silently disconnect - **Network becoming unreachable:** Most times channels will time out after a few seconds, but `ClientNetworkMonitor` will notify of these changes much faster - **Switching between background <> foreground:** No known issues Original issue: https://github.com/grpc/grpc-swift/issues/337. --- README.md | 28 ++- .../SwiftGRPC/Core/ClientNetworkMonitor.swift | 162 ++++++++++++++++++ 2 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift diff --git a/README.md b/README.md index 96e7ddf8f..12b49c5f8 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ By convention the `--swift_out` option invokes the `protoc-gen-swift` plugin and `--swiftgrpc_out` invokes `protoc-gen-swiftgrpc`. #### Parameters + To pass extra parameters to the plugin, use a comma-separated parameter list separated from the output directory by a colon. @@ -133,6 +134,29 @@ to directly build API clients and servers with no generated code. For an example of this in Swift, please see the [Simple](Examples/SimpleXcode) example. +### Known issues + +The SwiftGRPC implementation that is backed by [gRPC-Core](https://github.com/grpc/grpc) +(and not SwiftNIO) is known to have some connectivity issues on iOS clients - namely, silently +disconnecting (making it seem like active calls/connections are hanging) when switching +between wifi <> cellular. The root cause of these problems is that the +backing gRPC-Core doesn't get the optimizations made by iOS' networking stack when these +types of changes occur, and isn't able to handle them itself. + +To aid in this problem, there is a [`ClientNetworkMonitor`](./Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift) +that monitors the device for events that can cause gRPC to disconnect silently. We recommend utilizing this component to call `shutdown()` (or destroy) any active `Channel` instances, and start new ones when the network is reachable. + +Setting the [`keepAliveTimeout` argument](https://github.com/grpc/grpc-swift/blob/c401b44ea81b246b8e7fea191ea1ee11a834ee60/Sources/SwiftGRPC/Core/ChannelArgument.swift#L46) +on channels is also encouraged. + +Details: +- **Switching between wifi <> cellular:** Channels silently disconnect +- **Network becoming unreachable:** Most times channels will time out after a few seconds, but + `ClientNetworkMonitor` will notify of these changes much faster +- **Switching between background <> foreground:** No known issues + +Original issue: https://github.com/grpc/grpc-swift/issues/337. + ## Having build problems? grpc-swift depends on Swift, Xcode, and swift-protobuf. We are currently @@ -175,11 +199,11 @@ When issuing a new release, the following steps should be followed: 1. Run the CocoaPods linter to ensure that there are no new warnings/errors: `$ pod spec lint SwiftGRPC.podspec` - + 1. Update the Carthage Xcode project (diff will need to be checked in with the version bump): `$ make project-carthage` - + 1. Bump the version in the `SwiftGRPC.podspec` file 1. Merge these changes, then create a new `Release` with corresponding `Tag`. Be sure to include a list of changes in the message diff --git a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift new file mode 100644 index 000000000..460d1e175 --- /dev/null +++ b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift @@ -0,0 +1,162 @@ +/* + * Copyright 2019, gRPC Authors All rights reserved. + * + * 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. + */ +#if os(iOS) +import CoreTelephony +import Dispatch +import SystemConfiguration + +/// This class may be used to monitor changes on the device that can cause gRPC to silently disconnect (making +/// it seem like active calls/connections are hanging), then manually shut down / restart gRPC channels as +/// needed. The root cause of these problems is that the backing gRPC-Core doesn't get the optimizations +/// made by iOS' networking stack when changes occur on the device such as switching from wifi to cellular, +/// enabling/disabling airplane mode, etc. +/// Read more: https://github.com/grpc/grpc-swift/tree/master/README.md#known-issues +/// Original issue: https://github.com/grpc/grpc-swift/issues/337 +open class ClientNetworkMonitor { + private let queue: DispatchQueue + private let callback: (State) -> Void + private var reachability: SCNetworkReachability? + private var appWasInForeground: Bool? + + /// Instance of network info being used for obtaining cellular technology names. + public private(set) lazy var cellularInfo = CTTelephonyNetworkInfo() + /// Whether the network is currently reachable. Backed by `SCNetworkReachability`. + public private(set) var isReachable: Bool? + /// Whether the device is currently using wifi (versus cellular). + public private(set) var isUsingWifi: Bool? + /// Name of the cellular technology being used (i.e., `CTRadioAccessTechnologyLTE`). + public private(set) var cellularName: String? + + /// Represents a state of connectivity. + public struct State: Equatable { + /// The most recent change that was made to the state. + public let lastChange: Change + /// Whether this state is currently reachable/online. + public let isReachable: Bool + } + + /// A change in network condition. + public enum Change: Equatable { + /// Reachability changed (online <> offline). + case reachability(isReachable: Bool) + /// The device switched from cellular to wifi. + case cellularToWifi + /// The device switched from wifi to cellular. + case wifiToCellular + /// The cellular technology changed (i.e., 3G <> LTE). + case cellularTechnology(technology: String) + } + + /// Designated initializer for the network monitor. + /// + /// - Parameter queue: Queue on which to process and update network changes. Will create one if `nil`. + /// Should always be used when accessing properties of this class. + /// - Parameter host: Host to use for monitoring reachability. + /// - Parameter callback: Closure to call whenever state changes. + public init(host: String = "google.com", queue: DispatchQueue? = nil, callback: @escaping (State) -> Void) { + self.queue = queue ?? DispatchQueue(label: "SwiftGRPC.ClientNetworkMonitor.queue") + self.callback = callback + self.startMonitoringReachability(host: host) + self.startMonitoringCellular() + } + + deinit { + if let reachability = self.reachability { + SCNetworkReachabilitySetCallback(reachability, nil, nil) + SCNetworkReachabilityUnscheduleFromRunLoop(reachability, CFRunLoopGetMain(), + CFRunLoopMode.commonModes.rawValue) + } + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Cellular + + private func startMonitoringCellular() { + let notificationName: Notification.Name + if #available(iOS 13, *) { + notificationName = .CTServiceRadioAccessTechnologyDidChange + } else { + notificationName = .CTRadioAccessTechnologyDidChange + } + + NotificationCenter.default.addObserver(self, selector: #selector(self.cellularDidChange(_:)), + name: notificationName, object: nil) + } + + @objc + private func cellularDidChange(_ notification: NSNotification) { + self.queue.async { + let newCellularName: String? + if #available(iOS 13, *) { + let cellularKey = notification.object as? String + newCellularName = cellularKey.flatMap { self.cellularInfo.serviceCurrentRadioAccessTechnology?[$0] } + } else { + newCellularName = notification.object as? String ?? self.cellularInfo.currentRadioAccessTechnology + } + + if let newCellularName = newCellularName, self.cellularName != newCellularName { + self.cellularName = newCellularName + self.callback(State(lastChange: .cellularTechnology(technology: newCellularName), + isReachable: self.isReachable ?? false)) + } + } + } + + // MARK: - Reachability + + private func startMonitoringReachability(host: String) { + guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { + return + } + + let info = Unmanaged.passUnretained(self).toOpaque() + var context = SCNetworkReachabilityContext(version: 0, info: info, retain: nil, + release: nil, copyDescription: nil) + let callback: SCNetworkReachabilityCallBack = { _, flags, info in + let observer = info.map { Unmanaged.fromOpaque($0).takeUnretainedValue() } + observer?.reachabilityDidChange(with: flags) + } + + SCNetworkReachabilitySetCallback(reachability, callback, &context) + SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(), + CFRunLoopMode.commonModes.rawValue) + self.queue.async { [weak self] in + self?.reachability = reachability + + var flags = SCNetworkReachabilityFlags() + SCNetworkReachabilityGetFlags(reachability, &flags) + self?.reachabilityDidChange(with: flags) + } + } + + private func reachabilityDidChange(with flags: SCNetworkReachabilityFlags) { + self.queue.async { + let isReachable = flags.contains(.reachable) + if let wasReachable = self.isReachable, wasReachable != isReachable { + self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable)) + } + self.isReachable = isReachable + + let isUsingWifi = !flags.contains(.isWWAN) + if let wasUsingWifi = self.isUsingWifi, wasUsingWifi != isUsingWifi { + self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, + isReachable: self.isReachable ?? false)) + } + self.isUsingWifi = isUsingWifi + } + } +} +#endif From c6f2fd643d840f689b45190f01426dc2fea3fcb5 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Fri, 1 Mar 2019 07:01:11 -0800 Subject: [PATCH 2/7] CR --- .../SwiftGRPC/Core/ClientNetworkMonitor.swift | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift index 460d1e175..10bd38b6a 100644 --- a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift +++ b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift @@ -28,16 +28,15 @@ import SystemConfiguration open class ClientNetworkMonitor { private let queue: DispatchQueue private let callback: (State) -> Void - private var reachability: SCNetworkReachability? - private var appWasInForeground: Bool? + private let reachability: SCNetworkReachability /// Instance of network info being used for obtaining cellular technology names. - public private(set) lazy var cellularInfo = CTTelephonyNetworkInfo() + public let cellularInfo = CTTelephonyNetworkInfo() /// Whether the network is currently reachable. Backed by `SCNetworkReachability`. public private(set) var isReachable: Bool? /// Whether the device is currently using wifi (versus cellular). public private(set) var isUsingWifi: Bool? - /// Name of the cellular technology being used (i.e., `CTRadioAccessTechnologyLTE`). + /// Name of the cellular technology being used (e.g., `CTRadioAccessTechnologyLTE`). public private(set) var cellularName: String? /// Represents a state of connectivity. @@ -56,29 +55,32 @@ open class ClientNetworkMonitor { case cellularToWifi /// The device switched from wifi to cellular. case wifiToCellular - /// The cellular technology changed (i.e., 3G <> LTE). + /// The cellular technology changed (e.g., 3G <> LTE). case cellularTechnology(technology: String) } - /// Designated initializer for the network monitor. + /// Designated initializer for the network monitor. Initializer fails if reachability is unavailable. /// + /// - Parameter host: Host to use for monitoring reachability. /// - Parameter queue: Queue on which to process and update network changes. Will create one if `nil`. /// Should always be used when accessing properties of this class. - /// - Parameter host: Host to use for monitoring reachability. /// - Parameter callback: Closure to call whenever state changes. - public init(host: String = "google.com", queue: DispatchQueue? = nil, callback: @escaping (State) -> Void) { + public init?(host: String = "google.com", queue: DispatchQueue? = nil, callback: @escaping (State) -> Void) { + guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { + return nil + } + self.queue = queue ?? DispatchQueue(label: "SwiftGRPC.ClientNetworkMonitor.queue") self.callback = callback - self.startMonitoringReachability(host: host) + self.reachability = reachability + self.startMonitoringReachability(reachability) self.startMonitoringCellular() } deinit { - if let reachability = self.reachability { - SCNetworkReachabilitySetCallback(reachability, nil, nil) - SCNetworkReachabilityUnscheduleFromRunLoop(reachability, CFRunLoopGetMain(), - CFRunLoopMode.commonModes.rawValue) - } + SCNetworkReachabilitySetCallback(self.reachability, nil, nil) + SCNetworkReachabilityUnscheduleFromRunLoop(self.reachability, CFRunLoopGetMain(), + CFRunLoopMode.commonModes.rawValue) NotificationCenter.default.removeObserver(self) } @@ -86,7 +88,7 @@ open class ClientNetworkMonitor { private func startMonitoringCellular() { let notificationName: Notification.Name - if #available(iOS 13, *) { + if #available(iOS 12.0, *) { notificationName = .CTServiceRadioAccessTechnologyDidChange } else { notificationName = .CTRadioAccessTechnologyDidChange @@ -100,7 +102,7 @@ open class ClientNetworkMonitor { private func cellularDidChange(_ notification: NSNotification) { self.queue.async { let newCellularName: String? - if #available(iOS 13, *) { + if #available(iOS 12.0, *) { let cellularKey = notification.object as? String newCellularName = cellularKey.flatMap { self.cellularInfo.serviceCurrentRadioAccessTechnology?[$0] } } else { @@ -117,11 +119,7 @@ open class ClientNetworkMonitor { // MARK: - Reachability - private func startMonitoringReachability(host: String) { - guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { - return - } - + private func startMonitoringReachability(_ reachability: SCNetworkReachability) { let info = Unmanaged.passUnretained(self).toOpaque() var context = SCNetworkReachabilityContext(version: 0, info: info, retain: nil, release: nil, copyDescription: nil) @@ -134,8 +132,6 @@ open class ClientNetworkMonitor { SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue) self.queue.async { [weak self] in - self?.reachability = reachability - var flags = SCNetworkReachabilityFlags() SCNetworkReachabilityGetFlags(reachability, &flags) self?.reachabilityDidChange(with: flags) @@ -144,18 +140,20 @@ open class ClientNetworkMonitor { private func reachabilityDidChange(with flags: SCNetworkReachabilityFlags) { self.queue.async { + let isUsingWifi = !flags.contains(.isWWAN) let isReachable = flags.contains(.reachable) - if let wasReachable = self.isReachable, wasReachable != isReachable { - self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable)) - } + + let notifyForWifi = self.isUsingWifi != nil && self.isUsingWifi != isUsingWifi + let notifyForReachable = self.isReachable != nil && self.isReachable != isReachable + self.isReachable = isReachable + self.isUsingWifi = isUsingWifi - let isUsingWifi = !flags.contains(.isWWAN) - if let wasUsingWifi = self.isUsingWifi, wasUsingWifi != isUsingWifi { - self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, - isReachable: self.isReachable ?? false)) + if notifyForWifi { + self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, isReachable: isReachable)) + } else if notifyForReachable { + self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable)) } - self.isUsingWifi = isUsingWifi } } } From eb47da09464e68bdc86866f254e48e865e66b310 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Fri, 1 Mar 2019 07:16:22 -0800 Subject: [PATCH 3/7] 4 -> 2 --- .../SwiftGRPC/Core/ClientNetworkMonitor.swift | 230 +++++++++--------- 1 file changed, 115 insertions(+), 115 deletions(-) diff --git a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift index 10bd38b6a..74b6f98e9 100644 --- a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift +++ b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift @@ -26,135 +26,135 @@ import SystemConfiguration /// Read more: https://github.com/grpc/grpc-swift/tree/master/README.md#known-issues /// Original issue: https://github.com/grpc/grpc-swift/issues/337 open class ClientNetworkMonitor { - private let queue: DispatchQueue - private let callback: (State) -> Void - private let reachability: SCNetworkReachability - - /// Instance of network info being used for obtaining cellular technology names. - public let cellularInfo = CTTelephonyNetworkInfo() - /// Whether the network is currently reachable. Backed by `SCNetworkReachability`. - public private(set) var isReachable: Bool? - /// Whether the device is currently using wifi (versus cellular). - public private(set) var isUsingWifi: Bool? - /// Name of the cellular technology being used (e.g., `CTRadioAccessTechnologyLTE`). - public private(set) var cellularName: String? - - /// Represents a state of connectivity. - public struct State: Equatable { - /// The most recent change that was made to the state. - public let lastChange: Change - /// Whether this state is currently reachable/online. - public let isReachable: Bool + private let queue: DispatchQueue + private let callback: (State) -> Void + private let reachability: SCNetworkReachability + + /// Instance of network info being used for obtaining cellular technology names. + public let cellularInfo = CTTelephonyNetworkInfo() + /// Whether the network is currently reachable. Backed by `SCNetworkReachability`. + public private(set) var isReachable: Bool? + /// Whether the device is currently using wifi (versus cellular). + public private(set) var isUsingWifi: Bool? + /// Name of the cellular technology being used (e.g., `CTRadioAccessTechnologyLTE`). + public private(set) var cellularName: String? + + /// Represents a state of connectivity. + public struct State: Equatable { + /// The most recent change that was made to the state. + public let lastChange: Change + /// Whether this state is currently reachable/online. + public let isReachable: Bool + } + + /// A change in network condition. + public enum Change: Equatable { + /// Reachability changed (online <> offline). + case reachability(isReachable: Bool) + /// The device switched from cellular to wifi. + case cellularToWifi + /// The device switched from wifi to cellular. + case wifiToCellular + /// The cellular technology changed (e.g., 3G <> LTE). + case cellularTechnology(technology: String) + } + + /// Designated initializer for the network monitor. Initializer fails if reachability is unavailable. + /// + /// - Parameter host: Host to use for monitoring reachability. + /// - Parameter queue: Queue on which to process and update network changes. Will create one if `nil`. + /// Should always be used when accessing properties of this class. + /// - Parameter callback: Closure to call whenever state changes. + public init?(host: String = "google.com", queue: DispatchQueue? = nil, callback: @escaping (State) -> Void) { + guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { + return nil } - /// A change in network condition. - public enum Change: Equatable { - /// Reachability changed (online <> offline). - case reachability(isReachable: Bool) - /// The device switched from cellular to wifi. - case cellularToWifi - /// The device switched from wifi to cellular. - case wifiToCellular - /// The cellular technology changed (e.g., 3G <> LTE). - case cellularTechnology(technology: String) + self.queue = queue ?? DispatchQueue(label: "SwiftGRPC.ClientNetworkMonitor.queue") + self.callback = callback + self.reachability = reachability + self.startMonitoringReachability(reachability) + self.startMonitoringCellular() + } + + deinit { + SCNetworkReachabilitySetCallback(self.reachability, nil, nil) + SCNetworkReachabilityUnscheduleFromRunLoop(self.reachability, CFRunLoopGetMain(), + CFRunLoopMode.commonModes.rawValue) + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Cellular + + private func startMonitoringCellular() { + let notificationName: Notification.Name + if #available(iOS 12.0, *) { + notificationName = .CTServiceRadioAccessTechnologyDidChange + } else { + notificationName = .CTRadioAccessTechnologyDidChange } - /// Designated initializer for the network monitor. Initializer fails if reachability is unavailable. - /// - /// - Parameter host: Host to use for monitoring reachability. - /// - Parameter queue: Queue on which to process and update network changes. Will create one if `nil`. - /// Should always be used when accessing properties of this class. - /// - Parameter callback: Closure to call whenever state changes. - public init?(host: String = "google.com", queue: DispatchQueue? = nil, callback: @escaping (State) -> Void) { - guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { - return nil - } - - self.queue = queue ?? DispatchQueue(label: "SwiftGRPC.ClientNetworkMonitor.queue") - self.callback = callback - self.reachability = reachability - self.startMonitoringReachability(reachability) - self.startMonitoringCellular() + NotificationCenter.default.addObserver(self, selector: #selector(self.cellularDidChange(_:)), + name: notificationName, object: nil) + } + + @objc + private func cellularDidChange(_ notification: NSNotification) { + self.queue.async { + let newCellularName: String? + if #available(iOS 12.0, *) { + let cellularKey = notification.object as? String + newCellularName = cellularKey.flatMap { self.cellularInfo.serviceCurrentRadioAccessTechnology?[$0] } + } else { + newCellularName = notification.object as? String ?? self.cellularInfo.currentRadioAccessTechnology + } + + if let newCellularName = newCellularName, self.cellularName != newCellularName { + self.cellularName = newCellularName + self.callback(State(lastChange: .cellularTechnology(technology: newCellularName), + isReachable: self.isReachable ?? false)) + } } + } - deinit { - SCNetworkReachabilitySetCallback(self.reachability, nil, nil) - SCNetworkReachabilityUnscheduleFromRunLoop(self.reachability, CFRunLoopGetMain(), - CFRunLoopMode.commonModes.rawValue) - NotificationCenter.default.removeObserver(self) - } - - // MARK: - Cellular - - private func startMonitoringCellular() { - let notificationName: Notification.Name - if #available(iOS 12.0, *) { - notificationName = .CTServiceRadioAccessTechnologyDidChange - } else { - notificationName = .CTRadioAccessTechnologyDidChange - } - - NotificationCenter.default.addObserver(self, selector: #selector(self.cellularDidChange(_:)), - name: notificationName, object: nil) - } + // MARK: - Reachability - @objc - private func cellularDidChange(_ notification: NSNotification) { - self.queue.async { - let newCellularName: String? - if #available(iOS 12.0, *) { - let cellularKey = notification.object as? String - newCellularName = cellularKey.flatMap { self.cellularInfo.serviceCurrentRadioAccessTechnology?[$0] } - } else { - newCellularName = notification.object as? String ?? self.cellularInfo.currentRadioAccessTechnology - } - - if let newCellularName = newCellularName, self.cellularName != newCellularName { - self.cellularName = newCellularName - self.callback(State(lastChange: .cellularTechnology(technology: newCellularName), - isReachable: self.isReachable ?? false)) - } - } + private func startMonitoringReachability(_ reachability: SCNetworkReachability) { + let info = Unmanaged.passUnretained(self).toOpaque() + var context = SCNetworkReachabilityContext(version: 0, info: info, retain: nil, + release: nil, copyDescription: nil) + let callback: SCNetworkReachabilityCallBack = { _, flags, info in + let observer = info.map { Unmanaged.fromOpaque($0).takeUnretainedValue() } + observer?.reachabilityDidChange(with: flags) } - // MARK: - Reachability - - private func startMonitoringReachability(_ reachability: SCNetworkReachability) { - let info = Unmanaged.passUnretained(self).toOpaque() - var context = SCNetworkReachabilityContext(version: 0, info: info, retain: nil, - release: nil, copyDescription: nil) - let callback: SCNetworkReachabilityCallBack = { _, flags, info in - let observer = info.map { Unmanaged.fromOpaque($0).takeUnretainedValue() } - observer?.reachabilityDidChange(with: flags) - } - - SCNetworkReachabilitySetCallback(reachability, callback, &context) - SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(), - CFRunLoopMode.commonModes.rawValue) - self.queue.async { [weak self] in - var flags = SCNetworkReachabilityFlags() - SCNetworkReachabilityGetFlags(reachability, &flags) - self?.reachabilityDidChange(with: flags) - } + SCNetworkReachabilitySetCallback(reachability, callback, &context) + SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(), + CFRunLoopMode.commonModes.rawValue) + self.queue.async { [weak self] in + var flags = SCNetworkReachabilityFlags() + SCNetworkReachabilityGetFlags(reachability, &flags) + self?.reachabilityDidChange(with: flags) } + } - private func reachabilityDidChange(with flags: SCNetworkReachabilityFlags) { - self.queue.async { - let isUsingWifi = !flags.contains(.isWWAN) - let isReachable = flags.contains(.reachable) + private func reachabilityDidChange(with flags: SCNetworkReachabilityFlags) { + self.queue.async { + let isUsingWifi = !flags.contains(.isWWAN) + let isReachable = flags.contains(.reachable) - let notifyForWifi = self.isUsingWifi != nil && self.isUsingWifi != isUsingWifi - let notifyForReachable = self.isReachable != nil && self.isReachable != isReachable + let notifyForWifi = self.isUsingWifi != nil && self.isUsingWifi != isUsingWifi + let notifyForReachable = self.isReachable != nil && self.isReachable != isReachable - self.isReachable = isReachable - self.isUsingWifi = isUsingWifi + self.isReachable = isReachable + self.isUsingWifi = isUsingWifi - if notifyForWifi { - self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, isReachable: isReachable)) - } else if notifyForReachable { - self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable)) - } - } + if notifyForWifi { + self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, isReachable: isReachable)) + } else if notifyForReachable { + self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable)) + } } + } } #endif From d044f0aec97ba0116d476397ed11ee3bed44c191 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Fri, 1 Mar 2019 07:18:44 -0800 Subject: [PATCH 4/7] notify for both --- Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift index 74b6f98e9..27e2d8273 100644 --- a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift +++ b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift @@ -151,7 +151,9 @@ open class ClientNetworkMonitor { if notifyForWifi { self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, isReachable: isReachable)) - } else if notifyForReachable { + } + + if notifyForReachable { self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable)) } } From 8a985b8712e3fbb9079f8afc2cbdab842cfb9cdf Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Fri, 1 Mar 2019 07:21:54 -0800 Subject: [PATCH 5/7] tweak --- Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift index 27e2d8273..8059078d5 100644 --- a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift +++ b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift @@ -146,8 +146,8 @@ open class ClientNetworkMonitor { let notifyForWifi = self.isUsingWifi != nil && self.isUsingWifi != isUsingWifi let notifyForReachable = self.isReachable != nil && self.isReachable != isReachable - self.isReachable = isReachable self.isUsingWifi = isUsingWifi + self.isReachable = isReachable if notifyForWifi { self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, isReachable: isReachable)) From 6d72e87a0c3e7c8350d529406c2cf01811b35e09 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Fri, 1 Mar 2019 10:39:13 -0800 Subject: [PATCH 6/7] update readme after testing --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 12b49c5f8..425969213 100644 --- a/README.md +++ b/README.md @@ -139,23 +139,26 @@ For an example of this in Swift, please see the The SwiftGRPC implementation that is backed by [gRPC-Core](https://github.com/grpc/grpc) (and not SwiftNIO) is known to have some connectivity issues on iOS clients - namely, silently disconnecting (making it seem like active calls/connections are hanging) when switching -between wifi <> cellular. The root cause of these problems is that the +between wifi <> cellular or between cellular technologies (3G <> LTE). The root cause of these problems is that the backing gRPC-Core doesn't get the optimizations made by iOS' networking stack when these types of changes occur, and isn't able to handle them itself. +There is also documentation of this behavior in [this gRPC-Core readme](https://github.com/grpc/grpc/blob/v1.19.0/src/objective-c/NetworkTransitionBehavior.md). + To aid in this problem, there is a [`ClientNetworkMonitor`](./Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift) -that monitors the device for events that can cause gRPC to disconnect silently. We recommend utilizing this component to call `shutdown()` (or destroy) any active `Channel` instances, and start new ones when the network is reachable. +that monitors the device for events that can cause gRPC to disconnect silently. We recommend utilizing this component to +call `shutdown()` (or destroy) any active `Channel` instances, and start new ones when the network is reachable. -Setting the [`keepAliveTimeout` argument](https://github.com/grpc/grpc-swift/blob/c401b44ea81b246b8e7fea191ea1ee11a834ee60/Sources/SwiftGRPC/Core/ChannelArgument.swift#L46) +Setting the [`keepAliveTimeout` argument](https://github.com/grpc/grpc-swift/blob/0.7.0/Sources/SwiftGRPC/Core/ChannelArgument.swift#L46) on channels is also encouraged. Details: - **Switching between wifi <> cellular:** Channels silently disconnect -- **Network becoming unreachable:** Most times channels will time out after a few seconds, but - `ClientNetworkMonitor` will notify of these changes much faster +- **Switching between 3G <> LTE (etc.):** Channels silently disconnect +- **Network becoming unreachable:** Most times channels will time out after a few seconds, but `ClientNetworkMonitor` will notify of these changes much faster - **Switching between background <> foreground:** No known issues -Original issue: https://github.com/grpc/grpc-swift/issues/337. +Original SwiftGRPC issue: https://github.com/grpc/grpc-swift/issues/337. ## Having build problems? From cb1dd2b146c690cc7192566a3f6e2621cddf3423 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Fri, 1 Mar 2019 10:40:24 -0800 Subject: [PATCH 7/7] another doc --- Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift index 8059078d5..615494084 100644 --- a/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift +++ b/Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift @@ -22,7 +22,7 @@ import SystemConfiguration /// it seem like active calls/connections are hanging), then manually shut down / restart gRPC channels as /// needed. The root cause of these problems is that the backing gRPC-Core doesn't get the optimizations /// made by iOS' networking stack when changes occur on the device such as switching from wifi to cellular, -/// enabling/disabling airplane mode, etc. +/// switching between 3G and LTE, enabling/disabling airplane mode, etc. /// Read more: https://github.com/grpc/grpc-swift/tree/master/README.md#known-issues /// Original issue: https://github.com/grpc/grpc-swift/issues/337 open class ClientNetworkMonitor {