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,13 @@
### What did you change and why?
### Potential risks introduced?
### What tests were performed (include steps)?
### Checklist
- [ ] Unit/UI tests have been written (if necessary)
- [ ] Manually tested

View File

@ -0,0 +1,63 @@
# Xcode
## Build generated
build/
DerivedData/
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xcuserstate
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
Carthage/Checkouts
Carthage/Build
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
xcodebuild.log

View File

@ -0,0 +1,4 @@
[submodule "External/xcconfigs"]
path = External/xcconfigs
url = https://github.com/jspahrsummers/xcconfigs.git
branch = 0.9

View File

@ -0,0 +1 @@
5.0

View File

@ -0,0 +1,3 @@
disabled_rules:
- identifier_name
- switch_case_alignment

View File

@ -0,0 +1,19 @@
osx_image: xcode10.2
language: objective-c
env:
matrix:
- PLATFORM=iOS SDK=iphonesimulator SCHEME=TrueTime-iOS DESTINATION="platform=iOS Simulator,name=iPhone 6,OS=10.0"
- PLATFORM=Mac SDK=macosx SCHEME=TrueTime-Mac DESTINATION="platform=macOS"
- PLATFORM=tvOS SDK=appletvsimulator SCHEME=TrueTime-tvOS DESTINATION="platform=tvOS Simulator,name=Apple TV 1080p,OS=10.0"
install:
- brew remove swiftlint --force || true
- brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8d6cbc8/Formula/swiftlint.rb
- brew remove carthage --force || true
- brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/75d2a4a/Formula/carthage.rb
- gem install xcpretty
- carthage bootstrap --platform "$PLATFORM" --cache-builds --no-use-binaries --toolchain com.apple.dt.toolchain.Swift_3_1
script:
- Scripts/test
cache:
directories:
- Carthage/Build

View File

