diff --git a/Cartfile b/Cartfile index 80736e5772..ae7b99f99b 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1 @@ -github "lyft/Kronos" ~> 4.2 github "microsoft/plcrashreporter" ~> 1.10.1 diff --git a/Cartfile.resolved b/Cartfile.resolved index fbec679d38..f04d6c4ebb 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1 @@ -github "lyft/Kronos" "4.2.1" github "microsoft/plcrashreporter" "1.10.1" diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 6a1408baad..1f719c0f43 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -305,11 +305,7 @@ 61993657265BB6A6009D7EA8 /* Datadog.framework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* Datadog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6199365A265BB6A6009D7EA8 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; }; 6199365B265BB6A6009D7EA8 /* DatadogCrashReporting.framework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 6199365E265BB6A6009D7EA8 /* Kronos.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E0542CA25F8EBBE007A3D0B /* Kronos.xcframework */; }; - 6199365F265BB6A6009D7EA8 /* Kronos.xcframework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 9E0542CA25F8EBBE007A3D0B /* Kronos.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 61993668265BBEDC009D7EA8 /* E2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61993667265BBEDC009D7EA8 /* E2ETests.swift */; }; - 61993673265BC371009D7EA8 /* Kronos.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E0542CA25F8EBBE007A3D0B /* Kronos.xcframework */; }; - 61993674265BC371009D7EA8 /* Kronos.xcframework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 9E0542CA25F8EBBE007A3D0B /* Kronos.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 619A29F326E64910007D62A3 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; 619E16D82577C1CB00B2516B /* DataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619E16D72577C1CB00B2516B /* DataProcessor.swift */; }; 619E16E92578E73E00B2516B /* DataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619E16E82578E73E00B2516B /* DataMigrator.swift */; }; @@ -396,6 +392,21 @@ 61C5A8A624509FAA00DA608C /* SpanEventEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A8A424509FAA00DA608C /* SpanEventEncoder.swift */; }; 61C5A8A724509FAA00DA608C /* SpanEventBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A8A524509FAA00DA608C /* SpanEventBuilder.swift */; }; 61D03BE0273404E700367DE0 /* RUMDataModels+objcTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */; }; + 61D3E0D2277B23F1008BE766 /* KronosInternetAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0C8277B23F0008BE766 /* KronosInternetAddress.swift */; }; + 61D3E0D3277B23F1008BE766 /* KronosDNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0C9277B23F0008BE766 /* KronosDNSResolver.swift */; }; + 61D3E0D4277B23F1008BE766 /* KronosTimeStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CA277B23F0008BE766 /* KronosTimeStorage.swift */; }; + 61D3E0D5277B23F1008BE766 /* KronosNTPPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CB277B23F0008BE766 /* KronosNTPPacket.swift */; }; + 61D3E0D6277B23F1008BE766 /* KronosClock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CC277B23F0008BE766 /* KronosClock.swift */; }; + 61D3E0D7277B23F1008BE766 /* KronosData+Bytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CD277B23F0008BE766 /* KronosData+Bytes.swift */; }; + 61D3E0D8277B23F1008BE766 /* KronosNTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CE277B23F0008BE766 /* KronosNTPClient.swift */; }; + 61D3E0D9277B23F1008BE766 /* KronosNTPProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CF277B23F0008BE766 /* KronosNTPProtocol.swift */; }; + 61D3E0DA277B23F1008BE766 /* KronosTimeFreeze.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0D0277B23F1008BE766 /* KronosTimeFreeze.swift */; }; + 61D3E0DB277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0D1277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift */; }; + 61D3E0E3277B3D92008BE766 /* KronosNTPClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0DE277B3D92008BE766 /* KronosNTPClientTests.swift */; }; + 61D3E0E4277B3D92008BE766 /* KronosNTPPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0DF277B3D92008BE766 /* KronosNTPPacketTests.swift */; }; + 61D3E0E5277B3D92008BE766 /* KronosClockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0E0277B3D92008BE766 /* KronosClockTests.swift */; }; + 61D3E0E6277B3D92008BE766 /* KronosDNSResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0E1277B3D92008BE766 /* KronosDNSResolverTests.swift */; }; + 61D3E0E7277B3D92008BE766 /* KronosTimeStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */; }; 61D447E224917F8F00649287 /* DateFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D447E124917F8F00649287 /* DateFormatting.swift */; }; 61D50C3C2580EEF8006038A3 /* LoggingScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D50C3B2580EEF8006038A3 /* LoggingScenarios.swift */; }; 61D50C462580EF19006038A3 /* TracingScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D50C452580EF19006038A3 /* TracingScenarios.swift */; }; @@ -484,7 +495,6 @@ 61FF283024BC5E2D000B3D9B /* RUMEventFileOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF282F24BC5E2D000B3D9B /* RUMEventFileOutputTests.swift */; }; 61FF416225EE5FF400CE35EC /* CrashReportingWithLoggingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF416125EE5FF400CE35EC /* CrashReportingWithLoggingIntegrationTests.swift */; }; 61FF9A4525AC5DEA001058CC /* RUMViewIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF9A4425AC5DEA001058CC /* RUMViewIdentity.swift */; }; - 9E0542CB25F8EBBE007A3D0B /* Kronos.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E0542CA25F8EBBE007A3D0B /* Kronos.xcframework */; }; 9E26E6B924C87693000B3270 /* RUMDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E26E6B824C87693000B3270 /* RUMDataModels.swift */; }; 9E2EF44F2694FA14008A7DAE /* VitalInfoSamplerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2EF44E2694FA14008A7DAE /* VitalInfoSamplerTests.swift */; }; 9E307C3224C8846D0039607E /* RUMDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E26E6B824C87693000B3270 /* RUMDataModels.swift */; }; @@ -654,7 +664,6 @@ dstSubfolderSpec = 10; files = ( 61441C4E24619498003D8BB8 /* Datadog.framework in ⚙️ Embed Framework Dependencies */, - 61993674265BC371009D7EA8 /* Kronos.xcframework in ⚙️ Embed Framework Dependencies */, 614ED39B260357FA00C8C519 /* DatadogCrashReporting.framework in ⚙️ Embed Framework Dependencies */, 61570006246AAE5E00E96950 /* DatadogObjc.framework in ⚙️ Embed Framework Dependencies */, ); @@ -667,7 +676,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 6199365F265BB6A6009D7EA8 /* Kronos.xcframework in ⚙️ Embed Framework Dependencies */, 61993657265BB6A6009D7EA8 /* Datadog.framework in ⚙️ Embed Framework Dependencies */, 6199365B265BB6A6009D7EA8 /* DatadogCrashReporting.framework in ⚙️ Embed Framework Dependencies */, ); @@ -1070,6 +1078,21 @@ 61C5A8A424509FAA00DA608C /* SpanEventEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanEventEncoder.swift; sourceTree = ""; }; 61C5A8A524509FAA00DA608C /* SpanEventBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanEventBuilder.swift; sourceTree = ""; }; 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUMDataModels+objcTests.swift"; sourceTree = ""; }; + 61D3E0C8277B23F0008BE766 /* KronosInternetAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosInternetAddress.swift; sourceTree = ""; }; + 61D3E0C9277B23F0008BE766 /* KronosDNSResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosDNSResolver.swift; sourceTree = ""; }; + 61D3E0CA277B23F0008BE766 /* KronosTimeStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosTimeStorage.swift; sourceTree = ""; }; + 61D3E0CB277B23F0008BE766 /* KronosNTPPacket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosNTPPacket.swift; sourceTree = ""; }; + 61D3E0CC277B23F0008BE766 /* KronosClock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosClock.swift; sourceTree = ""; }; + 61D3E0CD277B23F0008BE766 /* KronosData+Bytes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KronosData+Bytes.swift"; sourceTree = ""; }; + 61D3E0CE277B23F0008BE766 /* KronosNTPClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosNTPClient.swift; sourceTree = ""; }; + 61D3E0CF277B23F0008BE766 /* KronosNTPProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosNTPProtocol.swift; sourceTree = ""; }; + 61D3E0D0277B23F1008BE766 /* KronosTimeFreeze.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosTimeFreeze.swift; sourceTree = ""; }; + 61D3E0D1277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KronosNSTimer+ClosureKit.swift"; sourceTree = ""; }; + 61D3E0DE277B3D92008BE766 /* KronosNTPClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosNTPClientTests.swift; sourceTree = ""; }; + 61D3E0DF277B3D92008BE766 /* KronosNTPPacketTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosNTPPacketTests.swift; sourceTree = ""; }; + 61D3E0E0277B3D92008BE766 /* KronosClockTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosClockTests.swift; sourceTree = ""; }; + 61D3E0E1277B3D92008BE766 /* KronosDNSResolverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosDNSResolverTests.swift; sourceTree = ""; }; + 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosTimeStorageTests.swift; sourceTree = ""; }; 61D447E124917F8F00649287 /* DateFormatting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateFormatting.swift; sourceTree = ""; }; 61D50C3B2580EEF8006038A3 /* LoggingScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingScenarios.swift; sourceTree = ""; }; 61D50C452580EF19006038A3 /* TracingScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingScenarios.swift; sourceTree = ""; }; @@ -1243,7 +1266,6 @@ buildActionMask = 2147483647; files = ( 61B03ECE274FF01F00EB1AE1 /* SwiftUI.framework in Frameworks */, - 61993673265BC371009D7EA8 /* Kronos.xcframework in Frameworks */, 614ED39A260357FA00C8C519 /* DatadogCrashReporting.framework in Frameworks */, 614ED37C2603533D00C8C519 /* CrashReporter.xcframework in Frameworks */, ); @@ -1271,7 +1293,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9E0542CB25F8EBBE007A3D0B /* Kronos.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1288,7 +1309,6 @@ files = ( 61993656265BB6A6009D7EA8 /* Datadog.framework in Frameworks */, 6199365A265BB6A6009D7EA8 /* DatadogCrashReporting.framework in Frameworks */, - 6199365E265BB6A6009D7EA8 /* Kronos.xcframework in Frameworks */, 61993654265BB6A6009D7EA8 /* CrashReporter.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1449,6 +1469,7 @@ 61216277247D1F2100AC5D67 /* FeaturesIntegration */, 61133BB72423979B00786299 /* Utils */, 61E909E524A24DD3005EA2DE /* OpenTracing */, + 61D3E0C7277B237D008BE766 /* Kronos */, ); name = Datadog; path = ../Sources/Datadog; @@ -1703,6 +1724,7 @@ 61216278247D20D500AC5D67 /* FeaturesIntegration */, 61133C352423990D00786299 /* Utils */, 61BAD46826415FA2001886CA /* OpenTracing */, + 61D3E0DD277B3D6E008BE766 /* Kronos */, ); path = Datadog; sourceTree = ""; @@ -2923,6 +2945,35 @@ path = RUM; sourceTree = ""; }; + 61D3E0C7277B237D008BE766 /* Kronos */ = { + isa = PBXGroup; + children = ( + 61D3E0CC277B23F0008BE766 /* KronosClock.swift */, + 61D3E0CD277B23F0008BE766 /* KronosData+Bytes.swift */, + 61D3E0C9277B23F0008BE766 /* KronosDNSResolver.swift */, + 61D3E0C8277B23F0008BE766 /* KronosInternetAddress.swift */, + 61D3E0D1277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift */, + 61D3E0CE277B23F0008BE766 /* KronosNTPClient.swift */, + 61D3E0CB277B23F0008BE766 /* KronosNTPPacket.swift */, + 61D3E0CF277B23F0008BE766 /* KronosNTPProtocol.swift */, + 61D3E0D0277B23F1008BE766 /* KronosTimeFreeze.swift */, + 61D3E0CA277B23F0008BE766 /* KronosTimeStorage.swift */, + ); + path = Kronos; + sourceTree = ""; + }; + 61D3E0DD277B3D6E008BE766 /* Kronos */ = { + isa = PBXGroup; + children = ( + 61D3E0E0277B3D92008BE766 /* KronosClockTests.swift */, + 61D3E0E1277B3D92008BE766 /* KronosDNSResolverTests.swift */, + 61D3E0DE277B3D92008BE766 /* KronosNTPClientTests.swift */, + 61D3E0DF277B3D92008BE766 /* KronosNTPPacketTests.swift */, + 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */, + ); + path = Kronos; + sourceTree = ""; + }; 61D50C3A2580EED5006038A3 /* ManualInstrumentation */ = { isa = PBXGroup; children = ( @@ -3991,8 +4042,10 @@ 61133BDC2423979B00786299 /* Logger.swift in Sources */, 6114FE1625766B310084E372 /* TrackingConsent.swift in Sources */, 61133BD02423979B00786299 /* DateProvider.swift in Sources */, + 61D3E0D2277B23F1008BE766 /* KronosInternetAddress.swift in Sources */, 6156CB8E24DDA1B5008CB2B2 /* RUMContextProvider.swift in Sources */, 61C2C21224C5951400C0321C /* RUMViewScope.swift in Sources */, + 61D3E0D5277B23F1008BE766 /* KronosNTPPacket.swift in Sources */, 61DE332625C826E4008E3EC2 /* CrashReportingFeature.swift in Sources */, 61E36A11254B2280001AD6F2 /* LaunchTimeProvider.swift in Sources */, 614872772485067300E3EBDB /* SpanTagsReducer.swift in Sources */, @@ -4033,6 +4086,7 @@ 619E16E92578E73E00B2516B /* DataMigrator.swift in Sources */, 618DCFD924C7269500589570 /* RUMUUIDGenerator.swift in Sources */, 9EFD112C24B32D29003A1A2B /* FirstPartyURLsFilter.swift in Sources */, + 61D3E0DB277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift in Sources */, 61B0386C2527247B00518F3C /* TaskInterception.swift in Sources */, 61B03892252724D900518F3C /* HTTPHeadersReader.swift in Sources */, 61122ECE25B1B74500F9C7F5 /* SpanSanitizer.swift in Sources */, @@ -4052,6 +4106,7 @@ 613E792F2577B0F900DFCC17 /* Reader.swift in Sources */, 61B0385A2527247000518F3C /* DDURLSessionDelegate.swift in Sources */, 61133BDF2423979B00786299 /* SwiftExtensions.swift in Sources */, + 61D3E0D3277B23F1008BE766 /* KronosDNSResolver.swift in Sources */, 6149FB3A2529D17F00EE387A /* InternalURLsFilter.swift in Sources */, 611529A525E3DD51004F740E /* ValuePublisher.swift in Sources */, 618DCFD724C7265300589570 /* RUMUUID.swift in Sources */, @@ -4063,6 +4118,7 @@ 61C5A88E24509A1F00DA608C /* Tracer.swift in Sources */, 61E909EE24A24DD3005EA2DE /* OTFormat.swift in Sources */, 9E26E6B924C87693000B3270 /* RUMDataModels.swift in Sources */, + 61D3E0D6277B23F1008BE766 /* KronosClock.swift in Sources */, 613E793B2577B6EE00DFCC17 /* DataReader.swift in Sources */, 614B0A5324EBFE5500A2A780 /* DDRUMMonitor.swift in Sources */, 61133BE32423979B00786299 /* UserInfoProvider.swift in Sources */, @@ -4083,9 +4139,11 @@ 61C5A88A24509A0C00DA608C /* SpanFileOutput.swift in Sources */, 61C3E63524BF1794008053F2 /* Attributes.swift in Sources */, 61133BD32423979B00786299 /* File.swift in Sources */, + 61D3E0D9277B23F1008BE766 /* KronosNTPProtocol.swift in Sources */, 61DE333625C8278A008E3EC2 /* DDCrashReportingPluginType.swift in Sources */, 6112B10225C83D4E00B37771 /* CrashReportingWithLoggingIntegration.swift in Sources */, 6161249E25CAB340009901BE /* CrashContext.swift in Sources */, + 61D3E0DA277B23F1008BE766 /* KronosTimeFreeze.swift in Sources */, 61D447E224917F8F00649287 /* DateFormatting.swift in Sources */, 61133BE72423979B00786299 /* LogUtilityOutputs.swift in Sources */, 61133BDA2423979B00786299 /* RequestBuilder.swift in Sources */, @@ -4094,6 +4152,7 @@ 61ED39D426C2A36B002C0F26 /* DataUploadStatus.swift in Sources */, 61133BE82423979B00786299 /* LogFileOutput.swift in Sources */, 61133BD72423979B00786299 /* DataUploadWorker.swift in Sources */, + 61D3E0D4277B23F1008BE766 /* KronosTimeStorage.swift in Sources */, 61133BD12423979B00786299 /* FilesOrchestrator.swift in Sources */, 61133BCD2423979B00786299 /* NetworkConnectionInfoProvider.swift in Sources */, 61FF282424B8A1C3000B3D9B /* RUMEventFileOutput.swift in Sources */, @@ -4118,6 +4177,7 @@ 61F3CDA72512144600C816E5 /* UIKitRUMViewsPredicate.swift in Sources */, 61133BCE2423979B00786299 /* BatteryStatusProvider.swift in Sources */, 613C6B902768FDDE00870CBF /* Sampler.swift in Sources */, + 61D3E0D8277B23F1008BE766 /* KronosNTPClient.swift in Sources */, 9E9973F1268DF69500D8059B /* VitalInfoSampler.swift in Sources */, 9EA3CA6926775A3500B16871 /* VitalRefreshRateReader.swift in Sources */, E13A880C257922EC004FB174 /* EnvironmentSpanIntegration.swift in Sources */, @@ -4129,6 +4189,7 @@ 61C3E63B24BF1A4B008053F2 /* RUMCommand.swift in Sources */, 61133BE92423979B00786299 /* LogOutput.swift in Sources */, 61C5A88524509A0C00DA608C /* DDNoOps.swift in Sources */, + 61D3E0D7277B23F1008BE766 /* KronosData+Bytes.swift in Sources */, 61E5332C24B75C51003D6C4E /* RUMFeature.swift in Sources */, 61E5333D24B8791A003D6C4E /* RUMEventEncoder.swift in Sources */, 6156CB9D24E18600008CB2B2 /* TracingWithRUMIntegration.swift in Sources */, @@ -4149,6 +4210,7 @@ 61B0387D252724AB00518F3C /* URLSessionSwizzlerTests.swift in Sources */, 617CD0DD24CEDDD300B0B557 /* RUMUserActionScopeTests.swift in Sources */, D29889C9273413ED00A4D1A9 /* RUMViewsHandlerTests.swift in Sources */, + 61D3E0E3277B3D92008BE766 /* KronosNTPClientTests.swift in Sources */, 61C5A8A024509C1100DA608C /* Casting+Tracing.swift in Sources */, 61133C662423990D00786299 /* LogSanitizerTests.swift in Sources */, 6114FE23257671F00084E372 /* ConsentAwareDataWriterTests.swift in Sources */, @@ -4173,6 +4235,7 @@ 61411B1024EC15AC0012EAB2 /* Casting+RUM.swift in Sources */, 61133C622423990D00786299 /* InternalLoggersTests.swift in Sources */, 61FF283024BC5E2D000B3D9B /* RUMEventFileOutputTests.swift in Sources */, + 61D3E0E7277B3D92008BE766 /* KronosTimeStorageTests.swift in Sources */, 9E986C302677B91400D62490 /* VitalRefreshRateReaderTests.swift in Sources */, 61133C582423990D00786299 /* FileWriterTests.swift in Sources */, 61E917D3246546BF00E6C631 /* TracerConfigurationTests.swift in Sources */, @@ -4196,6 +4259,7 @@ 61D03BE0273404E700367DE0 /* RUMDataModels+objcTests.swift in Sources */, 615C3196251DD5080018781C /* UIKitRUMUserActionsHandlerTests.swift in Sources */, 61E917CF2464270500E6C631 /* CodableValueTests.swift in Sources */, + 61D3E0E4277B3D92008BE766 /* KronosNTPPacketTests.swift in Sources */, 61133C542423990D00786299 /* NetworkConnectionInfoProviderTests.swift in Sources */, 616B668E259CC28E00968EE8 /* DDRUMMonitorTests.swift in Sources */, B3BBBCBC265E71D100943419 /* VitalMemoryReaderTests.swift in Sources */, @@ -4278,8 +4342,10 @@ 611F82032563C66100CB9BDB /* UIKitRUMViewsPredicateTests.swift in Sources */, 61133C652423990D00786299 /* LogBuilderTests.swift in Sources */, 61B5E42B26DFC433000B0A5F /* DDNSURLSessionDelegate+apiTests.m in Sources */, + 61D3E0E5277B3D92008BE766 /* KronosClockTests.swift in Sources */, 61F8CC092469295500FE2908 /* DatadogConfigurationBuilderTests.swift in Sources */, 61F1A623249B811200075390 /* Encoding.swift in Sources */, + 61D3E0E6277B3D92008BE766 /* KronosDNSResolverTests.swift in Sources */, 6114FE3B25768AA90084E372 /* ConsentProviderTests.swift in Sources */, 61133C642423990D00786299 /* LoggerTests.swift in Sources */, 617B953D24BF4D8F00E6F443 /* RUMMonitorTests.swift in Sources */, diff --git a/DatadogSDK.podspec b/DatadogSDK.podspec index d36c5db582..ac1f469296 100644 --- a/DatadogSDK.podspec +++ b/DatadogSDK.podspec @@ -25,5 +25,4 @@ Pod::Spec.new do |s| s.public_header_files = ["Datadog/TargetSupport/Datadog/Datadog.h", "Sources/_Datadog_Private/include/*.h"] - s.dependency 'Kronos', '~> 4.2' end diff --git a/DatadogSDK.podspec.src b/DatadogSDK.podspec.src index c6be426253..4f4578a4ea 100644 --- a/DatadogSDK.podspec.src +++ b/DatadogSDK.podspec.src @@ -25,5 +25,4 @@ Pod::Spec.new do |s| s.public_header_files = ["Datadog/TargetSupport/Datadog/Datadog.h", "Sources/_Datadog_Private/include/*.h"] - s.dependency 'Kronos', '~> 4.2' end diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 586d130941..cc3f578952 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -1,6 +1,6 @@ Component,Origin,License,Copyright import,io.opentracing,MIT,Copyright 2018 LightStep -import,com.Lyft.Kronos,Apache-2.0,Copyright (C) 2016 Lyft Inc. +import,com.Lyft.Kronos,Apache-2.0,Copyright (C) 2016 Lyft Inc. and MobileNativeFoundation import,PLCrashReporter,MIT,Copyright Microsoft Corporation import (tools),https://github.com/jpsim/SourceKitten,MIT,Copyright (c) 2014 JP Simard import (tools),https://github.com/apple/swift-argument-parser,Apache-2.0,(c) 2020 Apple Inc. and the Swift project authors diff --git a/Package.swift b/Package.swift index b613169d67..03baecbbe3 100644 --- a/Package.swift +++ b/Package.swift @@ -35,7 +35,6 @@ let package = Package( ), ], dependencies: [ - .package(name: "Kronos", url: "https://github.com/lyft/Kronos.git", from: "4.2.1"), .package(name: "PLCrashReporter", url: "https://github.com/microsoft/plcrashreporter.git", from: "1.10.0"), ], targets: [ @@ -43,7 +42,6 @@ let package = Package( name: "Datadog", dependencies: [ "_Datadog_Private", - .product(name: "Kronos", package: "Kronos"), ], swiftSettings: [.define("SPM_BUILD")] ), diff --git a/Sources/Datadog/Core/System/Time/ServerDateProvider.swift b/Sources/Datadog/Core/System/Time/ServerDateProvider.swift index 97748d5ef1..33b90628f4 100644 --- a/Sources/Datadog/Core/System/Time/ServerDateProvider.swift +++ b/Sources/Datadog/Core/System/Time/ServerDateProvider.swift @@ -5,7 +5,6 @@ */ import Foundation -import Kronos /// Abstract the monotonic clock synchronized with the server using NTP. internal protocol ServerDateProvider { @@ -28,7 +27,7 @@ internal class NTPServerDateProvider: ServerDateProvider { } func synchronize(with pool: String, completion: @escaping (TimeInterval?) -> Void) { - Clock.sync( + KronosClock.sync( from: pool, first: { [weak self] _, offset in self?.publisher.publishAsync(offset) @@ -50,6 +49,6 @@ internal class NTPServerDateProvider: ServerDateProvider { // `Kronos.sync` first loads the previous state from the `UserDefaults` if any. // We can invoke `Clock.now` to retrieve the stored offset. - publisher.publishAsync(Clock.now?.timeIntervalSinceNow) + publisher.publishAsync(KronosClock.now?.timeIntervalSinceNow) } } diff --git a/Sources/Datadog/Kronos/KronosClock.swift b/Sources/Datadog/Kronos/KronosClock.swift new file mode 100644 index 0000000000..e56299831e --- /dev/null +++ b/Sources/Datadog/Kronos/KronosClock.swift @@ -0,0 +1,114 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +/// Struct that has time + related metadata +internal typealias KronosAnnotatedTime = ( + /// Time that is being annotated + date: Date, + + /// Amount of time that has passed since the last NTP sync; in other words, the NTP response age. + timeSinceLastNtpSync: TimeInterval +) + +/// High level implementation for clock synchronization using NTP. All returned dates use the most accurate +/// synchronization and it's not affected by clock changes. The NTP synchronization implementation has sub- +/// second accuracy but given that Darwin doesn't support microseconds on bootTime, dates don't have sub- +/// second accuracy. +/// +/// Example usage: +/// +/// ```swift +/// KronosClock.sync { date, offset in +/// print(date) +/// } +/// // (... later on ...) +/// print(KronosClock.now) +/// ``` +internal struct KronosClock { + private static var stableTime: KronosTimeFreeze? { + didSet { + self.storage.stableTime = self.stableTime + } + } + + /// Determines where the most current stable time is stored. Use TimeStoragePolicy.appGroup to share + /// between your app and an extension. + static var storage = KronosTimeStorage(storagePolicy: .standard) + + /// The most accurate timestamp that we have so far (nil if no synchronization was done yet) + static var timestamp: TimeInterval? { + return self.stableTime?.adjustedTimestamp + } + + /// The most accurate date that we have so far (nil if no synchronization was done yet) + static var now: Date? { + return self.annotatedNow?.date + } + + /// Same as `now` except with analytic metadata about the time + static var annotatedNow: KronosAnnotatedTime? { + guard let stableTime = self.stableTime else { + return nil + } + + return KronosAnnotatedTime( + date: Date(timeIntervalSince1970: stableTime.adjustedTimestamp), + timeSinceLastNtpSync: stableTime.timeSinceLastNtpSync + ) + } + + /// Syncs the clock using NTP. Note that the full synchronization could take a few seconds. The given + /// closure will be called with the first valid NTP response which accuracy should be good enough for the + /// initial clock adjustment but it might not be the most accurate representation. After calling the + /// closure this method will continue syncing with multiple servers and multiple passes. + /// + /// - parameter pool: NTP pool that will be resolved into multiple NTP servers that will be used for + /// the synchronization. + /// - parameter samples: The number of samples to be acquired from each server (default 4). + /// - parameter completion: A closure that will be called after _all_ the NTP calls are finished. + /// - parameter first: A closure that will be called after the first valid date is calculated. + static func sync( + from pool: String = "time.apple.com", + samples: Int = 4, + first: ((Date, TimeInterval) -> Void)? = nil, + completion: ((Date?, TimeInterval?) -> Void)? = nil + ) { + self.loadFromDefaults() + + KronosNTPClient().query(pool: pool, numberOfSamples: samples) { offset, done, total in + if let offset = offset { + self.stableTime = KronosTimeFreeze(offset: offset) + + if done == 1, let now = self.now { + first?(now, offset) + } + } + + if done == total { + completion?(self.now, offset) + } + } + } + + /// Resets all state of the monotonic clock. Note that you won't be able to access `now` until you `sync` + /// again. + static func reset() { + self.stableTime = nil + } + + private static func loadFromDefaults() { + guard let previousStableTime = self.storage.stableTime else { + self.stableTime = nil + return + } + self.stableTime = previousStableTime + } +} diff --git a/Sources/Datadog/Kronos/KronosDNSResolver.swift b/Sources/Datadog/Kronos/KronosDNSResolver.swift new file mode 100644 index 0000000000..042de51de6 --- /dev/null +++ b/Sources/Datadog/Kronos/KronosDNSResolver.swift @@ -0,0 +1,101 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +private let kCopyNoOperation = unsafeBitCast(0, to: CFAllocatorCopyDescriptionCallBack.self) +private let kDefaultTimeout = 8.0 + +internal final class KronosDNSResolver { + private var completion: (([KronosInternetAddress]) -> Void)? + private var timer: Timer? + + private init() {} + + /// Performs DNS lookups and calls the given completion with the answers that are returned from the name + /// server(s) that were queried. + /// + /// - parameter host: The host to be looked up. + /// - parameter timeout: The connection timeout. + /// - parameter completion: A completion block that will be called both on failure and success with a list + /// of IPs. + static func resolve( + host: String, + timeout: TimeInterval = kDefaultTimeout, + completion: @escaping ([KronosInternetAddress]) -> Void + ) { + let callback: CFHostClientCallBack = { host, _, _, info in + guard let info = info else { + return + } + let retainedSelf = Unmanaged.fromOpaque(info) + let resolver = retainedSelf.takeUnretainedValue() + resolver.timer?.invalidate() + resolver.timer = nil + + var resolved: DarwinBoolean = false + guard let addresses = CFHostGetAddressing(host, &resolved), resolved.boolValue else { + resolver.completion?([]) + retainedSelf.release() + return + } + + let IPs = (addresses.takeUnretainedValue() as NSArray) + .compactMap { $0 as? NSData } + .compactMap(KronosInternetAddress.init) + + resolver.completion?(IPs) + retainedSelf.release() + } + + let resolver = KronosDNSResolver() + resolver.completion = completion + + let retainedClosure = Unmanaged.passRetained(resolver).toOpaque() + var clientContext = CFHostClientContext( + version: 0, + info: UnsafeMutableRawPointer(retainedClosure), + retain: nil, + release: nil, + copyDescription: kCopyNoOperation + ) + + let hostReference = CFHostCreateWithName(kCFAllocatorDefault, host as CFString).takeUnretainedValue() + resolver.timer = Timer.scheduledTimer( + timeInterval: timeout, + target: resolver, + selector: #selector(KronosDNSResolver.onTimeout), + userInfo: hostReference, + repeats: false + ) + + CFHostSetClient(hostReference, callback, &clientContext) + CFHostScheduleWithRunLoop(hostReference, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue) + CFHostStartInfoResolution(hostReference, .addresses, nil) + } + + @objc + private func onTimeout() { + defer { + self.completion?([]) + + // Manually release the previously retained self. + Unmanaged.passUnretained(self).release() + } + + guard let userInfo = self.timer?.userInfo else { + return + } + + let hostReference = unsafeBitCast(userInfo as AnyObject, to: CFHost.self) + CFHostCancelInfoResolution(hostReference, .addresses) + CFHostUnscheduleFromRunLoop(hostReference, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue) + CFHostSetClient(hostReference, nil, nil) + } +} diff --git a/Sources/Datadog/Kronos/KronosData+Bytes.swift b/Sources/Datadog/Kronos/KronosData+Bytes.swift new file mode 100644 index 0000000000..64b6dace5a --- /dev/null +++ b/Sources/Datadog/Kronos/KronosData+Bytes.swift @@ -0,0 +1,99 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +extension Data { + /// Creates an Data instance based on a hex string (example: "ffff" would be ). + /// + /// - parameter hex: The hex string without any spaces; should only have [0-9A-Fa-f]. + init?(hex: String) { + if hex.count % 2 != 0 { + return nil + } + + let hexArray = Array(hex) + var bytes: [UInt8] = [] + + for index in stride(from: 0, to: hexArray.count, by: 2) { + guard let byte = UInt8("\(hexArray[index])\(hexArray[index + 1])", radix: 16) else { + return nil + } + + bytes.append(byte) + } + + self.init(bytes: bytes, count: bytes.count) + } + + /// Gets one byte from the given index. + /// + /// - parameter index: The index of the byte to be retrieved. Note that this should never be >= length. + /// + /// - returns: The byte located at position `index`. + func getByte(at index: Int) -> Int8 { + let data: Int8 = self.subdata(in: index ..< (index + 1)).withUnsafeBytes { rawPointer in + rawPointer.bindMemory(to: Int8.self).baseAddress!.pointee // swiftlint:disable:this force_unwrapping + } + + return data + } + + /// Gets an unsigned int (32 bits => 4 bytes) from the given index. + /// + /// - parameter index: The index of the uint to be retrieved. Note that this should never be >= length - + /// 3. + /// + /// - returns: The unsigned int located at position `index`. + func getUnsignedInteger(at index: Int, bigEndian: Bool = true) -> UInt32 { + let data: UInt32 = self.subdata(in: index ..< (index + 4)).withUnsafeBytes { rawPointer in + rawPointer.bindMemory(to: UInt32.self).baseAddress!.pointee // swiftlint:disable:this force_unwrapping + } + + return bigEndian ? data.bigEndian : data.littleEndian + } + + /// Gets an unsigned long integer (64 bits => 8 bytes) from the given index. + /// + /// - parameter index: The index of the ulong to be retrieved. Note that this should never be >= length - + /// 7. + /// + /// - returns: The unsigned long integer located at position `index`. + func getUnsignedLong(at index: Int, bigEndian: Bool = true) -> UInt64 { + let data: UInt64 = self.subdata(in: index ..< (index + 8)).withUnsafeBytes { rawPointer in + rawPointer.bindMemory(to: UInt64.self).baseAddress!.pointee // swiftlint:disable:this force_unwrapping + } + + return bigEndian ? data.bigEndian : data.littleEndian + } + + /// Appends the given byte (8 bits) into the receiver Data. + /// + /// - parameter data: The byte to be appended. + mutating func append(byte data: Int8) { + var data = data + self.append(Data(bytes: &data, count: MemoryLayout.size)) + } + + /// Appends the given unsigned integer (32 bits; 4 bytes) into the receiver Data. + /// + /// - parameter data: The unsigned integer to be appended. + mutating func append(unsignedInteger data: UInt32, bigEndian: Bool = true) { + var data = bigEndian ? data.bigEndian : data.littleEndian + self.append(Data(bytes: &data, count: MemoryLayout.size)) + } + + /// Appends the given unsigned long (64 bits; 8 bytes) into the receiver Data. + /// + /// - parameter data: The unsigned long to be appended. + mutating func append(unsignedLong data: UInt64, bigEndian: Bool = true) { + var data = bigEndian ? data.bigEndian : data.littleEndian + self.append(Data(bytes: &data, count: MemoryLayout.size)) + } +} diff --git a/Sources/Datadog/Kronos/KronosInternetAddress.swift b/Sources/Datadog/Kronos/KronosInternetAddress.swift new file mode 100644 index 0000000000..9c912a87ff --- /dev/null +++ b/Sources/Datadog/Kronos/KronosInternetAddress.swift @@ -0,0 +1,117 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +/// This enum represents an internet address that can either be IPv4 or IPv6. +/// +/// - IPv6: An Internet Address of type IPv6 (e.g.: '::1'). +/// - IPv4: An Internet Address of type IPv4 (e.g.: '127.0.0.1'). +internal enum KronosInternetAddress: Hashable { + case ipv6(sockaddr_in6) + case ipv4(sockaddr_in) + + /// Human readable host represetnation (e.g. '192.168.1.1' or 'ab:ab:ab:ab:ab:ab:ab:ab'). + var host: String? { + switch self { + case .ipv6(var address): + var buffer = [CChar](repeating: 0, count: Int(INET6_ADDRSTRLEN)) + inet_ntop(AF_INET6, &address.sin6_addr, &buffer, socklen_t(INET6_ADDRSTRLEN)) + return String(cString: buffer) + + case .ipv4(var address): + var buffer = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN)) + inet_ntop(AF_INET, &address.sin_addr, &buffer, socklen_t(INET_ADDRSTRLEN)) + return String(cString: buffer) + } + } + + /// The protocol family that should be used on the socket creation for this address. + var family: Int32 { + switch self { + case .ipv4: + return PF_INET + + case .ipv6: + return PF_INET6 + } + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.host) + } + + init?(dataWithSockAddress data: NSData) { + let storage = sockaddr_storage.from(unsafeDataWithSockAddress: data) + switch Int32(storage.ss_family) { + case AF_INET: + self = storage.withUnsafeAddress { KronosInternetAddress.ipv4($0.pointee) } + + case AF_INET6: + self = storage.withUnsafeAddress { KronosInternetAddress.ipv6($0.pointee) } + + default: + return nil + } + } + + /// Returns the address struct (either sockaddr_in or sockaddr_in6) represented as an CFData. + /// + /// - parameter port: The port number to associate on the address struct. + /// + /// - returns: An address struct wrapped into a CFData type. + func addressData(withPort port: Int) -> CFData { + switch self { + case .ipv6(var address): + address.sin6_port = in_port_t(port).bigEndian + return Data(bytes: &address, count: MemoryLayout.size) as CFData + + case .ipv4(var address): + address.sin_port = in_port_t(port).bigEndian + return Data(bytes: &address, count: MemoryLayout.size) as CFData + } + } +} + +/// Compare InternetAddress(es) by making sure the host representation are equal. +internal func == (lhs: KronosInternetAddress, rhs: KronosInternetAddress) -> Bool { + return lhs.host == rhs.host +} + +// MARK: - sockaddr_storage helpers + +extension sockaddr_storage { + /// Creates a new storage value from a data type that contains the memory layout of a sockaddr_t. This + /// is used to create sockaddr_storage(s) from some of the CF C functions such as `CFHostGetAddressing`. + /// + /// !!! WARNING: This method is unsafe and assumes the memory layout is of `sockaddr_t`. !!! + /// + /// - parameter data: The data to be interpreted as sockaddr + /// - returns: The newly created sockaddr_storage value + fileprivate static func from(unsafeDataWithSockAddress data: NSData) -> sockaddr_storage { + var storage = sockaddr_storage() + data.getBytes(&storage, length: data.length) + return storage + } + + /// Calls a closure with traditional BSD Sockets address parameters. + /// + /// - parameter body: A closure to call with `self` referenced appropriately for calling + /// BSD Sockets APIs that take an address. + /// + /// - throws: Any error thrown by `body`. + /// + /// - returns: Any result returned by `body`. + fileprivate func withUnsafeAddress(_ body: (_ address: UnsafePointer) -> T) -> T { + var storage = self + return withUnsafePointer(to: &storage) { + $0.withMemoryRebound(to: U.self, capacity: 1) { address in body(address) } + } + } +} diff --git a/Sources/Datadog/Kronos/KronosNSTimer+ClosureKit.swift b/Sources/Datadog/Kronos/KronosNSTimer+ClosureKit.swift new file mode 100644 index 0000000000..903cddb4fd --- /dev/null +++ b/Sources/Datadog/Kronos/KronosNSTimer+ClosureKit.swift @@ -0,0 +1,66 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +internal typealias CKTimerHandler = (Timer) -> Void + +/// Simple closure implementation on NSTimer scheduling. +/// +/// Example: +/// +/// ```swift +/// KronosBlockTimer.scheduledTimer(withTimeInterval: 1.0) { timer in +/// print("Did something after 1s!") +/// } +/// ``` +internal final class KronosBlockTimer: NSObject { + /// Creates and returns a block-based NSTimer object and schedules it on the current run loop. + /// + /// - parameter interval: The number of seconds between firings of the timer. + /// - parameter repeated: If true, the timer will repeatedly reschedule itself until invalidated. If + /// false, the timer will be invalidated after it fires. + /// - parameter handler: The closure that the NSTimer fires. + /// + /// - returns: A new NSTimer object, configured according to the specified parameters. + class func scheduledTimer( + withTimeInterval interval: TimeInterval, + repeated: Bool = false, + handler: @escaping CKTimerHandler + ) -> Timer { + return Timer.scheduledTimer( + timeInterval: interval, + target: self, + selector: #selector(KronosBlockTimer.invokeFrom(timer:)), + userInfo: TimerClosureWrapper(handler: handler, repeats: repeated), + repeats: repeated + ) + } + + // MARK: Private methods + + @objc + private class func invokeFrom(timer: Timer) { + if let closureWrapper = timer.userInfo as? TimerClosureWrapper { + closureWrapper.handler(timer) + } + } +} + +// MARK: - Private classes + +private final class TimerClosureWrapper { + fileprivate var handler: CKTimerHandler + private var repeats: Bool + + init(handler: @escaping CKTimerHandler, repeats: Bool) { + self.handler = handler + self.repeats = repeats + } +} diff --git a/Sources/Datadog/Kronos/KronosNTPClient.swift b/Sources/Datadog/Kronos/KronosNTPClient.swift new file mode 100644 index 0000000000..53cdad6cad --- /dev/null +++ b/Sources/Datadog/Kronos/KronosNTPClient.swift @@ -0,0 +1,202 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +private let kDefaultTimeout = 6.0 +private let kDefaultSamples = 4 +private let kMaximumNTPServers = 5 +private let kMaximumResultDispersion = 10.0 + +private typealias ObjCCompletionType = @convention(block) (Data?, TimeInterval) -> Void + +/// Exception raised while sending / receiving NTP packets. +internal enum KronosNTPNetworkError: Error { + case noValidNTPPacketFound +} + +/// NTP client session. +internal final class KronosNTPClient { + /// Query the all ips that resolve from the given pool. + /// + /// - parameter pool: NTP pool that will be resolved into multiple NTP servers. + /// - parameter port: Server NTP port (default 123). + /// - parameter version: NTP version to use (default 3). + /// - parameter numberOfSamples: The number of samples to be acquired from each server (default 4). + /// - parameter maximumServers: The maximum number of servers to be queried (default 5). + /// - parameter timeout: The individual timeout for each of the NTP operations. + /// - parameter completion: A closure that will be response PDU on success or nil on error. + func query( + pool: String = "time.apple.com", + version: Int8 = 3, + port: Int = 123, + numberOfSamples: Int = kDefaultSamples, + maximumServers: Int = kMaximumNTPServers, + timeout: CFTimeInterval = kDefaultTimeout, + progress: @escaping (TimeInterval?, Int, Int) -> Void + ) { + var servers: [KronosInternetAddress: [KronosNTPPacket]] = [:] + var completed: Int = 0 + + let queryIPAndStoreResult = { (address: KronosInternetAddress, totalQueries: Int) -> Void in + self.query(ip: address, port: port, version: version, timeout: timeout, numberOfSamples: numberOfSamples) { packet in + defer { + completed += 1 + + let responses = Array(servers.values) + progress(try? self.offset(from: responses), completed, totalQueries) + } + + guard let PDU = packet else { + return + } + + if servers[address] == nil { + servers[address] = [] + } + + servers[address]?.append(PDU) + } + } + + KronosDNSResolver.resolve(host: pool) { addresses in + if addresses.count == 0 { + return progress(nil, 0, 0) + } + + let totalServers = min(addresses.count, maximumServers) + for address in addresses[0 ..< totalServers] { + queryIPAndStoreResult(address, totalServers * numberOfSamples) + } + } + } + + /// Query the given NTP server for the time exchange. + /// + /// - parameter ip: Server socket address. + /// - parameter port: Server NTP port (default 123). + /// - parameter version: NTP version to use (default 3). + /// - parameter timeout: Timeout on socket operations. + /// - parameter numberOfSamples: The number of samples to be acquired from the server (default 4). + /// - parameter completion: A closure that will be response PDU on success or nil on error. + func query( + ip: KronosInternetAddress, + port: Int = 123, + version: Int8 = 3, + timeout: CFTimeInterval = kDefaultTimeout, + numberOfSamples: Int = kDefaultSamples, + completion: @escaping (KronosNTPPacket?) -> Void + ) { + var timer: Timer? + let bridgeCallback: ObjCCompletionType = { data, destinationTime in + defer { + // If we still have samples left; we'll keep querying the same server + if numberOfSamples > 1 { + self.query(ip: ip, port: port, version: version, timeout: timeout, numberOfSamples: numberOfSamples - 1, completion: completion) + } + } + + timer?.invalidate() + guard + let data = data, let PDU = try? KronosNTPPacket(data: data, destinationTime: destinationTime), + PDU.isValidResponse() else + { + completion(nil) + return + } + + completion(PDU) + } + + let callback = unsafeBitCast(bridgeCallback, to: AnyObject.self) + let retainedCallback = Unmanaged.passRetained(callback) + let sourceAndSocket = self.sendAsyncUDPQuery( + to: ip, port: port, timeout: timeout, completion: UnsafeMutableRawPointer(retainedCallback.toOpaque()) + ) + + timer = KronosBlockTimer.scheduledTimer(withTimeInterval: timeout, repeated: true) { _ in + bridgeCallback(nil, TimeInterval.infinity) + retainedCallback.release() + + if let (source, socket) = sourceAndSocket { + CFSocketInvalidate(socket) + CFRunLoopRemoveSource(CFRunLoopGetMain(), source, CFRunLoopMode.commonModes) + } + } + } + + // MARK: - Private helpers (NTP Calculation) + + private func offset(from responses: [[KronosNTPPacket]]) throws -> TimeInterval { + let now = kronosCurrentTime() + var bestResponses: [KronosNTPPacket] = [] + for serverResponses in responses { + let filtered = serverResponses + .filter { abs($0.originTime - now) < kMaximumResultDispersion } + .min { $0.delay < $1.delay } + + if let filtered = filtered { + bestResponses.append(filtered) + } + } + + if bestResponses.count == 0 { + throw KronosNTPNetworkError.noValidNTPPacketFound + } + + bestResponses.sort { $0.offset < $1.offset } + return bestResponses[bestResponses.count / 2].offset + } + + // MARK: - Private helpers (CFSocket) + + private func sendAsyncUDPQuery( + to ip: KronosInternetAddress, + port: Int, + timeout: TimeInterval, + completion: UnsafeMutableRawPointer + ) -> (CFRunLoopSource, CFSocket)? { + signal(SIGPIPE, SIG_IGN) + + let callback: CFSocketCallBack = { socket, callbackType, _, data, info in + if callbackType == .writeCallBack { + var packet = KronosNTPPacket() + let PDU = packet.prepareToSend() as CFData + CFSocketSendData(socket, nil, PDU, kDefaultTimeout) + return + } + + guard let info = info else { + return + } + + CFSocketInvalidate(socket) + + let destinationTime = kronosCurrentTime() + let retainedClosure = Unmanaged.fromOpaque(info) + let completion = unsafeBitCast(retainedClosure.takeUnretainedValue(), to: ObjCCompletionType.self) + + let data = unsafeBitCast(data, to: CFData.self) as Data? + completion(data, destinationTime) + retainedClosure.release() + } + + let types = CFSocketCallBackType.dataCallBack.rawValue | CFSocketCallBackType.writeCallBack.rawValue + var context = CFSocketContext(version: 0, info: completion, retain: nil, release: nil, copyDescription: nil) + guard let socket = CFSocketCreate(nil, ip.family, SOCK_DGRAM, IPPROTO_UDP, types, callback, &context), + CFSocketIsValid(socket) else { + return nil + } + + let runLoopSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0) + CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, CFRunLoopMode.commonModes) + CFSocketConnectToAddress(socket, ip.addressData(withPort: port), timeout) + return (runLoopSource!, socket) // swiftlint:disable:this force_unwrapping + } +} diff --git a/Sources/Datadog/Kronos/KronosNTPPacket.swift b/Sources/Datadog/Kronos/KronosNTPPacket.swift new file mode 100644 index 0000000000..3b0cb42658 --- /dev/null +++ b/Sources/Datadog/Kronos/KronosNTPPacket.swift @@ -0,0 +1,207 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +/// Delta between system and NTP time +private let kEpochDelta = 2_208_988_800.0 + +/// This is the maximum that we'll tolerate for the client's time vs self.delay +private let kMaximumDelayDifference = 0.1 +private let kMaximumDispersion = 100.0 + +/// Returns the current time in decimal EPOCH timestamp format. +/// +/// - returns: The current time in EPOCH timestamp format. +internal func kronosCurrentTime() -> TimeInterval { + var current = timeval() + let systemTimeError = gettimeofday(¤t, nil) != 0 + assert(!systemTimeError, "system clock error: system time unavailable") + + return Double(current.tv_sec) + Double(current.tv_usec) / 1_000_000 +} + +internal struct KronosNTPPacket { + /// The leap indicator warning of an impending leap second to be inserted or deleted in the last + /// minute of the current month. + let leap: KronosLeapIndicator + + /// Version Number (VN): This is a three-bit integer indicating the NTP version number, currently 3. + let version: Int8 + + /// The current connection mode. + let mode: KronosMode + + /// Mode representing the stratum level of the local clock. + let stratum: KronosStratum + + /// Indicates the maximum interval between successive messages, in seconds to the nearest power of two. + /// The values that normally appear in this field range from 6 to 10, inclusive. + let poll: Int8 + + /// The precision of the local clock, in seconds to the nearest power of two. The values that normally + /// appear in this field range from -6 for mains-frequency clocks to -18 for microsecond clocks found + /// in some workstations. + let precision: Int8 + + /// The total roundtrip delay to the primary reference source, in seconds with fraction point between + /// bits 15 and 16. Note that this variable can take on both positive and negative values, depending on + /// the relative time and frequency errors. The values that normally appear in this field range from + /// negative values of a few milliseconds to positive values of several hundred milliseconds. + let rootDelay: TimeInterval + + /// Total dispersion to the reference clock, in EPOCH. + let rootDispersion: TimeInterval + + /// Server or reference clock. This value is generated based on a reference identifier maintained by IANA. + let clockSource: KronosClockSource + + /// Time when the system clock was last set or corrected, in EPOCH timestamp format. + let referenceTime: TimeInterval + + /// Time at the client when the request departed for the server, in EPOCH timestamp format. + let originTime: TimeInterval + + /// Time at the server when the request arrived from the client, in EPOCH timestamp format. + let receiveTime: TimeInterval + + /// Time at the server when the response left for the client, in EPOCH timestamp format. + var transmitTime: TimeInterval = 0.0 + + /// Time at the client when the response arrived, in EPOCH timestamp format. + let destinationTime: TimeInterval + + /// NTP protocol package representation. + /// + /// - parameter transmitTime: Packet transmission timestamp. + /// - parameter version: NTP protocol version. + /// - parameter mode: Packet mode (client, server). + init(version: Int8 = 3, mode: KronosMode = .client) { + self.version = version + self.leap = .noWarning + self.mode = mode + self.stratum = .unspecified + self.poll = 4 + self.precision = -6 + self.rootDelay = 1 + self.rootDispersion = 1 + self.clockSource = .referenceIdentifier(id: 0) + self.referenceTime = -kEpochDelta + self.originTime = -kEpochDelta + self.receiveTime = -kEpochDelta + self.destinationTime = -1 + } + + /// Creates a NTP package based on a network PDU. + /// + /// - parameter data: The PDU received from the NTP call. + /// - parameter destinationTime: The time where the package arrived (client time) in EPOCH format. + /// - throws: KronosNTPParsingError in case of an invalid response. + init(data: Data, destinationTime: TimeInterval) throws { + if data.count < 48 { + throw KronosNTPParsingError.invalidNTPPDU("Invalid PDU length: \(data.count)") + } + + self.leap = KronosLeapIndicator(rawValue: (data.getByte(at: 0) >> 6) & 0b11) ?? .noWarning + self.version = data.getByte(at: 0) >> 3 & 0b111 + self.mode = KronosMode(rawValue: data.getByte(at: 0) & 0b111) ?? .unknown + self.stratum = KronosStratum(value: data.getByte(at: 1)) + self.poll = data.getByte(at: 2) + self.precision = data.getByte(at: 3) + self.rootDelay = KronosNTPPacket.intervalFromNTPFormat(data.getUnsignedInteger(at: 4)) + self.rootDispersion = KronosNTPPacket.intervalFromNTPFormat(data.getUnsignedInteger(at: 8)) + self.clockSource = KronosClockSource(stratum: self.stratum, sourceID: data.getUnsignedInteger(at: 12)) + self.referenceTime = KronosNTPPacket.dateFromNTPFormat(data.getUnsignedLong(at: 16)) + self.originTime = KronosNTPPacket.dateFromNTPFormat(data.getUnsignedLong(at: 24)) + self.receiveTime = KronosNTPPacket.dateFromNTPFormat(data.getUnsignedLong(at: 32)) + self.transmitTime = KronosNTPPacket.dateFromNTPFormat(data.getUnsignedLong(at: 40)) + self.destinationTime = destinationTime + } + + /// Convert this NTPPacket to a buffer that can be sent over a socket. + /// + /// - returns: A bytes buffer representing this packet. + mutating func prepareToSend(transmitTime: TimeInterval? = nil) -> Data { + var data = Data() + data.append(byte: self.leap.rawValue << 6 | self.version << 3 | self.mode.rawValue) + data.append(byte: self.stratum.rawValue) + data.append(byte: self.poll) + data.append(byte: self.precision) + data.append(unsignedInteger: self.intervalToNTPFormat(self.rootDelay)) + data.append(unsignedInteger: self.intervalToNTPFormat(self.rootDispersion)) + data.append(unsignedInteger: self.clockSource.ID) + data.append(unsignedLong: self.dateToNTPFormat(self.referenceTime)) + data.append(unsignedLong: self.dateToNTPFormat(self.originTime)) + data.append(unsignedLong: self.dateToNTPFormat(self.receiveTime)) + + self.transmitTime = transmitTime ?? kronosCurrentTime() + data.append(unsignedLong: self.dateToNTPFormat(self.transmitTime)) + return data + } + + /// Checks properties to make sure that the received PDU is a valid response that we can use. + /// + /// - returns: A boolean indicating if the response is valid for the given version. + func isValidResponse() -> Bool { + return (self.mode == .server || self.mode == .symmetricPassive) && self.leap != .alarm + && self.stratum != .invalid && self.stratum != .unspecified + && self.rootDispersion < kMaximumDispersion + && abs(kronosCurrentTime() - self.originTime - self.delay) < kMaximumDelayDifference + } + + // MARK: - Private helpers + + private func dateToNTPFormat(_ time: TimeInterval) -> UInt64 { + let integer = UInt32(time + kEpochDelta) + let decimal = modf(time).1 * 4_294_967_296.0 // 2 ^ 32 + return UInt64(integer) << 32 | UInt64(decimal) + } + + private func intervalToNTPFormat(_ time: TimeInterval) -> UInt32 { + let integer = UInt16(time) + let decimal = modf(time).1 * 65_536 // 2 ^ 16 + return UInt32(integer) << 16 | UInt32(decimal) + } + + private static func dateFromNTPFormat(_ time: UInt64) -> TimeInterval { + let integer = Double(time >> 32) + let decimal = Double(time & 0xffffffff) / 4_294_967_296.0 + return integer - kEpochDelta + decimal + } + + private static func intervalFromNTPFormat(_ time: UInt32) -> TimeInterval { + let integer = Double(time >> 16) + let decimal = Double(time & 0xffff) / 65_536 + return integer + decimal + } +} + +/// From RFC 2030 (with a correction to the delay math): +/// +/// Timestamp Name ID When Generated +/// ------------------------------------------------------------ +/// Originate Timestamp T1 time request sent by client +/// Receive Timestamp T2 time request received by server +/// Transmit Timestamp T3 time reply sent by server +/// Destination Timestamp T4 time reply received by client +/// +/// The roundtrip delay d and local clock offset t are defined as +/// +/// d = (T4 - T1) - (T3 - T2) t = ((T2 - T1) + (T3 - T4)) / 2. +extension KronosNTPPacket { + /// Clocks offset in seconds. + var offset: TimeInterval { + return ((self.receiveTime - self.originTime) + (self.transmitTime - self.destinationTime)) / 2.0 + } + + /// Round-trip delay in seconds + var delay: TimeInterval { + return (self.destinationTime - self.originTime) - (self.transmitTime - self.receiveTime) + } +} diff --git a/Sources/Datadog/Kronos/KronosNTPProtocol.swift b/Sources/Datadog/Kronos/KronosNTPProtocol.swift new file mode 100644 index 0000000000..45e0b44540 --- /dev/null +++ b/Sources/Datadog/Kronos/KronosNTPProtocol.swift @@ -0,0 +1,137 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +/// Exception raised when the received PDU is invalid. +internal enum KronosNTPParsingError: Error { + case invalidNTPPDU(String) +} + +/// The leap indicator warning of an impending leap second to be inserted or deleted in the last minute of the +/// current month. +internal enum KronosLeapIndicator: Int8 { + case noWarning, sixtyOneSeconds, fiftyNineSeconds, alarm + + /// Human readable value of the leap warning. + var description: String { + switch self { + case .noWarning: + return "No warning" + case .sixtyOneSeconds: + return "Last minute of the day has 61 seconds" + case .fiftyNineSeconds: + return "Last minute of the day has 59 seconds" + case .alarm: + return "Unknown (clock unsynchronized)" + } + } +} + +/// The connection mode. +internal enum KronosMode: Int8 { + case reserved, symmetricActive, symmetricPassive, client, server, broadcast, reservedNTP, unknown +} + +/// Mode representing the stratum level of the clock. +internal enum KronosStratum: Int8 { + case unspecified, primary, secondary, invalid + + init(value: Int8) { + switch value { + case 0: + self = .unspecified + + case 1: + self = .primary + + case 0 ..< 15: + self = .secondary + + default: + self = .invalid + } + } +} + +/// Server or reference clock. This value is generated based on the server stratum. +/// +/// - ReferenceClock: Contains the sourceID and the description for the reference clock (stratum 1). +/// - Debug(id): Contains the kiss code for debug purposes (stratum 0). +/// - ReferenceIdentifier(id): The reference identifier of the server (stratum > 1). +internal enum KronosClockSource { + case referenceClock(id: UInt32, description: String) + case debug(id: UInt32) + case referenceIdentifier(id: UInt32) + + init(stratum: KronosStratum, sourceID: UInt32) { + switch stratum { + case .unspecified: + self = .debug(id: sourceID) + + case .primary: + let (id, description) = KronosClockSource.description(fromID: sourceID) + self = .referenceClock(id: id, description: description) + + case .secondary, .invalid: + self = .referenceIdentifier(id: sourceID) + } + } + + /// The id for the reference clock (IANA, stratum 1), debug (stratum 0) or referenceIdentifier + var ID: UInt32 { + switch self { + case .referenceClock(let id, _): + return id + + case .debug(let id): + return id + + case .referenceIdentifier(let id): + return id + } + } + + private static func description(fromID sourceID: UInt32) -> (UInt32, String) { + let sourceMap: [UInt32: String] = [ + 0x47505300: "Global Position System", + 0x47414c00: "Galileo Positioning System", + 0x50505300: "Generic pulse-per-second", + 0x49524947: "Inter-Range Instrumentation Group", + 0x57575642: "LF Radio WWVB Ft. Collins, CO 60 kHz", + 0x44434600: "LF Radio DCF77 Mainflingen, DE 77.5 kHz", + 0x48424700: "LF Radio HBG Prangins, HB 75 kHz", + 0x4d534600: "LF Radio MSF Anthorn, UK 60 kHz", + 0x4a4a5900: "LF Radio JJY Fukushima, JP 40 kHz, Saga, JP 60 kHz", + 0x4c4f5243: "MF Radio LORAN C station, 100 kHz", + 0x54444600: "MF Radio Allouis, FR 162 kHz", + 0x43485500: "HF Radio CHU Ottawa, Ontario", + 0x57575600: "HF Radio WWV Ft. Collins, CO", + 0x57575648: "HF Radio WWVH Kauai, HI", + 0x4e495354: "NIST telephone modem", + 0x41435453: "ACTS telephone modem", + 0x55534e4f: "USNO telephone modem", + 0x50544200: "European telephone modem", + 0x4c4f434c: "Uncalibrated local clock", + 0x4345534d: "Calibrated Cesium clock", + 0x5242444d: "Calibrated Rubidium clock", + 0x4f4d4547: "OMEGA radio navigation system", + 0x44434e00: "DCN routing protocol", + 0x54535000: "TSP time protocol", + 0x44545300: "Digital Time Service", + 0x41544f4d: "Atomic clock (calibrated)", + 0x564c4600: "VLF radio (OMEGA,, etc.)", + 0x31505053: "External 1 PPS input", + 0x46524545: "(Internal clock)", + 0x494e4954: "(Initialization)", + ] + + return (sourceID, sourceMap[sourceID] ?? "NULL") + } +} diff --git a/Sources/Datadog/Kronos/KronosTimeFreeze.swift b/Sources/Datadog/Kronos/KronosTimeFreeze.swift new file mode 100644 index 0000000000..856796cc98 --- /dev/null +++ b/Sources/Datadog/Kronos/KronosTimeFreeze.swift @@ -0,0 +1,96 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +private let kUptimeKey = "Uptime" +private let kTimestampKey = "Timestamp" +private let kOffsetKey = "Offset" + +internal struct KronosTimeFreeze { + private let uptime: TimeInterval + private let timestamp: TimeInterval + private let offset: TimeInterval + + /// The stable timestamp adjusted by the most accurate offset known so far. + var adjustedTimestamp: TimeInterval { + return self.offset + self.stableTimestamp + } + + /// The stable timestamp (calculated based on the uptime); note that this doesn't have sub-seconds + /// precision. See `systemUptime()` for more information. + var stableTimestamp: TimeInterval { + return (KronosTimeFreeze.systemUptime() - self.uptime) + self.timestamp + } + + /// Time interval between now and the time the NTP response represented by this TimeFreeze was received. + var timeSinceLastNtpSync: TimeInterval { + return KronosTimeFreeze.systemUptime() - uptime + } + + init(offset: TimeInterval) { + self.offset = offset + self.timestamp = kronosCurrentTime() + self.uptime = KronosTimeFreeze.systemUptime() + } + + init?(from dictionary: [String: TimeInterval]) { + guard let uptime = dictionary[kUptimeKey], + let timestamp = dictionary[kTimestampKey], + let offset = dictionary[kOffsetKey] else { + return nil + } + + let currentUptime = KronosTimeFreeze.systemUptime() + let currentTimestamp = kronosCurrentTime() + let currentBoot = currentUptime - currentTimestamp + let previousBoot = uptime - timestamp + if rint(currentBoot) - rint(previousBoot) != 0 { + return nil + } + + self.uptime = uptime + self.timestamp = timestamp + self.offset = offset + } + + /// Convert this TimeFreeze to a dictionary representation. + /// + /// - returns: A dictionary representation. + func toDictionary() -> [String: TimeInterval] { + return [ + kUptimeKey: self.uptime, + kTimestampKey: self.timestamp, + kOffsetKey: self.offset, + ] + } + + /// Returns a high-resolution measurement of system uptime, that continues ticking through device sleep + /// *and* user- or system-generated clock adjustments. This allows for stable differences to be calculated + /// between timestamps. + /// + /// Note: Due to an issue in BSD/darwin, sub-second precision will be lost; + /// see: https://github.com/darwin-on-arm/xnu/blob/master/osfmk/kern/clock.c#L522. + /// + /// - returns: An Int measurement of system uptime in microseconds. + static func systemUptime() -> TimeInterval { + var mib = [CTL_KERN, KERN_BOOTTIME] + var size = MemoryLayout.stride + var bootTime = timeval() + + let bootTimeError = sysctl(&mib, u_int(mib.count), &bootTime, &size, nil, 0) != 0 + assert(!bootTimeError, "system clock error: kernel boot time unavailable") + + let now = kronosCurrentTime() + let uptime = Double(bootTime.tv_sec) + Double(bootTime.tv_usec) / 1_000_000 + assert(now >= uptime, "inconsistent clock state: system time precedes boot time") + + return now - uptime + } +} diff --git a/Sources/Datadog/Kronos/KronosTimeStorage.swift b/Sources/Datadog/Kronos/KronosTimeStorage.swift new file mode 100644 index 0000000000..d633ea1801 --- /dev/null +++ b/Sources/Datadog/Kronos/KronosTimeStorage.swift @@ -0,0 +1,71 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +/// Defines where the user defaults are stored +internal enum KronosTimeStoragePolicy { + /// Uses `UserDefaults.Standard` + case standard + /// Attempts to use the specified App Group ID (which is the String) to access shared storage. + case appGroup(String) + + /// Creates an instance + /// + /// - parameter appGroupID: The App Group ID that maps to a shared container for `UserDefaults`. If this + /// is nil, the resulting instance will be `.standard` + init(appGroupID: String?) { + if let appGroupID = appGroupID { + self = .appGroup(appGroupID) + } else { + self = .standard + } + } +} + +/// Handles saving and retrieving instances of `KronosTimeFreeze` for quick retrieval +internal struct KronosTimeStorage { + private var userDefaults: UserDefaults + private let kDefaultsKey = "KronosStableTime" + + /// The most recent stored `TimeFreeze`. Getting retrieves from the UserDefaults defined by the storage + /// policy. Setting sets the value in UserDefaults + var stableTime: KronosTimeFreeze? { + get { + guard let stored = self.userDefaults.value(forKey: kDefaultsKey) as? [String: TimeInterval], + let previousStableTime = KronosTimeFreeze(from: stored) else { + return nil + } + + return previousStableTime + } + + set { + guard let newFreeze = newValue else { + return + } + + self.userDefaults.set(newFreeze.toDictionary(), forKey: kDefaultsKey) + } + } + + /// Creates an instance + /// + /// - parameter storagePolicy: Defines the storage location of `UserDefaults` + init(storagePolicy: KronosTimeStoragePolicy) { + switch storagePolicy { + case .standard: + self.userDefaults = .standard + case .appGroup(let groupName): + let sharedDefaults = UserDefaults(suiteName: groupName) + assert(sharedDefaults != nil, "Could not create UserDefaults for group: '\(groupName)'") + self.userDefaults = sharedDefaults ?? .standard + } + } +} diff --git a/Tests/DatadogTests/Datadog/Kronos/KronosClockTests.swift b/Tests/DatadogTests/Datadog/Kronos/KronosClockTests.swift new file mode 100644 index 0000000000..9aa9f735d5 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Kronos/KronosClockTests.swift @@ -0,0 +1,54 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import XCTest +@testable import Datadog + +final class KronosClockTests: XCTestCase { + override func setUp() { + super.setUp() + KronosClock.reset() + } + + func testFirst() { + let expectation = self.expectation(description: "Clock sync calls first closure") + KronosClock.sync(first: { date, _ in + XCTAssertNotNil(date) + expectation.fulfill() + }) + + self.waitForExpectations(timeout: 2) + } + + func testLast() { + let expectation = self.expectation(description: "Clock sync calls last closure") + KronosClock.sync(completion: { date, offset in + XCTAssertNotNil(date) + XCTAssertNotNil(offset) + expectation.fulfill() + }) + + self.waitForExpectations(timeout: 20) + } + + func testBoth() { + let firstExpectation = self.expectation(description: "Clock sync calls first closure") + let lastExpectation = self.expectation(description: "Clock sync calls last closure") + KronosClock.sync( + first: { _, _ in + firstExpectation.fulfill() + }, + completion: { _, _ in + lastExpectation.fulfill() + } + ) + + self.waitForExpectations(timeout: 20) + } +} diff --git a/Tests/DatadogTests/Datadog/Kronos/KronosDNSResolverTests.swift b/Tests/DatadogTests/Datadog/Kronos/KronosDNSResolverTests.swift new file mode 100644 index 0000000000..e7d07f83b1 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Kronos/KronosDNSResolverTests.swift @@ -0,0 +1,74 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import XCTest +@testable import Datadog + +final class KronosDNSResolverTests: XCTestCase { + func testResolveOneIP() { + let expectation = self.expectation(description: "Query host's DNS for a single IP") + KronosDNSResolver.resolve(host: "test.com") { addresses in + XCTAssertEqual(addresses.count, 1) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 5) + } + + func testResolveMultipleIP() { + let expectation = self.expectation(description: "Query host's DNS for multiple IPs") + KronosDNSResolver.resolve(host: "pool.ntp.org") { addresses in + XCTAssertGreaterThan(addresses.count, 1) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 5) + } + + func testResolveIPv6() { + let expectation = self.expectation(description: "Query host's DNS that supports IPv6") + KronosDNSResolver.resolve(host: "ipv6friday.org") { addresses in + XCTAssertGreaterThan(addresses.count, 0) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 5) + } + + func testInvalidIP() { + let expectation = self.expectation(description: "Query invalid host's DNS") + KronosDNSResolver.resolve(host: "l33t.h4x") { addresses in + XCTAssertEqual(addresses.count, 0) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 5) + } + + func testTimeout() { + let expectation = self.expectation(description: "DNS times out") + KronosDNSResolver.resolve(host: "ip6.nl", timeout: 0) { addresses in + XCTAssertEqual(addresses.count, 0) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 1.0) + } + + func testTemporaryRunloopHandling() { + let expectation = self.expectation(description: "Query works from async GCD queues") + DispatchQueue(label: "Ephemeral DNS test queue").async { + KronosDNSResolver.resolve(host: "lyft.com") { _ in + expectation.fulfill() + } + } + + self.waitForExpectations(timeout: 5) + } +} diff --git a/Tests/DatadogTests/Datadog/Kronos/KronosNTPClientTests.swift b/Tests/DatadogTests/Datadog/Kronos/KronosNTPClientTests.swift new file mode 100644 index 0000000000..45df7a964e --- /dev/null +++ b/Tests/DatadogTests/Datadog/Kronos/KronosNTPClientTests.swift @@ -0,0 +1,59 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import XCTest +@testable import Datadog + +final class KronosNTPClientTests: XCTestCase { + func testQueryIP() { + let expectation = self.expectation(description: "NTPClient queries single IPs") + + KronosDNSResolver.resolve(host: "time.apple.com") { addresses in + XCTAssertGreaterThan(addresses.count, 0) + + KronosNTPClient() + .query(ip: addresses.first!, version: 3, numberOfSamples: 1) { PDU in + XCTAssertNotNil(PDU) + + XCTAssertGreaterThanOrEqual(PDU!.version, 3) + XCTAssertTrue(PDU!.isValidResponse()) + + expectation.fulfill() + } + } + + self.waitForExpectations(timeout: 10) + } + + func testQueryPool() { + let expectation = self.expectation(description: "Offset from ref clock to local clock are accurate") + KronosNTPClient().query(pool: "0.pool.ntp.org", numberOfSamples: 1, maximumServers: 1) { offset, _, _ in + XCTAssertNotNil(offset) + + KronosNTPClient() + .query(pool: "0.pool.ntp.org", numberOfSamples: 1, maximumServers: 1) { offset2, _, _ in + XCTAssertNotNil(offset2) + XCTAssertLessThan(abs(offset! - offset2!), 0.10) + expectation.fulfill() + } + } + + self.waitForExpectations(timeout: 10) + } + + func testQueryPoolWithIPv6() { + let expectation = self.expectation(description: "NTPClient queries a pool that supports IPv6") + KronosNTPClient().query(pool: "2.pool.ntp.org", numberOfSamples: 1, maximumServers: 1) { offset, _, _ in + XCTAssertNotNil(offset) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DatadogTests/Datadog/Kronos/KronosNTPPacketTests.swift b/Tests/DatadogTests/Datadog/Kronos/KronosNTPPacketTests.swift new file mode 100644 index 0000000000..4b4b5d7595 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Kronos/KronosNTPPacketTests.swift @@ -0,0 +1,50 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import XCTest +@testable import Datadog + +final class KronosNTPPacketTests: XCTestCase { + func testToData() { + var packet = KronosNTPPacket() + let data = packet.prepareToSend(transmitTime: 1_463_303_662.776_552) + XCTAssertEqual(data, Data(hex: "1b0004fa0001000000010000000000000000000000000000" + + "00000000000000000000000000000000dae2bc6ec6cc1c00")!) + } + + func testParseInvalidData() { + let network = Data(hex: "0badface")! + let PDU = try? KronosNTPPacket(data: network, destinationTime: 0) + XCTAssertNil(PDU) + } + + func testParseData() { + let network = Data(hex: "1c0203e90000065700000a68ada2c09cdae2d084a5a76d5fdae2d3354a529000dae2d32b" + + "b38bab46dae2d32bb38d9e00")! + let PDU = try? KronosNTPPacket(data: network, destinationTime: 0) + XCTAssertEqual(PDU?.version, 3) + XCTAssertEqual(PDU?.leap, KronosLeapIndicator.noWarning) + XCTAssertEqual(PDU?.mode, KronosMode.server) + XCTAssertEqual(PDU?.stratum, KronosStratum.secondary) + XCTAssertEqual(PDU?.poll, 3) + XCTAssertEqual(PDU?.precision, -23) + } + + func testParseTimeData() { + let network = Data(hex: "1c0203e90000065700000a68ada2c09cdae2d084a5a76d5fdae2d3354a529000dae2d32b" + + "b38bab46dae2d32bb38d9e00")! + let PDU = try? KronosNTPPacket(data: network, destinationTime: 0) + XCTAssertEqual(PDU?.rootDelay, 0.024_765_014_648_437_5) + XCTAssertEqual(PDU?.rootDispersion, 0.040_649_414_062_5) + XCTAssertEqual(PDU?.clockSource.ID, 2_913_124_508) + XCTAssertEqual(PDU?.referenceTime, 1_463_308_804.647_085_905_1) + XCTAssertEqual(PDU?.originTime, 1_463_309_493.290_322_303_8) + XCTAssertEqual(PDU?.receiveTime, 1_463_309_483.701_349_973_7) + } +} diff --git a/Tests/DatadogTests/Datadog/Kronos/KronosTimeStorageTests.swift b/Tests/DatadogTests/Datadog/Kronos/KronosTimeStorageTests.swift new file mode 100644 index 0000000000..2d06f14147 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Kronos/KronosTimeStorageTests.swift @@ -0,0 +1,54 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import XCTest +@testable import Datadog + +class KronosTimeStoragePolicyTests: XCTestCase { + func testInitWithStringGivesAppGroupType() { + let group = KronosTimeStoragePolicy(appGroupID: "com.test.something.mygreatapp") + if case KronosTimeStoragePolicy.appGroup(_) = group { + XCTAssert(true) + } else { + XCTAssert(false) + } + } + + func testInitWithNIlGivesStandardType() { + let group = KronosTimeStoragePolicy(appGroupID: nil) + if case KronosTimeStoragePolicy.standard = group { + XCTAssert(true) + } else { + XCTAssert(false) + } + } +} + +class TimeStorageTests: XCTestCase { + func testStoringAndRetrievingTimeFreeze() { + var storage = KronosTimeStorage(storagePolicy: .standard) + let sampleFreeze = KronosTimeFreeze(offset: 5_000.324_23) + storage.stableTime = sampleFreeze + + let fromDefaults = storage.stableTime + XCTAssertNotNil(fromDefaults) + XCTAssertEqual(sampleFreeze.toDictionary(), fromDefaults!.toDictionary()) + } + + func testRetrievingTimeFreezeAfterReboot() { + let sampleFreeze = KronosTimeFreeze(offset: 5_000.324_23) + var storedData = sampleFreeze.toDictionary() + storedData["Uptime"] = storedData["Uptime"]! + 10 + + let beforeRebootFreeze = KronosTimeFreeze(from: sampleFreeze.toDictionary()) + let afterRebootFreeze = KronosTimeFreeze(from: storedData) + XCTAssertNil(afterRebootFreeze) + XCTAssertNotNil(beforeRebootFreeze) + } +} diff --git a/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj b/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj index bc7d22c723..4a9312c4bd 100644 --- a/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj +++ b/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj @@ -14,8 +14,6 @@ 61C36425243752A600C4D4E6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61C36423243752A600C4D4E6 /* LaunchScreen.storyboard */; }; 61C36430243752A600C4D4E6 /* CTProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3642F243752A600C4D4E6 /* CTProjectTests.swift */; }; 61C3643B243752A600C4D4E6 /* CTProjectUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3643A243752A600C4D4E6 /* CTProjectUITests.swift */; }; - 9E9D5E8825F90FC6002F12A0 /* Kronos.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E9D5E8525F90FC6002F12A0 /* Kronos.xcframework */; }; - 9E9D5E8925F90FC6002F12A0 /* Kronos.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9E9D5E8525F90FC6002F12A0 /* Kronos.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9E9D5E8A25F90FC6002F12A0 /* DatadogObjc.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E9D5E8625F90FC6002F12A0 /* DatadogObjc.xcframework */; }; 9E9D5E8B25F90FC6002F12A0 /* DatadogObjc.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9E9D5E8625F90FC6002F12A0 /* DatadogObjc.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9E9D5E8C25F90FC6002F12A0 /* Datadog.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E9D5E8725F90FC6002F12A0 /* Datadog.xcframework */; }; @@ -52,7 +50,6 @@ 9EF87B8D26B04E1F00998076 /* CrashReporter.xcframework in Embed Frameworks */, 9E9D5E8D25F90FC6002F12A0 /* Datadog.xcframework in Embed Frameworks */, 9E9D5E8B25F90FC6002F12A0 /* DatadogObjc.xcframework in Embed Frameworks */, - 9E9D5E8925F90FC6002F12A0 /* Kronos.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -90,7 +87,6 @@ 9EF87B8C26B04E1F00998076 /* CrashReporter.xcframework in Frameworks */, 9E9D5E8C25F90FC6002F12A0 /* Datadog.xcframework in Frameworks */, 9E9D5E8A25F90FC6002F12A0 /* DatadogObjc.xcframework in Frameworks */, - 9E9D5E8825F90FC6002F12A0 /* Kronos.xcframework in Frameworks */, 9EF87B8E26B04E1F00998076 /* DatadogCrashReporting.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/dependency-manager-tests/carthage/Makefile b/dependency-manager-tests/carthage/Makefile index f5542c32e9..bd97559c06 100644 --- a/dependency-manager-tests/carthage/Makefile +++ b/dependency-manager-tests/carthage/Makefile @@ -23,6 +23,5 @@ test: @[ -e "Carthage/Build/Datadog.xcframework" ] && echo "Datadog.xcframework - OK" || { echo "Datadog.xcframework - missing"; false; } @[ -e "Carthage/Build/DatadogObjc.xcframework" ] && echo "DatadogObjc.xcframework - OK" || { echo "DatadogObjc.xcframework - missing"; false; } @[ -e "Carthage/Build/DatadogCrashReporting.xcframework" ] && echo "DatadogCrashReporting.xcframework - OK" || { echo "DatadogCrashReporting.xcframework - missing"; false; } - @[ -e "Carthage/Build/Kronos.xcframework" ] && echo "Kronos.xcframework - OK" || { echo "Kronos.xcframework - missing"; false; } @[ -e "Carthage/Build/CrashReporter.xcframework" ] && echo "CrashReporter.xcframework - OK" || { echo "CrashReporter.xcframework - missing"; false; } @echo "🧪 SUCCEEDED"