This commit is contained in:
Vladimir Dubovik
2025-06-13 11:59:16 +03:00
parent 2204bb9fe0
commit 5abafda21b
152 changed files with 9783 additions and 69 deletions

View File

@ -0,0 +1,4 @@
module CTrueTime [system] {
header "ntp_types.h"
export *
}

View 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 */

View 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

View 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
}
}
}

View 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

View 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>

View 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)")
}
}
}

View 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
}
}

View 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

View 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

View 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
}
}
}

View 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)
}
}

View 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
}

View 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
}
}

View 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

View 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";

View 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) }