@ -0,0 +1,107 @@
# Changelog
All notable changes to this project will be documented in this file.
`TrueTime.swift` adheres to [Semantic Versioning](http://semver.org/).
## [5.1.0](https://github.com/instacart/TrueTime.swift/releases/tag/5.1.0)
- Changed: `CTrueTime` is now embeded in the project to avoid issues with Carthage.
## [5.0.3](https://github.com/instacart/TrueTime.swift/releases/tag/5.0.2)
- Fixed: Resolved race condition crash by removing unnecessary retain/release.
## [5.0.2](https://github.com/instacart/TrueTime.swift/releases/tag/5.0.2)
- Changed: Swift 5 support and use of built-in `Result` type.
## [5.0.1](https://github.com/instacart/TrueTime.swift/releases/tag/5.0.1)
- Fixed: `EXC_BAD_ACCESS` Crash.
## [5.0.0](https://github.com/instacart/TrueTime.swift/releases/tag/5.0.0)
- Added: Swift 4 support.
- Added: Exposed missing methods via Objective-C bridging.
- Fixed: Addressed issue with poll interval not being handled.
- Fixed: Addressed issue with resuming after pausing.
- Changed: Dropped support for Swift 3.
- Changed: Updated pool parameter in `TrueTime.start` to take an explicit
string an port instead of a URL.
## [4.1.5](https://github.com/instacart/TrueTime.swift/releases/tag/4.1.5)
- Fixed: Addressed issue with poll interval not being handled.
## [4.1.4](https://github.com/instacart/TrueTime.swift/releases/tag/4.1.4)
- Fixed: Addressed casting issue with latest Xcode.
## [4.1.3](https://github.com/instacart/TrueTime.swift/releases/tag/4.1.3)
- Fixed: Improved compile times when building from scratch.
- Fixed: Exposed uptime interval for reporting.
- Fixed: Updated dependencies to latest versions.
## [4.1.2](https://github.com/instacart/TrueTime.swift/releases/tag/4.1.2)
- Fixed: Addressed warning when building with Swift 3.1.
## [4.1.1](https://github.com/instacart/TrueTime.swift/releases/tag/4.1.1)
- Fixed: Addressed issue building project with latest swiftlint installed.
## [4.1.0](https://github.com/instacart/TrueTime.swift/releases/tag/4.1.0)
- Added: Now posting notification when reference time gets updated
- Fixed: Fixed crash when receiving empty packets from certain hosts.
## [4.0.0](https://github.com/instacart/TrueTime.swift/releases/tag/4.0.0)
- Added: Swift 3 support.
- Added: Support for configuring polling interval.
- Changed: `retrieveReferenceTime` has been renamed to `fetchIfNeeded`.
- Changed: Dropped support for Mac OS 10.9.
## [3.1.1](https://github.com/instacart/TrueTime.swift/releases/tag/3.1.1)
- Fixed: Addressed issue building project with latest swiftlint installed.
## [3.1.0](https://github.com/instacart/TrueTime.swift/releases/tag/3.1.0)
- Added: Now supporting CocoaPods.
## [3.0.0](https://github.com/instacart/TrueTime.swift/releases/tag/3.0.0)
- Added: Now polls at regular intervals and automatically updates reference
times.
- Fixed: Addressed assertion getting hit on certain devices when requesting
network time.
## [2.1.1](https://github.com/instacart/TrueTime.swift/releases/tag/2.1.1)
- Fixed: Addressed memory leak due to long interpolated strings in Swift 2.3.
- Fixed: Updated dispersion check and uptime function for more accurate times.
## [2.1.0](https://github.com/instacart/TrueTime.swift/releases/tag/2.1.0)
- Added: Now supporting full NTP integration.
- Fixed: Fixed rare crash when resolving hosts.
## [2.0.0](https://github.com/instacart/TrueTime.swift/releases/tag/2.0.0)
- Added: Now supporting Xcode 8 and Swift 2.3.
- Fixed: Fixed bundle identifier for tvOS framework.
## [1.1.0](https://github.com/instacart/TrueTime.swift/releases/tag/1.1.0)
- Fixed: Updated guard for outlier server responses to be more stringent.
- Added: IPv6 support.
## [1.0.1](https://github.com/instacart/TrueTime.swift/releases/tag/1.0.1)
- Fixed: Addresses issue when cloning submodules.
## [1.0.0](https://github.com/instacart/TrueTime.swift/releases/tag/1.0.0)
- Initial release

View File

@ -0,0 +1,77 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

View File

@ -0,0 +1,3 @@
github "Quick/Nimble"
github "Quick/Quick"
github "typelift/SwiftCheck"

View File

@ -0,0 +1,3 @@
github "Quick/Nimble" "v8.0.1"
github "Quick/Quick" "v2.1.0"
github "typelift/SwiftCheck" "0.12.0"

View File

@ -0,0 +1,26 @@
//
// AppDelegate.swift
// NTPExample
//
// Created by Michael Sanders on 7/9/16.
// Copyright © 2016 Instacart. All rights reserved.
//
import UIKit
import TrueTime
@UIApplicationMain
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
TrueTimeClient.sharedInstance.start()
window = UIWindow(frame: UIScreen.main.bounds)
window?.backgroundColor = .white
window?.makeKeyAndVisible()
window?.rootViewController = ExampleViewController()
return true
}
}

View File

@ -0,0 +1,93 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,29 @@
//
// Bridging.m
// TrueTime-iOS
//
// Created by Michael Sanders on 1/2/18.
// Copyright © 2018 Instacart. All rights reserved.
//
@import TrueTime;
@interface Bridging : NSObject
@end
@implementation Bridging
- (void)testBridging {
TrueTimeClient *client = [TrueTimeClient sharedInstance];
[client startWithPool:@[(id)[NSURL URLWithString:@"time.apple.com"]] port: 123];
NSDate *now = [[client referenceTime] now];
NSLog(@"True time: %@", now);
[client fetchIfNeededWithSuccess:^(NTPReferenceTime *referenceTime) {
NSLog(@"True time: %@", [referenceTime now]);
} failure:^(NSError *error) {
NSLog(@"Error! %@", error);
}];
}
@end

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,43 @@
<?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>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,98 @@
//
// ViewController.swift
// TrueTime
//
// Created by Michael Sanders on 10/26/16.
// Copyright © 2016 Instacart. All rights reserved.
//
import UIKit
import TrueTime
final class ExampleViewController: UIViewController {
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
NotificationCenter.default.addObserver(
self,
selector: #selector(startTimer),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(cancelTimer),
name: UIApplication.willResignActiveNotification,
object: nil
)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
label.frame = view.bounds.insetBy(dx: 15, dy: 15)
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(label)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
refresh()
startTimer()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
cancelTimer()
}
fileprivate var referenceTime: ReferenceTime?
fileprivate var timer: Timer?
fileprivate lazy var label: UILabel = {
let label = UILabel()
label.textColor = .black
label.textAlignment = .center
label.font = .systemFont(ofSize: 14)
label.numberOfLines = 0
return label
}()
}
private extension ExampleViewController {
@objc func startTimer() {
timer = .scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
self?.tick()
}
}
@objc func cancelTimer() {
timer?.invalidate()
timer = nil
}
func tick() {
if let referenceTime = referenceTime {
let trueTime = referenceTime.now()
label.text = "\(trueTime)\n\n\(referenceTime)"
}
}
func refresh() {
TrueTimeClient.sharedInstance.fetchIfNeeded { result in
switch result {
case let .success(referenceTime):
self.referenceTime = referenceTime
print("Got network time! \(referenceTime)")
case let .failure(error):
print("Error! \(error)")
}
}
}
}

