mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2024-12-27 10:02:37 +01:00
407 lines
15 KiB
Swift
407 lines
15 KiB
Swift
/*
|
|
Copyright (c) 2014, Ashley Mills
|
|
All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
modification, are permitted provided that the following conditions are met:
|
|
|
|
1. Redistributions of source code must retain the above copyright notice, this
|
|
list of conditions and the following disclaimer.
|
|
|
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
this list of conditions and the following disclaimer in the documentation
|
|
and/or other materials provided with the distribution.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
import SystemConfiguration
|
|
import Foundation
|
|
|
|
public enum ReachabilityError: Error {
|
|
case failedToCreateWithAddress(sockaddr, Int32)
|
|
case failedToCreateWithHostname(String, Int32)
|
|
case unableToSetCallback(Int32)
|
|
case unableToSetDispatchQueue(Int32)
|
|
case unableToGetFlags(Int32)
|
|
}
|
|
|
|
@available(*, unavailable, renamed: "Notification.Name.reachabilityChanged")
|
|
public let ReachabilityChangedNotification = NSNotification.Name("ReachabilityChangedNotification")
|
|
|
|
public extension Notification.Name {
|
|
static let reachabilityChanged = Notification.Name("reachabilityChanged")
|
|
}
|
|
|
|
public class Reachability {
|
|
|
|
public typealias NetworkReachable = (Reachability) -> ()
|
|
public typealias NetworkUnreachable = (Reachability) -> ()
|
|
|
|
@available(*, unavailable, renamed: "Connection")
|
|
public enum NetworkStatus: CustomStringConvertible {
|
|
case notReachable, reachableViaWiFi, reachableViaWWAN
|
|
public var description: String {
|
|
switch self {
|
|
case .reachableViaWWAN: return "Cellular"
|
|
case .reachableViaWiFi: return "WiFi"
|
|
case .notReachable: return "No Connection"
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum Connection: CustomStringConvertible {
|
|
@available(*, deprecated, renamed: "unavailable")
|
|
case none
|
|
case unavailable, wifi, cellular
|
|
public var description: String {
|
|
switch self {
|
|
case .cellular: return "Cellular"
|
|
case .wifi: return "WiFi"
|
|
case .unavailable: return "No Connection"
|
|
case .none: return "unavailable"
|
|
}
|
|
}
|
|
}
|
|
|
|
public var whenReachable: NetworkReachable?
|
|
public var whenUnreachable: NetworkUnreachable?
|
|
|
|
@available(*, deprecated, renamed: "allowsCellularConnection")
|
|
public let reachableOnWWAN: Bool = true
|
|
|
|
/// Set to `false` to force Reachability.connection to .none when on cellular connection (default value `true`)
|
|
public var allowsCellularConnection: Bool
|
|
|
|
// The notification center on which "reachability changed" events are being posted
|
|
public var notificationCenter: NotificationCenter = NotificationCenter.default
|
|
|
|
@available(*, deprecated, renamed: "connection.description")
|
|
public var currentReachabilityString: String {
|
|
return "\(connection)"
|
|
}
|
|
|
|
@available(*, unavailable, renamed: "connection")
|
|
public var currentReachabilityStatus: Connection {
|
|
return connection
|
|
}
|
|
|
|
public var connection: Connection {
|
|
if flags == nil {
|
|
try? setReachabilityFlags()
|
|
}
|
|
|
|
switch flags?.connection {
|
|
case .unavailable?, nil: return .unavailable
|
|
case .none?: return .unavailable
|
|
case .cellular?: return allowsCellularConnection ? .cellular : .unavailable
|
|
case .wifi?: return .wifi
|
|
}
|
|
}
|
|
|
|
fileprivate var isRunningOnDevice: Bool = {
|
|
#if targetEnvironment(simulator)
|
|
return false
|
|
#else
|
|
return true
|
|
#endif
|
|
}()
|
|
|
|
fileprivate(set) var notifierRunning = false
|
|
fileprivate let reachabilityRef: SCNetworkReachability
|
|
fileprivate let reachabilitySerialQueue: DispatchQueue
|
|
fileprivate let notificationQueue: DispatchQueue?
|
|
fileprivate(set) var flags: SCNetworkReachabilityFlags? {
|
|
didSet {
|
|
guard flags != oldValue else { return }
|
|
notifyReachabilityChanged()
|
|
}
|
|
}
|
|
|
|
required public init(reachabilityRef: SCNetworkReachability,
|
|
queueQoS: DispatchQoS = .default,
|
|
targetQueue: DispatchQueue? = nil,
|
|
notificationQueue: DispatchQueue? = .main) {
|
|
self.allowsCellularConnection = true
|
|
self.reachabilityRef = reachabilityRef
|
|
self.reachabilitySerialQueue = DispatchQueue(label: "uk.co.ashleymills.reachability", qos: queueQoS, target: targetQueue)
|
|
self.notificationQueue = notificationQueue
|
|
}
|
|
|
|
public convenience init(hostname: String,
|
|
queueQoS: DispatchQoS = .default,
|
|
targetQueue: DispatchQueue? = nil,
|
|
notificationQueue: DispatchQueue? = .main) throws {
|
|
guard let ref = SCNetworkReachabilityCreateWithName(nil, hostname) else {
|
|
throw ReachabilityError.failedToCreateWithHostname(hostname, SCError())
|
|
}
|
|
self.init(reachabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue, notificationQueue: notificationQueue)
|
|
}
|
|
|
|
public convenience init(queueQoS: DispatchQoS = .default,
|
|
targetQueue: DispatchQueue? = nil,
|
|
notificationQueue: DispatchQueue? = .main) throws {
|
|
var zeroAddress = sockaddr()
|
|
zeroAddress.sa_len = UInt8(MemoryLayout<sockaddr>.size)
|
|
zeroAddress.sa_family = sa_family_t(AF_INET)
|
|
|
|
guard let ref = SCNetworkReachabilityCreateWithAddress(nil, &zeroAddress) else {
|
|
throw ReachabilityError.failedToCreateWithAddress(zeroAddress, SCError())
|
|
}
|
|
|
|
self.init(reachabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue, notificationQueue: notificationQueue)
|
|
}
|
|
|
|
deinit {
|
|
stopNotifier()
|
|
}
|
|
}
|
|
|
|
public extension Reachability {
|
|
|
|
// MARK: - *** Notifier methods ***
|
|
func startNotifier() throws {
|
|
guard !notifierRunning else { return }
|
|
|
|
let callback: SCNetworkReachabilityCallBack = { (reachability, flags, info) in
|
|
guard let info = info else { return }
|
|
|
|
// `weakifiedReachability` is guaranteed to exist by virtue of our
|
|
// retain/release callbacks which we provided to the `SCNetworkReachabilityContext`.
|
|
let weakifiedReachability = Unmanaged<ReachabilityWeakifier>.fromOpaque(info).takeUnretainedValue()
|
|
|
|
// The weak `reachability` _may_ no longer exist if the `Reachability`
|
|
// object has since been deallocated but a callback was already in flight.
|
|
weakifiedReachability.reachability?.flags = flags
|
|
}
|
|
|
|
let weakifiedReachability = ReachabilityWeakifier(reachability: self)
|
|
let opaqueWeakifiedReachability = Unmanaged<ReachabilityWeakifier>.passUnretained(weakifiedReachability).toOpaque()
|
|
|
|
var context = SCNetworkReachabilityContext(
|
|
version: 0,
|
|
info: UnsafeMutableRawPointer(opaqueWeakifiedReachability),
|
|
retain: { (info: UnsafeRawPointer) -> UnsafeRawPointer in
|
|
let unmanagedWeakifiedReachability = Unmanaged<ReachabilityWeakifier>.fromOpaque(info)
|
|
_ = unmanagedWeakifiedReachability.retain()
|
|
return UnsafeRawPointer(unmanagedWeakifiedReachability.toOpaque())
|
|
},
|
|
release: { (info: UnsafeRawPointer) -> Void in
|
|
let unmanagedWeakifiedReachability = Unmanaged<ReachabilityWeakifier>.fromOpaque(info)
|
|
unmanagedWeakifiedReachability.release()
|
|
},
|
|
copyDescription: { (info: UnsafeRawPointer) -> Unmanaged<CFString> in
|
|
let unmanagedWeakifiedReachability = Unmanaged<ReachabilityWeakifier>.fromOpaque(info)
|
|
let weakifiedReachability = unmanagedWeakifiedReachability.takeUnretainedValue()
|
|
let description = weakifiedReachability.reachability?.description ?? "nil"
|
|
return Unmanaged.passRetained(description as CFString)
|
|
}
|
|
)
|
|
|
|
if !SCNetworkReachabilitySetCallback(reachabilityRef, callback, &context) {
|
|
stopNotifier()
|
|
throw ReachabilityError.unableToSetCallback(SCError())
|
|
}
|
|
|
|
if !SCNetworkReachabilitySetDispatchQueue(reachabilityRef, reachabilitySerialQueue) {
|
|
stopNotifier()
|
|
throw ReachabilityError.unableToSetDispatchQueue(SCError())
|
|
}
|
|
|
|
// Perform an initial check
|
|
try setReachabilityFlags()
|
|
|
|
notifierRunning = true
|
|
}
|
|
|
|
func stopNotifier() {
|
|
defer { notifierRunning = false }
|
|
|
|
SCNetworkReachabilitySetCallback(reachabilityRef, nil, nil)
|
|
SCNetworkReachabilitySetDispatchQueue(reachabilityRef, nil)
|
|
}
|
|
|
|
// MARK: - *** Connection test methods ***
|
|
@available(*, deprecated, message: "Please use `connection != .none`")
|
|
var isReachable: Bool {
|
|
return connection != .unavailable
|
|
}
|
|
|
|
@available(*, deprecated, message: "Please use `connection == .cellular`")
|
|
var isReachableViaWWAN: Bool {
|
|
// Check we're not on the simulator, we're REACHABLE and check we're on WWAN
|
|
return connection == .cellular
|
|
}
|
|
|
|
@available(*, deprecated, message: "Please use `connection == .wifi`")
|
|
var isReachableViaWiFi: Bool {
|
|
return connection == .wifi
|
|
}
|
|
|
|
var description: String {
|
|
return flags?.description ?? "unavailable flags"
|
|
}
|
|
}
|
|
|
|
fileprivate extension Reachability {
|
|
|
|
func setReachabilityFlags() throws {
|
|
try reachabilitySerialQueue.sync { [unowned self] in
|
|
var flags = SCNetworkReachabilityFlags()
|
|
if !SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags) {
|
|
self.stopNotifier()
|
|
throw ReachabilityError.unableToGetFlags(SCError())
|
|
}
|
|
|
|
self.flags = flags
|
|
}
|
|
}
|
|
|
|
|
|
func notifyReachabilityChanged() {
|
|
let notify = { [weak self] in
|
|
guard let self = self else { return }
|
|
self.connection != .unavailable ? self.whenReachable?(self) : self.whenUnreachable?(self)
|
|
self.notificationCenter.post(name: .reachabilityChanged, object: self)
|
|
}
|
|
|
|
// notify on the configured `notificationQueue`, or the caller's (i.e. `reachabilitySerialQueue`)
|
|
notificationQueue?.async(execute: notify) ?? notify()
|
|
}
|
|
}
|
|
|
|
extension SCNetworkReachabilityFlags {
|
|
|
|
typealias Connection = Reachability.Connection
|
|
|
|
var connection: Connection {
|
|
guard isReachableFlagSet else { return .unavailable }
|
|
|
|
// If we're reachable, but not on an iOS device (i.e. simulator), we must be on WiFi
|
|
#if targetEnvironment(simulator)
|
|
return .wifi
|
|
#else
|
|
var connection = Connection.unavailable
|
|
|
|
if !isConnectionRequiredFlagSet {
|
|
connection = .wifi
|
|
}
|
|
|
|
if isConnectionOnTrafficOrDemandFlagSet {
|
|
if !isInterventionRequiredFlagSet {
|
|
connection = .wifi
|
|
}
|
|
}
|
|
|
|
if isOnWWANFlagSet {
|
|
connection = .cellular
|
|
}
|
|
|
|
return connection
|
|
#endif
|
|
}
|
|
|
|
var isOnWWANFlagSet: Bool {
|
|
#if os(iOS)
|
|
return contains(.isWWAN)
|
|
#else
|
|
return false
|
|
#endif
|
|
}
|
|
var isReachableFlagSet: Bool {
|
|
return contains(.reachable)
|
|
}
|
|
var isConnectionRequiredFlagSet: Bool {
|
|
return contains(.connectionRequired)
|
|
}
|
|
var isInterventionRequiredFlagSet: Bool {
|
|
return contains(.interventionRequired)
|
|
}
|
|
var isConnectionOnTrafficFlagSet: Bool {
|
|
return contains(.connectionOnTraffic)
|
|
}
|
|
var isConnectionOnDemandFlagSet: Bool {
|
|
return contains(.connectionOnDemand)
|
|
}
|
|
var isConnectionOnTrafficOrDemandFlagSet: Bool {
|
|
return !intersection([.connectionOnTraffic, .connectionOnDemand]).isEmpty
|
|
}
|
|
var isTransientConnectionFlagSet: Bool {
|
|
return contains(.transientConnection)
|
|
}
|
|
var isLocalAddressFlagSet: Bool {
|
|
return contains(.isLocalAddress)
|
|
}
|
|
var isDirectFlagSet: Bool {
|
|
return contains(.isDirect)
|
|
}
|
|
var isConnectionRequiredAndTransientFlagSet: Bool {
|
|
return intersection([.connectionRequired, .transientConnection]) == [.connectionRequired, .transientConnection]
|
|
}
|
|
|
|
var description: String {
|
|
let W = isOnWWANFlagSet ? "W" : "-"
|
|
let R = isReachableFlagSet ? "R" : "-"
|
|
let c = isConnectionRequiredFlagSet ? "c" : "-"
|
|
let t = isTransientConnectionFlagSet ? "t" : "-"
|
|
let i = isInterventionRequiredFlagSet ? "i" : "-"
|
|
let C = isConnectionOnTrafficFlagSet ? "C" : "-"
|
|
let D = isConnectionOnDemandFlagSet ? "D" : "-"
|
|
let l = isLocalAddressFlagSet ? "l" : "-"
|
|
let d = isDirectFlagSet ? "d" : "-"
|
|
|
|
return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)"
|
|
}
|
|
}
|
|
|
|
/**
|
|
`ReachabilityWeakifier` weakly wraps the `Reachability` class
|
|
in order to break retain cycles when interacting with CoreFoundation.
|
|
|
|
CoreFoundation callbacks expect a pair of retain/release whenever an
|
|
opaque `info` parameter is provided. These callbacks exist to guard
|
|
against memory management race conditions when invoking the callbacks.
|
|
|
|
#### Race Condition
|
|
|
|
If we passed `SCNetworkReachabilitySetCallback` a direct reference to our
|
|
`Reachability` class without also providing corresponding retain/release
|
|
callbacks, then a race condition can lead to crashes when:
|
|
- `Reachability` is deallocated on thread X
|
|
- A `SCNetworkReachability` callback(s) is already in flight on thread Y
|
|
|
|
#### Retain Cycle
|
|
|
|
If we pass `Reachability` to CoreFoundtion while also providing retain/
|
|
release callbacks, we would create a retain cycle once CoreFoundation
|
|
retains our `Reachability` class. This fixes the crashes and his how
|
|
CoreFoundation expects the API to be used, but doesn't play nicely with
|
|
Swift/ARC. This cycle would only be broken after manually calling
|
|
`stopNotifier()` — `deinit` would never be called.
|
|
|
|
#### ReachabilityWeakifier
|
|
|
|
By providing both retain/release callbacks and wrapping `Reachability` in
|
|
a weak wrapper, we:
|
|
- interact correctly with CoreFoundation, thereby avoiding a crash.
|
|
See "Memory Management Programming Guide for Core Foundation".
|
|
- don't alter the public API of `Reachability.swift` in any way
|
|
- still allow for automatic stopping of the notifier on `deinit`.
|
|
*/
|
|
private class ReachabilityWeakifier {
|
|
weak var reachability: Reachability?
|
|
init(reachability: Reachability) {
|
|
self.reachability = reachability
|
|
}
|
|
}
|