diff --git a/SYNQueue/SYNQueue.xcodeproj/project.pbxproj b/SYNQueue/SYNQueue.xcodeproj/project.pbxproj index 94153bc..e09bffb 100644 --- a/SYNQueue/SYNQueue.xcodeproj/project.pbxproj +++ b/SYNQueue/SYNQueue.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 9FD63F871B333B81001BD09A /* SYNQueueTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD63F861B333B81001BD09A /* SYNQueueTask.swift */; }; 9FD63FBF1B334316001BD09A /* NSDate+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD63FBE1B334316001BD09A /* NSDate+Utils.swift */; }; 9FD63FC11B335579001BD09A /* SYNQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD63FC01B335579001BD09A /* SYNQueue.swift */; }; + DC98BF381BBEE8E300CD6DFA /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC98BF371BBEE8E300CD6DFA /* Reachability.swift */; settings = {ASSET_TAGS = (); }; }; DCD110DA1BBA166B003AF0F0 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD110D81BBA166B003AF0F0 /* ConsoleLogger.swift */; settings = {ASSET_TAGS = (); }; }; DCD110DB1BBA166B003AF0F0 /* NSUserDefaultsSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD110D91BBA166B003AF0F0 /* NSUserDefaultsSerializer.swift */; settings = {ASSET_TAGS = (); }; }; DCD110E01BBA2F1E003AF0F0 /* SYNQueue.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F62022B1B333AAF0026CE2C /* SYNQueue.framework */; }; @@ -39,6 +40,7 @@ 9FD63F861B333B81001BD09A /* SYNQueueTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SYNQueueTask.swift; sourceTree = ""; }; 9FD63FBE1B334316001BD09A /* NSDate+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSDate+Utils.swift"; sourceTree = ""; }; 9FD63FC01B335579001BD09A /* SYNQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SYNQueue.swift; sourceTree = ""; }; + DC98BF371BBEE8E300CD6DFA /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; DCD110D81BBA166B003AF0F0 /* ConsoleLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = ""; }; DCD110D91BBA166B003AF0F0 /* NSUserDefaultsSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaultsSerializer.swift; sourceTree = ""; }; DCD110E11BBA2FB0003AF0F0 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; @@ -114,6 +116,7 @@ 9F62023B1B333AAF0026CE2C /* Supporting Files */ = { isa = PBXGroup; children = ( + DC98BF371BBEE8E300CD6DFA /* Reachability.swift */, DCD110D81BBA166B003AF0F0 /* ConsoleLogger.swift */, DCD110D91BBA166B003AF0F0 /* NSUserDefaultsSerializer.swift */, 9F62023C1B333AAF0026CE2C /* Info.plist */, @@ -244,6 +247,7 @@ DCD110E31BBA2FBC003AF0F0 /* Utils.swift in Sources */, DCD110DB1BBA166B003AF0F0 /* NSUserDefaultsSerializer.swift in Sources */, DCD110DA1BBA166B003AF0F0 /* ConsoleLogger.swift in Sources */, + DC98BF381BBEE8E300CD6DFA /* Reachability.swift in Sources */, 9F62023E1B333AAF0026CE2C /* SYNQueueTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SYNQueue/SYNQueueTests/Reachability.swift b/SYNQueue/SYNQueueTests/Reachability.swift new file mode 100644 index 0000000..85bbb60 --- /dev/null +++ b/SYNQueue/SYNQueueTests/Reachability.swift @@ -0,0 +1,377 @@ +/* +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 let ReachabilityChangedNotification = "ReachabilityChangedNotification" + +func callback(reachability:SCNetworkReachability, flags: SCNetworkReachabilityFlags, info: UnsafeMutablePointer) { + let reachability = Unmanaged.fromOpaque(COpaquePointer(info)).takeUnretainedValue() + + dispatch_async(dispatch_get_main_queue()) { + reachability.reachabilityChanged(flags) + } +} + + +public class Reachability: NSObject { + + public typealias NetworkReachable = (Reachability) -> () + public typealias NetworkUnreachable = (Reachability) -> () + + 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" + } + } + } + + // MARK: - *** Public properties *** + + public var whenReachable: NetworkReachable? + public var whenUnreachable: NetworkUnreachable? + public var reachableOnWWAN: Bool + public var notificationCenter = NSNotificationCenter.defaultCenter() + + public var currentReachabilityStatus: NetworkStatus { + if isReachable() { + if isReachableViaWiFi() { + return .ReachableViaWiFi + } + if isRunningOnDevice { + return .ReachableViaWWAN + } + } + + return .NotReachable + } + + public var currentReachabilityString: String { + return "\(currentReachabilityStatus)" + } + + // MARK: - *** Initialisation methods *** + + required public init?(reachabilityRef: SCNetworkReachability?) { + reachableOnWWAN = true + self.reachabilityRef = reachabilityRef + } + + public convenience init?(hostname: String) { + + let nodename = (hostname as NSString).UTF8String + let ref = SCNetworkReachabilityCreateWithName(nil, nodename) + self.init(reachabilityRef: ref) + } + + public class func reachabilityForInternetConnection() -> Reachability? { + + var zeroAddress = sockaddr_in() + zeroAddress.sin_len = UInt8(sizeofValue(zeroAddress)) + zeroAddress.sin_family = sa_family_t(AF_INET) + + let ref = withUnsafePointer(&zeroAddress) { + SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0)) + } + + return Reachability(reachabilityRef: ref) + } + + public class func reachabilityForLocalWiFi() -> Reachability? { + + var localWifiAddress: sockaddr_in = sockaddr_in(sin_len: __uint8_t(0), sin_family: sa_family_t(0), sin_port: in_port_t(0), sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) + localWifiAddress.sin_len = UInt8(sizeofValue(localWifiAddress)) + localWifiAddress.sin_family = sa_family_t(AF_INET) + + // IN_LINKLOCALNETNUM is defined in as 169.254.0.0 + let address: UInt32 = 0xA9FE0000 + localWifiAddress.sin_addr.s_addr = in_addr_t(address.bigEndian) + + let ref = withUnsafePointer(&localWifiAddress) { + SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0)) + } + return Reachability(reachabilityRef: ref) + } + + // MARK: - *** Notifier methods *** + public func startNotifier() -> Bool { + + if notifierRunning { return true } + + var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) + context.info = UnsafeMutablePointer(Unmanaged.passUnretained(self).toOpaque()) + + if SCNetworkReachabilitySetCallback(reachabilityRef!, callback, &context) { + if SCNetworkReachabilitySetDispatchQueue(reachabilityRef!, reachabilitySerialQueue) { + notifierRunning = true + return true + } + } + + stopNotifier() + + return false + } + + + public func stopNotifier() { + if let reachabilityRef = reachabilityRef { + SCNetworkReachabilitySetCallback(reachabilityRef, nil, nil) + } + notifierRunning = false + } + + // MARK: - *** Connection test methods *** + public func isReachable() -> Bool { + return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in + return self.isReachableWithFlags(flags) + }) + } + + public func isReachableViaWWAN() -> Bool { + + if isRunningOnDevice { + return isReachableWithTest() { flags -> Bool in + // Check we're REACHABLE + if self.isReachable(flags) { + + // Now, check we're on WWAN + if self.isOnWWAN(flags) { + return true + } + } + return false + } + } + return false + } + + public func isReachableViaWiFi() -> Bool { + + return isReachableWithTest() { flags -> Bool in + + // Check we're reachable + if self.isReachable(flags) { + + if self.isRunningOnDevice { + // Check we're NOT on WWAN + if self.isOnWWAN(flags) { + return false + } + } + return true + } + + return false + } + } + + // MARK: - *** Private methods *** + private var isRunningOnDevice: Bool = { + #if (arch(i386) || arch(x86_64)) && os(iOS) + return false + #else + return true + #endif + }() + + private var notifierRunning = false + private var reachabilityRef: SCNetworkReachability? + private let reachabilitySerialQueue = dispatch_queue_create("SYNQueueTests.reachability", DISPATCH_QUEUE_SERIAL) + + private func reachabilityChanged(flags: SCNetworkReachabilityFlags) { + if isReachableWithFlags(flags) { + if let block = whenReachable { + block(self) + } + } else { + if let block = whenUnreachable { + block(self) + } + } + + notificationCenter.postNotificationName(ReachabilityChangedNotification, object:self) + } + + private func isReachableWithFlags(flags: SCNetworkReachabilityFlags) -> Bool { + + let reachable = isReachable(flags) + + if !reachable { + return false + } + + if isConnectionRequiredOrTransient(flags) { + return false + } + + if isRunningOnDevice { + if isOnWWAN(flags) && !reachableOnWWAN { + // We don't want to connect when on 3G. + return false + } + } + + return true + } + + private func isReachableWithTest(test: (SCNetworkReachabilityFlags) -> (Bool)) -> Bool { + + if let reachabilityRef = reachabilityRef { + + var flags = SCNetworkReachabilityFlags(rawValue: 0) + let gotFlags = withUnsafeMutablePointer(&flags) { + SCNetworkReachabilityGetFlags(reachabilityRef, UnsafeMutablePointer($0)) + } + + if gotFlags { + return test(flags) + } + } + + return false + } + + // WWAN may be available, but not active until a connection has been established. + // WiFi may require a connection for VPN on Demand. + private func isConnectionRequired() -> Bool { + return connectionRequired() + } + + private func connectionRequired() -> Bool { + return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in + return self.isConnectionRequired(flags) + }) + } + + // Dynamic, on demand connection? + private func isConnectionOnDemand() -> Bool { + return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in + return self.isConnectionRequired(flags) && self.isConnectionOnTrafficOrDemand(flags) + }) + } + + // Is user intervention required? + private func isInterventionRequired() -> Bool { + return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in + return self.isConnectionRequired(flags) && self.isInterventionRequired(flags) + }) + } + + private func isOnWWAN(flags: SCNetworkReachabilityFlags) -> Bool { + #if os(iOS) + return flags.contains(.IsWWAN) + #else + return false + #endif + } + private func isReachable(flags: SCNetworkReachabilityFlags) -> Bool { + return flags.contains(.Reachable) + } + private func isConnectionRequired(flags: SCNetworkReachabilityFlags) -> Bool { + return flags.contains(.ConnectionRequired) + } + private func isInterventionRequired(flags: SCNetworkReachabilityFlags) -> Bool { + return flags.contains(.InterventionRequired) + } + private func isConnectionOnTraffic(flags: SCNetworkReachabilityFlags) -> Bool { + return flags.contains(.ConnectionOnTraffic) + } + private func isConnectionOnDemand(flags: SCNetworkReachabilityFlags) -> Bool { + return flags.contains(.ConnectionOnDemand) + } + func isConnectionOnTrafficOrDemand(flags: SCNetworkReachabilityFlags) -> Bool { + return !flags.intersect([.ConnectionOnTraffic, .ConnectionOnDemand]).isEmpty + } + private func isTransientConnection(flags: SCNetworkReachabilityFlags) -> Bool { + return flags.contains(.TransientConnection) + } + private func isLocalAddress(flags: SCNetworkReachabilityFlags) -> Bool { + return flags.contains(.IsLocalAddress) + } + private func isDirect(flags: SCNetworkReachabilityFlags) -> Bool { + return flags.contains(.IsDirect) + } + private func isConnectionRequiredOrTransient(flags: SCNetworkReachabilityFlags) -> Bool { + let testcase:SCNetworkReachabilityFlags = [.ConnectionRequired, .TransientConnection] + return flags.intersect(testcase) == testcase + } + + private var reachabilityFlags: SCNetworkReachabilityFlags { + if let reachabilityRef = reachabilityRef { + + var flags = SCNetworkReachabilityFlags(rawValue: 0) + let gotFlags = withUnsafeMutablePointer(&flags) { + SCNetworkReachabilityGetFlags(reachabilityRef, UnsafeMutablePointer($0)) + } + + if gotFlags { + return flags + } + } + + return [] + } + + override public var description: String { + + var W: String + if isRunningOnDevice { + W = isOnWWAN(reachabilityFlags) ? "W" : "-" + } else { + W = "X" + } + let R = isReachable(reachabilityFlags) ? "R" : "-" + let c = isConnectionRequired(reachabilityFlags) ? "c" : "-" + let t = isTransientConnection(reachabilityFlags) ? "t" : "-" + let i = isInterventionRequired(reachabilityFlags) ? "i" : "-" + let C = isConnectionOnTraffic(reachabilityFlags) ? "C" : "-" + let D = isConnectionOnDemand(reachabilityFlags) ? "D" : "-" + let l = isLocalAddress(reachabilityFlags) ? "l" : "-" + let d = isDirect(reachabilityFlags) ? "d" : "-" + + return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)" + } + + deinit { + stopNotifier() + + reachabilityRef = nil + whenReachable = nil + whenUnreachable = nil + } +} diff --git a/SYNQueue/SYNQueueTests/SYNQueueTests.swift b/SYNQueue/SYNQueueTests/SYNQueueTests.swift index 5baa310..287bb69 100644 --- a/SYNQueue/SYNQueueTests/SYNQueueTests.swift +++ b/SYNQueue/SYNQueueTests/SYNQueueTests.swift @@ -107,6 +107,32 @@ class SYNQueueTests: XCTestCase { queue.addOperation(task) } } + + func testInternetDependency() { + let name = randomQueueName() + + let queue = SYNQueue(queueName: name, maxConcurrency: 3, maxRetries: 2, logProvider: logger, serializationProvider: serializer) { (error: NSError?, task: SYNQueueTask) -> Void in + // + } + + queue.addTaskHandler(testTaskType) { + NSThread.sleepForTimeInterval(1) + $0.completed(nil) + } + + queue.addTaskHandler("internetDependencyType") { task in + let reachability = Reachability.reachabilityForInternetConnection() + reachability?.whenReachable = { reachability in + task.completed(nil) + } + } + + let task = SYNQueueTask(queue: queue, taskType: testTaskType) + let internetDependency = SYNQueueTask(queue: queue, type: "internetDependencyType", retries: 0) + task.addDependency(internetDependency) + queue.addOperation(task) + + } } // MARK: Helper methods