View File

@ -0,0 +1 @@
Carthage/Build

View File

@ -0,0 +1,179 @@
//
// This file defines common settings that should be enabled for every new
// project. Typically, you want to use Debug, Release, or a similar variant
// instead.
//
// Disable legacy-compatible header searching
ALWAYS_SEARCH_USER_PATHS = NO
// Architectures to build
ARCHS = $(ARCHS_STANDARD)
// Whether to warn when a floating-point value is used as a loop counter
CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES
// Whether to warn about use of rand() and random() being used instead of arc4random()
CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES
// Whether to warn about strcpy() and strcat()
CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES
// Whether to enable module imports
CLANG_ENABLE_MODULES = YES
// Enable ARC
CLANG_ENABLE_OBJC_ARC = YES
// Warn about implicit conversions to boolean values that are suspicious.
// For example, writing 'if (foo)' with 'foo' being the name a function will trigger a warning.
CLANG_WARN_BOOL_CONVERSION = YES
// Warn about implicit conversions of constant values that cause the constant value to change,
// either through a loss of precision, or entirely in its meaning.
CLANG_WARN_CONSTANT_CONVERSION = YES
// Whether to warn when overriding deprecated methods
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES
// Warn about direct accesses to the Objective-C 'isa' pointer instead of using a runtime API.
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR
// Warn about declaring the same method more than once within the same @interface.
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
// Warn about loop bodies that are suspiciously empty.
CLANG_WARN_EMPTY_BODY = YES
// Warn about implicit conversions between different kinds of enum values.
// For example, this can catch issues when using the wrong enum flag as an argument to a function or method.
CLANG_WARN_ENUM_CONVERSION = YES
// Whether to warn on implicit conversions between signed/unsigned types
CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO
// Warn about implicit conversions between pointers and integers.
// For example, this can catch issues when one incorrectly intermixes using NSNumbers and raw integers.
CLANG_WARN_INT_CONVERSION = YES
// Don't warn about repeatedly using a weak reference without assigning the weak reference to a strong reference. Too many false positives.
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = NO
// Warn about classes that unintentionally do not subclass a root class (such as NSObject).
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR
// Whether to warn on suspicious implicit conversions
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES
// Warn about potentially unreachable code
CLANG_WARN_UNREACHABLE_CODE = YES
// The format of debugging symbols
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
// Whether to compile assertions in
ENABLE_NS_ASSERTIONS = YES
// Whether to require objc_msgSend to be cast before invocation
ENABLE_STRICT_OBJC_MSGSEND = YES
// Which C variant to use
GCC_C_LANGUAGE_STANDARD = gnu99
// Whether to enable exceptions for Objective-C
GCC_ENABLE_OBJC_EXCEPTIONS = YES
// Whether to generate debugging symbols
GCC_GENERATE_DEBUGGING_SYMBOLS = YES
// Whether to precompile the prefix header (if one is specified)
GCC_PRECOMPILE_PREFIX_HEADER = YES
// Whether to enable strict aliasing, meaning that two pointers of different
// types (other than void * or any id type) cannot point to the same memory
// location
GCC_STRICT_ALIASING = YES
// Whether symbols not explicitly exported are hidden by default (this primarily
// only affects C++ code)
GCC_SYMBOLS_PRIVATE_EXTERN = NO
// Whether static variables are thread-safe by default
GCC_THREADSAFE_STATICS = NO
// Which compiler to use
GCC_VERSION = com.apple.compilers.llvm.clang.1_0
// Whether warnings are treated as errors
GCC_TREAT_WARNINGS_AS_ERRORS = YES
// Whether to warn about 64-bit values being implicitly shortened to 32 bits
GCC_WARN_64_TO_32_BIT_CONVERSION = YES
// Whether to warn about fields missing from structure initializers (only if
// designated initializers aren't used)
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES
// Whether to warn about missing function prototypes
GCC_WARN_ABOUT_MISSING_PROTOTYPES = NO
// Whether to warn about implicit conversions in the signedness of the type
// a pointer is pointing to (e.g., 'int *' getting converted to 'unsigned int *')
GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES
// Whether to warn when the value returned from a function/method/block does not
// match its return type
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR
// Whether to warn on a class not implementing all the required methods of
// a protocol it declares conformance to
GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES
// Whether to warn when switching on an enum value, and all possibilities are
// not accounted for
GCC_WARN_CHECK_SWITCH_STATEMENTS = YES
// Whether to warn about the use of four-character constants
GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES
// Whether to warn about an aggregate data type's initializer not being fully
// bracketed (e.g., array initializer syntax)
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES
// Whether to warn about missing braces or parentheses that make the meaning of
// the code ambiguous
GCC_WARN_MISSING_PARENTHESES = YES
// Whether to warn about unsafe comparisons between values of different
// signedness
GCC_WARN_SIGN_COMPARE = YES
// Whether to warn about the arguments to printf-style functions not matching
// the format specifiers
GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES
// Warn if a "@selector(...)" expression referring to an undeclared selector is found
GCC_WARN_UNDECLARED_SELECTOR = YES
// Warn if a variable might be clobbered by a setjmp call or if an automatic variable is used without prior initialization.
GCC_WARN_UNINITIALIZED_AUTOS = YES
// Whether to warn about static functions that are unused
GCC_WARN_UNUSED_FUNCTION = YES
// Whether to warn about labels that are unused
GCC_WARN_UNUSED_LABEL = YES
// Whether to warn about variables that are never used
GCC_WARN_UNUSED_VARIABLE = YES
// Whether to run the static analyzer with every build
RUN_CLANG_STATIC_ANALYZER = YES
// Don't treat unknown warnings as errors, and disable GCC compatibility warnings and unused static const variable warnings
WARNING_CFLAGS = -Wno-error=unknown-warning-option -Wno-gcc-compat -Wno-unused-const-variable -Wno-nullability-completeness
// This setting is on for new projects as of Xcode ~6.3, though it is still not
// the default. It warns if the same variable is declared in two binaries that
// are linked together.
GCC_NO_COMMON_BLOCKS = YES

