194 lines
6.7 KiB
Swift
194 lines
6.7 KiB
Swift
//
|
|
// HostResolver.swift
|
|
// TrueTime
|
|
//
|
|
// Created by Michael Sanders on 8/10/16.
|
|
// Copyright © 2016 Instacart. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
typealias HostResult = Result<[SocketAddress], NSError>
|
|
typealias HostCallback = (HostResolver, HostResult) -> Void
|
|
|
|
final class HostResolver {
|
|
let host: String
|
|
let port: Int
|
|
let timeout: TimeInterval
|
|
let onComplete: HostCallback
|
|
let callbackQueue: DispatchQueue
|
|
var logger: LogCallback?
|
|
|
|
/// Resolves the given hosts in order, returning the first resolved
|
|
/// addresses or an error if none succeeded.
|
|
///
|
|
/// - parameter pool: Pool to resolve
|
|
/// - parameter port: Port to use when resolving each pool
|
|
/// - parameter timeout: duration after which to time out DNS resolution
|
|
/// - parameter logger: logging callback for each host
|
|
/// - parameter callbackQueue: queue to fire `onComplete` callback
|
|
/// - parameter onComplete: invoked upon first successfully resolved host
|
|
/// or when all hosts fail
|
|
static func resolve(hosts: [(host: String, port: Int)],
|
|
timeout: TimeInterval,
|
|
logger: LogCallback?,
|
|
callbackQueue: DispatchQueue,
|
|
onComplete: @escaping HostCallback) {
|
|
precondition(!hosts.isEmpty, "Must include at least one URL")
|
|
let host = HostResolver(host: hosts[0].host,
|
|
port: hosts[0].port,
|
|
timeout: timeout,
|
|
logger: logger,
|
|
callbackQueue: callbackQueue) { host, result in
|
|
switch result {
|
|
case .success,
|
|
.failure where hosts.count == 1: onComplete(host, result)
|
|
case .failure:
|
|
resolve(hosts: Array(hosts.dropFirst()),
|
|
timeout: timeout,
|
|
logger: logger,
|
|
callbackQueue: callbackQueue,
|
|
onComplete: onComplete)
|
|
}
|
|
}
|
|
|
|
host.resolve()
|
|
}
|
|
|
|
required init(host: String,
|
|
port: Int,
|
|
timeout: TimeInterval,
|
|
logger: LogCallback?,
|
|
callbackQueue: DispatchQueue,
|
|
onComplete: @escaping HostCallback) {
|
|
self.host = host
|
|
self.port = port
|
|
self.timeout = timeout
|
|
self.logger = logger
|
|
self.onComplete = onComplete
|
|
self.callbackQueue = callbackQueue
|
|
}
|
|
|
|
deinit {
|
|
assert(!self.started, "Unclosed host")
|
|
}
|
|
|
|
func resolve() {
|
|
lockQueue.async {
|
|
guard self.networkHost == nil else { return }
|
|
self.resolved = false
|
|
self.networkHost = CFHostCreateWithName(nil, self.host as CFString).takeRetainedValue()
|
|
var ctx = CFHostClientContext(
|
|
version: 0,
|
|
info: UnsafeMutableRawPointer(Unmanaged.passRetained(self).toOpaque()),
|
|
retain: nil,
|
|
release: nil,
|
|
copyDescription: nil
|
|
)
|
|
self.callbackPending = true
|
|
|
|
if let networkHost = self.networkHost {
|
|
CFHostSetClient(networkHost, self.hostCallback, &ctx)
|
|
CFHostScheduleWithRunLoop(networkHost,
|
|
CFRunLoopGetMain(),
|
|
CFRunLoopMode.commonModes.rawValue)
|
|
|
|
var err: CFStreamError = CFStreamError()
|
|
if !CFHostStartInfoResolution(networkHost, .addresses, &err) {
|
|
self.complete(.failure(NSError(trueTimeError: .cannotFindHost)))
|
|
} else {
|
|
self.startTimer()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func stop(waitUntilFinished wait: Bool = false) {
|
|
let work = {
|
|
self.cancelTimer()
|
|
if let networkHost = self.networkHost {
|
|
CFHostCancelInfoResolution(networkHost, .addresses)
|
|
CFHostSetClient(networkHost, nil, nil)
|
|
CFHostUnscheduleFromRunLoop(networkHost, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue)
|
|
self.networkHost = nil
|
|
}
|
|
if self.callbackPending {
|
|
Unmanaged.passUnretained(self).release()
|
|
self.callbackPending = false
|
|
}
|
|
}
|
|
|
|
if wait {
|
|
lockQueue.sync(execute: work)
|
|
} else {
|
|
lockQueue.async(execute: work)
|
|
}
|
|
}
|
|
|
|
func debugLog(_ message: @autoclosure () -> String) {
|
|
#if DEBUG_LOGGING
|
|
logger?(message())
|
|
#endif
|
|
}
|
|
|
|
var timer: DispatchSourceTimer?
|
|
fileprivate let lockQueue = DispatchQueue(label: "com.instacart.dns.host")
|
|
fileprivate var networkHost: CFHost?
|
|
fileprivate var resolved: Bool = false
|
|
fileprivate var callbackPending: Bool = false
|
|
private let hostCallback: CFHostClientCallBack = { host, infoType, error, info in
|
|
guard let info = info else { return }
|
|
let retainedClient = Unmanaged<HostResolver>.fromOpaque(info)
|
|
let client = retainedClient.takeUnretainedValue()
|
|
client.callbackPending = false
|
|
client.connect(host)
|
|
retainedClient.release()
|
|
}
|
|
}
|
|
|
|
extension HostResolver: TimedOperation {
|
|
var timerQueue: DispatchQueue { return lockQueue }
|
|
var started: Bool { return self.networkHost != nil }
|
|
|
|
func timeoutError(_ error: NSError) {
|
|
complete(.failure(error))
|
|
}
|
|
}
|
|
|
|
private extension HostResolver {
|
|
func complete(_ result: HostResult) {
|
|
stop()
|
|
callbackQueue.async {
|
|
self.onComplete(self, result)
|
|
}
|
|
}
|
|
|
|
func connect(_ host: CFHost) {
|
|
debugLog("Got CFHostStartInfoResolution callback")
|
|
lockQueue.async {
|
|
guard self.started && !self.resolved else {
|
|
self.debugLog("Closed")
|
|
return
|
|
}
|
|
|
|
var resolved: DarwinBoolean = false
|
|
let addressData = CFHostGetAddressing(host, &resolved)?.takeUnretainedValue() as [AnyObject]?
|
|
guard let addresses = addressData as? [Data], resolved.boolValue else {
|
|
self.complete(.failure(NSError(trueTimeError: .dnsLookupFailed)))
|
|
return
|
|
}
|
|
|
|
let socketAddresses = addresses.map { data -> SocketAddress? in
|
|
let storage = (data as NSData).bytes.bindMemory(to: sockaddr_storage.self, capacity: data.count)
|
|
return SocketAddress(storage: storage, port: UInt16(self.port))
|
|
}.compactMap { $0 }
|
|
|
|
self.resolved = true
|
|
self.debugLog("Resolved hosts: \(socketAddresses)")
|
|
self.complete(.success(socketAddresses))
|
|
}
|
|
}
|
|
}
|
|
|
|
private let defaultNTPPort: Int = 123
|