Commit
This commit is contained in:
4
Carthage/Checkouts/TrueTime.swift/Sources/CTrueTime/module.modulemap
vendored
Normal file
4
Carthage/Checkouts/TrueTime.swift/Sources/CTrueTime/module.modulemap
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
module CTrueTime [system] {
|
||||
header "ntp_types.h"
|
||||
export *
|
||||
}
|
45
Carthage/Checkouts/TrueTime.swift/Sources/CTrueTime/ntp_types.h
vendored
Normal file
45
Carthage/Checkouts/TrueTime.swift/Sources/CTrueTime/ntp_types.h
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// ntp_types.h
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 7/11/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef NTP_TYPES_H
|
||||
#define NTP_TYPES_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
uint16_t whole;
|
||||
uint16_t fraction;
|
||||
} __attribute__((packed, aligned(1))) ntp_time32_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t whole;
|
||||
uint32_t fraction;
|
||||
} __attribute__((packed, aligned(1))) ntp_time64_t;
|
||||
|
||||
typedef ntp_time64_t ntp_time_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t client_mode: 3;
|
||||
uint8_t version_number: 3;
|
||||
uint8_t leap_indicator: 2;
|
||||
|
||||
uint8_t stratum;
|
||||
uint8_t poll;
|
||||
uint8_t precision;
|
||||
|
||||
ntp_time32_t root_delay;
|
||||
ntp_time32_t root_dispersion;
|
||||
uint8_t reference_id[4];
|
||||
|
||||
ntp_time_t reference_time;
|
||||
ntp_time_t originate_time;
|
||||
ntp_time_t receive_time;
|
||||
ntp_time_t transmit_time;
|
||||
} __attribute__((packed, aligned(1))) ntp_packet_t;
|
||||
|
||||
#endif /* NTP_TYPES_H */
|
89
Carthage/Checkouts/TrueTime.swift/Sources/Endian.swift
vendored
Normal file
89
Carthage/Checkouts/TrueTime.swift/Sources/Endian.swift
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
//
|
||||
// Endian.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 7/11/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol NetworkOrderConvertible {
|
||||
var byteSwapped: Self { get }
|
||||
}
|
||||
|
||||
extension NetworkOrderConvertible {
|
||||
var bigEndian: Self {
|
||||
return isLittleEndian ? byteSwapped : self
|
||||
}
|
||||
|
||||
var littleEndian: Self {
|
||||
return isLittleEndian ? self : byteSwapped
|
||||
}
|
||||
|
||||
/// Returns the native representation converted from big-endian, changing
|
||||
/// the byte order if necessary.
|
||||
var nativeEndian: Self {
|
||||
return isLittleEndian ? byteSwapped : self
|
||||
}
|
||||
}
|
||||
|
||||
extension Int: NetworkOrderConvertible {}
|
||||
extension ntp_time32_t: NetworkOrderConvertible {
|
||||
var byteSwapped: ntp_time32_t {
|
||||
return ntp_time32_t(whole: whole.byteSwapped, fraction: fraction.byteSwapped)
|
||||
}
|
||||
}
|
||||
|
||||
extension ntp_time64_t: NetworkOrderConvertible {
|
||||
var byteSwapped: ntp_time64_t {
|
||||
return ntp_time64_t(whole: whole.byteSwapped, fraction: fraction.byteSwapped)
|
||||
}
|
||||
}
|
||||
|
||||
extension ntp_packet_t: NetworkOrderConvertible {
|
||||
var byteSwapped: ntp_packet_t {
|
||||
return ntp_packet_t(client_mode: client_mode,
|
||||
version_number: version_number,
|
||||
leap_indicator: leap_indicator,
|
||||
stratum: stratum,
|
||||
poll: poll,
|
||||
precision: precision,
|
||||
root_delay: root_delay.byteSwapped,
|
||||
root_dispersion: root_dispersion.byteSwapped,
|
||||
reference_id: reference_id,
|
||||
reference_time: reference_time.byteSwapped,
|
||||
originate_time: originate_time.byteSwapped,
|
||||
receive_time: receive_time.byteSwapped,
|
||||
transmit_time: transmit_time.byteSwapped)
|
||||
}
|
||||
}
|
||||
|
||||
extension sockaddr_in6: NetworkOrderConvertible {
|
||||
var byteSwapped: sockaddr_in6 {
|
||||
return sockaddr_in6(sin6_len: sin6_len,
|
||||
sin6_family: sin6_family,
|
||||
sin6_port: sin6_port.byteSwapped,
|
||||
sin6_flowinfo: sin6_flowinfo.byteSwapped,
|
||||
sin6_addr: sin6_addr,
|
||||
sin6_scope_id: sin6_scope_id.byteSwapped)
|
||||
}
|
||||
}
|
||||
|
||||
extension sockaddr_in: NetworkOrderConvertible {
|
||||
var byteSwapped: sockaddr_in {
|
||||
return sockaddr_in(sin_len: sin_len,
|
||||
sin_family: sin_family,
|
||||
sin_port: sin_port.byteSwapped,
|
||||
sin_addr: in_addr(s_addr: sin_addr.s_addr.byteSwapped),
|
||||
sin_zero: sin_zero)
|
||||
}
|
||||
}
|
||||
|
||||
private enum ByteOrder {
|
||||
static let BigEndian = CFByteOrder(CFByteOrderBigEndian.rawValue)
|
||||
static let LittleEndian = CFByteOrder(CFByteOrderLittleEndian.rawValue)
|
||||
static let Unknown = CFByteOrder(CFByteOrderUnknown.rawValue)
|
||||
}
|
||||
|
||||
private let isLittleEndian = CFByteOrderGetCurrent() == ByteOrder.LittleEndian
|
31
Carthage/Checkouts/TrueTime.swift/Sources/GCDLock.swift
vendored
Normal file
31
Carthage/Checkouts/TrueTime.swift/Sources/GCDLock.swift
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
//
|
||||
// GCDLock.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 10/27/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class GCDLock<Value> {
|
||||
var value: Value
|
||||
let queue = DispatchQueue(label: "")
|
||||
init(value: Value) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
func read() -> Value {
|
||||
var value: Value?
|
||||
queue.sync {
|
||||
value = self.value
|
||||
}
|
||||
return value!
|
||||
}
|
||||
|
||||
func write(_ newValue: Value) {
|
||||
queue.async {
|
||||
self.value = newValue
|
||||
}
|
||||
}
|
||||
}
|
193
Carthage/Checkouts/TrueTime.swift/Sources/HostResolver.swift
vendored
Normal file
193
Carthage/Checkouts/TrueTime.swift/Sources/HostResolver.swift
vendored
Normal file
@ -0,0 +1,193 @@
|
||||
//
|
||||
// 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
|
28
Carthage/Checkouts/TrueTime.swift/Sources/Info.plist
vendored
Normal file
28
Carthage/Checkouts/TrueTime.swift/Sources/Info.plist
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016 Instacart. All rights reserved.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
263
Carthage/Checkouts/TrueTime.swift/Sources/NTPClient.swift
vendored
Normal file
263
Carthage/Checkouts/TrueTime.swift/Sources/NTPClient.swift
vendored
Normal file
@ -0,0 +1,263 @@
|
||||
//
|
||||
// NTPClient.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 10/12/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
struct NTPConfig {
|
||||
let timeout: TimeInterval
|
||||
let maxRetries: Int
|
||||
let maxConnections: Int
|
||||
let maxServers: Int
|
||||
let numberOfSamples: Int
|
||||
let pollInterval: TimeInterval
|
||||
}
|
||||
|
||||
final class NTPClient {
|
||||
let config: NTPConfig
|
||||
init(config: NTPConfig) {
|
||||
self.config = config
|
||||
}
|
||||
|
||||
func start(pool: [String], port: Int) {
|
||||
precondition(!pool.isEmpty, "Must include at least one pool URL")
|
||||
queue.async {
|
||||
precondition(self.reachability.callback == nil, "Already started")
|
||||
self.pool = pool
|
||||
self.port = port
|
||||
self.reachability.callbackQueue = self.queue
|
||||
self.reachability.callback = self.updateReachability
|
||||
self.reachability.startMonitoring()
|
||||
self.startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
queue.async {
|
||||
self.cancelTimer()
|
||||
self.reachability.stopMonitoring()
|
||||
self.reachability.callback = nil
|
||||
self.stopQueue()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchIfNeeded(queue callbackQueue: DispatchQueue,
|
||||
first: ReferenceTimeCallback?,
|
||||
completion: ReferenceTimeCallback?) {
|
||||
queue.async {
|
||||
precondition(self.reachability.callback != nil,
|
||||
"Must start client before retrieving time")
|
||||
if let time = self.referenceTime {
|
||||
callbackQueue.async { first?(.success(time)) }
|
||||
} else if let first = first {
|
||||
self.startCallbacks.append((callbackQueue, first))
|
||||
}
|
||||
|
||||
if let time = self.referenceTime, self.finished {
|
||||
callbackQueue.async { completion?(.success(time)) }
|
||||
} else {
|
||||
if let completion = completion {
|
||||
self.completionCallbacks.append((callbackQueue, completion))
|
||||
}
|
||||
self.updateReachability(status: self.reachability.status ?? .notReachable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let referenceTimeLock: GCDLock<ReferenceTime?> = GCDLock(value: nil)
|
||||
var referenceTime: ReferenceTime? {
|
||||
get { return referenceTimeLock.read() }
|
||||
set { referenceTimeLock.write(newValue) }
|
||||
}
|
||||
|
||||
fileprivate func debugLog(_ message: @autoclosure () -> String) {
|
||||
#if DEBUG_LOGGING
|
||||
logger?(message())
|
||||
#endif
|
||||
}
|
||||
|
||||
var logger: LogCallback? = defaultLogger
|
||||
private let queue = DispatchQueue(label: "com.instacart.ntp.client")
|
||||
private let reachability = Reachability()
|
||||
private var completionCallbacks: [(DispatchQueue, ReferenceTimeCallback)] = []
|
||||
private var connections: [NTPConnection] = []
|
||||
private var finished: Bool = false
|
||||
private var invalidated: Bool = false
|
||||
private var startCallbacks: [(DispatchQueue, ReferenceTimeCallback)] = []
|
||||
private var startTime: TimeInterval?
|
||||
private var timer: DispatchSourceTimer?
|
||||
private var port: Int = 123
|
||||
private var pool: [String] = [] {
|
||||
didSet { invalidate() }
|
||||
}
|
||||
}
|
||||
|
||||
private extension NTPClient {
|
||||
var started: Bool { return startTime != nil }
|
||||
func updateReachability(status: ReachabilityStatus) {
|
||||
switch status {
|
||||
case .notReachable:
|
||||
debugLog("Network unreachable")
|
||||
cancelTimer()
|
||||
finish(.failure(NSError(trueTimeError: .offline)))
|
||||
case .reachableViaWWAN, .reachableViaWiFi:
|
||||
debugLog("Network reachable")
|
||||
startTimer()
|
||||
startPool(pool: pool, port: port)
|
||||
}
|
||||
}
|
||||
|
||||
func startTimer() {
|
||||
cancelTimer()
|
||||
if let referenceTime = referenceTime {
|
||||
let remainingInterval = max(0, config.pollInterval - referenceTime.underlyingValue.uptimeInterval)
|
||||
timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
|
||||
timer?.setEventHandler(handler: invalidate)
|
||||
timer?.schedule(deadline: .now() + remainingInterval)
|
||||
timer?.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func cancelTimer() {
|
||||
timer?.cancel()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
func startPool(pool: [String], port: Int) {
|
||||
guard !started && !finished else {
|
||||
debugLog("Already \(started ? "started" : "finished")")
|
||||
return
|
||||
}
|
||||
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
debugLog("Resolving pool: \(pool)")
|
||||
HostResolver.resolve(hosts: pool.map { ($0, port) },
|
||||
timeout: config.timeout,
|
||||
logger: logger,
|
||||
callbackQueue: queue) { resolver, result in
|
||||
guard self.started && !self.finished else {
|
||||
self.debugLog("Got DNS response after queue stopped: \(resolver), \(result)")
|
||||
return
|
||||
}
|
||||
guard pool == self.pool, port == self.port else {
|
||||
self.debugLog("Got DNS response after pool URLs changed: \(resolver), \(result)")
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case let .success(addresses): self.query(addresses: addresses, host: resolver.host)
|
||||
case let .failure(error): self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopQueue() {
|
||||
debugLog("Stopping queue")
|
||||
startTime = nil
|
||||
connections.forEach { $0.close(waitUntilFinished: true) }
|
||||
connections = []
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
stopQueue()
|
||||
finished = false
|
||||
if let referenceTime = referenceTime,
|
||||
reachability.status != .notReachable && !pool.isEmpty {
|
||||
debugLog("Invalidated time \(referenceTime.debugDescription)")
|
||||
startPool(pool: pool, port: port)
|
||||
}
|
||||
}
|
||||
|
||||
func query(addresses: [SocketAddress], host: String) {
|
||||
var results: [String: [FrozenNetworkTimeResult]] = [:]
|
||||
connections = NTPConnection.query(addresses: addresses,
|
||||
config: config,
|
||||
logger: logger,
|
||||
callbackQueue: queue) { connection, result in
|
||||
guard self.started && !self.finished else {
|
||||
self.debugLog("Got NTP response after queue stopped: \(result)")
|
||||
return
|
||||
}
|
||||
|
||||
let host = connection.address.host
|
||||
results[host] = (results[host] ?? []) + [result]
|
||||
|
||||
let responses = Array(results.values)
|
||||
let sampleSize = responses.map { $0.count }.reduce(0, +)
|
||||
let expectedCount = addresses.count * self.config.numberOfSamples
|
||||
let atEnd = sampleSize == expectedCount
|
||||
let times = responses.map { results in
|
||||
results.compactMap { try? $0.get() }
|
||||
}
|
||||
|
||||
self.debugLog("Got \(sampleSize) out of \(expectedCount)")
|
||||
if let time = bestTime(fromResponses: times) {
|
||||
let time = FrozenNetworkTime(networkTime: time, sampleSize: sampleSize, host: host)
|
||||
self.debugLog("\(atEnd ? "Final" : "Best") time: \(time), " +
|
||||
"δ: \(time.serverResponse.delay), " +
|
||||
"θ: \(time.serverResponse.offset)")
|
||||
|
||||
let referenceTime = self.referenceTime ?? ReferenceTime(time)
|
||||
if self.referenceTime == nil {
|
||||
self.referenceTime = referenceTime
|
||||
} else {
|
||||
referenceTime.underlyingValue = time
|
||||
}
|
||||
|
||||
if atEnd {
|
||||
self.finish(.success(referenceTime))
|
||||
} else {
|
||||
self.updateProgress(.success(referenceTime))
|
||||
}
|
||||
} else if atEnd {
|
||||
let error: NSError
|
||||
if case let .failure(failure) = result {
|
||||
error = failure as NSError
|
||||
} else {
|
||||
error = NSError(trueTimeError: .noValidPacket)
|
||||
}
|
||||
|
||||
self.finish(ReferenceTimeResult.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateProgress(_ result: ReferenceTimeResult) {
|
||||
let endTime = CFAbsoluteTimeGetCurrent()
|
||||
let hasStartCallbacks = !startCallbacks.isEmpty
|
||||
startCallbacks.forEach { queue, callback in
|
||||
queue.async {
|
||||
callback(result)
|
||||
}
|
||||
}
|
||||
startCallbacks = []
|
||||
if hasStartCallbacks {
|
||||
logDuration(endTime, to: "get first result")
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(Notification(name: .TrueTimeUpdated, object: self, userInfo: nil))
|
||||
}
|
||||
|
||||
func finish(_ result: ReferenceTimeResult) {
|
||||
let endTime = CFAbsoluteTimeGetCurrent()
|
||||
updateProgress(result)
|
||||
completionCallbacks.forEach { queue, callback in
|
||||
queue.async {
|
||||
callback(result)
|
||||
}
|
||||
}
|
||||
completionCallbacks = []
|
||||
logDuration(endTime, to: "get last result")
|
||||
finished = (try? result.get()) != nil
|
||||
stopQueue()
|
||||
startTimer()
|
||||
}
|
||||
|
||||
func logDuration(_ endTime: CFAbsoluteTime, to description: String) {
|
||||
if let startTime = startTime {
|
||||
debugLog("Took \(endTime - startTime)s to \(description)")
|
||||
}
|
||||
}
|
||||
}
|
264
Carthage/Checkouts/TrueTime.swift/Sources/NTPConnection.swift
vendored
Normal file
264
Carthage/Checkouts/TrueTime.swift/Sources/NTPConnection.swift
vendored
Normal file
@ -0,0 +1,264 @@
|
||||
//
|
||||
// NTPConnection.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 8/10/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
typealias NTPConnectionCallback = (NTPConnection, FrozenNetworkTimeResult) -> Void
|
||||
|
||||
final class NTPConnection {
|
||||
let address: SocketAddress
|
||||
let timeout: TimeInterval
|
||||
let maxRetries: Int
|
||||
var logger: LogCallback?
|
||||
|
||||
static func query(addresses: [SocketAddress],
|
||||
config: NTPConfig,
|
||||
logger: LogCallback?,
|
||||
callbackQueue: DispatchQueue,
|
||||
progress: @escaping NTPConnectionCallback) -> [NTPConnection] {
|
||||
let connections = addresses.flatMap { address in
|
||||
(0..<config.numberOfSamples).map { _ in
|
||||
NTPConnection(address: address,
|
||||
timeout: config.timeout,
|
||||
maxRetries: config.maxRetries,
|
||||
logger: logger)
|
||||
}
|
||||
}
|
||||
|
||||
var throttleConnections: (() -> Void)?
|
||||
let onComplete: NTPConnectionCallback = { connection, result in
|
||||
progress(connection, result)
|
||||
throttleConnections?()
|
||||
}
|
||||
|
||||
throttleConnections = {
|
||||
let remainingConnections = connections.filter { $0.canRetry }
|
||||
let activeConnections = Array(remainingConnections[0..<min(config.maxConnections,
|
||||
remainingConnections.count)])
|
||||
activeConnections.forEach { $0.start(callbackQueue, onComplete: onComplete) }
|
||||
}
|
||||
throttleConnections?()
|
||||
return connections
|
||||
}
|
||||
|
||||
required init(address: SocketAddress,
|
||||
timeout: TimeInterval,
|
||||
maxRetries: Int,
|
||||
logger: LogCallback?) {
|
||||
self.address = address
|
||||
self.timeout = timeout
|
||||
self.maxRetries = maxRetries
|
||||
self.logger = logger
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(!self.started, "Unclosed connection")
|
||||
}
|
||||
|
||||
var canRetry: Bool {
|
||||
var canRetry: Bool = false
|
||||
lockQueue.sync {
|
||||
canRetry = self.attempts < self.maxRetries && !self.didTimeout && !self.finished
|
||||
}
|
||||
return canRetry
|
||||
}
|
||||
|
||||
func start(_ callbackQueue: DispatchQueue, onComplete: @escaping NTPConnectionCallback) {
|
||||
lockQueue.async {
|
||||
guard !self.started else { return }
|
||||
var ctx = CFSocketContext(
|
||||
version: 0,
|
||||
info: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
|
||||
retain: nil,
|
||||
release: nil,
|
||||
copyDescription: nil
|
||||
)
|
||||
|
||||
self.attempts += 1
|
||||
self.callbackQueue = callbackQueue
|
||||
self.onComplete = onComplete
|
||||
self.socket = CFSocketCreate(nil,
|
||||
self.address.family,
|
||||
SOCK_DGRAM,
|
||||
IPPROTO_UDP,
|
||||
NTPConnection.callbackFlags,
|
||||
self.dataCallback,
|
||||
&ctx)
|
||||
|
||||
if let socket = self.socket {
|
||||
CFSocketSetSocketFlags(socket, kCFSocketCloseOnInvalidate)
|
||||
self.source = CFSocketCreateRunLoopSource(nil, socket, 0)
|
||||
}
|
||||
|
||||
if let source = self.source {
|
||||
CFRunLoopAddSource(CFRunLoopGetMain(), source, CFRunLoopMode.commonModes)
|
||||
self.startTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func close(waitUntilFinished wait: Bool = false) {
|
||||
let work = {
|
||||
self.cancelTimer()
|
||||
guard let socket = self.socket, let source = self.source else { return }
|
||||
let disabledFlags = NTPConnection.callbackFlags |
|
||||
kCFSocketAutomaticallyReenableDataCallBack |
|
||||
kCFSocketAutomaticallyReenableReadCallBack |
|
||||
kCFSocketAutomaticallyReenableWriteCallBack |
|
||||
kCFSocketAutomaticallyReenableAcceptCallBack
|
||||
CFSocketDisableCallBacks(socket, disabledFlags)
|
||||
CFSocketInvalidate(socket)
|
||||
CFRunLoopRemoveSource(CFRunLoopGetMain(), source, CFRunLoopMode.commonModes)
|
||||
self.socket = nil
|
||||
self.source = nil
|
||||
self.debugLog("Connection closed \(self.address)")
|
||||
}
|
||||
|
||||
if wait {
|
||||
lockQueue.sync(execute: work)
|
||||
} else {
|
||||
lockQueue.async(execute: work)
|
||||
}
|
||||
}
|
||||
|
||||
func debugLog(_ message: @autoclosure () -> String) {
|
||||
#if DEBUG_LOGGING
|
||||
logger?(message())
|
||||
#endif
|
||||
}
|
||||
|
||||
private let dataCallback: CFSocketCallBack = { socket, type, address, data, info in
|
||||
guard let info = info else { return }
|
||||
let client = Unmanaged<NTPConnection>.fromOpaque(info).takeUnretainedValue()
|
||||
guard let socket = socket, CFSocketIsValid(socket) else { return }
|
||||
|
||||
// Can't use switch here as these aren't defined as an enum.
|
||||
if type == .dataCallBack {
|
||||
let data = unsafeBitCast(data, to: CFData.self) as Data
|
||||
client.handleResponse(data)
|
||||
} else if type == .writeCallBack {
|
||||
client.debugLog("Buffer \(client.address) writable - requesting time")
|
||||
client.requestTime()
|
||||
} else {
|
||||
assertionFailure("Unexpected socket callback")
|
||||
}
|
||||
}
|
||||
|
||||
var timer: DispatchSourceTimer?
|
||||
private static let callbackTypes: [CFSocketCallBackType] = [.dataCallBack, .writeCallBack]
|
||||
private static let callbackFlags: CFOptionFlags = callbackTypes.map {
|
||||
$0.rawValue
|
||||
}.reduce(0, |)
|
||||
private let lockQueue = DispatchQueue(label: "com.instacart.ntp.connection")
|
||||
private var attempts: Int = 0
|
||||
private var callbackQueue: DispatchQueue?
|
||||
private var didTimeout: Bool = false
|
||||
private var onComplete: NTPConnectionCallback?
|
||||
private var requestTicks: timeval?
|
||||
private var socket: CFSocket?
|
||||
private var source: CFRunLoopSource?
|
||||
private var startTime: ntp_time_t?
|
||||
private var finished: Bool = false
|
||||
}
|
||||
|
||||
extension NTPConnection: TimedOperation {
|
||||
var timerQueue: DispatchQueue { return lockQueue }
|
||||
var started: Bool { return self.socket != nil }
|
||||
|
||||
func timeoutError(_ error: NSError) {
|
||||
self.didTimeout = true
|
||||
complete(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
private extension NTPConnection {
|
||||
func complete(_ result: FrozenNetworkTimeResult) {
|
||||
guard let callbackQueue = callbackQueue, let onComplete = onComplete else {
|
||||
assertionFailure("Completion callback not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
close()
|
||||
switch result {
|
||||
case let .failure(error) where attempts < maxRetries && !didTimeout:
|
||||
debugLog("Got error from \(address) (attempt \(attempts)), " +
|
||||
"trying again. \(error)")
|
||||
start(callbackQueue, onComplete: onComplete)
|
||||
case .failure, .success:
|
||||
finished = true
|
||||
callbackQueue.async {
|
||||
onComplete(self, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requestTime() {
|
||||
lockQueue.async {
|
||||
guard let socket = self.socket else {
|
||||
self.debugLog("Socket closed")
|
||||
return
|
||||
}
|
||||
|
||||
self.startTime = ntp_time_t(timeSince1970: .now())
|
||||
self.requestTicks = .uptime()
|
||||
if let startTime = self.startTime {
|
||||
let packet = self.requestPacket(startTime).bigEndian
|
||||
let interval = TimeInterval(milliseconds: startTime.milliseconds)
|
||||
self.debugLog("Sending time: \(Date(timeIntervalSince1970: interval))")
|
||||
let err = CFSocketSendData(socket,
|
||||
self.address.networkData as CFData,
|
||||
packet.data as CFData,
|
||||
self.timeout)
|
||||
if err != .success {
|
||||
self.complete(.failure(NSError(errno: errno)))
|
||||
} else {
|
||||
self.startTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleResponse(_ data: Data) {
|
||||
let responseTicks = timeval.uptime()
|
||||
lockQueue.async {
|
||||
guard self.started else { return } // Socket closed.
|
||||
guard data.count == MemoryLayout<ntp_packet_t>.size else { return } // Invalid packet length.
|
||||
guard let startTime = self.startTime, let requestTicks = self.requestTicks else {
|
||||
assertionFailure("Uninitialized timestamps")
|
||||
return
|
||||
}
|
||||
|
||||
let packet = data.withUnsafeBytes { $0.load(as: ntp_packet_t.self) }.nativeEndian
|
||||
let responseTime = startTime.milliseconds + (responseTicks.milliseconds -
|
||||
requestTicks.milliseconds)
|
||||
|
||||
guard let response = NTPResponse(packet: packet, responseTime: responseTime) else {
|
||||
self.complete(.failure(NSError(trueTimeError: .badServerResponse)))
|
||||
return
|
||||
}
|
||||
|
||||
self.debugLog("Buffer \(self.address) has read data!")
|
||||
self.debugLog("Start time: \(startTime.milliseconds) ms, " +
|
||||
"response: \(packet.timeDescription)")
|
||||
self.debugLog("Clock offset: \(response.offset) milliseconds")
|
||||
self.debugLog("Round-trip delay: \(response.delay) milliseconds")
|
||||
self.complete(.success(FrozenNetworkTime(time: response.networkDate,
|
||||
uptime: responseTicks,
|
||||
serverResponse: response,
|
||||
startTime: startTime)))
|
||||
}
|
||||
}
|
||||
|
||||
func requestPacket(_ time: ntp_time_t) -> ntp_packet_t {
|
||||
var packet = ntp_packet_t()
|
||||
packet.client_mode = 3
|
||||
packet.version_number = 3
|
||||
packet.transmit_time = time
|
||||
return packet
|
||||
}
|
||||
}
|
240
Carthage/Checkouts/TrueTime.swift/Sources/NTPExtensions.swift
vendored
Normal file
240
Carthage/Checkouts/TrueTime.swift/Sources/NTPExtensions.swift
vendored
Normal file
@ -0,0 +1,240 @@
|
||||
//
|
||||
// NTPExtensions.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 7/10/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension timeval {
|
||||
static func uptime() -> timeval {
|
||||
let now = timeval.now()
|
||||
var boottime = timeval()
|
||||
var mib: [CInt] = [CTL_KERN, KERN_BOOTTIME]
|
||||
var size = MemoryLayout.stride(ofValue: boottime)
|
||||
withFatalErrno { sysctl(&mib, 2, &boottime, &size, nil, 0) }
|
||||
return timeval(tv_sec: now.tv_sec - boottime.tv_sec, tv_usec: now.tv_usec - boottime.tv_usec)
|
||||
}
|
||||
|
||||
var milliseconds: Int64 {
|
||||
return Int64(tv_sec) * Int64(MSEC_PER_SEC) + Int64(tv_usec) / Int64(USEC_PER_MSEC)
|
||||
}
|
||||
}
|
||||
|
||||
extension timeval {
|
||||
static func now() -> timeval {
|
||||
var tv = timeval()
|
||||
withFatalErrno { gettimeofday(&tv, nil) }
|
||||
return tv
|
||||
}
|
||||
}
|
||||
|
||||
// Represents an amount of time since the NTP epoch, January 1, 1900.
|
||||
// https://en.wikipedia.org/wiki/Network_Time_Protocol#Timestamps
|
||||
protocol NTPTimeType {
|
||||
associatedtype ValueType: UnsignedInteger
|
||||
init(whole: ValueType, fraction: ValueType)
|
||||
var whole: ValueType { get }
|
||||
var fraction: ValueType { get }
|
||||
}
|
||||
|
||||
protocol NTPTimevalConvertible: NTPTimeType {}
|
||||
|
||||
extension NTPTimeType {
|
||||
// Interprets the receiver as an elapsed time in milliseconds.
|
||||
var durationInMilliseconds: Int64 {
|
||||
return Int64(whole) * Int64(MSEC_PER_SEC) +
|
||||
fractionInMicroseconds / Int64(USEC_PER_MSEC)
|
||||
}
|
||||
|
||||
var fractionInMicroseconds: Int64 {
|
||||
return Int64(fraction) / Int64(1<<32 / USEC_PER_SEC)
|
||||
}
|
||||
}
|
||||
|
||||
extension NTPTimevalConvertible {
|
||||
init(timeSince1970 time: timeval) {
|
||||
precondition(time.tv_sec >= 0 && time.tv_usec >= 0, "Time must be positive \(time)")
|
||||
self.init(whole: ValueType(UInt64(time.tv_sec) + UInt64(secondsFrom1900To1970)),
|
||||
fraction: ValueType(UInt64(time.tv_usec) * UInt64(1<<32 / USEC_PER_SEC)))
|
||||
}
|
||||
|
||||
var milliseconds: Int64 {
|
||||
return (Int64(whole) - secondsFrom1900To1970) * Int64(MSEC_PER_SEC) +
|
||||
fractionInMicroseconds / Int64(USEC_PER_MSEC)
|
||||
}
|
||||
}
|
||||
|
||||
extension ntp_time32_t: NTPTimeType {}
|
||||
extension ntp_time64_t: NTPTimevalConvertible {}
|
||||
|
||||
extension TimeInterval {
|
||||
init(milliseconds: Int64) {
|
||||
self = Double(milliseconds) / Double(MSEC_PER_SEC)
|
||||
}
|
||||
|
||||
init(_ timestamp: timeval) {
|
||||
self = Double(timestamp.tv_sec) + Double(timestamp.tv_usec) / Double(USEC_PER_SEC)
|
||||
}
|
||||
}
|
||||
|
||||
protocol ByteRepresentable {
|
||||
init()
|
||||
}
|
||||
|
||||
extension ByteRepresentable {
|
||||
var data: Data {
|
||||
var buffer = self
|
||||
return Data(bytes: &buffer, count: MemoryLayout.size(ofValue: buffer))
|
||||
}
|
||||
}
|
||||
|
||||
extension ntp_packet_t: ByteRepresentable {}
|
||||
extension sockaddr_in: ByteRepresentable {}
|
||||
extension sockaddr_in6: ByteRepresentable {}
|
||||
extension sockaddr_in6: CustomStringConvertible {
|
||||
public var description: String {
|
||||
var buffer = [Int8](repeating: 0, count: Int(INET6_ADDRSTRLEN))
|
||||
var addr = sin6_addr
|
||||
inet_ntop(AF_INET6, &addr, &buffer, socklen_t(INET6_ADDRSTRLEN))
|
||||
|
||||
let host = String(cString: buffer)
|
||||
let port = Int(sin6_port)
|
||||
return "\(host):\(port)"
|
||||
}
|
||||
}
|
||||
|
||||
extension sockaddr_in: CustomStringConvertible {
|
||||
public var description: String {
|
||||
let host = String(cString: inet_ntoa(sin_addr))
|
||||
let port = Int(sin_port)
|
||||
return "\(host):\(port)"
|
||||
}
|
||||
}
|
||||
|
||||
extension HostResolver: CustomStringConvertible {
|
||||
var description: String {
|
||||
return "\(type(of: self))(host: \(host), port: \(port) timeout: \(timeout))"
|
||||
}
|
||||
}
|
||||
|
||||
extension NTPConnection: CustomStringConvertible {
|
||||
var description: String {
|
||||
return "\(type(of: self))(socketAddress: \(address), " +
|
||||
"timeout: \(timeout), " +
|
||||
"maxRetries: \(maxRetries))"
|
||||
}
|
||||
}
|
||||
|
||||
extension FrozenNetworkTime: CustomStringConvertible {
|
||||
var description: String {
|
||||
return "\(type(of: self))(time: \(time), " +
|
||||
"uptime: \(uptime.milliseconds) ms, " +
|
||||
"serverResponse: \(serverResponse), " +
|
||||
"startTime: \(startTime.milliseconds) ms, " +
|
||||
"sampleSize: \((sampleSize ?? 0)), " +
|
||||
"host: \(host ?? "nil"))"
|
||||
}
|
||||
}
|
||||
|
||||
extension NTPResponse: CustomStringConvertible {
|
||||
var description: String {
|
||||
return "\(type(of: self))(packet: \(packet.description), " +
|
||||
"responseTime: \(responseTime) ms, " +
|
||||
"receiveTime: \(receiveTime.milliseconds) ms)"
|
||||
}
|
||||
}
|
||||
|
||||
extension ntp_packet_t: CustomStringConvertible {
|
||||
public var description: String {
|
||||
let referenceTime = reference_time.milliseconds
|
||||
let originateTime = originate_time.milliseconds
|
||||
let receiveTime = receive_time.milliseconds
|
||||
let transmitTime = transmit_time.milliseconds
|
||||
return "\(type(of: self))(client_mode: \(client_mode.description), " +
|
||||
"version_number: \(version_number.description), " +
|
||||
"leap_indicator: \(leap_indicator.description), " +
|
||||
"stratum: \(stratum.description), " +
|
||||
"poll: \(poll.description), " +
|
||||
"precision: \(precision.description), " +
|
||||
"root_delay: \(root_delay), " +
|
||||
"root_dispersion: \(root_dispersion), " +
|
||||
"reference_id: \(reference_id), " +
|
||||
"reference_time: \(referenceTime) ms, " +
|
||||
"originate_time: \(originateTime) ms, " +
|
||||
"receive_time: \(receiveTime) ms, " +
|
||||
"transmit_time: \(transmitTime) ms)"
|
||||
}
|
||||
}
|
||||
|
||||
extension ntp_packet_t {
|
||||
var timeDescription: String {
|
||||
return "\(type(of: self))(reference_time: + \(reference_time.milliseconds) ms, " +
|
||||
"originate_time: \(originate_time.milliseconds) ms, " +
|
||||
"receive_time: \(receive_time.milliseconds) ms, " +
|
||||
"transmit_time: \(transmit_time.milliseconds) ms)"
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
var localized: String {
|
||||
return Bundle.main.localizedString(forKey: self, value: "", table: "TrueTime")
|
||||
}
|
||||
}
|
||||
|
||||
extension TrueTimeError: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .cannotFindHost: return "The connection failed because the host could not be found.".localized
|
||||
case .dnsLookupFailed: return "The connection failed because the DNS lookup failed.".localized
|
||||
case .timedOut: return "The connection timed out.".localized
|
||||
case .offline: return "The connection failed because the device is not connected to the internet.".localized
|
||||
case .badServerResponse: return "The connection received an invalid server response.".localized
|
||||
case .noValidPacket: return "No valid NTP packet was found.".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSError {
|
||||
convenience init(errno code: Int32) {
|
||||
var userInfo: [String: AnyObject]?
|
||||
if let description = String(validatingUTF8: strerror(code)) {
|
||||
userInfo = [NSLocalizedDescriptionKey: description as AnyObject]
|
||||
}
|
||||
self.init(domain: NSPOSIXErrorDomain, code: Int(code), userInfo: userInfo)
|
||||
}
|
||||
|
||||
convenience init(trueTimeError: TrueTimeError) {
|
||||
self.init(domain: TrueTimeErrorDomain, code: trueTimeError.rawValue, userInfo: [
|
||||
NSLocalizedDescriptionKey: trueTimeError.description
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func withErrno<X: SignedInteger>(_ block: () -> X) throws -> X {
|
||||
let result = block()
|
||||
if result < 0 {
|
||||
throw NSError(errno: errno)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Equivalent to `withErrno` but asserts at runtime.
|
||||
// Useful when `errno` can only be used to indicate programmer error.
|
||||
@discardableResult
|
||||
func withFatalErrno<X: SignedInteger>(_ block: () -> X) -> X {
|
||||
// swiftlint:disable force_try
|
||||
return try! withErrno(block)
|
||||
// swiftlint:enable force_try
|
||||
}
|
||||
|
||||
// Number of seconds between Jan 1, 1900 and Jan 1, 1970
|
||||
// 70 years plus 17 leap days
|
||||
private let secondsFrom1900To1970: Int64 = ((365 * 70) + 17) * 24 * 60 * 60
|
||||
|
||||
// swiftlint:disable identifier_name
|
||||
let MSEC_PER_SEC: UInt64 = 1000
|
||||
let USEC_PER_MSEC: UInt64 = 1000
|
||||
// swiftlint:enable identifier_name
|
68
Carthage/Checkouts/TrueTime.swift/Sources/NTPResponse.swift
vendored
Normal file
68
Carthage/Checkouts/TrueTime.swift/Sources/NTPResponse.swift
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
//
|
||||
// NTPResponse.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 10/14/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct NTPResponse {
|
||||
let packet: ntp_packet_t
|
||||
let responseTime: Int64
|
||||
let receiveTime: timeval
|
||||
init?(packet: ntp_packet_t, responseTime: Int64, receiveTime: timeval = .now()) {
|
||||
self.packet = packet
|
||||
self.responseTime = responseTime
|
||||
self.receiveTime = receiveTime
|
||||
guard isValidResponse else { return nil }
|
||||
}
|
||||
|
||||
// See https://en.wikipedia.org/wiki/Network_Time_Protocol#Clock_synchronization_algorithm
|
||||
var offset: Int64 {
|
||||
let T = offsetValues
|
||||
return ((T[1] - T[0]) + (T[2] - T[3])) / 2
|
||||
}
|
||||
|
||||
var delay: Int64 {
|
||||
let T = offsetValues
|
||||
return (T[3] - T[0]) - (T[2] - T[1])
|
||||
}
|
||||
|
||||
var networkDate: Date {
|
||||
let interval = TimeInterval(milliseconds: responseTime + offset)
|
||||
return Date(timeIntervalSince1970: interval)
|
||||
}
|
||||
}
|
||||
|
||||
func bestTime(fromResponses times: [[FrozenNetworkTime]]) -> FrozenNetworkTime? {
|
||||
let bestTimes = times.map { serverTimes -> FrozenNetworkTime? in
|
||||
serverTimes.min { $0.serverResponse.delay < $1.serverResponse.delay }
|
||||
}.compactMap { $0 }.sorted { $0.serverResponse.offset < $1.serverResponse.offset }
|
||||
|
||||
return bestTimes.isEmpty ? nil : bestTimes[bestTimes.count / 2]
|
||||
}
|
||||
|
||||
private extension NTPResponse {
|
||||
var isValidResponse: Bool {
|
||||
return packet.stratum > 0 && packet.stratum < 16 &&
|
||||
packet.root_delay.durationInMilliseconds < maxRootDispersion &&
|
||||
packet.root_dispersion.durationInMilliseconds < maxRootDispersion &&
|
||||
packet.client_mode == ntpModeServer &&
|
||||
packet.leap_indicator != leapIndicatorUnknown &&
|
||||
abs(receiveTime.milliseconds - packet.originate_time.milliseconds - delay) < maxDelayDelta
|
||||
}
|
||||
|
||||
var offsetValues: [Int64] {
|
||||
return [packet.originate_time.milliseconds,
|
||||
packet.receive_time.milliseconds,
|
||||
packet.transmit_time.milliseconds,
|
||||
responseTime]
|
||||
}
|
||||
}
|
||||
|
||||
private let maxRootDispersion: Int64 = 100
|
||||
private let maxDelayDelta: Int64 = 100
|
||||
private let ntpModeServer: UInt8 = 4
|
||||
private let leapIndicatorUnknown: UInt8 = 3
|
112
Carthage/Checkouts/TrueTime.swift/Sources/Reachability.swift
vendored
Normal file
112
Carthage/Checkouts/TrueTime.swift/Sources/Reachability.swift
vendored
Normal file
@ -0,0 +1,112 @@
|
||||
//
|
||||
// Reachability.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 7/21/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SystemConfiguration
|
||||
|
||||
enum ReachabilityStatus {
|
||||
case notReachable
|
||||
case reachableViaWWAN
|
||||
case reachableViaWiFi
|
||||
}
|
||||
|
||||
final class Reachability {
|
||||
var callback: ((ReachabilityStatus) -> Void)?
|
||||
var callbackQueue: DispatchQueue = .main
|
||||
var status: ReachabilityStatus? {
|
||||
if let networkReachability = self.networkReachability {
|
||||
var flags = SCNetworkReachabilityFlags()
|
||||
if SCNetworkReachabilityGetFlags(networkReachability, &flags) {
|
||||
return ReachabilityStatus(flags)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var online: Bool {
|
||||
return status != nil && status != .notReachable
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopMonitoring()
|
||||
}
|
||||
|
||||
func startMonitoring() {
|
||||
var address = sockaddr_in()
|
||||
address.sin_len = UInt8(MemoryLayout.size(ofValue: address))
|
||||
address.sin_family = sa_family_t(AF_INET)
|
||||
networkReachability = withUnsafePointer(to: &address) { pointer in
|
||||
pointer.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size) {
|
||||
SCNetworkReachabilityCreateWithAddress(nil, $0)
|
||||
}
|
||||
}
|
||||
|
||||
guard let networkReachability = networkReachability else {
|
||||
assertionFailure("SCNetworkReachabilityCreateWithAddress returned NULL")
|
||||
return
|
||||
}
|
||||
|
||||
var context = SCNetworkReachabilityContext(
|
||||
version: 0,
|
||||
info: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
|
||||
retain: nil,
|
||||
release: nil,
|
||||
copyDescription: nil
|
||||
)
|
||||
|
||||
SCNetworkReachabilitySetCallback(networkReachability, Reachability.reachabilityCallback, &context)
|
||||
SCNetworkReachabilitySetDispatchQueue(networkReachability, .global())
|
||||
|
||||
if let status = status {
|
||||
updateStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
func stopMonitoring() {
|
||||
if let networkReachability = networkReachability {
|
||||
SCNetworkReachabilitySetCallback(networkReachability, nil, nil)
|
||||
SCNetworkReachabilitySetDispatchQueue(networkReachability, nil)
|
||||
self.networkReachability = nil
|
||||
}
|
||||
}
|
||||
|
||||
private var networkReachability: SCNetworkReachability?
|
||||
private static let reachabilityCallback: SCNetworkReachabilityCallBack = { _, flags, info in
|
||||
guard let info = info else { return }
|
||||
let reachability = Unmanaged<Reachability>.fromOpaque(info).takeUnretainedValue()
|
||||
reachability.updateStatus(ReachabilityStatus(flags))
|
||||
}
|
||||
}
|
||||
|
||||
private extension Reachability {
|
||||
func updateStatus(_ status: ReachabilityStatus) {
|
||||
callbackQueue.async {
|
||||
self.callback?(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ReachabilityStatus {
|
||||
init(_ flags: SCNetworkReachabilityFlags) {
|
||||
let isReachable = flags.contains(.reachable)
|
||||
let needsConnection = flags.contains(.connectionRequired)
|
||||
let connectsAutomatically = flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic)
|
||||
let connectsWithoutInteraction = connectsAutomatically && !flags.contains(.interventionRequired)
|
||||
let isNetworkReachable = isReachable && (!needsConnection || connectsWithoutInteraction)
|
||||
if !isNetworkReachable {
|
||||
self = .notReachable
|
||||
} else {
|
||||
#if os(iOS)
|
||||
if flags.contains(.isWWAN) {
|
||||
self = .reachableViaWWAN
|
||||
return
|
||||
}
|
||||
#endif
|
||||
self = .reachableViaWiFi
|
||||
}
|
||||
}
|
||||
}
|
66
Carthage/Checkouts/TrueTime.swift/Sources/ReferenceTime.swift
vendored
Normal file
66
Carthage/Checkouts/TrueTime.swift/Sources/ReferenceTime.swift
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
//
|
||||
// FrozenReferenceTime.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 10/26/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
typealias FrozenTimeResult = Result<FrozenTime, NSError>
|
||||
typealias FrozenTimeCallback = (FrozenTimeResult) -> Void
|
||||
|
||||
typealias FrozenNetworkTimeResult = Result<FrozenNetworkTime, NSError>
|
||||
typealias FrozenNetworkTimeCallback = (FrozenNetworkTimeResult) -> Void
|
||||
|
||||
protocol FrozenTime {
|
||||
var time: Date { get }
|
||||
var uptime: timeval { get }
|
||||
}
|
||||
|
||||
struct FrozenReferenceTime: FrozenTime {
|
||||
let time: Date
|
||||
let uptime: timeval
|
||||
}
|
||||
|
||||
struct FrozenNetworkTime: FrozenTime {
|
||||
let time: Date
|
||||
let uptime: timeval
|
||||
let serverResponse: NTPResponse
|
||||
let startTime: ntp_time_t
|
||||
let sampleSize: Int?
|
||||
let host: String?
|
||||
|
||||
init(time: Date,
|
||||
uptime: timeval,
|
||||
serverResponse: NTPResponse,
|
||||
startTime: ntp_time_t,
|
||||
sampleSize: Int? = 0,
|
||||
host: String? = nil) {
|
||||
self.time = time
|
||||
self.uptime = uptime
|
||||
self.serverResponse = serverResponse
|
||||
self.startTime = startTime
|
||||
self.sampleSize = sampleSize
|
||||
self.host = host
|
||||
}
|
||||
|
||||
init(networkTime time: FrozenNetworkTime, sampleSize: Int, host: String) {
|
||||
self.init(time: time.time,
|
||||
uptime: time.uptime,
|
||||
serverResponse: time.serverResponse,
|
||||
startTime: time.startTime,
|
||||
sampleSize: sampleSize,
|
||||
host: host)
|
||||
}
|
||||
}
|
||||
|
||||
extension FrozenTime {
|
||||
var uptimeInterval: TimeInterval {
|
||||
let currentUptime = timeval.uptime()
|
||||
return TimeInterval(milliseconds: currentUptime.milliseconds - uptime.milliseconds)
|
||||
}
|
||||
|
||||
func now() -> Date {
|
||||
return time.addingTimeInterval(uptimeInterval)
|
||||
}
|
||||
}
|
69
Carthage/Checkouts/TrueTime.swift/Sources/SocketAddress.swift
vendored
Normal file
69
Carthage/Checkouts/TrueTime.swift/Sources/SocketAddress.swift
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
//
|
||||
// SocketAddress.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 9/14/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum SocketAddress {
|
||||
case iPv4(sockaddr_in)
|
||||
case iPv6(sockaddr_in6)
|
||||
|
||||
init?(storage: UnsafePointer<sockaddr_storage>, port: UInt16? = nil) {
|
||||
switch Int32(storage.pointee.ss_family) {
|
||||
case AF_INET:
|
||||
self = storage.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { pointer in
|
||||
var addr = pointer.pointee.nativeEndian
|
||||
addr.sin_port = port ?? addr.sin_port
|
||||
return .iPv4(addr)
|
||||
}
|
||||
case AF_INET6:
|
||||
self = storage.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { pointer in
|
||||
var addr = pointer.pointee.nativeEndian
|
||||
addr.sin6_port = port ?? addr.sin6_port
|
||||
return .iPv6(addr)
|
||||
}
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var family: Int32 {
|
||||
switch self {
|
||||
case .iPv4: return PF_INET
|
||||
case .iPv6: return PF_INET6
|
||||
}
|
||||
}
|
||||
|
||||
var networkData: Data {
|
||||
switch self {
|
||||
case .iPv4(let address): return address.bigEndian.data as Data
|
||||
case .iPv6(let address): return address.bigEndian.data as Data
|
||||
}
|
||||
}
|
||||
|
||||
var host: String {
|
||||
switch self {
|
||||
case .iPv4(let address): return address.description
|
||||
case .iPv6(let address): return address.description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SocketAddress: CustomStringConvertible {
|
||||
var description: String {
|
||||
return host
|
||||
}
|
||||
}
|
||||
|
||||
extension SocketAddress: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(host.hashValue)
|
||||
}
|
||||
}
|
||||
|
||||
func == (lhs: SocketAddress, rhs: SocketAddress) -> Bool {
|
||||
return lhs.host == rhs.host
|
||||
}
|
38
Carthage/Checkouts/TrueTime.swift/Sources/TimedOperation.swift
vendored
Normal file
38
Carthage/Checkouts/TrueTime.swift/Sources/TimedOperation.swift
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// TimedOperation.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 7/18/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol TimedOperation: class {
|
||||
var started: Bool { get }
|
||||
var timeout: TimeInterval { get }
|
||||
var timer: DispatchSourceTimer? { get set }
|
||||
var timerQueue: DispatchQueue { get }
|
||||
|
||||
func debugLog(_ message: @autoclosure () -> String)
|
||||
func timeoutError(_ error: NSError)
|
||||
}
|
||||
|
||||
extension TimedOperation {
|
||||
func startTimer() {
|
||||
cancelTimer()
|
||||
timer = DispatchSource.makeTimerSource(flags: [], queue: timerQueue)
|
||||
timer?.schedule(deadline: .now() + timeout)
|
||||
timer?.setEventHandler {
|
||||
guard self.started else { return }
|
||||
self.debugLog("Got timeout for \(self)")
|
||||
self.timeoutError(NSError(trueTimeError: .timedOut))
|
||||
}
|
||||
timer?.resume()
|
||||
}
|
||||
|
||||
func cancelTimer() {
|
||||
timer?.cancel()
|
||||
timer = nil
|
||||
}
|
||||
}
|
26
Carthage/Checkouts/TrueTime.swift/Sources/TrueTime.h
vendored
Normal file
26
Carthage/Checkouts/TrueTime.swift/Sources/TrueTime.h
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
//
|
||||
// TrueTime.h
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 7/9/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
#import "ntp_types.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
//! Project version number for TrueTime.
|
||||
FOUNDATION_EXPORT double TrueTimeVersionNumber;
|
||||
|
||||
//! Project version string for TrueTime.
|
||||
FOUNDATION_EXPORT const unsigned char TrueTimeVersionNumberString[];
|
||||
|
||||
//! Domain for TrueTime errors.
|
||||
FOUNDATION_EXPORT NSString * const TrueTimeErrorDomain;
|
||||
|
||||
//! Notification sent whenever a TrueTimeClient's reference time is updated.
|
||||
FOUNDATION_EXPORT NSString * const TrueTimeUpdatedNotification;
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
12
Carthage/Checkouts/TrueTime.swift/Sources/TrueTime.m
vendored
Normal file
12
Carthage/Checkouts/TrueTime.swift/Sources/TrueTime.m
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
//
|
||||
// TrueTime.m
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 8/15/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TrueTime.h"
|
||||
|
||||
NSString * const TrueTimeErrorDomain = @"com.instacart.TrueTimeErrorDomain";
|
||||
NSString * const TrueTimeUpdatedNotification = @"TrueTimeUpdatedNotification";
|
137
Carthage/Checkouts/TrueTime.swift/Sources/TrueTime.swift
vendored
Normal file
137
Carthage/Checkouts/TrueTime.swift/Sources/TrueTime.swift
vendored
Normal file
@ -0,0 +1,137 @@
|
||||
//
|
||||
// TrueTime.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 7/9/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc public enum TrueTimeError: Int {
|
||||
case cannotFindHost
|
||||
case dnsLookupFailed
|
||||
case timedOut
|
||||
case offline
|
||||
case badServerResponse
|
||||
case noValidPacket
|
||||
}
|
||||
|
||||
@objc(NTPReferenceTime)
|
||||
public final class ReferenceTime: NSObject {
|
||||
@objc public var uptimeInterval: TimeInterval { return underlyingValue.uptimeInterval }
|
||||
@objc public var time: Date { return underlyingValue.time }
|
||||
@objc public var uptime: timeval { return underlyingValue.uptime }
|
||||
@objc public func now() -> Date { return underlyingValue.now() }
|
||||
|
||||
public convenience init(time: Date, uptime: timeval) {
|
||||
self.init(FrozenReferenceTime(time: time, uptime: uptime))
|
||||
}
|
||||
|
||||
init(_ underlyingValue: FrozenTime) {
|
||||
self.underlyingValueLock = GCDLock(value: underlyingValue)
|
||||
}
|
||||
|
||||
public override var description: String {
|
||||
return "\(type(of: self))(underlyingValue: \(underlyingValue)"
|
||||
}
|
||||
|
||||
private let underlyingValueLock: GCDLock<FrozenTime>
|
||||
var underlyingValue: FrozenTime {
|
||||
get { return underlyingValueLock.read() }
|
||||
set { underlyingValueLock.write(newValue) }
|
||||
}
|
||||
}
|
||||
|
||||
public typealias ReferenceTimeResult = Result<ReferenceTime, NSError>
|
||||
public typealias ReferenceTimeCallback = (ReferenceTimeResult) -> Void
|
||||
public typealias LogCallback = (String) -> Void
|
||||
|
||||
@objc public final class TrueTimeClient: NSObject {
|
||||
@objc public static let sharedInstance = TrueTimeClient()
|
||||
@objc required public init(timeout: TimeInterval = 8,
|
||||
maxRetries: Int = 3,
|
||||
maxConnections: Int = 5,
|
||||
maxServers: Int = 5,
|
||||
numberOfSamples: Int = 4,
|
||||
pollInterval: TimeInterval = 512) {
|
||||
config = NTPConfig(timeout: timeout,
|
||||
maxRetries: maxRetries,
|
||||
maxConnections: maxConnections,
|
||||
maxServers: maxServers,
|
||||
numberOfSamples: numberOfSamples,
|
||||
pollInterval: pollInterval)
|
||||
ntp = NTPClient(config: config)
|
||||
}
|
||||
|
||||
@objc public func start(pool: [String] = ["time.apple.com"], port: Int = 123) {
|
||||
ntp.start(pool: pool, port: port)
|
||||
}
|
||||
|
||||
@objc public func pause() {
|
||||
ntp.pause()
|
||||
}
|
||||
|
||||
public func fetchIfNeeded(queue callbackQueue: DispatchQueue = .main,
|
||||
first: ReferenceTimeCallback? = nil,
|
||||
completion: ReferenceTimeCallback? = nil) {
|
||||
ntp.fetchIfNeeded(queue: callbackQueue, first: first, completion: completion)
|
||||
}
|
||||
|
||||
#if DEBUG_LOGGING
|
||||
@objc public var logCallback: LogCallback? = defaultLogger {
|
||||
didSet {
|
||||
ntp.logger = logCallback
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@objc public var referenceTime: ReferenceTime? { return ntp.referenceTime }
|
||||
@objc public var timeout: TimeInterval { return config.timeout }
|
||||
@objc public var maxRetries: Int { return config.maxRetries }
|
||||
@objc public var maxConnections: Int { return config.maxConnections }
|
||||
@objc public var maxServers: Int { return config.maxServers}
|
||||
@objc public var numberOfSamples: Int { return config.numberOfSamples}
|
||||
|
||||
private let config: NTPConfig
|
||||
private let ntp: NTPClient
|
||||
}
|
||||
|
||||
extension TrueTimeClient {
|
||||
@objc public func fetchFirstIfNeeded(success: @escaping (ReferenceTime) -> Void, failure: ((NSError) -> Void)?) {
|
||||
fetchFirstIfNeeded(success: success, failure: failure, onQueue: .main)
|
||||
}
|
||||
|
||||
@objc public func fetchIfNeeded(success: @escaping (ReferenceTime) -> Void, failure: ((NSError) -> Void)?) {
|
||||
fetchIfNeeded(success: success, failure: failure, onQueue: .main)
|
||||
}
|
||||
|
||||
@objc public func fetchFirstIfNeeded(success: @escaping (ReferenceTime) -> Void,
|
||||
failure: ((NSError) -> Void)?,
|
||||
onQueue queue: DispatchQueue) {
|
||||
fetchIfNeeded(queue: queue, first: { result in
|
||||
self.mapBridgedResult(result, success: success, failure: failure)
|
||||
})
|
||||
}
|
||||
|
||||
@objc public func fetchIfNeeded(success: @escaping (ReferenceTime) -> Void,
|
||||
failure: ((NSError) -> Void)?,
|
||||
onQueue queue: DispatchQueue) {
|
||||
fetchIfNeeded(queue: queue) { result in
|
||||
self.mapBridgedResult(result, success: success, failure: failure)
|
||||
}
|
||||
}
|
||||
|
||||
private func mapBridgedResult(_ result: ReferenceTimeResult,
|
||||
success: (ReferenceTime) -> Void,
|
||||
failure: ((NSError) -> Void)?) {
|
||||
switch result {
|
||||
case let .success(value):
|
||||
success(value)
|
||||
case let .failure(error):
|
||||
failure?(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let defaultLogger: LogCallback = { print($0) }
|
Reference in New Issue
Block a user