View File

@ -0,0 +1,43 @@
//
// This file defines the base configuration for a Debug build of any project.
// This should be set at the project level for the Debug configuration.
//
#include "../Common.xcconfig"
// Whether to strip debugging symbols when copying resources (like included
// binaries)
COPY_PHASE_STRIP = NO
// The optimization level (0, 1, 2, 3, s) for the produced binary
GCC_OPTIMIZATION_LEVEL = 0
// Preproccessor definitions to apply to each file compiled
GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1
// Whether to enable link-time optimizations (such as inlining across translation
// units)
LLVM_LTO = NO
// Whether to only build the active architecture
ONLY_ACTIVE_ARCH = YES
// Other compiler flags
//
// These settings catch some errors in integer arithmetic
OTHER_CFLAGS = -ftrapv
// Other flags to pass to the Swift compiler
//
// This enables conditional compilation with #if DEBUG
OTHER_SWIFT_FLAGS = -D DEBUG
// Whether to strip debugging symbols when copying the built product to its
// final installation location
STRIP_INSTALLED_PRODUCT = NO
// The optimization level (-Onone, -O, -Ofast) for the produced Swift binary
SWIFT_OPTIMIZATION_LEVEL = -Onone
// Disable Developer ID timestamping
OTHER_CODE_SIGN_FLAGS = --timestamp=none

View File

@ -0,0 +1,27 @@
//
// This file defines the base configuration for an optional profiling-specific
// build of any project. To use these settings, create a Profile configuration
// in your project, and use this file at the project level for the new
// configuration.
//
// based on the Release configuration, with some stuff related to debugging
// symbols re-enabled
#include "Release.xcconfig"
// Whether to strip debugging symbols when copying resources (like included
// binaries)
COPY_PHASE_STRIP = NO
// Whether to only build the active architecture
ONLY_ACTIVE_ARCH = YES
// Whether to strip debugging symbols when copying the built product to its
// final installation location
STRIP_INSTALLED_PRODUCT = NO
// Whether to perform App Store validation checks
VALIDATE_PRODUCT = NO
// Disable Developer ID timestamping
OTHER_CODE_SIGN_FLAGS = --timestamp=none

View File

@ -0,0 +1,34 @@
//
// This file defines the base configuration for a Release build of any project.
// This should be set at the project level for the Release configuration.
//
#include "../Common.xcconfig"
// Whether to strip debugging symbols when copying resources (like included
// binaries)
COPY_PHASE_STRIP = YES
// The optimization level (0, 1, 2, 3, s) for the produced binary
GCC_OPTIMIZATION_LEVEL = s
// Preproccessor definitions to apply to each file compiled
GCC_PREPROCESSOR_DEFINITIONS = NDEBUG=1
// Whether to enable link-time optimizations (such as inlining across translation
// units)
LLVM_LTO = NO
// Whether to only build the active architecture
ONLY_ACTIVE_ARCH = NO
// Whether to strip debugging symbols when copying the built product to its
// final installation location
STRIP_INSTALLED_PRODUCT = YES
// The optimization level (-Onone, -O, -Owholemodule) for the produced Swift binary
SWIFT_OPTIMIZATION_LEVEL = -Owholemodule
// Whether to perform App Store validation checks
VALIDATE_PRODUCT = YES

View File

@ -0,0 +1,10 @@
//
// This file defines the base configuration for a Test build of any project.
// This should be set at the project level for the Test configuration.
//
#include "Debug.xcconfig"
// Sandboxed apps can't be unit tested since they can't load some random
// external bundle. So we disable sandboxing for testing.
CODE_SIGN_ENTITLEMENTS =

View File

@ -0,0 +1,12 @@
//
// This file defines additional configuration options that are appropriate only
// for an application. Typically, you want to use a platform-specific variant
// instead.
//
// Whether to strip out code that isn't called from anywhere
DEAD_CODE_STRIPPING = NO
// Sets the @rpath for the application such that it can include frameworks in
// the application bundle (inside the "Frameworks" folder)
LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @loader_path/../Frameworks @executable_path/Frameworks

View File

@ -0,0 +1,32 @@
//
// This file defines additional configuration options that are appropriate only
// for a framework. Typically, you want to use a platform-specific variant
// instead.
//
// Whether to strip out code that isn't called from anywhere
DEAD_CODE_STRIPPING = NO
// Whether this framework should define an LLVM module
DEFINES_MODULE = YES
// Whether function calls should be position-dependent (should always be
// disabled for library code)
GCC_DYNAMIC_NO_PIC = NO
// Default frameworks to the name of the project, instead of any
// platform-specific target
PRODUCT_NAME = $(PROJECT_NAME)
// Enables the framework to be included from any location as long as the
// loaders runpath search paths includes it. For example from an application
// bundle (inside the "Frameworks" folder) or shared folder
INSTALL_PATH = @rpath
LD_DYLIB_INSTALL_NAME = @rpath/$(PRODUCT_NAME).$(WRAPPER_EXTENSION)/$(PRODUCT_NAME)
SKIP_INSTALL = YES
// Disallows use of APIs that are not available
// to app extensions and linking to frameworks
// that have not been built with this setting enabled.
APPLICATION_EXTENSION_API_ONLY = YES

View File

@ -0,0 +1,32 @@
//
// This file defines additional configuration options that are appropriate only
// for a static library. Typically, you want to use a platform-specific variant
// instead.
//
// Whether to strip out code that isn't called from anywhere
DEAD_CODE_STRIPPING = NO
// Whether to strip debugging symbols when copying resources (like included
// binaries).
//
// Overrides Release.xcconfig when used at the target level.
COPY_PHASE_STRIP = NO
// Whether function calls should be position-dependent (should always be
// disabled for library code)
GCC_DYNAMIC_NO_PIC = NO
// Copy headers to "include/LibraryName" in the build folder by default. This
// lets consumers use #import <LibraryName/LibraryName.h> syntax even for static
// libraries
PUBLIC_HEADERS_FOLDER_PATH = include/$PRODUCT_NAME
// Don't include in an xcarchive
SKIP_INSTALL = YES
// Disallows use of APIs that are not available
// to app extensions and linking to frameworks
// that have not been built with this setting enabled.
APPLICATION_EXTENSION_API_ONLY = YES

View File

@ -0,0 +1,15 @@
//
// This file defines additional configuration options that are appropriate only
// for an application on Mac OS X. This should be set at the target level for
// each project configuration.
//
// Import base application settings
#include "../Base/Targets/Application.xcconfig"
// Apply common settings specific to Mac OS X
#include "Mac-Base.xcconfig"
// Whether function calls should be position-dependent (should always be
// disabled for library code)
GCC_DYNAMIC_NO_PIC = YES

View File

@ -0,0 +1,19 @@
//
// This file defines additional configuration options that are appropriate only
// for Mac OS X. This file is not standalone -- it is meant to be included into
// a configuration file for a specific type of target.
//
// Whether to combine multiple image resolutions into a multirepresentational
// TIFF
COMBINE_HIDPI_IMAGES = YES
// Where to find embedded frameworks
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/../Frameworks
// The base SDK to use (if no version is specified, the latest version is
// assumed)
SDKROOT = macosx
// Supported build architectures
VALID_ARCHS = x86_64

View File

@ -0,0 +1,18 @@
//
// This file defines additional configuration options that are appropriate only
// for a dynamic library on Mac OS X. This should be set at the target level
// for each project configuration.
//
// Import common settings specific to Mac OS X
#include "Mac-Base.xcconfig"
// Whether to strip out code that isn't called from anywhere
DEAD_CODE_STRIPPING = NO
// Whether function calls should be position-dependent (should always be
// disabled for library code)
GCC_DYNAMIC_NO_PIC = NO
// Don't include in an xcarchive
SKIP_INSTALL = YES

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for a framework on OS X. This should be set at the target level for each
// project configuration.
//
// Import base framework settings
#include "../Base/Targets/Framework.xcconfig"
// Import common settings specific to Mac OS X
#include "Mac-Base.xcconfig"

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for a static library on Mac OS X. This should be set at the target level for
// each project configuration.
//
// Import base static library settings
#include "../Base/Targets/StaticLibrary.xcconfig"
// Apply common settings specific to Mac OS X
#include "Mac-Base.xcconfig"

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for an application on iOS. This should be set at the target level for each
// project configuration.
//
// Import base application settings
#include "../Base/Targets/Application.xcconfig"
// Apply common settings specific to iOS
#include "iOS-Base.xcconfig"

View File

@ -0,0 +1,18 @@
//
// This file defines additional configuration options that are appropriate only
// for iOS. This file is not standalone -- it is meant to be included into
// a configuration file for a specific type of target.
//
// Xcode needs this to find archived headers if SKIP_INSTALL is set
HEADER_SEARCH_PATHS = $(OBJROOT)/UninstalledProducts/include
// Where to find embedded frameworks
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks
// The base SDK to use (if no version is specified, the latest version is
// assumed)
SDKROOT = iphoneos
// Supported device families (1 is iPhone, 2 is iPad)
TARGETED_DEVICE_FAMILY = 1,2

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for a framework on iOS. This should be set at the target level for each
// project configuration.
//
// Import base framework settings
#include "../Base/Targets/Framework.xcconfig"
// Import common settings specific to iOS
#include "iOS-Base.xcconfig"

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for a static library on iOS. This should be set at the target level for each
// project configuration.
//
// Import base static library settings
#include "../Base/Targets/StaticLibrary.xcconfig"
// Apply common settings specific to iOS
#include "iOS-Base.xcconfig"

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for an application on watchOS. This should be set at the target level for
// each project configuration.
//
// Import base application settings
#include "../Base/Targets/Application.xcconfig"
// Apply common settings specific to watchOS
#include "tvOS-Base.xcconfig"

View File

@ -0,0 +1,15 @@
//
// This file defines additional configuration options that are appropriate only
// for watchOS. This file is not standalone -- it is meant to be included into
// a configuration file for a specific type of target.
//
// Where to find embedded frameworks
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks
// The base SDK to use (if no version is specified, the latest version is
// assumed)
SDKROOT = appletvos
// Supported device families
TARGETED_DEVICE_FAMILY = 3

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for a framework on watchOS. This should be set at the target level for each
// project configuration.
//
// Import base framework settings
#include "../Base/Targets/Framework.xcconfig"
// Import common settings specific to iOS
#include "tvOS-Base.xcconfig"

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for a static library on watchOS. This should be set at the target level for
// each project configuration.
//
// Import base static library settings
#include "../Base/Targets/StaticLibrary.xcconfig"
// Apply common settings specific to watchOS
#include "tvOS-Base.xcconfig"

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for an application on watchOS. This should be set at the target level for
// each project configuration.
//
// Import base application settings
#include "../Base/Targets/Application.xcconfig"
// Apply common settings specific to watchOS
#include "watchOS-Base.xcconfig"

View File

@ -0,0 +1,15 @@
//
// This file defines additional configuration options that are appropriate only
// for watchOS. This file is not standalone -- it is meant to be included into
// a configuration file for a specific type of target.
//
// Where to find embedded frameworks
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks
// The base SDK to use (if no version is specified, the latest version is
// assumed)
SDKROOT = watchos
// Supported device families
TARGETED_DEVICE_FAMILY = 4

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for a framework on watchOS. This should be set at the target level for each
// project configuration.
//
// Import base framework settings
#include "../Base/Targets/Framework.xcconfig"
// Import common settings specific to iOS
#include "watchOS-Base.xcconfig"

View File

@ -0,0 +1,11 @@
//
// This file defines additional configuration options that are appropriate only
// for a static library on watchOS. This should be set at the target level for
// each project configuration.
//
// Import base static library settings
#include "../Base/Targets/StaticLibrary.xcconfig"
// Apply common settings specific to watchOS
#include "watchOS-Base.xcconfig"

View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -o nounset -o errexit -o pipefail
BUILD_LOG="xcodebuild.log"
xcodebuild clean test -sdk "$SDK" -scheme "$SCHEME" -destination "$DESTINATION" | tee "$BUILD_LOG" | xcpretty
STATUS="${PIPESTATUS[0]}"
if [ -f "$BUILD_LOG" ]; then curl -sT "$BUILD_LOG" chunk.io; fi
exit "$STATUS"

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

View File

@ -0,0 +1,22 @@
//
// ArbitraryExtensions.swift
// TrueTime
//
// Created by Michael Sanders on 7/19/16.
// Copyright © 2016 Instacart. All rights reserved.
//
@testable import TrueTime
import SwiftCheck
extension timeval: Arbitrary {
public static var arbitrary: Gen<timeval> {
return Gen<(Int, Int32)>.zip(Int.arbitrary, Int32.arbitrary).map(timeval.init)
}
}
extension timeval {
static var arbitraryPositive: Gen<timeval> {
return arbitrary.suchThat { $0.tv_sec > 0 && $0.tv_usec > 0 }
}
}

View File

@ -0,0 +1,24 @@
<?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>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@ -0,0 +1,23 @@
//
// NTPExtensionsSpec.swift
// TrueTime
//
// Created by Michael Sanders on 7/18/16.
// Copyright © 2016 Instacart. All rights reserved.
//
@testable import TrueTime
import Nimble
import Quick
import SwiftCheck
final class NTPExtensionsSpec: QuickSpec {
override func spec() {
it("ntp_time64_t") {
property("Matches timeval precision") <- forAll(timeval.arbitraryPositive) { time in
let ntp = ntp_time64_t(timeSince1970: time)
return ntp.milliseconds == time.milliseconds
}
}
}
}

View File

@ -0,0 +1,69 @@
//
// NTPIntegrationSpec.swift
// TrueTime
//
// Created by Michael Sanders on 8/1/16.
// Copyright © 2016 Instacart. All rights reserved.
//
@testable import TrueTime
import Nimble
import Quick
final class NTPIntegrationSpec: QuickSpec {
override func spec() {
describe("fetchIfNeeded") {
it("should ignore outliers") {
self.testReferenceTimeOutliers()
}
}
}
}
private extension NTPIntegrationSpec {
func testReferenceTimeOutliers() {
let clients = (0..<100).map { _ in TrueTimeClient() }
waitUntil(timeout: 60) { done in
var results: [ReferenceTimeResult?] = Array(repeating: nil, count: clients.count)
let start = NSDate()
let finish = {
let end = NSDate()
let results = results.compactMap { $0 }
let times = results.compactMap { try? $0.get() }
let errors: [Error] = results.compactMap {
guard case let .failure(failure) = $0 else { return nil }
return failure
}
expect(times).notTo(beEmpty(), description: "Expected times, got: \(errors)")
print("Got \(times.count) times for \(results.count) results")
let sortedTimes = times.sorted {
$0.time.timeIntervalSince1970 < $1.time.timeIntervalSince1970
}
if !sortedTimes.isEmpty {
let medianTime = sortedTimes[sortedTimes.count / 2]
let maxDelta = end.timeIntervalSince1970 - start.timeIntervalSince1970
for time in times {
let delta = abs(time.time.timeIntervalSince1970 -
medianTime.time.timeIntervalSince1970)
expect(delta) <= maxDelta
}
}
done()
}
for (idx, client) in clients.enumerated() {
client.start(pool: ["time.apple.com"])
client.fetchIfNeeded { result in
results[idx] = result
if !results.contains(where: { $0 == nil }) {
finish()
}
}
}
}
}
}

View File

@ -0,0 +1,19 @@
Pod::Spec.new do |s|
s.name = 'TrueTime'
s.version = '5.1.0'
s.summary = 'NTP library for Swift. Get the true time impervious to device clock changes.'
s.homepage = 'https://github.com/instacart/TrueTime.swift'
s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' }
s.author = { 'Michael Sanders' => 'msanders@instacart.com' }
s.source = { :git => 'https://github.com/instacart/TrueTime.swift.git', :tag => s.version }
s.swift_version = '5.0'
s.requires_arc = true
s.ios.deployment_target = '8.0'
s.osx.deployment_target = '10.10'
s.tvos.deployment_target = '9.0'
s.source_files = 'Sources/**/*.{swift,h,m}'
s.public_header_files = 'Sources/**/*.h'
end

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:/Users/yam/Documents/src/instacart/NetworkTime.swift/TrueTime.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "287C37A31D4419F800084D47"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-Mac"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "280090491D444B64004C788E"
BuildableName = "TrueTime-MacTests.xctest"
BlueprintName = "TrueTime-MacTests"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "280090491D444B64004C788E"
BuildableName = "TrueTime-MacTests.xctest"
BlueprintName = "TrueTime-MacTests"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "287C37A31D4419F800084D47"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-Mac"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "287C37A31D4419F800084D47"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-Mac"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "287C37A31D4419F800084D47"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-Mac"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "28482DC11D314E7B003491D9"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-iOS"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "28482DCB1D314E7B003491D9"
BuildableName = "TrueTimeTests.xctest"
BlueprintName = "TrueTime-iOSTests"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "28482DCB1D314E7B003491D9"
BuildableName = "TrueTimeTests.xctest"
BlueprintName = "TrueTime-iOSTests"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "28482DC11D314E7B003491D9"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-iOS"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "28482DC11D314E7B003491D9"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-iOS"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "28482DC11D314E7B003491D9"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-iOS"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "285047DE1D4D641400DE4CE8"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-tvOS"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "28682B611D4D2F4600D65223"
BuildableName = "TrueTime-TVTests.xctest"
BlueprintName = "TrueTime-TVTests"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "285047DE1D4D641400DE4CE8"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-tvOS"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "285047DE1D4D641400DE4CE8"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-tvOS"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "285047DE1D4D641400DE4CE8"
BuildableName = "TrueTime.framework"
BlueprintName = "TrueTime-tvOS"
ReferencedContainer = "container:TrueTime.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB