diff --git a/.buildkite/pipeline.full.yml b/.buildkite/pipeline.full.yml index 2b18dbfdc..5bb920a83 100644 --- a/.buildkite/pipeline.full.yml +++ b/.buildkite/pipeline.full.yml @@ -338,6 +338,8 @@ steps: - xcodebuild -allowProvisioningUpdates -workspace objective-c-ios.xcworkspace -scheme objective-c-ios -configuration Release -destination generic/platform=iOS -derivedDataPath DerivedData -quiet build GCC_TREAT_WARNINGS_AS_ERRORS=YES - echo "+++ Build Debug iOS Simulator" - xcodebuild -allowProvisioningUpdates -workspace objective-c-ios.xcworkspace -scheme objective-c-ios -configuration Debug -destination generic/platform=iOS\ Simulator -derivedDataPath DerivedData -quiet build GCC_TREAT_WARNINGS_AS_ERRORS=YES + - echo "+++ Build Debug Mac Catalyst" + - xcodebuild -allowProvisioningUpdates -workspace objective-c-ios.xcworkspace -scheme objective-c-ios -configuration Debug -destination generic/platform=macOS -derivedDataPath DerivedData -quiet build - label: 'examples/objective-c-osx' timeout_in_minutes: 30 diff --git a/.jazzy.yaml b/.jazzy.yaml index 32c45559d..6aa14e669 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -2,11 +2,11 @@ author_url: "https://www.bugsnag.com" author: "Bugsnag Inc" clean: false # avoid deleting docs/.git framework_root: "Bugsnag" -github_file_prefix: "https://github.com/bugsnag/bugsnag-cocoa/tree/v6.16.8/Bugsnag" +github_file_prefix: "https://github.com/bugsnag/bugsnag-cocoa/tree/v6.17.0/Bugsnag" github_url: "https://github.com/bugsnag/bugsnag-cocoa" hide_documentation_coverage: true module: "Bugsnag" -module_version: "6.16.8" +module_version: "6.17.0" objc: true output: "docs" readme: "README.md" diff --git a/Bugsnag.podspec.json b/Bugsnag.podspec.json index 1b9682ac6..bf58e7d81 100644 --- a/Bugsnag.podspec.json +++ b/Bugsnag.podspec.json @@ -1,6 +1,6 @@ { "name": "Bugsnag", - "version": "6.16.8", + "version": "6.17.0", "summary": "The Bugsnag crash reporting framework for Apple platforms.", "homepage": "https://bugsnag.com", "license": "MIT", @@ -9,7 +9,7 @@ }, "source": { "git": "https://github.com/bugsnag/bugsnag-cocoa.git", - "tag": "v6.16.8" + "tag": "v6.17.0" }, "frameworks": [ "Foundation", diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index 9cfdbc2f5..dede89eaf 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -627,6 +627,13 @@ 01468F5625876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */ = {isa = PBXBuildFile; fileRef = 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */; }; 01468F5725876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */ = {isa = PBXBuildFile; fileRef = 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */; }; 01468F5825876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */ = {isa = PBXBuildFile; fileRef = 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */; }; + 0154E20228070AEA009044E4 /* BSGRunContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 0154E20028070AEA009044E4 /* BSGRunContext.h */; }; + 0154E20328070AEA009044E4 /* BSGRunContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 0154E20028070AEA009044E4 /* BSGRunContext.h */; }; + 0154E20428070AEA009044E4 /* BSGRunContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 0154E20028070AEA009044E4 /* BSGRunContext.h */; }; + 0154E20528070AEA009044E4 /* BSGRunContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 0154E20128070AEA009044E4 /* BSGRunContext.m */; }; + 0154E20628070AEA009044E4 /* BSGRunContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 0154E20128070AEA009044E4 /* BSGRunContext.m */; }; + 0154E20728070AEA009044E4 /* BSGRunContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 0154E20128070AEA009044E4 /* BSGRunContext.m */; }; + 0154E20828070AEA009044E4 /* BSGRunContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 0154E20128070AEA009044E4 /* BSGRunContext.m */; }; 015F528425C15BB7000D1915 /* MRCCanary.m in Sources */ = {isa = PBXBuildFile; fileRef = 015F528325C15BB7000D1915 /* MRCCanary.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 015F528525C15BB7000D1915 /* MRCCanary.m in Sources */ = {isa = PBXBuildFile; fileRef = 015F528325C15BB7000D1915 /* MRCCanary.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 015F528625C15BB7000D1915 /* MRCCanary.m in Sources */ = {isa = PBXBuildFile; fileRef = 015F528325C15BB7000D1915 /* MRCCanary.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; @@ -798,9 +805,6 @@ 3A700AF824A6492F0068CD1B /* BugsnagError.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3A700A9124A63A8E0068CD1B /* BugsnagError.h */; }; 3A700AF924A6492F0068CD1B /* BugsnagDeviceWithState.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3A700A9224A63A8E0068CD1B /* BugsnagDeviceWithState.h */; }; 3A700AFA24A6492F0068CD1B /* BugsnagMetadata.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3A700A9324A63A8E0068CD1B /* BugsnagMetadata.h */; }; - CB10E53F250BA8DE00AF5824 /* BugsnagKVStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CBAA6AB9250BA6B100713376 /* BugsnagKVStoreTest.m */; }; - CB10E540250BA8DF00AF5824 /* BugsnagKVStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CBAA6AB9250BA6B100713376 /* BugsnagKVStoreTest.m */; }; - CB10E541250BA8E000AF5824 /* BugsnagKVStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CBAA6AB9250BA6B100713376 /* BugsnagKVStoreTest.m */; }; CB156241270707740097334C /* KSCrashNames_Test.m in Sources */ = {isa = PBXBuildFile; fileRef = CB156240270707740097334C /* KSCrashNames_Test.m */; }; CB156242270707740097334C /* KSCrashNames_Test.m in Sources */ = {isa = PBXBuildFile; fileRef = CB156240270707740097334C /* KSCrashNames_Test.m */; }; CB156243270707740097334C /* KSCrashNames_Test.m in Sources */ = {isa = PBXBuildFile; fileRef = CB156240270707740097334C /* KSCrashNames_Test.m */; }; @@ -819,20 +823,6 @@ CBA2249B251E429C00B87416 /* TestSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = CBA2249A251E429C00B87416 /* TestSupport.m */; }; CBA2249C251E429C00B87416 /* TestSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = CBA2249A251E429C00B87416 /* TestSupport.m */; }; CBA2249D251E429C00B87416 /* TestSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = CBA2249A251E429C00B87416 /* TestSupport.m */; }; - CBAA6AB5250BA01D00713376 /* BugsnagKVStore.c in Sources */ = {isa = PBXBuildFile; fileRef = CBAA6AB4250BA01D00713376 /* BugsnagKVStore.c */; }; - CBAA6AB6250BA01D00713376 /* BugsnagKVStore.c in Sources */ = {isa = PBXBuildFile; fileRef = CBAA6AB4250BA01D00713376 /* BugsnagKVStore.c */; }; - CBAA6AB7250BA01D00713376 /* BugsnagKVStore.c in Sources */ = {isa = PBXBuildFile; fileRef = CBAA6AB4250BA01D00713376 /* BugsnagKVStore.c */; }; - CBAA6AB8250BA01D00713376 /* BugsnagKVStore.c in Sources */ = {isa = PBXBuildFile; fileRef = CBAA6AB4250BA01D00713376 /* BugsnagKVStore.c */; }; - CBAA6ABB250BA70E00713376 /* BugsnagKVStore.h in Headers */ = {isa = PBXBuildFile; fileRef = CBAA6AB3250BA00500713376 /* BugsnagKVStore.h */; }; - CBAA6ABC250BA70E00713376 /* BugsnagKVStore.h in Headers */ = {isa = PBXBuildFile; fileRef = CBAA6AB3250BA00500713376 /* BugsnagKVStore.h */; }; - CBAA6ABD250BA70F00713376 /* BugsnagKVStore.h in Headers */ = {isa = PBXBuildFile; fileRef = CBAA6AB3250BA00500713376 /* BugsnagKVStore.h */; }; - CBAB4DD52510D2460092CBAA /* BugsnagKVStoreObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = CBAB4DD32510D2460092CBAA /* BugsnagKVStoreObjC.h */; }; - CBAB4DD62510D2460092CBAA /* BugsnagKVStoreObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = CBAB4DD32510D2460092CBAA /* BugsnagKVStoreObjC.h */; }; - CBAB4DD72510D2460092CBAA /* BugsnagKVStoreObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = CBAB4DD32510D2460092CBAA /* BugsnagKVStoreObjC.h */; }; - CBAB4DD82510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = CBAB4DD42510D2460092CBAA /* BugsnagKVStoreObjC.m */; }; - CBAB4DD92510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = CBAB4DD42510D2460092CBAA /* BugsnagKVStoreObjC.m */; }; - CBAB4DDA2510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = CBAB4DD42510D2460092CBAA /* BugsnagKVStoreObjC.m */; }; - CBAB4DDB2510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = CBAB4DD42510D2460092CBAA /* BugsnagKVStoreObjC.m */; }; CBB0928C2519F891007698BC /* BugsnagSystemState.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0928A2519F891007698BC /* BugsnagSystemState.m */; }; CBB0928D2519F891007698BC /* BugsnagSystemState.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0928A2519F891007698BC /* BugsnagSystemState.m */; }; CBB0928E2519F891007698BC /* BugsnagSystemState.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0928A2519F891007698BC /* BugsnagSystemState.m */; }; @@ -1328,6 +1318,8 @@ 0140D24725765F8F00FD0306 /* BSGUIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGUIKit.h; sourceTree = ""; }; 01468F5025876DC1002B0519 /* BSGNotificationBreadcrumbs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSGNotificationBreadcrumbs.h; sourceTree = ""; }; 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGNotificationBreadcrumbs.m; sourceTree = ""; }; + 0154E20028070AEA009044E4 /* BSGRunContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGRunContext.h; sourceTree = ""; }; + 0154E20128070AEA009044E4 /* BSGRunContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGRunContext.m; sourceTree = ""; }; 015F528325C15BB7000D1915 /* MRCCanary.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MRCCanary.m; sourceTree = ""; }; 0163BF5825823D8D008DC28B /* BSGNotificationBreadcrumbsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGNotificationBreadcrumbsTests.m; sourceTree = ""; }; 016875C4258D003200DFFF69 /* NSUserDefaultsStub.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSUserDefaultsStub.h; sourceTree = ""; }; @@ -1347,6 +1339,7 @@ 019480D32625F3EB00E833ED /* BSGAppKitTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGAppKitTests.m; sourceTree = ""; }; 0195FC3B256BC81400DE6646 /* BugsnagEvent+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagEvent+Private.h"; sourceTree = ""; }; 0198762E2567D5AB000A7AF3 /* BugsnagStackframe+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagStackframe+Private.h"; sourceTree = ""; }; + 019D165A2822B791009B35C9 /* BSGDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGDefines.h; sourceTree = ""; }; 01A2C540271EB9B300A27B23 /* BSG_Symbolicate.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = BSG_Symbolicate.c; sourceTree = ""; }; 01A2C541271EB9B300A27B23 /* BSG_Symbolicate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSG_Symbolicate.h; sourceTree = ""; }; 01A6176B2733CFEA00024A0B /* TestHost-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TestHost-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1405,11 +1398,6 @@ CB9103632502320A00E9D1E2 /* BugsnagApiClientTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugsnagApiClientTest.m; sourceTree = ""; }; CBA22499251E429C00B87416 /* TestSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestSupport.h; sourceTree = ""; }; CBA2249A251E429C00B87416 /* TestSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestSupport.m; sourceTree = ""; }; - CBAA6AB3250BA00500713376 /* BugsnagKVStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagKVStore.h; sourceTree = ""; }; - CBAA6AB4250BA01D00713376 /* BugsnagKVStore.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = BugsnagKVStore.c; sourceTree = ""; }; - CBAA6AB9250BA6B100713376 /* BugsnagKVStoreTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugsnagKVStoreTest.m; sourceTree = ""; }; - CBAB4DD32510D2460092CBAA /* BugsnagKVStoreObjC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagKVStoreObjC.h; sourceTree = ""; }; - CBAB4DD42510D2460092CBAA /* BugsnagKVStoreObjC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugsnagKVStoreObjC.m; sourceTree = ""; }; CBB0928A2519F891007698BC /* BugsnagSystemState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagSystemState.m; sourceTree = ""; }; CBB0928B2519F891007698BC /* BugsnagSystemState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugsnagSystemState.h; sourceTree = ""; }; CBCAF6F825A457F90095771F /* BSGFileLocations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGFileLocations.h; sourceTree = ""; }; @@ -1772,7 +1760,6 @@ 008966AC2486D43500DC48C2 /* BugsnagEventPersistLoadTest.m */, 008966B32486D43500DC48C2 /* BugsnagEventTests.m */, 008966B92486D43500DC48C2 /* BugsnagHandledStateTest.m */, - CBAA6AB9250BA6B100713376 /* BugsnagKVStoreTest.m */, 008966A62486D43400DC48C2 /* BugsnagMetadataRedactionTest.m */, 008966BF2486D43500DC48C2 /* BugsnagMetadataTests.m */, 008966CF2486D43600DC48C2 /* BugsnagNotifierTest.m */, @@ -1882,6 +1869,7 @@ 010FF28225ED2A8D00E4F2B0 /* BSGAppHangDetector.h */, 010FF28325ED2A8D00E4F2B0 /* BSGAppHangDetector.m */, 019480C42625EE9800E833ED /* BSGAppKit.h */, + 019D165A2822B791009B35C9 /* BSGDefines.h */, 0109939A273D13D800128BBE /* BSGFeatureFlagStore.h */, 0109939B273D13D800128BBE /* BSGFeatureFlagStore.m */, 01847D942644140F00ADA4C7 /* BSGInternalErrorReporter.h */, @@ -1889,6 +1877,8 @@ CBCF77A125010648004AF22A /* BSGJSONSerialization.h */, CBCF77A225010648004AF22A /* BSGJSONSerialization.m */, 008968152486DA5600DC48C2 /* BSGKeys.h */, + 0154E20028070AEA009044E4 /* BSGRunContext.h */, + 0154E20128070AEA009044E4 /* BSGRunContext.m */, 008968112486DA5600DC48C2 /* BSGSerialization.h */, 008968162486DA5600DC48C2 /* BSGSerialization.m */, 0140D24725765F8F00FD0306 /* BSGUIKit.h */, @@ -1896,10 +1886,6 @@ 01B79DA8267CC4A000C8CC5E /* BSGUtils.m */, 008968102486DA5600DC48C2 /* BugsnagCollections.h */, 008968172486DA5600DC48C2 /* BugsnagCollections.m */, - CBAA6AB4250BA01D00713376 /* BugsnagKVStore.c */, - CBAA6AB3250BA00500713376 /* BugsnagKVStore.h */, - CBAB4DD32510D2460092CBAA /* BugsnagKVStoreObjC.h */, - CBAB4DD42510D2460092CBAA /* BugsnagKVStoreObjC.m */, 008968142486DA5600DC48C2 /* BugsnagLogger.h */, 015F528325C15BB7000D1915 /* MRCCanary.m */, ); @@ -2081,7 +2067,6 @@ 3A700AA724A63ADC0068CD1B /* BugsnagMetadata.h in Headers */, 008967F72486DA4500DC48C2 /* BugsnagApiClient.h in Headers */, 008969CC2486DAD100DC48C2 /* BSG_KSMach.h in Headers */, - CBAB4DD52510D2460092CBAA /* BugsnagKVStoreObjC.h in Headers */, 008968282486DA5600DC48C2 /* BSGKeys.h in Headers */, CBCAF6FA25A457F90095771F /* BSGFileLocations.h in Headers */, 01A2C546271EB9B400A27B23 /* BSG_Symbolicate.h in Headers */, @@ -2095,9 +2080,9 @@ 008969DE2486DAD100DC48C2 /* BSG_KSCrashC.h in Headers */, 0089699F2486DAD100DC48C2 /* BSG_KSJSONCodecObjC.h in Headers */, 0089697E2486DAD100DC48C2 /* BSG_KSArchSpecific.h in Headers */, + 0154E20228070AEA009044E4 /* BSGRunContext.h in Headers */, 008969BA2486DAD100DC48C2 /* BSG_KSJSONCodec.h in Headers */, 008969E72486DAD100DC48C2 /* BSG_KSSystemInfoC.h in Headers */, - CBAA6ABB250BA70E00713376 /* BugsnagKVStore.h in Headers */, 00AD1F102486A17900A27979 /* BugsnagSessionTracker.h in Headers */, 0126F79B25DD510E008483C2 /* BSGEventUploadObjectOperation.h in Headers */, 008968F42486DAB800DC48C2 /* BugsnagSessionFileStore.h in Headers */, @@ -2185,7 +2170,6 @@ 3A700ABB24A63CFD0068CD1B /* BugsnagMetadata.h in Headers */, 008967F82486DA4500DC48C2 /* BugsnagApiClient.h in Headers */, 008969CD2486DAD100DC48C2 /* BSG_KSMach.h in Headers */, - CBAB4DD62510D2460092CBAA /* BugsnagKVStoreObjC.h in Headers */, 008968292486DA5600DC48C2 /* BSGKeys.h in Headers */, CBCAF6FB25A457F90095771F /* BSGFileLocations.h in Headers */, 01A2C547271EB9B400A27B23 /* BSG_Symbolicate.h in Headers */, @@ -2199,9 +2183,9 @@ 008969DF2486DAD100DC48C2 /* BSG_KSCrashC.h in Headers */, 008969A02486DAD100DC48C2 /* BSG_KSJSONCodecObjC.h in Headers */, 0089697F2486DAD100DC48C2 /* BSG_KSArchSpecific.h in Headers */, + 0154E20328070AEA009044E4 /* BSGRunContext.h in Headers */, 008969BB2486DAD100DC48C2 /* BSG_KSJSONCodec.h in Headers */, 008969E82486DAD100DC48C2 /* BSG_KSSystemInfoC.h in Headers */, - CBAA6ABC250BA70E00713376 /* BugsnagKVStore.h in Headers */, 00AD1F112486A17900A27979 /* BugsnagSessionTracker.h in Headers */, 008968F52486DAB800DC48C2 /* BugsnagSessionFileStore.h in Headers */, 0126F79C25DD510E008483C2 /* BSGEventUploadObjectOperation.h in Headers */, @@ -2289,7 +2273,6 @@ 3A700ACF24A63D110068CD1B /* BugsnagMetadata.h in Headers */, 008967F92486DA4500DC48C2 /* BugsnagApiClient.h in Headers */, 008969CE2486DAD100DC48C2 /* BSG_KSMach.h in Headers */, - CBAB4DD72510D2460092CBAA /* BugsnagKVStoreObjC.h in Headers */, 0089682A2486DA5600DC48C2 /* BSGKeys.h in Headers */, CBCAF6FC25A457F90095771F /* BSGFileLocations.h in Headers */, 01A2C548271EB9B400A27B23 /* BSG_Symbolicate.h in Headers */, @@ -2303,9 +2286,9 @@ 008969E02486DAD100DC48C2 /* BSG_KSCrashC.h in Headers */, 008969A12486DAD100DC48C2 /* BSG_KSJSONCodecObjC.h in Headers */, 008969802486DAD100DC48C2 /* BSG_KSArchSpecific.h in Headers */, + 0154E20428070AEA009044E4 /* BSGRunContext.h in Headers */, 008969BC2486DAD100DC48C2 /* BSG_KSJSONCodec.h in Headers */, 008969E92486DAD100DC48C2 /* BSG_KSSystemInfoC.h in Headers */, - CBAA6ABD250BA70F00713376 /* BugsnagKVStore.h in Headers */, 00AD1F122486A17900A27979 /* BugsnagSessionTracker.h in Headers */, 008968F62486DAB800DC48C2 /* BugsnagSessionFileStore.h in Headers */, 0126F79D25DD510E008483C2 /* BSGEventUploadObjectOperation.h in Headers */, @@ -2650,7 +2633,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CBAA6AB5250BA01D00713376 /* BugsnagKVStore.c in Sources */, + 0154E20528070AEA009044E4 /* BSGRunContext.m in Sources */, 008969992486DAD100DC48C2 /* BSG_KSMach_Arm64.c in Sources */, 008967E82486DA2D00DC48C2 /* BugsnagErrorTypes.m in Sources */, 0126F7AE25DD5118008483C2 /* BSGEventUploadFileOperation.m in Sources */, @@ -2707,7 +2690,6 @@ 008969CF2486DAD100DC48C2 /* BSG_KSCrashState.m in Sources */, 01840B7225DC26E200F95648 /* BSGEventUploader.m in Sources */, 008968C32486DA9600DC48C2 /* BugsnagUser.m in Sources */, - CBAB4DD82510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */, 01CB95C2278F0C830077744A /* BSG_KSFile.c in Sources */, 008968A72486DA9600DC48C2 /* BugsnagSession.m in Sources */, 0089683A2486DA6C00DC48C2 /* BugsnagMetadata.m in Sources */, @@ -2813,7 +2795,6 @@ CB9103642502320A00E9D1E2 /* BugsnagApiClientTest.m in Sources */, 008967602486D43700DC48C2 /* BugsnagBreadcrumbsTest.m in Sources */, 01B74E9C27903326004B9765 /* BSG_KSFileTests.m in Sources */, - CB10E53F250BA8DE00AF5824 /* BugsnagKVStoreTest.m in Sources */, CB6419AB25A73E8C00613D25 /* BSGStorageMigratorTests.m in Sources */, E701FAA72490EF77008D842F /* ClientApiValidationTest.m in Sources */, 008967362486D43700DC48C2 /* BugsnagMetadataTests.m in Sources */, @@ -2825,7 +2806,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CBAA6AB6250BA01D00713376 /* BugsnagKVStore.c in Sources */, CB33CD022703438400C76656 /* BSG_KSCrashNames.c in Sources */, 0089699A2486DAD100DC48C2 /* BSG_KSMach_Arm64.c in Sources */, 008967E92486DA2D00DC48C2 /* BugsnagErrorTypes.m in Sources */, @@ -2884,7 +2864,6 @@ 01099397273D123800128BBE /* BugsnagFeatureFlag.m in Sources */, 01840B7325DC26E200F95648 /* BSGEventUploader.m in Sources */, 008968C42486DA9600DC48C2 /* BugsnagUser.m in Sources */, - CBAB4DD92510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */, 008968A82486DA9600DC48C2 /* BugsnagSession.m in Sources */, 0089683B2486DA6C00DC48C2 /* BugsnagMetadata.m in Sources */, 008969F72486DAD100DC48C2 /* BSG_KSCrash.m in Sources */, @@ -2895,6 +2874,7 @@ 008969AF2486DAD100DC48C2 /* NSError+BSG_SimpleConstructor.m in Sources */, 008968CC2486DA9600DC48C2 /* BugsnagThread.m in Sources */, 0126F78F25DD508C008483C2 /* BSGEventUploadOperation.m in Sources */, + 0154E20628070AEA009044E4 /* BSGRunContext.m in Sources */, 008968032486DA4500DC48C2 /* BSGConnectivity.m in Sources */, CBE9062E25A34DAB0045B965 /* BSGStorageMigratorV0V1.m in Sources */, 010FF28825ED2A8D00E4F2B0 /* BSGAppHangDetector.m in Sources */, @@ -2920,7 +2900,6 @@ E701FAA02490EF4A008D842F /* BugsnagApiValidationTest.m in Sources */, 0089677F2486D43700DC48C2 /* KSLogger_Tests.m in Sources */, 008967342486D43700DC48C2 /* BugsnagClientTests.m in Sources */, - CB10E540250BA8DF00AF5824 /* BugsnagKVStoreTest.m in Sources */, CBCF77AC250142E0004AF22A /* BSGJSONSerializationTests.m in Sources */, 0089679A2486D43700DC48C2 /* FileBasedTestCase.m in Sources */, 008967912486D43700DC48C2 /* KSJSONCodec_Tests.m in Sources */, @@ -2997,7 +2976,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CBAA6AB7250BA01D00713376 /* BugsnagKVStore.c in Sources */, + 0154E20728070AEA009044E4 /* BSGRunContext.m in Sources */, 0089699B2486DAD100DC48C2 /* BSG_KSMach_Arm64.c in Sources */, 008967EA2486DA2D00DC48C2 /* BugsnagErrorTypes.m in Sources */, 008968742486DA9500DC48C2 /* BugsnagDevice.m in Sources */, @@ -3054,7 +3033,6 @@ 008969D12486DAD100DC48C2 /* BSG_KSCrashState.m in Sources */, 01840B7425DC26E200F95648 /* BSGEventUploader.m in Sources */, 008968C52486DA9600DC48C2 /* BugsnagUser.m in Sources */, - CBAB4DDA2510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */, 01CB95C4278F0C830077744A /* BSG_KSFile.c in Sources */, 008968A92486DA9600DC48C2 /* BugsnagSession.m in Sources */, 0089683C2486DA6C00DC48C2 /* BugsnagMetadata.m in Sources */, @@ -3092,7 +3070,6 @@ 008967802486D43700DC48C2 /* KSLogger_Tests.m in Sources */, 008967352486D43700DC48C2 /* BugsnagClientTests.m in Sources */, 0089679B2486D43700DC48C2 /* FileBasedTestCase.m in Sources */, - CB10E541250BA8E000AF5824 /* BugsnagKVStoreTest.m in Sources */, 008967922486D43700DC48C2 /* KSJSONCodec_Tests.m in Sources */, 008967742486D43700DC48C2 /* KSSysCtl_Tests.m in Sources */, CBDD6D0F25AC3EFF00A2E12B /* BSGStorageMigratorTests.m in Sources */, @@ -3170,7 +3147,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CBAA6AB8250BA01D00713376 /* BugsnagKVStore.c in Sources */, + 0154E20828070AEA009044E4 /* BSGRunContext.m in Sources */, CB33CD042703438400C76656 /* BSG_KSCrashNames.c in Sources */, E7462909248907E500F92D67 /* BSG_KSMach_x86_32.c in Sources */, E746290B248907E500F92D67 /* BSG_KSMach_Arm.c in Sources */, @@ -3221,7 +3198,6 @@ 008968AA2486DA9600DC48C2 /* BugsnagSession.m in Sources */, 008968982486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, 008968B52486DA9600DC48C2 /* BugsnagDeviceWithState.m in Sources */, - CBAB4DDB2510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */, 00AD1F2A2486A17900A27979 /* BSGCrashSentry.m in Sources */, 008968052486DA4500DC48C2 /* BSGConnectivity.m in Sources */, 0126F7A125DD510E008483C2 /* BSGEventUploadObjectOperation.m in Sources */, diff --git a/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m index ac6f0f426..ac04f801e 100644 --- a/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m +++ b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m @@ -39,6 +39,7 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration #endif _breadcrumbSink = breadcrumbSink; _notificationNameMap = @{ + @"NSProcessInfoThermalStateDidChangeNotification" : @"Thermal State Changed", // Using string to avoid availability issues NSUndoManagerDidRedoChangeNotification : @"Redo Operation", NSUndoManagerDidUndoChangeNotification : @"Undo Operation", #if TARGET_OS_TV @@ -227,6 +228,20 @@ - (void)start { object:nil]; } #endif + +#if TARGET_OS_IOS + [self.notificationCenter addObserver:self + selector:@selector(orientationDidChange:) + name:UIDeviceOrientationDidChangeNotification + object:nil]; +#endif + + if (@available(iOS 11.0, tvOS 11.0, *)) { + [self.notificationCenter addObserver:self + selector:@selector(thermalStateDidChange:) + name:NSProcessInfoThermalStateDidChangeNotification + object:nil]; + } } // Navigation events @@ -375,4 +390,47 @@ - (void)addBreadcrumbForControlNotification:(__attribute__((unused)) NSNotificat #endif } +#pragma mark - + +#if TARGET_OS_IOS + +- (void)orientationDidChange:(NSNotification *)notification { + UIDevice *device = notification.object; + + static UIDeviceOrientation previousOrientation; + if (device.orientation == UIDeviceOrientationUnknown || + device.orientation == previousOrientation) { + return; + } + + NSMutableDictionary *metadata = [NSMutableDictionary dictionary]; + metadata[@"from"] = BSGStringFromDeviceOrientation(previousOrientation); + metadata[@"to"] = BSGStringFromDeviceOrientation(device.orientation); + previousOrientation = device.orientation; + + [self addBreadcrumbWithType:BSGBreadcrumbTypeState + forNotificationName:notification.name + metadata:metadata]; +} + +#endif + +- (void)thermalStateDidChange:(NSNotification *)notification API_AVAILABLE(ios(11.0), tvos(11.0)) { + NSProcessInfo *processInfo = notification.object; + + static NSProcessInfoThermalState previousThermalState; + if (processInfo.thermalState == previousThermalState) { + return; + } + + NSMutableDictionary *metadata = [NSMutableDictionary dictionary]; + metadata[@"from"] = BSGStringFromThermalState(previousThermalState); + metadata[@"to"] = BSGStringFromThermalState(processInfo.thermalState); + previousThermalState = processInfo.thermalState; + + [self addBreadcrumbWithType:BSGBreadcrumbTypeState + forNotificationName:notification.name + metadata:metadata]; +} + @end diff --git a/Bugsnag/BugsnagSessionTracker.h b/Bugsnag/BugsnagSessionTracker.h index aca682fe9..d26549a55 100644 --- a/Bugsnag/BugsnagSessionTracker.h +++ b/Bugsnag/BugsnagSessionTracker.h @@ -13,18 +13,15 @@ NS_ASSUME_NONNULL_BEGIN -typedef void (^SessionTrackerCallback)(BugsnagSession *_Nullable session); - @interface BugsnagSessionTracker : NSObject /** Create a new session tracker @param config The Bugsnag configuration to use - @param callback A callback invoked each time a new session is started @return A new session tracker */ -- (instancetype)initWithConfig:(BugsnagConfiguration *)config client:(nullable BugsnagClient *)client callback:(SessionTrackerCallback)callback; +- (instancetype)initWithConfig:(BugsnagConfiguration *)config client:(nullable BugsnagClient *)client; - (void)startWithNotificationCenter:(NSNotificationCenter *)notificationCenter isInForeground:(BOOL)isInForeground; diff --git a/Bugsnag/BugsnagSessionTracker.m b/Bugsnag/BugsnagSessionTracker.m index 188bf0ce0..2beefddbb 100644 --- a/Bugsnag/BugsnagSessionTracker.m +++ b/Bugsnag/BugsnagSessionTracker.m @@ -34,23 +34,16 @@ @interface BugsnagSessionTracker () @property (weak, nonatomic) BugsnagClient *client; @property (strong, nonatomic) BSGSessionUploader *sessionUploader; @property (strong, nonatomic) NSDate *backgroundStartTime; - -/** - * Called when a session is altered - */ -@property (nonatomic, strong, readonly) SessionTrackerCallback callback; - @property (nonatomic) NSMutableDictionary *extraRuntimeInfo; @end @implementation BugsnagSessionTracker -- (instancetype)initWithConfig:(BugsnagConfiguration *)config client:(BugsnagClient *)client callback:(SessionTrackerCallback)callback { +- (instancetype)initWithConfig:(BugsnagConfiguration *)config client:(BugsnagClient *)client { if ((self = [super init])) { _config = config; _client = client; _sessionUploader = [[BSGSessionUploader alloc] initWithConfig:config notifier:client.notifier]; - _callback = callback; _extraRuntimeInfo = [NSMutableDictionary new]; } return self; @@ -118,7 +111,7 @@ - (void)setCodeBundleId:(NSString *)codeBundleId { - (void)pauseSession { self.currentSession.stopped = YES; - self.callback(nil); + BSGSessionUpdateRunContext(nil); } - (BOOL)resumeSession { @@ -130,7 +123,7 @@ - (BOOL)resumeSession { } else { BOOL stopped = session.isStopped; session.stopped = NO; - self.callback(session); + BSGSessionUpdateRunContext(session); return stopped; } } @@ -185,7 +178,7 @@ - (void)startNewSession { self.currentSession = newSession; - self.callback(newSession); + BSGSessionUpdateRunContext(newSession); [self.sessionUploader uploadSession:newSession]; } @@ -213,7 +206,7 @@ - (void)registerExistingSession:(NSString *)sessionId self.currentSession.handledCount = handledCount; self.currentSession.unhandledCount = unhandledCount; } - self.callback(self.currentSession); + BSGSessionUpdateRunContext(self.currentSession); } #pragma mark - Handling events @@ -243,7 +236,7 @@ - (void)incrementEventCountUnhandled:(BOOL)unhandled { } else { session.handledCount++; } - self.callback(session); + BSGSessionUpdateRunContext(session); } } diff --git a/Bugsnag/BugsnagSystemState.h b/Bugsnag/BugsnagSystemState.h index 697d62c03..8ee336f4b 100644 --- a/Bugsnag/BugsnagSystemState.h +++ b/Bugsnag/BugsnagSystemState.h @@ -12,21 +12,9 @@ #import "BSGKeys.h" -@class BugsnagSession; - #define SYSTEMSTATE_KEY_APP @"app" #define SYSTEMSTATE_KEY_DEVICE @"device" -#define SYSTEMSTATE_APP_WAS_TERMINATED @"wasTerminated" -#define SYSTEMSTATE_APP_IS_IN_FOREGROUND @"inForeground" -#define SYSTEMSTATE_APP_IS_LAUNCHING BSGKeyIsLaunching -#define SYSTEMSTATE_APP_VERSION @"version" -#define SYSTEMSTATE_APP_BUNDLE_VERSION @"bundleVersion" -#define SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE @"debuggerIsActive" - -#define SYSTEMSTATE_DEVICE_BOOT_TIME @"bootTime" -#define SYSTEMSTATE_DEVICE_CRITICAL_THERMAL_STATE @"criticalThermalState" - #define PLATFORM_WORD_SIZE sizeof(void*)*8 NS_ASSUME_NONNULL_BEGIN @@ -43,21 +31,11 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSUInteger consecutiveLaunchCrashes; -@property (readonly, nonatomic) BOOL lastLaunchCriticalThermalState; - -@property (readonly, nonatomic) BOOL lastLaunchTerminatedUnexpectedly; - -- (void)markLaunchCompleted; - /** * Purge all stored system state. */ - (void)purge; -- (void)setSession:(nullable BugsnagSession *)session; - -- (void)setThermalState:(NSProcessInfoThermalState)thermalState API_AVAILABLE(ios(11.0), tvos(11.0)); - @end NS_ASSUME_NONNULL_END diff --git a/Bugsnag/BugsnagSystemState.m b/Bugsnag/BugsnagSystemState.m index 837370e4a..2fa0b4cc8 100644 --- a/Bugsnag/BugsnagSystemState.m +++ b/Bugsnag/BugsnagSystemState.m @@ -20,57 +20,24 @@ #import "BSGJSONSerialization.h" #import "BSGUtils.h" #import "BSG_KSCrashState.h" -#import "BSG_KSMach.h" #import "BSG_KSSystemInfo.h" -#import "BSG_RFC3339DateTool.h" -#import "BugsnagKVStoreObjC.h" #import "BugsnagLogger.h" -#import "BugsnagSession+Private.h" -#import "BugsnagSessionTracker.h" -#import "BugsnagSystemState.h" #import static NSString * const ConsecutiveLaunchCrashesKey = @"consecutiveLaunchCrashes"; static NSString * const InternalKey = @"internal"; -static NSDictionary* loadPreviousState(BugsnagKVStore *kvstore, NSString *jsonPath) { - NSData *data = [NSData dataWithContentsOfFile:jsonPath]; - if(data == nil) { - return @{}; - } - +static NSDictionary * loadPreviousState(NSString *jsonPath) { NSError *error = nil; - NSMutableDictionary *state = [BSGJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; - if(error != nil) { - bsg_log_err(@"Invalid previous system state data: %@", error); - return @{}; - } - if(state == nil) { - bsg_log_err(@"Could not load previous system state"); - return @{}; - } + NSMutableDictionary *state = [BSGJSONSerialization JSONObjectWithContentsOfFile:jsonPath options:NSJSONReadingMutableContainers error:&error]; if(![state isKindOfClass:[NSMutableDictionary class]]) { - bsg_log_err(@"Previous system state has incorrect structure"); - return @{}; - } - - NSMutableDictionary *app = state[SYSTEMSTATE_KEY_APP]; - - // KV-store versions of these are authoritative - for (NSString *key in @[SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE, - SYSTEMSTATE_APP_IS_IN_FOREGROUND, - SYSTEMSTATE_APP_IS_LAUNCHING, - SYSTEMSTATE_APP_WAS_TERMINATED]) { - NSNumber *value = [kvstore NSBooleanForKey:key defaultValue:nil]; - if (value != nil) { - app[key] = value; + if (!(error.domain == NSCocoaErrorDomain && error.code == NSFileReadNoSuchFileError)) { + bsg_log_err(@"Could not load system_state.json: %@", error); } + return @{}; } - state[SYSTEMSTATE_KEY_DEVICE][SYSTEMSTATE_DEVICE_CRITICAL_THERMAL_STATE] = - [kvstore NSBooleanForKey:SYSTEMSTATE_DEVICE_CRITICAL_THERMAL_STATE defaultValue:nil]; - return state; } @@ -81,28 +48,9 @@ id blankIfNil(id value) { return value; } -static NSMutableDictionary* initCurrentState(BugsnagKVStore *kvstore, BugsnagConfiguration *config) { +static NSMutableDictionary * initCurrentState(BugsnagConfiguration *config) { NSDictionary *systemInfo = [BSG_KSSystemInfo systemInfo]; - bool isBeingDebugged = bsg_ksmachisBeingTraced(); - bool isInForeground = true; -#if TARGET_OS_OSX - // MacOS "active" serves the same purpose as "foreground" in iOS - isInForeground = [NSAPPLICATION sharedApplication].active; -#else - const BSG_KSCrash_State *crashState = bsg_kscrashstate_currentState(); - NSCParameterAssert(crashState != nil); - if (crashState) { - isInForeground = crashState->applicationIsInForeground; - } -#endif - - [kvstore deleteKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - [kvstore deleteKey:@"isActive"]; // Deprecated key - [kvstore setBoolean:isInForeground forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - [kvstore setBoolean:true forKey:SYSTEMSTATE_APP_IS_LAUNCHING]; - [kvstore setBoolean:isBeingDebugged forKey:SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE]; - NSMutableDictionary *app = [NSMutableDictionary new]; app[BSGKeyId] = blankIfNil(systemInfo[@BSG_KSSystemField_BundleID]); app[BSGKeyName] = blankIfNil(systemInfo[@BSG_KSSystemField_BundleName]); @@ -111,7 +59,6 @@ id blankIfNil(id value) { app[BSGKeyBundleVersion] = blankIfNil(systemInfo[@BSG_KSSystemField_BundleVersion]); app[BSGKeyMachoUUID] = systemInfo[@BSG_KSSystemField_AppUUID]; app[@"binaryArch"] = systemInfo[@BSG_KSSystemField_BinaryArch]; - app[@"inForeground"] = @(isInForeground); #if TARGET_OS_TV app[BSGKeyType] = @"tvOS"; #elif TARGET_OS_IOS @@ -119,10 +66,8 @@ id blankIfNil(id value) { #elif TARGET_OS_OSX app[BSGKeyType] = @"macOS"; #endif - app[SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE] = @(isBeingDebugged); NSMutableDictionary *device = [NSMutableDictionary new]; - device[SYSTEMSTATE_DEVICE_BOOT_TIME] = [BSG_RFC3339DateTool stringFromDate:systemInfo[@BSG_KSSystemField_BootTime]]; device[@"id"] = systemInfo[@BSG_KSSystemField_DeviceAppHash]; device[@"jailbroken"] = systemInfo[@BSG_KSSystemField_Jailbroken]; device[@"osBuild"] = systemInfo[@BSG_KSSystemField_OSVersion]; @@ -164,7 +109,6 @@ @interface BugsnagSystemState () @property(readwrite,atomic) NSDictionary *currentLaunchState; @property(readwrite,nonatomic) NSDictionary *lastLaunchState; @property(readonly,nonatomic) NSString *persistenceFilePath; -@property(readonly,nonatomic) BugsnagKVStore *kvStore; @end @@ -172,74 +116,15 @@ @implementation BugsnagSystemState - (instancetype)initWithConfiguration:(BugsnagConfiguration *)config { if ((self = [super init])) { - _kvStore = [BugsnagKVStore new]; _persistenceFilePath = [BSGFileLocations current].systemState; - _lastLaunchState = loadPreviousState(_kvStore, _persistenceFilePath); - _currentLaunchState = initCurrentState(_kvStore, config); + _lastLaunchState = loadPreviousState(_persistenceFilePath); + _currentLaunchState = initCurrentState(config); _consecutiveLaunchCrashes = [_lastLaunchState[InternalKey][ConsecutiveLaunchCrashesKey] unsignedIntegerValue]; - if (@available(iOS 11.0, tvOS 11.0, *)) { - [self setThermalState:NSProcessInfo.processInfo.thermalState]; - } [self sync]; - - __weak __typeof__(self) weakSelf = self; - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; -#if TARGET_OS_OSX - [center addObserverForName:NSApplicationWillTerminateNotification object:nil queue:nil - usingBlock:^(__attribute__((unused)) NSNotification * _Nonnull note) { - __strong __typeof__(self) strongSelf = weakSelf; - [strongSelf.kvStore setBoolean:YES forKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - // No need to update since we are shutting down. - }]; - // MacOS "active" serves the same purpose as "foreground" in iOS - [center addObserverForName:NSApplicationDidBecomeActiveNotification object:nil queue:nil - usingBlock:^(__attribute__((unused)) NSNotification * _Nonnull note) { - __strong __typeof__(self) strongSelf = weakSelf; - [strongSelf.kvStore setBoolean:YES forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - [strongSelf setValue:@YES forAppKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - }]; - [center addObserverForName:NSApplicationDidResignActiveNotification object:nil queue:nil - usingBlock:^(__attribute__((unused)) NSNotification * _Nonnull note) { - __strong __typeof__(self) strongSelf = weakSelf; - [strongSelf.kvStore setBoolean:NO forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - [strongSelf setValue:@NO forAppKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - }]; -#else - [center addObserverForName:UIApplicationWillTerminateNotification object:nil queue:nil - usingBlock:^(__attribute__((unused)) NSNotification * _Nonnull note) { - __strong __typeof__(self) strongSelf = weakSelf; - [strongSelf.kvStore setBoolean:YES forKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - // No need to update since we are shutting down. - }]; - [center addObserverForName:UIApplicationWillEnterForegroundNotification object:nil queue:nil - usingBlock:^(__attribute__((unused)) NSNotification * _Nonnull note) { - __strong __typeof__(self) strongSelf = weakSelf; - [strongSelf.kvStore setBoolean:YES forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - [strongSelf setValue:@YES forAppKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - }]; - [center addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:nil - usingBlock:^(__attribute__((unused)) NSNotification * _Nonnull note) { - __strong __typeof__(self) strongSelf = weakSelf; - [strongSelf.kvStore setBoolean:NO forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - [strongSelf setValue:@NO forAppKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - }]; - [center addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:nil - usingBlock:^(__attribute__((unused)) NSNotification * _Nonnull note) { - __strong __typeof__(self) strongSelf = weakSelf; - [strongSelf.kvStore setBoolean:YES forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - [strongSelf setValue:@YES forAppKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - }]; -#endif } return self; } -- (void)setSession:(nullable BugsnagSession *)session { - [self mutateLaunchState:^(NSMutableDictionary *state) { - state[BSGKeySession] = session ? BSGSessionToEventJson((BugsnagSession *_Nonnull)session) : nil; - }]; -} - - (void)setCodeBundleID:(NSString*)codeBundleID { [self setValue:codeBundleID forAppKey:BSGKeyCodeBundleId]; } @@ -248,18 +133,6 @@ - (void)setConsecutiveLaunchCrashes:(NSUInteger)consecutiveLaunchCrashes { [self setValue:@(_consecutiveLaunchCrashes = consecutiveLaunchCrashes) forKey:ConsecutiveLaunchCrashesKey inSection:InternalKey]; } -- (void)markLaunchCompleted { - [self.kvStore setBoolean:false forKey:SYSTEMSTATE_APP_IS_LAUNCHING]; -} - -- (void)setThermalState:(NSProcessInfoThermalState)thermalState { - if (thermalState == NSProcessInfoThermalStateCritical) { - [self.kvStore setBoolean:true forKey:SYSTEMSTATE_DEVICE_CRITICAL_THERMAL_STATE]; - } else { - [self.kvStore deleteKey:SYSTEMSTATE_DEVICE_CRITICAL_THERMAL_STATE]; - } -} - - (void)setValue:(id)value forAppKey:(NSString *)key { [self setValue:value forKey:key inSection:SYSTEMSTATE_KEY_APP]; } @@ -313,57 +186,7 @@ - (void)purge { if(![fm removeItemAtPath:self.persistenceFilePath error:&error]) { bsg_log_err(@"Could not remove persistence file: %@", error); } - [self.kvStore purge]; - self.lastLaunchState = loadPreviousState(self.kvStore, self.persistenceFilePath); -} - -// MARK: - - -- (BOOL)lastLaunchCriticalThermalState { - NSNumber *value = self.lastLaunchState[SYSTEMSTATE_KEY_DEVICE][SYSTEMSTATE_DEVICE_CRITICAL_THERMAL_STATE]; - return value.boolValue; -} - -- (BOOL)lastLaunchTerminatedUnexpectedly { - // App extensions have a different lifecycle and the heuristic used for finding app terminations rooted in fixable code does not apply - if ([BSG_KSSystemInfo isRunningInAppExtension]) { - return NO; - } - - NSDictionary *currentAppState = self.currentLaunchState[SYSTEMSTATE_KEY_APP]; - NSDictionary *previousAppState = self.lastLaunchState[SYSTEMSTATE_KEY_APP]; - NSDictionary *currentDeviceState = self.currentLaunchState[SYSTEMSTATE_KEY_DEVICE]; - NSDictionary *previousDeviceState = self.lastLaunchState[SYSTEMSTATE_KEY_DEVICE]; - - if ([previousAppState[SYSTEMSTATE_APP_WAS_TERMINATED] boolValue]) { - return NO; // The app terminated normally - } - - if ([previousAppState[SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE] boolValue]) { - return NO; // The debugger may have killed the app - } - - // If the app was in the background, we cannot determine whether the termination was unexpected - if (![previousAppState[SYSTEMSTATE_APP_IS_IN_FOREGROUND] boolValue]) { - return NO; - } - - // Ignore unexpected terminations that may have been due to the app being upgraded - NSString *currentAppVersion = currentAppState[SYSTEMSTATE_APP_VERSION]; - NSString *currentAppBundleVersion = currentAppState[SYSTEMSTATE_APP_BUNDLE_VERSION]; - if (!currentAppVersion || ![previousAppState[SYSTEMSTATE_APP_VERSION] isEqualToString:currentAppVersion] || - !currentAppBundleVersion || ![previousAppState[SYSTEMSTATE_APP_BUNDLE_VERSION] isEqualToString:currentAppBundleVersion]) { - return NO; - } - - id currentBootTime = currentDeviceState[SYSTEMSTATE_DEVICE_BOOT_TIME]; - id previousBootTime = previousDeviceState[SYSTEMSTATE_DEVICE_BOOT_TIME]; - BOOL didReboot = currentBootTime && previousBootTime && ![currentBootTime isEqual:previousBootTime]; - if (didReboot) { - return NO; // The app may have been terminated due to the reboot - } - - return YES; + self.lastLaunchState = loadPreviousState(self.persistenceFilePath); } @end diff --git a/Bugsnag/Client/BugsnagClient+Private.h b/Bugsnag/Client/BugsnagClient+Private.h index f3002012b..ac488f2fc 100644 --- a/Bugsnag/Client/BugsnagClient+Private.h +++ b/Bugsnag/Client/BugsnagClient+Private.h @@ -56,10 +56,6 @@ typedef void (^ BSGClientObserver)(BSGClientObserverEvent event, _Nullable id va @property (nonatomic) NSMutableDictionary *extraRuntimeInfo; -#if TARGET_OS_IOS -@property (strong, nullable, nonatomic) NSString *lastOrientation; -#endif - @property (strong, nonatomic) BugsnagMetadata *metadata; // Used in BugsnagReactNative @property (readonly, nonatomic) BugsnagNotifier *notifier; // Used in BugsnagReactNative @@ -78,17 +74,10 @@ typedef void (^ BSGClientObserver)(BSGClientObserverEvent event, _Nullable id va /// { /// "app": { /// "codeBundleId": "com.example.app", -/// "isLaunching": true /// }, /// "client": { /// "context": "MyViewController", /// }, -/// "deviceState": { -/// "batteryLevel": 0.5, -/// "charging": false, -/// "lowMemoryWarning": "2021-01-01T15:29:02.170Z", -/// "orientation": "portrait" -/// }, /// "user": { /// "id": "abc123", /// "name": "bob" @@ -122,8 +111,6 @@ typedef void (^ BSGClientObserver)(BSGClientObserverEvent event, _Nullable id va - (BugsnagDeviceWithState *)generateDeviceWithState:(NSDictionary *)systemInfo; -- (BugsnagEvent *)generateOutOfMemoryEvent; - - (void)notifyInternal:(BugsnagEvent *)event block:(nullable BugsnagOnErrorBlock)block; - (void)start; diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index 7d6609fb4..b20474d5f 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -35,16 +35,11 @@ #import "BSGJSONSerialization.h" #import "BSGKeys.h" #import "BSGNotificationBreadcrumbs.h" +#import "BSGRunContext.h" #import "BSGSerialization.h" #import "BSGUtils.h" -#import "BSG_KSCrash.h" #import "BSG_KSCrashC.h" -#import "BSG_KSCrashReport.h" -#import "BSG_KSCrashState.h" -#import "BSG_KSCrashType.h" -#import "BSG_KSMach.h" #import "BSG_KSSystemInfo.h" -#import "BSG_RFC3339DateTool.h" #import "Bugsnag.h" #import "BugsnagApp+Private.h" #import "BugsnagAppWithState+Private.h" @@ -99,12 +94,8 @@ void (*onCrash)(const BSG_KSCrashReportWriter *writer); } bsg_g_bugsnag_data; -static char sessionId[128]; -static char sessionStartDate[128]; static char *watchdogSentinelPath = NULL; static char *crashSentinelPath; -static NSUInteger handledCount; -static NSUInteger unhandledCount; /** * Handler executed when the application crashes. Writes information about the @@ -114,19 +105,25 @@ */ void BSSerializeDataCrashHandler(const BSG_KSCrashReportWriter *writer) { BOOL isCrash = YES; - if (sessionId[0]) { // a session is available - // persist session info - writer->addStringElement(writer, "id", (const char *) sessionId); - writer->addStringElement(writer, "startedAt", (const char *) sessionStartDate); - writer->addUIntegerElement(writer, "handledCount", handledCount); - NSUInteger unhandledEvents = unhandledCount + (isCrash ? 1 : 0); - writer->addUIntegerElement(writer, "unhandledCount", unhandledEvents); - } + BSGSessionWriteCrashReport(writer); + if (isCrash) { writer->addJSONFileElement(writer, "config", bsg_g_bugsnag_data.configPath); writer->addJSONElement(writer, "metaData", bsg_g_bugsnag_data.metadataJSON); writer->addJSONElement(writer, "state", bsg_g_bugsnag_data.stateJSON); + +#if TARGET_OS_IOS + if (bsg_runContext->batteryState != UIDeviceBatteryStateUnknown) { + writer->addFloatingPointElement(writer, "batteryLevel", bsg_runContext->batteryLevel); + writer->addBooleanElement(writer, "charging", bsg_runContext->batteryState >= UIDeviceBatteryStateCharging); + } + writer->addIntegerElement(writer, "orientation", bsg_runContext->lastKnownOrientation); +#endif + writer->addBooleanElement(writer, "isLaunching", bsg_runContext->isLaunching); + writer->addIntegerElement(writer, "thermalState", bsg_runContext->thermalState); + BugsnagBreadcrumbsWriteCrashReport(writer); + if (watchdogSentinelPath != NULL) { // Delete the file to indicate a handled termination unlink(watchdogSentinelPath); @@ -145,29 +142,6 @@ void BSSerializeDataCrashHandler(const BSG_KSCrashReportWriter *writer) { } } -/** - Save info about the current session to crash data. Ensures that session - data is written to unhandled error reports. - - @param session The current session - */ -void BSGWriteSessionCrashData(BugsnagSession *session) { - if (session == nil) { - sessionId[0] = 0; - sessionStartDate[0] = 0; - return; - } - - [session.id getCString:sessionId maxLength:sizeof(sessionId) encoding:NSUTF8StringEncoding]; - - NSString *dateString = [BSG_RFC3339DateTool stringFromDate:session.startedAt]; - [dateString getCString:sessionStartDate maxLength:sizeof(sessionStartDate) encoding:NSUTF8StringEncoding]; - - // record info for C JSON serialiser - handledCount = session.handledCount; - unhandledCount = session.unhandledCount; -} - // ============================================================================= // MARK: - @@ -182,8 +156,6 @@ @interface BugsnagClient () applicationIsInForeground]; + self.sessionTracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:self]; + [self.sessionTracker startWithNotificationCenter:center isInForeground:bsg_runContext->isForeground]; // Record a "Bugsnag Loaded" message [self addAutoBreadcrumbOfType:BSGBreadcrumbTypeState withMessage:@"Bugsnag loaded" andMetadata:nil]; @@ -391,8 +297,8 @@ - (void)appLaunchTimerFired:(__attribute__((unused)) NSTimer *)timer { - (void)markLaunchCompleted { bsg_log_debug(@"App has finished launching"); [self.appLaunchTimer invalidate]; - [self.state addMetadata:@NO withKey:BSGKeyIsLaunching toSection:BSGKeyApp]; - [self.systemState markLaunchCompleted]; + bsg_runContext->isLaunching = NO; + BSGRunContextUpdateTimestamp(); } - (void)sendLaunchCrashSynchronously { @@ -432,8 +338,8 @@ - (void)computeDidCrashLastLaunch { bsg_log_info(@"Last run terminated during an app hang."); didCrash = YES; } - else if (self.configuration.autoDetectErrors && self.systemState.lastLaunchTerminatedUnexpectedly) { - if (self.systemState.lastLaunchCriticalThermalState) { + else if (self.configuration.autoDetectErrors && BSGRunContextWasKilled()) { + if (BSGRunContextWasCriticalThermalState()) { bsg_log_info(@"Last run terminated during a critical thermal state."); if (self.configuration.enabledErrorTypes.thermalKills) { self.eventFromLastLaunch = [self generateThermalKillEvent]; @@ -451,12 +357,7 @@ - (void)computeDidCrashLastLaunch { self.appDidCrashLastLaunch = didCrash; - NSNumber *wasLaunching = ({ - // BugsnagSystemState's KV-store is now the reliable source of the isLaunching status. - self.systemState.lastLaunchState[SYSTEMSTATE_KEY_APP][SYSTEMSTATE_APP_IS_LAUNCHING]; - }); - - BOOL didCrashDuringLaunch = didCrash && wasLaunching.boolValue; + BOOL didCrashDuringLaunch = didCrash && BSGRunContextWasLaunching(); if (didCrashDuringLaunch) { self.systemState.consecutiveLaunchCrashes++; } else { @@ -489,28 +390,6 @@ - (void)applicationWillTerminate:(__unused NSNotification *)notification { #endif } -- (void)thermalStateDidChange:(NSNotification *)notification API_AVAILABLE(ios(11.0), tvos(11.0)) { - NSProcessInfo *processInfo = notification.object; - - [self.systemState setThermalState:processInfo.thermalState]; - - NSString *thermalStateString = BSGStringFromThermalState(processInfo.thermalState); - - [self.metadata addMetadata:thermalStateString - withKey:BSGKeyThermalState - toSection:BSGKeyDevice]; - - NSMutableDictionary *breadcrumbMetadata = [NSMutableDictionary dictionary]; - breadcrumbMetadata[@"from"] = BSGStringFromThermalState(self.lastThermalState); - breadcrumbMetadata[@"to"] = thermalStateString; - - [self addAutoBreadcrumbOfType:BSGBreadcrumbTypeState - withMessage:@"Thermal State Changed" - andMetadata:breadcrumbMetadata]; - - self.lastThermalState = processInfo.thermalState; -} - // ============================================================================= // MARK: - Session Tracking // ============================================================================= @@ -804,12 +683,19 @@ - (void)notifyInternal:(BugsnagEvent *_Nonnull)event return; } - // enhance device information with additional metadata - NSDictionary *deviceFields = [self.state getMetadataFromSection:BSGKeyDeviceState]; - - if (deviceFields) { - [event.metadata addMetadata:deviceFields toSection:BSGKeyDevice]; + // Device information that isn't part of `event.device` + NSMutableDictionary *deviceMetadata = [NSMutableDictionary dictionary]; +#if TARGET_OS_IOS + if (bsg_runContext->batteryState != UIDeviceBatteryStateUnknown) { + deviceMetadata[BSGKeyBatteryLevel] = @(bsg_runContext->batteryLevel); + // Our intepretation of "charging" really means "plugged in" + deviceMetadata[BSGKeyCharging] = bsg_runContext->batteryState >= UIDeviceBatteryStateCharging ? @YES : @NO; } +#endif + if (@available(iOS 11.0, tvOS 11.0, *)) { + deviceMetadata[BSGKeyThermalState] = BSGStringFromThermalState(bsg_runContext->thermalState); + } + [event.metadata addMetadata:deviceMetadata toSection:BSGKeyDevice]; // App hang events will already contain feature flags if (!event.featureFlagStore.count) { @@ -871,71 +757,9 @@ - (void)addAutoBreadcrumbForEvent:(BugsnagEvent *)event { - (void)addBreadcrumbWithBlock:(void (^)(BugsnagBreadcrumb *))block { [self.breadcrumbs addBreadcrumbWithBlock:block]; + BSGRunContextUpdateTimestamp(); } -/** - * Update the device status in response to a battery change notification - * - * @param notification The change notification - */ -#if TARGET_OS_IOS -- (void)batteryChanged:(__attribute__((unused)) NSNotification *)notification { - if (![UIDEVICE currentDevice]) { - return; - } - - NSNumber *batteryLevel = @([UIDEVICE currentDevice].batteryLevel); - BOOL charging = [UIDEVICE currentDevice].batteryState == UIDeviceBatteryStateCharging || - [UIDEVICE currentDevice].batteryState == UIDeviceBatteryStateFull; - - [self.state addMetadata:@{BSGKeyBatteryLevel: batteryLevel, - BSGKeyCharging: charging ? @YES : @NO} - toSection:BSGKeyDeviceState]; -} - -/** - * Called when an orientation change notification is received to record an - * equivalent breadcrumb. - * - * @param notification The orientation-change notification - */ -- (void)orientationDidChange:(NSNotification *)notification { - UIDevice *device = notification.object; - NSString *orientation = BSGStringFromDeviceOrientation(device.orientation); - - // No orientation, nothing to be done - if (!orientation) { - return; - } - - // Update the device orientation in metadata - [self.state addMetadata:orientation - withKey:BSGKeyOrientation - toSection:BSGKeyDeviceState]; - - // Short-circuit the exit if we don't have enough info to record a full breadcrumb - // or the orientation hasn't changed (false positive). - if (!self.lastOrientation || [self.lastOrientation isEqualToString:orientation]) { - self.lastOrientation = orientation; - return; - } - - // We have an orientation, it's not a dupe and we have a lastOrientation. - // Send a breadcrumb and preserve the orientation. - - NSMutableDictionary *breadcrumbMetadata = [NSMutableDictionary dictionary]; - breadcrumbMetadata[@"from"] = self.lastOrientation; - breadcrumbMetadata[@"to"] = orientation; - - [self addAutoBreadcrumbOfType:BSGBreadcrumbTypeState - withMessage:[self.notificationBreadcrumbs messageForNotificationName:notification.name] - andMetadata:breadcrumbMetadata]; - - self.lastOrientation = orientation; -} - -#endif - /** * A convenience safe-wrapper for conditionally recording automatic breadcrumbs * based on the configuration. @@ -1055,17 +879,18 @@ - (void)clearMetadataFromSection:(NSString *_Nonnull)sectionName // MARK: - event data population - (BugsnagAppWithState *)generateAppWithState:(NSDictionary *)systemInfo { - // Replicate the parts of a KSCrashReport that +[BugsnagAppWithState appWithDictionary:config:codeBundleId:] examines - NSDictionary *kscrashDict = @{BSGKeySystem: systemInfo, @"user": @{@"state": [self.state deepCopy].dictionary}}; - return [BugsnagAppWithState appWithDictionary:kscrashDict config:self.configuration codeBundleId:self.codeBundleId]; + BugsnagAppWithState *app = [BugsnagAppWithState appWithDictionary:@{BSGKeySystem: systemInfo} + config:self.configuration codeBundleId:self.codeBundleId]; + app.isLaunching = bsg_runContext->isLaunching; + return app; } - (BugsnagDeviceWithState *)generateDeviceWithState:(NSDictionary *)systemInfo { - BugsnagDeviceWithState *device = [BugsnagDeviceWithState deviceWithKSCrashReport:@{@"system": systemInfo}]; + BugsnagDeviceWithState *device = [BugsnagDeviceWithState deviceWithKSCrashReport:@{BSGKeySystem: systemInfo}]; device.time = [NSDate date]; // default to current time for handled errors [device appendRuntimeInfo:self.extraRuntimeInfo]; #if TARGET_OS_IOS - device.orientation = self.lastOrientation; + device.orientation = BSGStringFromDeviceOrientation(bsg_runContext->lastKnownOrientation); #endif return device; } @@ -1233,7 +1058,7 @@ - (nullable BugsnagEvent *)loadAppHangEvent { } // Receipt of the willTerminateNotification indicates that an app hang was not the cause of the termination, so treat as non-fatal. - if ([self.systemState.lastLaunchState[SYSTEMSTATE_KEY_APP][SYSTEMSTATE_APP_WAS_TERMINATED] boolValue]) { + if (BSGRunContextWasTerminating()) { if (self.configuration.appHangThresholdMillis == BugsnagAppHangThresholdFatalOnly) { return nil; } @@ -1256,7 +1081,7 @@ - (nullable BugsnagEvent *)loadAppHangEvent { // MARK: - Event generation -- (BugsnagEvent *)generateOutOfMemoryEvent { +- (nullable BugsnagEvent *)generateOutOfMemoryEvent { return [self generateEventForLastLaunchWithError: [[BugsnagError alloc] initWithErrorClass:@"Out Of Memory" errorMessage:@"The app was likely terminated by the operating system while in the foreground" @@ -1265,7 +1090,7 @@ - (BugsnagEvent *)generateOutOfMemoryEvent { handledState:[BugsnagHandledState handledStateWithSeverityReason:LikelyOutOfMemory]]; } -- (BugsnagEvent *)generateThermalKillEvent { +- (nullable BugsnagEvent *)generateThermalKillEvent { return [self generateEventForLastLaunchWithError: [[BugsnagError alloc] initWithErrorClass:@"Thermal Kill" errorMessage:@"The app was terminated by the operating system due to a critical thermal state" @@ -1274,13 +1099,18 @@ - (BugsnagEvent *)generateThermalKillEvent { handledState:[BugsnagHandledState handledStateWithSeverityReason:ThermalKill]]; } -- (BugsnagEvent *)generateEventForLastLaunchWithError:(BugsnagError *)error handledState:(BugsnagHandledState *)handledState { +- (nullable BugsnagEvent *)generateEventForLastLaunchWithError:(BugsnagError *)error handledState:(BugsnagHandledState *)handledState { + if (!bsg_lastRunContext) { + return nil; + } + NSDictionary *stateDict = [BSGJSONSerialization JSONObjectWithContentsOfFile:BSGFileLocations.current.state options:0 error:nil]; NSDictionary *appDict = self.systemState.lastLaunchState[SYSTEMSTATE_KEY_APP]; BugsnagAppWithState *app = [BugsnagAppWithState appFromJson:appDict]; app.dsymUuid = appDict[BSGKeyMachoUUID]; - app.isLaunching = [stateDict[BSGKeyApp][BSGKeyIsLaunching] boolValue]; + app.inForeground = bsg_lastRunContext->isForeground; + app.isLaunching = bsg_lastRunContext->isLaunching; NSDictionary *configDict = [BSGJSONSerialization JSONObjectWithContentsOfFile:BSGFileLocations.current.configuration options:0 error:nil]; if (configDict) { @@ -1290,20 +1120,39 @@ - (BugsnagEvent *)generateEventForLastLaunchWithError:(BugsnagError *)error hand NSDictionary *deviceDict = self.systemState.lastLaunchState[SYSTEMSTATE_KEY_DEVICE]; BugsnagDeviceWithState *device = [BugsnagDeviceWithState deviceFromJson:deviceDict]; device.manufacturer = @"Apple"; - device.orientation = stateDict[BSGKeyDeviceState][BSGKeyOrientation]; +#if TARGET_OS_IOS + device.orientation = BSGStringFromDeviceOrientation(bsg_lastRunContext->lastKnownOrientation); +#endif + if (bsg_lastRunContext->timestamp > 0) { + device.time = [NSDate dateWithTimeIntervalSinceReferenceDate:bsg_lastRunContext->timestamp]; + } + if (bsg_lastRunContext->availableMemory) { + device.freeMemory = @(bsg_lastRunContext->availableMemory); + } NSDictionary *metadataDict = [BSGJSONSerialization JSONObjectWithContentsOfFile:BSGFileLocations.current.metadata options:0 error:nil]; BugsnagMetadata *metadata = [[BugsnagMetadata alloc] initWithDictionary:metadataDict ?: @{}]; - NSDictionary *deviceState = stateDict[BSGKeyDeviceState]; - if ([deviceState isKindOfClass:[NSDictionary class]]) { - [metadata addMetadata:deviceState toSection:BSGKeyDevice]; + + // Device information that isn't part of `event.device` + NSMutableDictionary *deviceMetadata = [NSMutableDictionary dictionary]; +#if TARGET_OS_IOS + if (bsg_lastRunContext->batteryState != UIDeviceBatteryStateUnknown) { + deviceMetadata[BSGKeyBatteryLevel] = @(bsg_lastRunContext->batteryLevel); + // Our intepretation of "charging" really means "plugged in" + deviceMetadata[BSGKeyCharging] = bsg_lastRunContext->batteryState >= UIDeviceBatteryStateCharging ? @YES : @NO; + } + // Don't set to @NO because server may interpret any non-nil value as meaning true + deviceMetadata[BSGKeyLowMemoryWarning] = BSGRunContextWasMemoryWarning() ? @YES : nil; +#endif + if (@available(iOS 11.0, tvOS 11.0, *)) { + deviceMetadata[BSGKeyThermalState] = BSGStringFromThermalState(bsg_lastRunContext->thermalState); } + [metadata addMetadata:deviceMetadata toSection:BSGKeyDevice]; NSDictionary *userDict = stateDict[BSGKeyUser]; BugsnagUser *user = [[BugsnagUser alloc] initWithDictionary:userDict]; - NSDictionary *sessionDict = self.systemState.lastLaunchState[BSGKeySession]; - BugsnagSession *session = BSGSessionFromEventJson(sessionDict, app, device, user); + BugsnagSession *session = BSGSessionFromLastRunContext(app, device, user); session.unhandledCount += 1; BugsnagEvent *event = diff --git a/Bugsnag/Helpers/BSGAppHangDetector.m b/Bugsnag/Helpers/BSGAppHangDetector.m index a9a4eba32..4e3344d76 100644 --- a/Bugsnag/Helpers/BSGAppHangDetector.m +++ b/Bugsnag/Helpers/BSGAppHangDetector.m @@ -11,7 +11,7 @@ #import #import -#import "BSG_KSCrashState.h" +#import "BSGRunContext.h" #import "BSG_KSMach.h" #import "BSG_KSSystemInfo.h" #import "BugsnagCollections.h" @@ -146,7 +146,7 @@ - (void)detectAppHangs { } #endif - if (shouldReportAppHang && !bsg_kscrashstate_currentState()->applicationIsInForeground) { + if (shouldReportAppHang && !bsg_runContext->isForeground) { bsg_log_debug(@"Ignoring app hang because app is in the background"); shouldReportAppHang = NO; } diff --git a/Bugsnag/Helpers/BSGDefines.h b/Bugsnag/Helpers/BSGDefines.h new file mode 100644 index 000000000..bcd28f6dc --- /dev/null +++ b/Bugsnag/Helpers/BSGDefines.h @@ -0,0 +1,13 @@ +// +// BSGDefines.h +// Bugsnag +// +// Copyright © 2022 Bugsnag Inc. All rights reserved. +// + +#ifndef BSGDefines_h +#define BSGDefines_h + +#define BSG_PRIVATE __attribute__((visibility("hidden"))) + +#endif /* BSGDefines_h */ diff --git a/Bugsnag/Helpers/BSGKeys.h b/Bugsnag/Helpers/BSGKeys.h index a5d67335e..b439b41cb 100644 --- a/Bugsnag/Helpers/BSGKeys.h +++ b/Bugsnag/Helpers/BSGKeys.h @@ -35,7 +35,6 @@ static BSGKey const BSGKeyContext = @"context"; static BSGKey const BSGKeyCppException = @"cpp_exception"; static BSGKey const BSGKeyDevelopment = @"development"; static BSGKey const BSGKeyDevice = @"device"; -static BSGKey const BSGKeyDeviceState = @"deviceState"; static BSGKey const BSGKeyEmail = @"email"; static BSGKey const BSGKeyEnabledReleaseStages = @"enabledReleaseStages"; static BSGKey const BSGKeyEndpoints = @"endpoints"; diff --git a/Bugsnag/Helpers/BSGRunContext.h b/Bugsnag/Helpers/BSGRunContext.h new file mode 100644 index 000000000..43efe4be3 --- /dev/null +++ b/Bugsnag/Helpers/BSGRunContext.h @@ -0,0 +1,90 @@ +// +// BSGRunContext.h +// Bugsnag +// +// Copyright © 2022 Bugsnag Inc. All rights reserved. +// + +#include +#include +#include +#include + +#include "BSGDefines.h" + +// +// The struct version should be incremented prior to a release if changes have +// been made to BSGRunContext. +// +// During development this is not strictly necessary since last run's data will +// not be loaded if the struct's size has changed. +// +#define BSGRUNCONTEXT_VERSION 1 + +struct BSGRunContext { + long structVersion; + bool isDebuggerAttached; + bool isLaunching; + bool isForeground; + bool isTerminating; + long thermalState; + uint64_t bootTime; + uuid_t machoUUID; + uuid_string_t sessionId; + double sessionStartTime; + unsigned long handledCount; + unsigned long unhandledCount; +#if TARGET_OS_IOS + float batteryLevel; + long batteryState; + long lastKnownOrientation; + dispatch_source_memorypressure_flags_t memoryPressure; +#endif + double timestamp; + size_t availableMemory; +}; + +/// Information about the current run of the app / process. +/// +/// This structure is mapped to a file so that changes will be persisted by the OS. +/// +/// Guaranteed to be non-null once BSGRunContextInit() is called. +extern struct BSGRunContext *_Nonnull bsg_runContext; + +/// Information about the last run of the app / process, if it could be loaded. +extern const struct BSGRunContext *_Nullable bsg_lastRunContext; + +#pragma mark - + +void BSGRunContextInit(const char *_Nonnull path); + +#pragma mark - + +BSG_PRIVATE void BSGRunContextUpdateTimestamp(void); + +#pragma mark - + +#ifdef FOUNDATION_EXTERN +static inline bool BSGRunContextWasCriticalThermalState() { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + return bsg_lastRunContext && bsg_lastRunContext->thermalState == NSProcessInfoThermalStateCritical; +#pragma clang diagnostic pop +} +#endif + +bool BSGRunContextWasKilled(void); + +static inline bool BSGRunContextWasLaunching() { + return bsg_lastRunContext && bsg_lastRunContext->isLaunching; +} + +#if TARGET_OS_IOS +static inline bool BSGRunContextWasMemoryWarning() { + return bsg_lastRunContext && bsg_lastRunContext->memoryPressure > DISPATCH_MEMORYPRESSURE_NORMAL; +} +#endif + +static inline bool BSGRunContextWasTerminating() { + return bsg_lastRunContext && bsg_lastRunContext->isTerminating; +} diff --git a/Bugsnag/Helpers/BSGRunContext.m b/Bugsnag/Helpers/BSGRunContext.m new file mode 100644 index 000000000..5f2def1bd --- /dev/null +++ b/Bugsnag/Helpers/BSGRunContext.m @@ -0,0 +1,403 @@ +// +// BSGRunContext.m +// Bugsnag +// +// Copyright © 2022 Bugsnag Inc. All rights reserved. +// + +#import "BSGRunContext.h" + +#import "BSG_KSLogger.h" +#import "BSG_KSMach.h" +#import "BSG_KSMachHeaders.h" +#import "BSG_KSSystemInfo.h" + +#import +#import +#import +#import + +#if __has_include() && TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST +#include +#endif + +#if TARGET_OS_IOS +#import "BSGUIKit.h" +#endif + +#if TARGET_OS_OSX +#import "BSGAppKit.h" +#endif + + +#pragma mark Forward declarations + +static uint64_t GetBootTime(void); +static bool GetIsForeground(void); +static void InstallTimer(void); +static void UpdateAvailableMemory(void); + + +#pragma mark - Initial setup + +/// Populates `bsg_runContext` +static void InitRunContext() { + bsg_runContext->isDebuggerAttached = bsg_ksmachisBeingTraced(); + + bsg_runContext->isLaunching = YES; + + // On iOS/tvOS, the app may have launched in the background due to a fetch + // event or notification (or prewarming on iOS 15+) + bsg_runContext->isForeground = GetIsForeground(); + + if (@available(iOS 11.0, tvOS 11.0, *)) { + bsg_runContext->thermalState = NSProcessInfo.processInfo.thermalState; + } + + bsg_runContext->bootTime = GetBootTime(); + + BSG_Mach_Header_Info *image = bsg_mach_headers_get_main_image(); + if (image && image->uuid) { + uuid_copy(bsg_runContext->machoUUID, image->uuid); + } + + BSGRunContextUpdateTimestamp(); + InstallTimer(); + + UpdateAvailableMemory(); + + // Set `structVersion` last so that BSGRunContextLoadLast() will reject data + // that is not fully initialised. + bsg_runContext->structVersion = BSGRUNCONTEXT_VERSION; +} + +static uint64_t GetBootTime() { + struct timeval tv; + size_t len = sizeof(tv); + int ret = sysctl((int[]){CTL_KERN, KERN_BOOTTIME}, 2, &tv, &len, NULL, 0); + if (ret == -1) return 0; + return (uint64_t)tv.tv_sec * USEC_PER_SEC + (uint64_t)tv.tv_usec; +} + +static bool GetIsForeground() { +#if TARGET_OS_OSX + return [[NSAPPLICATION sharedApplication] isActive]; +#endif + +#if TARGET_OS_IOS + // + // Work around unreliability of -[UIApplication applicationState] which + // always returns UIApplicationStateBackground during the launch of UIScene + // based apps (until the first scene has been created.) + // + task_category_policy_data_t policy; + mach_msg_type_number_t count = TASK_CATEGORY_POLICY_COUNT; + boolean_t get_default = FALSE; + // task_policy_get() is prohibited on tvOS and watchOS + kern_return_t kr = task_policy_get(mach_task_self(), TASK_CATEGORY_POLICY, + (void *)&policy, &count, &get_default); + if (kr == KERN_SUCCESS) { + // TASK_FOREGROUND_APPLICATION -> normal foreground launch + // TASK_NONUI_APPLICATION -> background launch + // TASK_DARWINBG_APPLICATION -> iOS 15 prewarming launch + // TASK_UNSPECIFIED -> iOS 9 Simulator + if (!get_default && policy.role == TASK_FOREGROUND_APPLICATION) { + return true; + } + } else { + bsg_log_err(@"task_policy_get failed: %s", mach_error_string(kr)); + } +#endif + +#if TARGET_OS_IOS || TARGET_OS_TV + // +sharedApplication is unavailable to app extensions + if ([BSG_KSSystemInfo isRunningInAppExtension]) { + // Returning "foreground" seems wrong but matches what + // +[BSG_KSSystemInfo currentAppState] used to return + return true; + } + + // Using performSelector: to avoid a compile-time check that + // +sharedApplication is not called from app extensions + UIApplication *application = [UIAPPLICATION performSelector: + @selector(sharedApplication)]; + + // There will be no UIApplication if UIApplicationMain() has not yet been + // called - e.g. from a SwiftUI app's init() function or UIKit app's main() + if (!application) { + return false; + } + + __block UIApplicationState applicationState; + if ([[NSThread currentThread] isMainThread]) { + applicationState = [application applicationState]; + } else { + // -[UIApplication applicationState] is a main thread-only API + dispatch_sync(dispatch_get_main_queue(), ^{ + applicationState = [application applicationState]; + }); + } + + return applicationState != UIApplicationStateBackground; +#endif +} + +static void InstallTimer() { + static dispatch_source_t timer; + + dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0); + + timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + + dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0), + /* interval */ NSEC_PER_SEC / 2, + /* leeway */ NSEC_PER_SEC / 4); + + dispatch_source_set_event_handler(timer, ^{ + BSGRunContextUpdateTimestamp(); + UpdateAvailableMemory(); + }); + + dispatch_resume(timer); +} + + +#pragma mark - Observation + +#if TARGET_OS_IOS || TARGET_OS_OSX + +static void NoteAppBackground() { + bsg_runContext->isForeground = NO; + BSGRunContextUpdateTimestamp(); +} + +static void NoteAppForeground() { + bsg_runContext->isForeground = YES; + BSGRunContextUpdateTimestamp(); +} + +static void NoteAppWillTerminate() { + bsg_runContext->isTerminating = YES; + BSGRunContextUpdateTimestamp(); +} + +#endif + +#if TARGET_OS_IOS + +static void NoteBatteryLevel() { + bsg_runContext->batteryLevel = [UIDEVICE currentDevice].batteryLevel; +} + +static void NoteBatteryState() { + bsg_runContext->batteryState = [UIDEVICE currentDevice].batteryState; +} + +static void NoteOrientation() { + UIDeviceOrientation orientation = [UIDEVICE currentDevice].orientation; + if (orientation != UIDeviceOrientationUnknown) { + bsg_runContext->lastKnownOrientation = orientation; + } + BSGRunContextUpdateTimestamp(); +} + +#endif + +static void NoteThermalState(__unused CFNotificationCenterRef center, + __unused void *observer, + __unused CFNotificationName name, + const void *object, + __unused CFDictionaryRef userInfo) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + bsg_runContext->thermalState = ((__bridge NSProcessInfo *)object).thermalState; +#pragma clang diagnostic pop + BSGRunContextUpdateTimestamp(); +} + +#if TARGET_OS_IOS + +static void ObserveMemoryPressure() { + // DISPATCH_SOURCE_TYPE_MEMORYPRESSURE arrives slightly sooner than + // UIApplicationDidReceiveMemoryWarningNotification + dispatch_source_t source = + dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0, + DISPATCH_MEMORYPRESSURE_NORMAL | + DISPATCH_MEMORYPRESSURE_WARN | + DISPATCH_MEMORYPRESSURE_CRITICAL, + // Using a high pririty queue to increase chances of + // running before OS kills the app. + dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)); + dispatch_source_set_event_handler(source, ^{ + bsg_runContext->memoryPressure = dispatch_source_get_data(source); + BSGRunContextUpdateTimestamp(); + UpdateAvailableMemory(); + }); + dispatch_resume(source); +} + +#endif + +static void AddObservers() { + CFNotificationCenterRef center = CFNotificationCenterGetLocalCenter(); + +#define OBSERVE(name, function) CFNotificationCenterAddObserver(\ + center, NULL, function, (__bridge CFStringRef)name, NULL, \ + CFNotificationSuspensionBehaviorDeliverImmediately) + +#if TARGET_OS_IOS + OBSERVE(UIApplicationDidBecomeActiveNotification, NoteAppForeground); + OBSERVE(UIApplicationDidEnterBackgroundNotification, NoteAppBackground); + OBSERVE(UIApplicationWillEnterForegroundNotification, NoteAppForeground); + OBSERVE(UIApplicationWillTerminateNotification, NoteAppWillTerminate); +#endif + +#if TARGET_OS_OSX + OBSERVE(NSApplicationDidBecomeActiveNotification, NoteAppForeground); + OBSERVE(NSApplicationDidResignActiveNotification, NoteAppBackground); + OBSERVE(NSApplicationWillTerminateNotification, NoteAppWillTerminate); +#endif + + if (@available(iOS 11.0, tvOS 11.0, *)) { + OBSERVE(NSProcessInfoThermalStateDidChangeNotification, NoteThermalState); + } + +#if TARGET_OS_IOS + UIDevice *currentDevice = [UIDEVICE currentDevice]; + + currentDevice.batteryMonitoringEnabled = YES; + bsg_runContext->batteryLevel = currentDevice.batteryLevel; + OBSERVE(UIDeviceBatteryLevelDidChangeNotification, NoteBatteryLevel); + + bsg_runContext->batteryState = currentDevice.batteryState; + OBSERVE(UIDeviceBatteryStateDidChangeNotification, NoteBatteryState); + + [currentDevice beginGeneratingDeviceOrientationNotifications]; + bsg_runContext->lastKnownOrientation = currentDevice.orientation; + OBSERVE(UIDeviceOrientationDidChangeNotification, NoteOrientation); + + ObserveMemoryPressure(); +#endif +} + + +#pragma mark - Misc + +void BSGRunContextUpdateTimestamp() { + bsg_runContext->timestamp = CFAbsoluteTimeGetCurrent(); +} + +static void UpdateAvailableMemory() { + // Deliberately avoids use of bsg_ksmachfreeMemory() because that falls back + // to a much more expensive (~5x) system call on earlier releases. +#if __has_include() && TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST + if (__builtin_available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)) { + bsg_runContext->availableMemory = os_proc_available_memory(); + } +#endif +} + + +#pragma mark - Kill detection + +bool BSGRunContextWasKilled() { + // App extensions have a different lifecycle and the heuristic used for + // finding app terminations rooted in fixable code does not apply + if ([BSG_KSSystemInfo isRunningInAppExtension]) { + return NO; + } + + if (!bsg_lastRunContext) { + return NO; + } + + if (bsg_lastRunContext->isTerminating) { + return NO; // The app terminated normally + } + + if (bsg_lastRunContext->isDebuggerAttached) { + return NO; // The debugger may have killed the app + } + + // Once the app is in the background we cannot determine between good (user + // swiping up to close app) and bad (OS killing the app) terminations. + if (!bsg_lastRunContext->isForeground) { + return NO; + } + + if (bsg_lastRunContext->bootTime != bsg_runContext->bootTime) { + return NO; // The app may have been terminated due to the reboot + } + + // Ignore unexpected terminations due to the app being upgraded + if (uuid_compare(bsg_lastRunContext->machoUUID, bsg_runContext->machoUUID)) { + return NO; + } + + return YES; +} + + +#pragma mark - File handling & memory mapping + +#define SIZEOF_STRUCT sizeof(struct BSGRunContext) + +struct BSGRunContext *bsg_runContext; + +const struct BSGRunContext *bsg_lastRunContext; + +/// Loads the contents of the state file into memory and sets the +/// `bsg_lastRunContext` pointer if the contents are valid. +static void LoadLastRunContext(int fd) { + struct stat sb; + // Only expose previous state if size matches... + if (fstat(fd, &sb) == 0 && sb.st_size == SIZEOF_STRUCT) { + static struct BSGRunContext context; + if (read(fd, &context, SIZEOF_STRUCT) == SIZEOF_STRUCT && + // ...and so does the structVersion + context.structVersion == BSGRUNCONTEXT_VERSION) { + bsg_lastRunContext = &context; + } + } +} + +/// Truncates or extends the file to the size of struct BSGRunContext, +/// maps it into memory, and sets the `bsg_runContext` pointer. +static void ResizeAndMapFile(int fd) { + static struct BSGRunContext fallback; + + // Note: ftruncate fills the file with zeros when extending. + if (ftruncate(fd, SIZEOF_STRUCT) != 0) { + bsg_log_warn(@"ftruncate failed: %d", errno); + goto fail; + } + + const int prot = PROT_READ | PROT_WRITE; + const int flags = MAP_FILE | MAP_SHARED; + void *ptr = mmap(0, SIZEOF_STRUCT, prot, flags, fd, 0); + if (ptr == MAP_FAILED) { + bsg_log_warn(@"mmap failed: %d", errno); + goto fail; + } + + memset(ptr, 0, SIZEOF_STRUCT); + bsg_runContext = ptr; + return; + +fail: + bsg_runContext = &fallback; +} + +void BSGRunContextInit(const char *path) { + int fd = open(path, O_RDWR | O_CREAT, 0600); + if (fd < 0) { + bsg_log_warn(@"Could not open %s", path); + } + LoadLastRunContext(fd); + ResizeAndMapFile(fd); + InitRunContext(); + AddObservers(); + if (fd > 0) { + close(fd); + } +} diff --git a/Bugsnag/Helpers/BugsnagKVStore.c b/Bugsnag/Helpers/BugsnagKVStore.c deleted file mode 100644 index 148ce1b08..000000000 --- a/Bugsnag/Helpers/BugsnagKVStore.c +++ /dev/null @@ -1,219 +0,0 @@ -// -// BugsnagKVStore.c -// Bugsnag -// -// Created by Karl Stenerud on 11.09.20. -// Copyright © 2020 Bugsnag Inc. All rights reserved. -// - -#include "BugsnagKVStore.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static DIR* g_currentDir = NULL; -static int g_currentDirFD = 0; -static char g_path[PATH_MAX+1]; - -static int openKeyRead(const char* name) { - return openat(g_currentDirFD, name, O_RDONLY, 0); -} - -static int openKeyWrite(const char* name) { - return openat(g_currentDirFD, name, O_CREAT | O_RDWR | O_TRUNC, 0600); -} - -void bsgkv_open(const char* path, int* err) { - bsgkv_close(); - - g_currentDir = opendir(path); - if(g_currentDir == NULL) { - if(errno == ENOENT) { - if(mkdir(path, 0700) != 0) { - *err = errno; - return; - } - g_currentDir = opendir(path); - if(g_currentDir == NULL) { - *err = errno; - return; - } - } else { - *err = errno; - return; - } - } - - g_currentDirFD = dirfd(g_currentDir); - if(g_currentDirFD < 0) { - closedir(g_currentDir); - g_currentDir = NULL; - *err = errno; - return; - } - - strncpy(g_path, path, sizeof(g_path)); - g_path[sizeof(g_path)-1] = 0; - *err = 0; -} - -void bsgkv_close(void) { - if(g_currentDir != NULL) { - closedir(g_currentDir); - g_currentDir = NULL; - // Note: closedir() closes the underlying file descriptor. - // Attempting to close it again is a programming error. - g_currentDirFD = 0; - } -} - -void bsgkv_purge(int* err) { - // Set up a baseline path buffer to append the file names onto. - char path[sizeof(g_path)]; - strcpy(path, g_path); - size_t basePathLength = strlen(path); - if(basePathLength >= PATH_MAX-2) { - *err = E2BIG; - return; - } - path[basePathLength++] = '/'; - path[basePathLength] = 0; - char* const filename = path + basePathLength; - const size_t nameMaxLength = PATH_MAX - basePathLength; - - *err = 0; - - // Step through K-V store directory, deleting all regular files. - rewinddir(g_currentDir); - for(;;) { - errno = 0; - struct dirent* dent = readdir(g_currentDir); - if(dent == NULL) { - if(errno != 0) { - *err = errno; - return; - } - break; - } - if(dent->d_type != DT_REG) { - continue; - } - if(dent->d_namlen > nameMaxLength) { - // Set an error but don't stop purging - *err = E2BIG; - } else { - memcpy(filename, dent->d_name, dent->d_namlen); - filename[dent->d_namlen] = 0; - if(unlink(path) != 0) { - // Set an error but don't stop purging - *err = errno; - } - } - } - - rewinddir(g_currentDir); -} - -void bsgkv_delete(const char* key, int* err) { - if(unlinkat(g_currentDirFD, key, 0) < 0) { - if(errno != ENOENT) { - *err = errno; - return; - } - } - *err = 0; -} - -void bsgkv_setBytes(const char* key, const uint8_t* value, int length, int* err) { - int fd = openKeyWrite(key); - if(fd < 0) { - *err = errno; - goto cleanup_fail; - } - -retry_after_eintr: - if (write(fd, value, (size_t)length) == length) { - goto cleanup_success; - } - - if(errno == EINTR) { - if(lseek(fd, 0, SEEK_SET) < 0) - { - *err = errno; - goto cleanup_fail; - } - goto retry_after_eintr; - } - - *err = errno; - if(*err == 0) { - // Disk full, or OS resource limit reached. Just call it IO error since these are - // catastrophic, we can't do anything about it, and the app WILL die very soon. - *err = EIO; - } - goto cleanup_fail; - -cleanup_success: - *err = 0; -cleanup_fail: - if(fd > 0) { - close(fd); - } -} - -void bsgkv_getBytes(const char* key, uint8_t* value, int* length, int* err) { - int fd = openKeyRead(key); - if(fd < 0) { - *err = errno; - return; - } - size_t bytesRequested = (size_t)*length; - ssize_t bytesRead = read(fd, value, bytesRequested); - if(bytesRead < 0) { - *err = errno; - return; - } - close(fd); - *length = (int)bytesRead; - *err = 0; -} - -void bsgkv_setString(const char* key, const char* value, int* err) { - bsgkv_setBytes(key, (const uint8_t*)value, (int)strlen(value), err); -} - -void bsgkv_getString(const char* key, char* value, int maxLength, int* err) { - int length = maxLength; - bsgkv_getBytes(key, (uint8_t*)value, &length, err); - if(*err != 0) { - return; - } - - if(length >= maxLength) { - value[maxLength-1] = 0; - } else { - value[length] = 0; - } -} - -#define DECLARE_SCALAR_GETTER_SETTER(NAME, TYPE) \ -void bsgkv_set##NAME(const char* key, TYPE value, int* err) { \ - bsgkv_setBytes(key, (uint8_t*)&value, sizeof(value), err); \ -} \ -TYPE bsgkv_get##NAME(const char* key, int* err) { \ - TYPE value = 0; \ - int length = sizeof(value); \ - bsgkv_getBytes(key, (uint8_t*)&value, &length, err); \ - return value; \ -} - -DECLARE_SCALAR_GETTER_SETTER(Int, int64_t) -DECLARE_SCALAR_GETTER_SETTER(Float, double) -DECLARE_SCALAR_GETTER_SETTER(Boolean, bool) diff --git a/Bugsnag/Helpers/BugsnagKVStore.h b/Bugsnag/Helpers/BugsnagKVStore.h deleted file mode 100644 index 88413f39a..000000000 --- a/Bugsnag/Helpers/BugsnagKVStore.h +++ /dev/null @@ -1,140 +0,0 @@ -// -// BugsnagKVStore.h -// Bugsnag -// -// Created by Karl Stenerud on 11.09.20. -// Copyright © 2020 Bugsnag Inc. All rights reserved. -// -// A low-level filesystem based key-value store. The goal is to keep -// as much processing as possible in syscalls to minimize opportunities -// for the operation to be aborted due to a crash or app shutdown. -// -// Values are stored individually in files (mode 600), with the keys -// used as the file names (so beware of using problematic characters). -// https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations -// -// Warning: Some operating systems (Mac, Windows) have case-insensitive -// filesystems, which will cause problems if you have two keys -// with only the letter case different! -// -// Safety: -// - Thread-safe: NO -// - Async-safe: YES -// - Idempotent: YES - -#ifndef BugsnagKVStore_h -#define BugsnagKVStore_h - -#include -#include - -/** - * Open the key-value store. If the directory doesn't exist, it will be created with mode 700. - * If the store was already open, it will be closed and reopened at the specified path. - * - * Note: This must be called before any other API. - * - * File names will be the keys themselves, so it's best to put the kv-store in its own directory. - * - * err: 0 on success, errno value on filesystem errors (see errno.h). - */ -void bsgkv_open(const char* restrict path, int* restrict err); - -/** - * Close the key-value store. - */ -void bsgkv_close(void); - -/** - * Purge the KV store, deleting all values. - */ -void bsgkv_purge(int* err); - -/** - * Delete the value associated with the specified key. - * Deleting a non-existent key has no effect, and returns success. - * - * err: 0 on success, errno value on filesystem errors (see errno.h). - */ -void bsgkv_delete(const char* restrict key, int* restrict err); - -/** - * Store a boolean value, associated with the specified key. - * - * err: 0 on success, errno value on filesystem errors (see errno.h). - */ -void bsgkv_setBoolean(const char* restrict key, bool value, int* restrict err); - -/** - * Get the boolean value associated with the specified key. - * If the actual stored value is not a boolean, the result is undefined. - * - * err: 0 on success, ENOENT if not found, and errno value on filesystem errors (see errno.h). - */ -bool bsgkv_getBoolean(const char* restrict key, int* restrict err); - -/** - * Store an integer value, associated with the specified key. - * - * err: 0 on success, errno value on filesystem errors (see errno.h). - */ -void bsgkv_setInt(const char* restrict key, int64_t value, int* restrict err); - -/** - * Get the integer value associated with the specified key. - * If the actual stored value is not an integer, the result is undefined. - * - * err: 0 on success, ENOENT if not found, and errno value on filesystem errors (see errno.h). - */ -int64_t bsgkv_getInt(const char* restrict key, int* restrict err); - -/** - * Store a float value, associated with the specified key. - * - * err: 0 on success, errno value on filesystem errors (see errno.h). - */ -void bsgkv_setFloat(const char* restrict key, double value, int* restrict err); - -/** - * Get the float value associated with the specified key. - * If the actual stored value is not a float, the result is undefined. - * - * err: 0 on success, ENOENT if not found, and errno value on filesystem errors (see errno.h). - */ -double bsgkv_getFloat(const char* restrict key, int* restrict err); - -/** - * Store a null-terminated string value, associated with the specified key. - * - * err: 0 on success, errno value on filesystem errors (see errno.h). - */ -void bsgkv_setString(const char* restrict key, const char* restrict value, int* restrict err); - -/** - * Get the null-terminated string value associated with the specified key. - * If the actual stored value is not a string, the result is undefined. - * If the key contains >= [maxLength] bytes of data, it will be truncated to [maxLength-1] and null-terminated. - * - * err: 0 on success, ENOENT if not found, and errno value on filesystem errors (see errno.h). - */ -void bsgkv_getString(const char* restrict key, char* restrict value, int maxLength, int* restrict err); - -/** - * Store a byte array value, associated with the specified key. - * - * err: 0 on success, errno value on filesystem errors (see errno.h). - */ -void bsgkv_setBytes(const char* restrict key, const uint8_t* restrict value, int length, int* restrict err); - -/** - * Get the byte array value associated with the specified key. - * If the key contains more then [length] bytes of data, it will be truncated to [length]. - * - * [length] must contain the length of the buffer (value) being passed in. - * On return, [length] will contain the number of bytes actually filled. - * - * err: 0 on success, ENOENT if not found, and errno value on filesystem errors (see errno.h). - */ -void bsgkv_getBytes(const char* restrict key, uint8_t* restrict value, int* length, int* err); - -#endif /* BugsnagKVStore_h */ diff --git a/Bugsnag/Helpers/BugsnagKVStoreObjC.h b/Bugsnag/Helpers/BugsnagKVStoreObjC.h deleted file mode 100644 index bb70af8df..000000000 --- a/Bugsnag/Helpers/BugsnagKVStoreObjC.h +++ /dev/null @@ -1,42 +0,0 @@ -// -// BugsnagKVStoreObjC.h -// Bugsnag -// -// Created by Karl Stenerud on 15.09.20. -// Copyright © 2020 Bugsnag Inc. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - * BugsnagKVStore is an objective-c wrapper around the C version of the KV store. - * All instances of BugsnagKVStore share the same underlying C implementation, initialized - * to the same directory under the caches directory. If in future we need multiple KV stores, - * this will have to be changed. For now, it's simplest to only have one. - */ -@interface BugsnagKVStore : NSObject - -/** - * Purge the KV store, deleting all values. - */ -- (void)purge; - -- (void)deleteKey:(NSString *)key; - -- (void)setBoolean:(bool)value forKey:(NSString*)key; - -- (bool)booleanForKey:(NSString*)key defaultValue:(bool)defaultValue; - -- (nullable NSNumber*)NSBooleanForKey:(NSString*)key defaultValue:(nullable NSNumber*)defaultValue; - -- (void)setString:(NSString*)value forKey:(NSString*)key; - -- (NSString*)stringForKey:(NSString*)key defaultValue:(NSString*)defaultValue; - -// Note: Other types such as float and bytes can be added later if needed. - -@end - -NS_ASSUME_NONNULL_END diff --git a/Bugsnag/Helpers/BugsnagKVStoreObjC.m b/Bugsnag/Helpers/BugsnagKVStoreObjC.m deleted file mode 100644 index c51b3e859..000000000 --- a/Bugsnag/Helpers/BugsnagKVStoreObjC.m +++ /dev/null @@ -1,98 +0,0 @@ -// -// BugsnagKVStoreObjC.m -// Bugsnag -// -// Created by Karl Stenerud on 15.09.20. -// Copyright © 2020 Bugsnag Inc. All rights reserved. -// - -#import "BugsnagKVStoreObjC.h" -#import "BugsnagKVStore.h" -#import "BSGFileLocations.h" -#import "BugsnagLogger.h" - -static void bsgkv_init() { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - const char *kvstoreDir = [[BSGFileLocations current].kvStore UTF8String]; - int err = 0; - bsgkv_open(kvstoreDir, &err); - if(err != 0) { - bsg_log_err(@"Error (errno %d) initializing KV Store at %s", err, kvstoreDir); - } - }); -} - -@implementation BugsnagKVStore - -+ (void)initialize { - bsgkv_init(); -} - -- (void)purge { - int err = 0; - bsgkv_purge(&err); - if(err != 0) { - bsg_log_err(@"Error purging kv store. errno = %d", err); - } -} - -- (void)deleteKey:(NSString *)key { - int err = 0; - bsgkv_delete([key UTF8String], &err); - if(err != 0) { - bsg_log_err(@"Error deleting key %@ from kv store. errno = %d", key, err); - } -} - -- (void)setBoolean:(bool)value forKey:(NSString*)key { - int err = 0; - bsgkv_setBoolean([key UTF8String], value, &err); - if(err != 0) { - bsg_log_err(@"Error writing boolean key %@ to kv store. errno = %d", key, err); - return; - } -} - -- (bool)booleanForKey:(NSString*)key defaultValue:(bool)defaultValue { - int err = 0; - bool value = bsgkv_getBoolean([key UTF8String], &err); - if(err != 0) { - value = defaultValue; - } - return value; -} - -- (nullable NSNumber*)NSBooleanForKey:(NSString*)key defaultValue:(nullable NSNumber*)defaultValue { - int err = 0; - bool boolValue = bsgkv_getBoolean([key UTF8String], &err); - if (err != 0) { - return defaultValue; - } - return [NSNumber numberWithBool:boolValue]; -} - -- (void)setString:(NSString*)value forKey:(NSString*)key { - int err = 0; - if(value == nil || (id)value == [NSNull null]) { - [self deleteKey:key]; - } else { - bsgkv_setString([key UTF8String], [value UTF8String], &err); - if(err != 0) { - bsg_log_err(@"Error writing string key %@ to kv store. errno = %d", key, err); - } - } -} - -- (NSString*)stringForKey:(NSString*)key defaultValue:(NSString*)defaultValue { - NSString *value = defaultValue; - char buffer[500]; - int err = 0; - bsgkv_getString([key UTF8String], buffer, sizeof(buffer), &err); - if(err == 0) { - value = [NSString stringWithUTF8String:buffer]; - } - return value; -} - -@end diff --git a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashState.m b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashState.m index ad85d8cff..3fa62a2ff 100644 --- a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashState.m +++ b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashState.m @@ -27,6 +27,7 @@ #import "BSG_KSCrashState.h" #import "BSGJSONSerialization.h" +#import "BSGRunContext.h" #import "BSG_KSFile.h" #import "BSG_KSJSONCodec.h" #import "BSG_KSLogger.h" @@ -49,8 +50,6 @@ #import #import -static bool bsg_kscrashstate_i_isInForeground(void); - // ============================================================================ #pragma mark - Constants - // ============================================================================ @@ -187,78 +186,13 @@ bool bsg_kscrashstate_init(const char *const stateFilePath, bsg_kscrashstate_i_loadState(state, stateFilePath); state->appLaunchTime = timeNow; state->lastUpdateDurationsTime = timeNow; - - // On iOS/tvOS, the app may have launched in the background due to a fetch - // event or notification (or prewarming on iOS 15+) - state->applicationIsInForeground = bsg_kscrashstate_i_isInForeground(); + state->applicationIsInForeground = bsg_runContext->isForeground; return bsg_kscrashstate_i_saveState(state, stateFilePath); } -static bool bsg_kscrashstate_i_isInForeground(void) { -#if TARGET_OS_IOS - // - // Work around unreliability of -[UIApplication applicationState] which - // always returns UIApplicationStateBackground during the launch of UIScene - // based apps (until the first scene has been created.) - // - task_category_policy_data_t policy; - mach_msg_type_number_t count = TASK_CATEGORY_POLICY_COUNT; - boolean_t get_default = FALSE; - // task_policy_get() is prohibited on tvOS and watchOS - kern_return_t kr = task_policy_get(mach_task_self(), TASK_CATEGORY_POLICY, - (void *)&policy, &count, &get_default); - if (kr == KERN_SUCCESS) { - // TASK_FOREGROUND_APPLICATION -> normal foreground launch - // TASK_NONUI_APPLICATION -> background launch - // TASK_DARWINBG_APPLICATION -> iOS 15 prewarming launch - // TASK_UNSPECIFIED -> iOS 9 Simulator - if (!get_default && policy.role == TASK_FOREGROUND_APPLICATION) { - return true; - } - } else { - bsg_log_err(@"task_policy_get failed: %s", mach_error_string(kr)); - } -#endif - -#if TARGET_OS_IOS || TARGET_OS_TV - // +sharedApplication is unavailable to app extensions - if ([BSG_KSSystemInfo isRunningInAppExtension]) { - // Returning "foreground" seems wrong but matches what - // +[BSG_KSSystemInfo currentAppState] used to return - return true; - } - - // Using performSelector: to avoid a compile-time check that - // +sharedApplication is not called from app extensions - UIApplication *application = [UIAPPLICATION performSelector: - @selector(sharedApplication)]; - - // There will be no UIApplication if UIApplicationMain() has not yet been - // called - e.g. from a SwiftUI app's init() function or UIKit app's main() - if (!application) { - return false; - } - - __block UIApplicationState applicationState; - if ([[NSThread currentThread] isMainThread]) { - applicationState = [application applicationState]; - } else { - // -[UIApplication applicationState] is a main thread-only API - dispatch_sync(dispatch_get_main_queue(), ^{ - applicationState = [application applicationState]; - }); - } - - return applicationState != UIApplicationStateBackground; -#else - return true; -#endif -} - void bsg_kscrashstate_notifyAppInForeground(const bool isInForeground) { BSG_KSCrash_State *const state = bsg_g_state; - const char *const stateFilePath = bsg_g_stateFilePath; if (state->applicationIsInForeground == isInForeground) { return; @@ -271,7 +205,6 @@ void bsg_kscrashstate_notifyAppInForeground(const bool isInForeground) { state->backgroundDurationSinceLaunch += duration; } else { state->foregroundDurationSinceLaunch += duration; - bsg_kscrashstate_i_saveState(state, stateFilePath); } state->lastUpdateDurationsTime = timeNow; } diff --git a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSSystemInfo.h b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSSystemInfo.h index bf7b6c228..f36e08be6 100644 --- a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSSystemInfo.h +++ b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSSystemInfo.h @@ -26,7 +26,6 @@ #define BSG_KSSystemField_AppUUID "app_uuid" #define BSG_KSSystemField_BinaryArch "binary_arch" -#define BSG_KSSystemField_BootTime "boot_time" #define BSG_KSSystemField_BundleID "CFBundleIdentifier" #define BSG_KSSystemField_BundleName "CFBundleName" #define BSG_KSSystemField_BundleExecutable "CFBundleExecutable" diff --git a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSSystemInfo.m b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSSystemInfo.m index fb8c8e02b..fd3f57651 100644 --- a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSSystemInfo.m +++ b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSSystemInfo.m @@ -399,7 +399,6 @@ + (NSDictionary *)buildSystemInfoStatic { #endif // TARGET_OS_SIMULATOR sysInfo[@BSG_KSSystemField_OSVersion] = [self osBuildVersion]; - sysInfo[@BSG_KSSystemField_BootTime] = [self dateSysctl:@"kern.boottime"]; sysInfo[@BSG_KSSystemField_BundleID] = infoDict[@"CFBundleIdentifier"]; sysInfo[@BSG_KSSystemField_BundleName] = infoDict[@"CFBundleName"]; sysInfo[@BSG_KSSystemField_BundleExecutable] = infoDict[@"CFBundleExecutable"]; diff --git a/Bugsnag/Payload/BugsnagAppWithState.m b/Bugsnag/Payload/BugsnagAppWithState.m index e760eda40..547129f1d 100644 --- a/Bugsnag/Payload/BugsnagAppWithState.m +++ b/Bugsnag/Payload/BugsnagAppWithState.m @@ -64,7 +64,7 @@ + (BugsnagAppWithState *)appWithDictionary:(NSDictionary *)event app.durationInForeground = activeTimeSinceLaunch; app.duration = @([activeTimeSinceLaunch longValue] + [backgroundTimeSinceLaunch longValue]); app.inForeground = [stats[@BSG_KSCrashField_AppInFG] boolValue]; - app.isLaunching = [[event valueForKeyPath:@"user.state.app.isLaunching"] boolValue]; + app.isLaunching = [[event valueForKeyPath:@"user.isLaunching"] boolValue]; [BugsnagApp populateFields:app dictionary:event config:config codeBundleId:codeBundleId]; return app; } diff --git a/Bugsnag/Payload/BugsnagEvent.m b/Bugsnag/Payload/BugsnagEvent.m index fcd395cdb..14db55109 100644 --- a/Bugsnag/Payload/BugsnagEvent.m +++ b/Bugsnag/Payload/BugsnagEvent.m @@ -11,6 +11,7 @@ #import "BSGFeatureFlagStore.h" #import "BSGKeys.h" #import "BSGSerialization.h" +#import "BSGUtils.h" #import "BSG_KSCrashReportFields.h" #import "BSG_RFC3339DateTool.h" #import "Bugsnag+Private.h" @@ -281,8 +282,20 @@ - (instancetype)initWithKSCrashReport:(NSDictionary *)event { metadata = [BugsnagMetadata new]; } - // Cocoa-specific, non-spec., device and app data - [metadata addMetadata:BSGParseDeviceMetadata(event) toSection:BSGKeyDevice]; + // Device information that isn't part of `event.device` + NSMutableDictionary *deviceMetadata = BSGParseDeviceMetadata(event); +#if TARGET_OS_IOS + deviceMetadata[BSGKeyBatteryLevel] = [event valueForKeyPath:@"user.batteryLevel"]; + deviceMetadata[BSGKeyCharging] = [event valueForKeyPath:@"user.charging"]; +#endif + if (@available(iOS 11.0, tvOS 11.0, *)) { + NSNumber *thermalState = [event valueForKeyPath:@"user.thermalState"]; + if ([thermalState isKindOfClass:[NSNumber class]]) { + deviceMetadata[BSGKeyThermalState] = BSGStringFromThermalState(thermalState.longValue); + } + } + [metadata addMetadata:deviceMetadata toSection:BSGKeyDevice]; + [metadata addMetadata:BSGParseAppMetadata(event) toSection:BSGKeyApp]; NSDictionary *recordedState = [event valueForKeyPath:@"user.handledState"]; @@ -294,8 +307,6 @@ - (instancetype)initWithKSCrashReport:(NSDictionary *)event { depth = 0; } - BugsnagSession *session = BSGSessionFromDictionary(event[BSGKeyUser]); - // generate threads/error info NSArray *binaryImages = event[@"binary_images"]; NSArray *threadDict = [event valueForKeyPath:@"crash.threads"]; @@ -345,6 +356,13 @@ - (instancetype)initWithKSCrashReport:(NSDictionary *)event { NSString *deviceAppHash = [event valueForKeyPath:@"system.device_app_hash"]; BugsnagDeviceWithState *device = [BugsnagDeviceWithState deviceWithKSCrashReport:event]; +#if TARGET_OS_IOS + NSNumber *orientation = [event valueForKeyPath:@"user.orientation"]; + if ([orientation isKindOfClass:[NSNumber class]]) { + device.orientation = BSGStringFromDeviceOrientation(orientation.longValue); + } +#endif + BugsnagUser *user = [self parseUser:event deviceAppHash:deviceAppHash deviceId:device.id]; NSDictionary *configDict = [event valueForKeyPath:@"user.config"]; @@ -352,6 +370,9 @@ - (instancetype)initWithKSCrashReport:(NSDictionary *)event { [configDict isKindOfClass:[NSDictionary class]] ? configDict : @{}]; BugsnagAppWithState *app = [BugsnagAppWithState appWithDictionary:event config:config codeBundleId:self.codeBundleId]; + + BugsnagSession *session = BSGSessionFromCrashReport(event, app, device, user); + BugsnagEvent *obj = [self initWithApp:app device:device handledState:handledState @@ -411,17 +432,22 @@ - (NSMutableDictionary *)parseOnCrashData:(NSDictionary *)report { @BSG_KSCrashField_State, @BSG_KSCrashField_Config, @BSG_KSCrashField_DiscardDepth, + @"batteryLevel", @"breadcrumbs", - @"startedAt", - @"unhandledCount", + @"charging", @"handledCount", @"id", + @"isLaunching", + @"orientation", + @"startedAt", + @"thermalState", + @"unhandledCount", ]; [userAtCrash removeObjectsForKeys:keysToRemove]; for (NSString *key in [userAtCrash allKeys]) { // remove any non-dictionary values if (![userAtCrash[key] isKindOfClass:[NSDictionary class]]) { - bsg_log_warn(@"Removing value added in onCrashHandler for key %@ as it is not a dictionary value", key); + bsg_log_debug(@"Removing value added in onCrashHandler for key %@ as it is not a dictionary value", key); [userAtCrash removeObjectForKey:key]; } } diff --git a/Bugsnag/Payload/BugsnagNotifier.m b/Bugsnag/Payload/BugsnagNotifier.m index 512038900..14f2ba349 100644 --- a/Bugsnag/Payload/BugsnagNotifier.m +++ b/Bugsnag/Payload/BugsnagNotifier.m @@ -21,7 +21,7 @@ - (instancetype)init { #else _name = @"Bugsnag Objective-C"; #endif - _version = @"6.16.8"; + _version = @"6.17.0"; _url = @"https://github.com/bugsnag/bugsnag-cocoa"; _dependencies = @[]; } diff --git a/Bugsnag/Payload/BugsnagSession+Private.h b/Bugsnag/Payload/BugsnagSession+Private.h index 2a0a179db..0cea2e121 100644 --- a/Bugsnag/Payload/BugsnagSession+Private.h +++ b/Bugsnag/Payload/BugsnagSession+Private.h @@ -8,6 +8,9 @@ #import +#import "BSGDefines.h" +#import "BSG_KSCrashReportWriter.h" + NS_ASSUME_NONNULL_BEGIN @class BugsnagUser; @@ -48,4 +51,16 @@ NSDictionary * BSGSessionToEventJson(BugsnagSession *session); /// Parses a session dictionary from an event's JSON representation. BugsnagSession *_Nullable BSGSessionFromEventJson(NSDictionary *_Nullable json, BugsnagApp *app, BugsnagDevice *device, BugsnagUser *user); +/// Saves the session info into bsg_runContext. +BSG_PRIVATE void BSGSessionUpdateRunContext(BugsnagSession *_Nullable session); + +/// Returns session information from bsg_lastRunContext. +BSG_PRIVATE BugsnagSession *_Nullable BSGSessionFromLastRunContext(BugsnagApp *app, BugsnagDevice *device, BugsnagUser *user); + +/// Saves current session information (from bsg_runContext) into a crash report. +BSG_PRIVATE void BSGSessionWriteCrashReport(const BSG_KSCrashReportWriter *writer); + +/// Returns session information from a crash report previously written to by BSGSessionWriteCrashReport or BSSerializeDataCrashHandler. +BSG_PRIVATE BugsnagSession *_Nullable BSGSessionFromCrashReport(NSDictionary *report, BugsnagApp *app, BugsnagDevice *device, BugsnagUser *user); + NS_ASSUME_NONNULL_END diff --git a/Bugsnag/Payload/BugsnagSession.m b/Bugsnag/Payload/BugsnagSession.m index d1c654e42..e64fc31e9 100644 --- a/Bugsnag/Payload/BugsnagSession.m +++ b/Bugsnag/Payload/BugsnagSession.m @@ -9,6 +9,7 @@ #import "BugsnagSession+Private.h" #import "BSGKeys.h" +#import "BSGRunContext.h" #import "BSG_RFC3339DateTool.h" #import "BugsnagApp+Private.h" #import "BugsnagDevice+Private.h" @@ -51,7 +52,7 @@ - (void)setUser:(NSString *_Nullable)userId @end -#pragma mark - +#pragma mark - Serialization NSDictionary * BSGSessionToDictionary(BugsnagSession *session) { return @{ @@ -103,3 +104,58 @@ - (void)setUser:(NSString *_Nullable)userId session.unhandledCount = [events[BSGKeyUnhandled] unsignedIntegerValue]; return session; } + +void BSGSessionUpdateRunContext(BugsnagSession *_Nullable session) { + if (session) { + [session.id getCString:bsg_runContext->sessionId maxLength:sizeof(bsg_runContext->sessionId) encoding:NSUTF8StringEncoding]; + bsg_runContext->sessionStartTime = session.startedAt.timeIntervalSinceReferenceDate; + bsg_runContext->handledCount = session.handledCount; + bsg_runContext->unhandledCount = session.unhandledCount; + } else { + bzero(bsg_runContext->sessionId, sizeof(bsg_runContext->sessionId)); + bsg_runContext->sessionStartTime = 0; + } + BSGRunContextUpdateTimestamp(); +} + +BugsnagSession * BSGSessionFromLastRunContext(BugsnagApp *app, BugsnagDevice *device, BugsnagUser *user) { + if (bsg_lastRunContext && bsg_lastRunContext->sessionId[0] && bsg_lastRunContext->sessionStartTime > 0) { + NSString *sessionId = @(bsg_lastRunContext->sessionId); + NSDate *startedAt = [NSDate dateWithTimeIntervalSinceReferenceDate:bsg_lastRunContext->sessionStartTime]; + BugsnagSession *session = [[BugsnagSession alloc] initWithId:sessionId startedAt:startedAt user:user app:app device:device]; + session.handledCount = bsg_lastRunContext->handledCount; + session.unhandledCount = bsg_lastRunContext->unhandledCount; + return session; + } else { + return nil; + } +} + +void BSGSessionWriteCrashReport(const BSG_KSCrashReportWriter *writer) { + if (bsg_runContext->sessionId[0] && bsg_runContext->sessionStartTime > 0) { + writer->addStringElement(writer, "id", bsg_runContext->sessionId); + writer->addFloatingPointElement(writer, "startedAt", bsg_runContext->sessionStartTime); + writer->addUIntegerElement(writer, "handledCount", bsg_runContext->handledCount); + writer->addUIntegerElement(writer, "unhandledCount", bsg_runContext->unhandledCount + 1); + } +} + +BugsnagSession * BSGSessionFromCrashReport(NSDictionary *report, BugsnagApp *app, BugsnagDevice *device, BugsnagUser *user) { + NSDictionary *json = report[BSGKeyUser]; + NSString *sessionId = json[BSGKeyId]; + id startedAt = json[BSGKeyStartedAt]; + NSDate *date = nil; + if ([startedAt isKindOfClass:[NSNumber class]]) { + date = [NSDate dateWithTimeIntervalSinceReferenceDate:[startedAt doubleValue]]; + } else if ([startedAt isKindOfClass:[NSString class]]) { + // BSSerializeDataCrashHandler used to store the date as a string + date = [BSG_RFC3339DateTool dateFromString:startedAt]; + } + if (!sessionId || !date) { + return nil; + } + BugsnagSession *session = [[BugsnagSession alloc] initWithId:sessionId startedAt:date user:user app:app device:device]; + session.handledCount = [json[BSGKeyHandledCount] unsignedLongValue]; + session.unhandledCount = [json[BSGKeyUnhandledCount] unsignedLongValue]; + return session; +} diff --git a/Bugsnag/Storage/BSGFileLocations.h b/Bugsnag/Storage/BSGFileLocations.h index bfe2a9f22..5ad3cdf01 100644 --- a/Bugsnag/Storage/BSGFileLocations.h +++ b/Bugsnag/Storage/BSGFileLocations.h @@ -12,7 +12,6 @@ NS_ASSUME_NONNULL_BEGIN @interface BSGFileLocations : NSObject -@property (readonly, nonatomic) NSString *kvStore; @property (readonly, nonatomic) NSString *breadcrumbs; @property (readonly, nonatomic) NSString *events; @property (readonly, nonatomic) NSString *kscrashReports; @@ -39,6 +38,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (readonly, nonatomic) NSString *metadata; +/** + * BSGRunContext + */ +@property (readonly, nonatomic) NSString *runContext; + /** * State info that gets added to the low level crash report. */ diff --git a/Bugsnag/Storage/BSGFileLocations.m b/Bugsnag/Storage/BSGFileLocations.m index f842ba3ae..9d09ba017 100644 --- a/Bugsnag/Storage/BSGFileLocations.m +++ b/Bugsnag/Storage/BSGFileLocations.m @@ -94,11 +94,11 @@ - (instancetype)initWithVersion1 { _sessions = getAndCreateSubdir(root, @"sessions"); _breadcrumbs = getAndCreateSubdir(root, @"breadcrumbs"); _kscrashReports = getAndCreateSubdir(root, @"KSCrashReports"); - _kvStore = getAndCreateSubdir(root, @"kvstore"); _appHangEvent = [root stringByAppendingPathComponent:@"app_hang.json"]; _flagHandledCrash = [root stringByAppendingPathComponent:@"bugsnag_handled_crash.txt"]; _configuration = [root stringByAppendingPathComponent:@"config.json"]; _metadata = [root stringByAppendingPathComponent:@"metadata.json"]; + _runContext = [root stringByAppendingPathComponent:@"run_context"]; _state = [root stringByAppendingPathComponent:@"state.json"]; _systemState = [root stringByAppendingPathComponent:@"system_state.json"]; } diff --git a/Bugsnag/Storage/BSGStorageMigratorV0V1.m b/Bugsnag/Storage/BSGStorageMigratorV0V1.m index 561fe5848..632cfa3bf 100644 --- a/Bugsnag/Storage/BSGStorageMigratorV0V1.m +++ b/Bugsnag/Storage/BSGStorageMigratorV0V1.m @@ -13,6 +13,13 @@ @implementation BSGStorageMigratorV0V1 +static void RemoveItem(NSFileManager *fileManager, NSString *path) { + NSError *error = nil; + if (![fileManager removeItemAtPath:path error:&error] && error.code != NSFileNoSuchFileError) { + bsg_log_err(@"%@", error); + } +} + + (BOOL) migrate { NSString *bundleName = [[NSBundle mainBundle] infoDictionary][@"CFBundleName"]; NSString *cachesDir = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; @@ -29,12 +36,11 @@ + (BOOL) migrate { @"bugsnag/state.json": files.state, @"bugsnag/state/system_state.json": files.systemState, @"bugsnag/breadcrumbs": files.breadcrumbs, - @"bsg_kvstore": files.kvStore, [@"Sessions" stringByAppendingPathComponent:bundleName]: files.sessions, [@"KSCrashReports" stringByAppendingPathComponent:bundleName]: files.kscrashReports, }; - NSFileManager *fm = [NSFileManager defaultManager]; + NSFileManager *fm = [[NSFileManager alloc] init]; NSError *err = nil; bool success = true; @@ -42,11 +48,7 @@ + (BOOL) migrate { NSString *srcPath = [cachesDir stringByAppendingPathComponent:key]; NSString *dstPath = mappings[key]; if ([fm fileExistsAtPath:srcPath]) { - if([fm fileExistsAtPath:dstPath]) { - if(![fm removeItemAtPath:dstPath error:&err]) { - bsg_log_err(@"Could not remove %@: %@", dstPath, err); - } - } + RemoveItem(fm, dstPath); if(![fm moveItemAtPath:srcPath toPath:dstPath error:&err]) { bsg_log_err(@"Could not move %@ to %@: %@", srcPath, dstPath, err); success = false; @@ -67,6 +69,9 @@ + (BOOL) migrate { } } + NSString *root = [files.events stringByDeletingLastPathComponent]; + RemoveItem(fm, [root stringByAppendingPathComponent:@"kvstore"]); + return success; } diff --git a/BugsnagNetworkRequestPlugin.podspec.json b/BugsnagNetworkRequestPlugin.podspec.json index 78d4679e5..174c5f2f8 100644 --- a/BugsnagNetworkRequestPlugin.podspec.json +++ b/BugsnagNetworkRequestPlugin.podspec.json @@ -1,16 +1,16 @@ { "name": "BugsnagNetworkRequestPlugin", - "version": "6.16.8", + "version": "6.17.0", "summary": "Network request monitoring support for Bugsnag.", "homepage": "https://bugsnag.com", "license": "MIT", "authors": { "Bugsnag": "notifiers@bugsnag.com" }, - "readme": "https://raw.githubusercontent.com/bugsnag/bugsnag-cocoa/v6.16.8/BugsnagNetworkRequestPlugin/README.md", + "readme": "https://raw.githubusercontent.com/bugsnag/bugsnag-cocoa/v6.17.0/BugsnagNetworkRequestPlugin/README.md", "source": { "git": "https://github.com/bugsnag/bugsnag-cocoa.git", - "tag": "v6.16.8" + "tag": "v6.17.0" }, "dependencies": { "Bugsnag": "~> 6.13" diff --git a/CHANGELOG.md b/CHANGELOG.md index bab3c3b23..b02d9f45c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ Changelog ========= +## 6.17.0 (2022-05-11) + +### Enhancements + +* Add `device.freeMemory` to OOM and Thermal Kill events on iOS 13 and later. + [#1357](https://github.com/bugsnag/bugsnag-cocoa/pull/1357) + +* Add `device.time` to OOM and Thermal Kill events. + [#1355](https://github.com/bugsnag/bugsnag-cocoa/pull/1355) + ## 6.16.8 (2022-05-04) ### Changes diff --git a/Dangerfile b/Dangerfile index e3f70ed31..cb4122bd2 100644 --- a/Dangerfile +++ b/Dangerfile @@ -33,4 +33,11 @@ if defined?(github) && github.branch_for_base == 'master' && !github.branch_for_ failure 'Only release PRs should target the master branch' end +begin + diff = git.diff_for_file Dir['Bugsnag/**/BSGRunContext.h'][0] + if diff && diff.patch !~ /BSGRUNCONTEXT_VERSION/ + warn 'This PR modifies `BSGRunContext.h` but does not change `BSGRUNCONTEXT_VERSION`' + end +end + framework_size diff --git a/Framework/Info.plist b/Framework/Info.plist index 585685f86..167e55b3d 100644 --- a/Framework/Info.plist +++ b/Framework/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 6.16.8 + 6.17.0 CFBundleVersion 1 diff --git a/Gemfile b/Gemfile index a35534d86..4e34287df 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ gem 'jazzy' # A reference to Maze Runner is only needed for running tests locally and if committed it must be # portable for CI, e.g. a specific release. However, leaving it commented out would mean quicker CI. -gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v6.9.0' +gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v6.15.0' # Use a specific branch #gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'master' diff --git a/Gemfile.lock b/Gemfile.lock index 83f01bbfb..e2475e3e7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/bugsnag/maze-runner - revision: 4979ecccf0b3f21fa4f9cb439191081993ae5704 - tag: v6.9.0 + revision: d71ba8a1b770e86800b4e9f6782a63fef019a989 + tag: v6.15.0 specs: - bugsnag-maze-runner (6.9.0) + bugsnag-maze-runner (6.15.0) appium_lib (~> 11.2.0) bugsnag (~> 6.24) cucumber (~> 7.1) @@ -41,7 +41,7 @@ GEM faye-websocket (~> 0.11.0) selenium-webdriver (~> 3.14, >= 3.14.1) atomos (0.1.3) - bugsnag (6.24.1) + bugsnag (6.24.2) concurrent-ruby (~> 1.0) builder (3.2.4) childprocess (3.0.0) @@ -98,10 +98,10 @@ GEM mime-types (~> 3.3, >= 3.3.1) multi_test (~> 0.1, >= 0.1.2) sys-uname (~> 1.2, >= 1.2.2) - cucumber-core (10.1.0) + cucumber-core (10.1.1) cucumber-gherkin (~> 22.0, >= 22.0.0) cucumber-messages (~> 17.1, >= 17.1.1) - cucumber-tag-expressions (~> 4.0, >= 4.0.2) + cucumber-tag-expressions (~> 4.1, >= 4.1.0) cucumber-create-meta (6.0.4) cucumber-messages (~> 17.1, >= 17.1.1) sys-uname (~> 1.2, >= 1.2.2) @@ -155,7 +155,7 @@ GEM nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) - nokogiri (1.13.3) + nokogiri (1.13.6) mini_portile2 (~> 2.8.0) racc (~> 1.4) open4 (1.3.4) diff --git a/Makefile b/Makefile index 9a53b510a..1f9063976 100644 --- a/Makefile +++ b/Makefile @@ -122,7 +122,9 @@ test-fixtures: ## Build the end-to-end test fixture e2e_ios_local: @./features/scripts/export_ios_app.sh - bundle exec maze-runner --app=features/fixtures/ios/output/iOSTestApp.ipa --farm=local --os=ios --os-version=14 --apple-team-id=372ZUL2ZB7 --udid="$(shell idevice_id -l)" $(FEATURES) + # Recent Appium versions don't always uninstall the old version of the app ¯\_(ツ)_/¯ + -ideviceinstaller --uninstall com.bugsnag.iOSTestApp + bundle exec maze-runner --app=features/fixtures/ios/output/iOSTestApp.ipa --farm=local --os=ios --apple-team-id=372ZUL2ZB7 --udid="$(shell idevice_id -l)" $(FEATURES) e2e_macos: ./features/scripts/export_mac_app.sh diff --git a/Tests/BugsnagTests/BSGNotificationBreadcrumbsTests.m b/Tests/BugsnagTests/BSGNotificationBreadcrumbsTests.m index 5f31ad44f..84cff200c 100644 --- a/Tests/BugsnagTests/BSGNotificationBreadcrumbsTests.m +++ b/Tests/BugsnagTests/BSGNotificationBreadcrumbsTests.m @@ -84,6 +84,25 @@ - (UIViewController *)rootViewController {return (UIViewController *)self.mockVi #endif @end + +#if TARGET_OS_IOS +@interface MockDevice : NSObject +@property UIDeviceOrientation orientation; +@end + +@implementation MockDevice +@end +#endif + + +@interface MockProcessInfo : NSObject +@property NSProcessInfoThermalState thermalState API_AVAILABLE(ios(11.0), tvos(11.0)); +@end + +@implementation MockProcessInfo +@end + + #pragma mark - @implementation BSGNotificationBreadcrumbsTests @@ -94,7 +113,9 @@ - (void)setUp { self.breadcrumb = nil; BugsnagConfiguration *configuration = [[BugsnagConfiguration alloc] initWithApiKey:@"0192837465afbecd0192837465afbecd"]; self.notificationBreadcrumbs = [[BSGNotificationBreadcrumbs alloc] initWithConfiguration:configuration breadcrumbSink:self]; - self.notificationCenter = NSNotificationCenter.defaultCenter; + self.notificationBreadcrumbs.notificationCenter = [[NSNotificationCenter alloc] init]; + self.notificationBreadcrumbs.workspaceNotificationCenter = [[NSNotificationCenter alloc] init]; + self.notificationCenter = self.notificationBreadcrumbs.notificationCenter; self.notificationObject = nil; self.notificationUserInfo = nil; [self.notificationBreadcrumbs start]; @@ -134,6 +155,25 @@ - (void)testNSUndoManagerNotifications { TEST(NSUndoManagerDidUndoChangeNotification, BSGBreadcrumbTypeState, @"Undo Operation", @{}); } +- (void)testNSProcessInfoThermalStateThermalStateNotifications { + if (@available(iOS 13.0, tvOS 13.0, *)) { + MockProcessInfo *processInfo = [[MockProcessInfo alloc] init]; + self.notificationObject = processInfo; + + // Set initial state + processInfo.thermalState = NSProcessInfoThermalStateNominal; + [self breadcrumbForNotificationWithName:NSProcessInfoThermalStateDidChangeNotification]; + + processInfo.thermalState = NSProcessInfoThermalStateCritical; + TEST(NSProcessInfoThermalStateDidChangeNotification, BSGBreadcrumbTypeState, + @"Thermal State Changed", (@{@"from": @"nominal", @"to": @"critical"})); + + processInfo.thermalState = NSProcessInfoThermalStateCritical; + XCTAssertNil([self breadcrumbForNotificationWithName:NSProcessInfoThermalStateDidChangeNotification], + @"No breadcrumb should be left if state did not change"); + } +} + #pragma mark iOS Tests #if TARGET_OS_IOS @@ -146,6 +186,27 @@ - (void)testUIApplicationNotifications { TEST(UIApplicationWillTerminateNotification, BSGBreadcrumbTypeState, @"App Will Terminate", @{}); } +- (void)testUIDeviceOrientationNotifications { + MockDevice *device = [[MockDevice alloc] init]; + self.notificationObject = device; + + // Set initial state + device.orientation = UIDeviceOrientationPortrait; + [self breadcrumbForNotificationWithName:UIDeviceOrientationDidChangeNotification]; + + device.orientation = UIDeviceOrientationLandscapeLeft; + TEST(UIDeviceOrientationDidChangeNotification, BSGBreadcrumbTypeState, + @"Orientation Changed", (@{@"from": @"portrait", @"to": @"landscapeleft"})); + + device.orientation = UIDeviceOrientationUnknown; + XCTAssertNil([self breadcrumbForNotificationWithName:UIDeviceOrientationDidChangeNotification], + @"UIDeviceOrientationUnknown should be ignored"); + + device.orientation = UIDeviceOrientationLandscapeLeft; + XCTAssertNil([self breadcrumbForNotificationWithName:UIDeviceOrientationDidChangeNotification], + @"No breadcrumb should be left if orientation did not change"); +} + - (void)testUIKeyboardNotifications { TEST(UIKeyboardDidHideNotification, BSGBreadcrumbTypeState, @"Keyboard Became Hidden", @{}); TEST(UIKeyboardDidShowNotification, BSGBreadcrumbTypeState, @"Keyboard Became Visible", @{}); @@ -380,7 +441,7 @@ - (void)testNSWindowNotificationsWithData { } - (void)testNSWorkspaceNotifications { - self.notificationCenter = NSWorkspace.sharedWorkspace.notificationCenter; + self.notificationCenter = self.notificationBreadcrumbs.workspaceNotificationCenter; TEST(NSWorkspaceScreensDidSleepNotification, BSGBreadcrumbTypeState, @"Workspace Screen Slept", @{}); TEST(NSWorkspaceScreensDidWakeNotification, BSGBreadcrumbTypeState, @"Workspace Screen Awoke", @{}); } diff --git a/Tests/BugsnagTests/BSGOutOfMemoryTests.m b/Tests/BugsnagTests/BSGOutOfMemoryTests.m index a7d4dbbb3..f120a1711 100644 --- a/Tests/BugsnagTests/BSGOutOfMemoryTests.m +++ b/Tests/BugsnagTests/BSGOutOfMemoryTests.m @@ -1,12 +1,12 @@ #import #import "BSGFileLocations.h" +#import "BSGRunContext.h" #import "BSG_KSCrashState.h" #import "BSG_KSSystemInfo.h" #import "Bugsnag.h" #import "BugsnagClient+Private.h" #import "BugsnagConfiguration.h" -#import "BugsnagKVStoreObjC.h" #import "BugsnagSystemState.h" #import "BugsnagTestConstants.h" @@ -43,7 +43,6 @@ - (void)testOOMFieldsSetCorrectly { NSDictionary *app = [state objectForKey:@"app"]; XCTAssertNotNil([app objectForKey:@"bundleVersion"]); XCTAssertNotNil([app objectForKey:@"id"]); - XCTAssertNotNil([app objectForKey:@"inForeground"]); XCTAssertNotNil([app objectForKey:@"version"]); XCTAssertNotNil([app objectForKey:@"name"]); XCTAssertEqualObjects([app valueForKey:@"codeBundleId"], @"codeBundleIdHere"); @@ -71,59 +70,66 @@ -(void)testBadJSONData { } - (void)testLastLaunchTerminatedUnexpectedly { - BugsnagKVStore *kvStore = [BugsnagKVStore new]; - BugsnagSystemState *(^ systemState)() = ^{ - return [[BugsnagSystemState alloc] initWithConfiguration:[[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]]; - }; + if (!bsg_runContext) { + BSGRunContextInit(BSGFileLocations.current.runContext.fileSystemRepresentation); + } + const struct BSGRunContext *oldContext = bsg_lastRunContext; + struct BSGRunContext lastRunContext = *bsg_runContext; + bsg_lastRunContext = &lastRunContext; // Debugger active - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE]; - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - XCTAssertFalse(systemState().lastLaunchTerminatedUnexpectedly); - - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE]; - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - XCTAssertFalse(systemState().lastLaunchTerminatedUnexpectedly); - - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE]; - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - XCTAssertFalse(systemState().lastLaunchTerminatedUnexpectedly); - - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE]; - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - XCTAssertFalse(systemState().lastLaunchTerminatedUnexpectedly); + lastRunContext.isDebuggerAttached = true; + lastRunContext.isTerminating = true; + lastRunContext.isForeground = true; + XCTAssertFalse(BSGRunContextWasKilled()); + + lastRunContext.isDebuggerAttached = true; + lastRunContext.isTerminating = true; + lastRunContext.isForeground = false; + XCTAssertFalse(BSGRunContextWasKilled()); + + lastRunContext.isDebuggerAttached = true; + lastRunContext.isTerminating = false; + lastRunContext.isForeground = true; + XCTAssertFalse(BSGRunContextWasKilled()); + + lastRunContext.isDebuggerAttached = true; + lastRunContext.isTerminating = false; + lastRunContext.isForeground = false; + XCTAssertFalse(BSGRunContextWasKilled()); // Debugger inactive - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE]; - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - XCTAssertFalse(systemState().lastLaunchTerminatedUnexpectedly); - - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE]; - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - XCTAssertFalse(systemState().lastLaunchTerminatedUnexpectedly); - - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE]; - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - [kvStore setBoolean:true forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - XCTAssertTrue(systemState().lastLaunchTerminatedUnexpectedly); - - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE]; - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - [kvStore setBoolean:false forKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; - XCTAssertFalse(systemState().lastLaunchTerminatedUnexpectedly); - - // Delete keys so they don't interfere with other tests. - [kvStore deleteKey:SYSTEMSTATE_APP_DEBUGGER_IS_ACTIVE]; - [kvStore deleteKey:SYSTEMSTATE_APP_WAS_TERMINATED]; - [kvStore deleteKey:SYSTEMSTATE_APP_IS_IN_FOREGROUND]; + lastRunContext.isDebuggerAttached = false; + lastRunContext.isTerminating = true; + lastRunContext.isForeground = true; + XCTAssertFalse(BSGRunContextWasKilled()); + + lastRunContext.isDebuggerAttached = false; + lastRunContext.isTerminating = true; + lastRunContext.isForeground = false; + XCTAssertFalse(BSGRunContextWasKilled()); + + lastRunContext.isDebuggerAttached = false; + lastRunContext.isTerminating = false; + lastRunContext.isForeground = true; + XCTAssertTrue(BSGRunContextWasKilled()); + + uuid_generate(lastRunContext.machoUUID); + XCTAssertFalse(BSGRunContextWasKilled()); + uuid_copy(lastRunContext.machoUUID, bsg_runContext->machoUUID); + + lastRunContext.bootTime = 0; + XCTAssertFalse(BSGRunContextWasKilled()); + lastRunContext.bootTime = bsg_runContext->bootTime; + + lastRunContext.isDebuggerAttached = false; + lastRunContext.isTerminating = false; + lastRunContext.isForeground = false; + XCTAssertFalse(BSGRunContextWasKilled()); + + bsg_lastRunContext = oldContext; } @end diff --git a/Tests/BugsnagTests/BSGStorageMigratorTests.m b/Tests/BugsnagTests/BSGStorageMigratorTests.m index a272305de..946a132f7 100644 --- a/Tests/BugsnagTests/BSGStorageMigratorTests.m +++ b/Tests/BugsnagTests/BSGStorageMigratorTests.m @@ -60,7 +60,6 @@ - (NSDictionary *)getDirs { return @{ [c stringByAppendingPathComponent:@"bugsnag/breadcrumbs"]: [r stringByAppendingPathComponent:@"breadcrumbs"], - [c stringByAppendingPathComponent:@"bsg_kvstore"]: [r stringByAppendingPathComponent:@"kvstore"], [c stringByAppendingPathComponent:@"Sessions/xctest"]: [r stringByAppendingPathComponent:@"sessions"], [c stringByAppendingPathComponent:@"KSCrashReports/xctest"]: [r stringByAppendingPathComponent:@"KSCrashReports"], }; diff --git a/Tests/BugsnagTests/BSGUtilsTests.m b/Tests/BugsnagTests/BSGUtilsTests.m index 10842d4b6..70e9d9853 100644 --- a/Tests/BugsnagTests/BSGUtilsTests.m +++ b/Tests/BugsnagTests/BSGUtilsTests.m @@ -24,6 +24,7 @@ - (void)testBSGStringFromDeviceOrientation { XCTAssertEqualObjects(BSGStringFromDeviceOrientation(UIDeviceOrientationLandscapeLeft), @"landscapeleft"); XCTAssertEqualObjects(BSGStringFromDeviceOrientation(UIDeviceOrientationFaceUp), @"faceup"); XCTAssertEqualObjects(BSGStringFromDeviceOrientation(UIDeviceOrientationFaceDown), @"facedown"); + XCTAssertNil(BSGStringFromDeviceOrientation(UIDeviceOrientationUnknown)); XCTAssertNil(BSGStringFromDeviceOrientation(-1)); XCTAssertNil(BSGStringFromDeviceOrientation(99)); } diff --git a/Tests/BugsnagTests/BugsnagApiValidationTest.m b/Tests/BugsnagTests/BugsnagApiValidationTest.m index 01cacb3a0..b204e0c48 100644 --- a/Tests/BugsnagTests/BugsnagApiValidationTest.m +++ b/Tests/BugsnagTests/BugsnagApiValidationTest.m @@ -9,7 +9,6 @@ #import #import #import "BugsnagTestConstants.h" -#import "BugsnagKVStoreObjC.h" #import "TestSupport.h" /** diff --git a/Tests/BugsnagTests/BugsnagClientMirrorTest.m b/Tests/BugsnagTests/BugsnagClientMirrorTest.m index 3002beeb1..85125d123 100644 --- a/Tests/BugsnagTests/BugsnagClientMirrorTest.m +++ b/Tests/BugsnagTests/BugsnagClientMirrorTest.m @@ -46,7 +46,6 @@ - (void)setUp { @"automaticBreadcrumbMenuItemEvents @16@0:8", @"automaticBreadcrumbStateEvents @16@0:8", @"automaticBreadcrumbTableItemEvents @16@0:8", - @"batteryChanged: v24@0:8@16", @"breadcrumbs @16@0:8", @"codeBundleId @16@0:8", @"collectAppWithState @16@0:8", @@ -73,8 +72,6 @@ - (void)setUp { @"generateThreads @16@0:8", @"initWithConfiguration: @24@0:8@16", @"initializeNotificationNameMap v16@0:8", - @"lastOrientation @16@0:8", - @"lastThermalState q16@0:8", @"leaveBreadcrumbForEvent: v24@0:8@16", @"loadAppHangEvent @16@0:8", @"metadata @16@0:8", @@ -109,9 +106,7 @@ - (void)setUp { @"setExtraRuntimeInfo: v24@0:8@16", @"setFeatureFlagStore: v24@0:8@16", @"setFeatureFlagStore: v24@0:8^{NSMutableDictionary=#}16", - @"setLastOrientation: v24@0:8@16", @"setLastRunInfo: v24@0:8@16", - @"setLastThermalState: v24@0:8q16", @"setMetadata: v24@0:8@16", @"setMetadataFromLastLaunch: v24@0:8@16", @"setMetadataLock: v24@0:8@16", diff --git a/Tests/BugsnagTests/BugsnagClientTests.m b/Tests/BugsnagTests/BugsnagClientTests.m index cfc787508..5cb04d87d 100644 --- a/Tests/BugsnagTests/BugsnagClientTests.m +++ b/Tests/BugsnagTests/BugsnagClientTests.m @@ -321,31 +321,4 @@ - (NSDictionary *)generateLargeMetadata { return dict; } -#if TARGET_OS_IOS || TARGET_OS_TV - -static BOOL testOnCrashHandlerNotCalledForOOM_didCallOnCrashHandler; - -static void testOnCrashHandlerNotCalledForOOM_onCrashHandler(const BSG_KSCrashReportWriter *writer) { - testOnCrashHandlerNotCalledForOOM_didCallOnCrashHandler = YES; -} - -static BOOL testOnCrashHandlerNotCalledForOOM_lastLaunchTerminatedUnexpectedly(id self, SEL _cmd) { - return YES; -} - -- (void)testOnCrashHandlerNotCalledForOOM { - BugsnagConfiguration *configuration = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; - configuration.onCrashHandler = testOnCrashHandlerNotCalledForOOM_onCrashHandler; - BugsnagClient *client = [[BugsnagClient alloc] initWithConfiguration:configuration]; - Method method = class_getInstanceMethod([BugsnagSystemState class], @selector(lastLaunchTerminatedUnexpectedly)); - NSParameterAssert(method != NULL); - void *originalImplementation = method_setImplementation(method, (void *)testOnCrashHandlerNotCalledForOOM_lastLaunchTerminatedUnexpectedly); - [client start]; - method_setImplementation(method, originalImplementation); - XCTAssertFalse(testOnCrashHandlerNotCalledForOOM_didCallOnCrashHandler, @"onCrashHandler should not be called for OOMs"); - XCTAssertTrue(client.lastRunInfo.crashed); -} - -#endif - @end diff --git a/Tests/BugsnagTests/BugsnagConfigurationTests.m b/Tests/BugsnagTests/BugsnagConfigurationTests.m index 50e698f1e..e5d4a89a6 100644 --- a/Tests/BugsnagTests/BugsnagConfigurationTests.m +++ b/Tests/BugsnagTests/BugsnagConfigurationTests.m @@ -63,7 +63,7 @@ - (void)testSetEmptySessionsEndpoint { config.endpoints = [[BugsnagEndpointConfiguration alloc] initWithNotify:@"http://notify.example.com" sessions:@""]; BugsnagSessionTracker *sessionTracker - = [[BugsnagSessionTracker alloc] initWithConfig:config client:nil callback:^(BugsnagSession *session) {}]; + = [[BugsnagSessionTracker alloc] initWithConfig:config client:nil]; XCTAssertNil(sessionTracker.runningSession); [sessionTracker startNewSession]; @@ -75,7 +75,7 @@ - (void)testSetMalformedSessionsEndpoint { config.endpoints = [[BugsnagEndpointConfiguration alloc] initWithNotify:@"http://notify.example.com" sessions:@"f"]; BugsnagSessionTracker *sessionTracker - = [[BugsnagSessionTracker alloc] initWithConfig:config client:nil callback:^(BugsnagSession *session) {}]; + = [[BugsnagSessionTracker alloc] initWithConfig:config client:nil]; XCTAssertNil(sessionTracker.runningSession); [sessionTracker startNewSession]; diff --git a/Tests/BugsnagTests/BugsnagKVStoreTest.m b/Tests/BugsnagTests/BugsnagKVStoreTest.m deleted file mode 100644 index da0232bfa..000000000 --- a/Tests/BugsnagTests/BugsnagKVStoreTest.m +++ /dev/null @@ -1,248 +0,0 @@ -// -// BugsnagKVStoreTest.m -// Bugsnag-iOSTests -// -// Created by Karl Stenerud on 11.09.20. -// Copyright © 2020 Bugsnag Inc. All rights reserved. -// - -#import -#import "BugsnagKVStore.h" - -#define AssertEqualCString(actual, expected) \ - actual == NULL \ - ? XCTFail("Expected '%s', got NULL", expected) \ - : XCTAssertEqual(strcmp(actual, expected), 0, "Expected '%s', got '%s'", expected, actual) - -@interface BugsnagKVStoreTest : XCTestCase - -@end - -@implementation BugsnagKVStoreTest - -- (NSString *)getCachesDir { - NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - if ([dirs count] == 0) { - XCTFail(@"Could not locate cache directory path."); - return nil; - } - - if ([dirs[0] length] == 0) { - XCTFail(@"Cache directory path is empty!"); - return nil; - } - return dirs[0]; -} - -- (BOOL)openKVStore { - int err = 0; - NSString* path = [[self getCachesDir] stringByAppendingPathComponent:@"bsgkv"]; - bsgkv_open([path cStringUsingEncoding:NSUTF8StringEncoding], &err); - XCTAssertEqual(err, 0); - return err == 0; -} - -- (void)setUp { - [self openKVStore]; -} - -- (void)tearDown { - bsgkv_close(); -} - -#define DECLARE_SCALAR_TEST(FUNC, TYPE, EXPECTED1, EXPECTED2) \ -- (void)test##FUNC { \ - const char* name = "test-" #TYPE; \ - TYPE expected = EXPECTED1; \ - TYPE expected2 = EXPECTED2; \ - TYPE actual = 0; \ - int err = 0; \ -\ - bsgkv_delete(name, &err); \ - XCTAssertEqual(err, 0); \ -\ - bsgkv_delete(name, &err); \ - XCTAssertEqual(err, 0); \ -\ - bsgkv_set##FUNC(name, expected, &err); \ - XCTAssertEqual(err, 0); \ -\ - actual = bsgkv_get##FUNC(name, &err); \ - XCTAssertEqual(err, 0); \ - XCTAssertEqual(actual, expected); \ -\ - bsgkv_set##FUNC(name, expected2, &err); \ - XCTAssertEqual(err, 0); \ -\ - actual = bsgkv_get##FUNC(name, &err); \ - XCTAssertEqual(err, 0); \ - XCTAssertEqual(actual, expected2); \ -\ - bsgkv_delete(name, &err); \ - XCTAssertEqual(err, 0); \ -\ - actual = bsgkv_get##FUNC(name, &err); \ - XCTAssertEqual(err, ENOENT); \ -\ - bsgkv_set##FUNC(name, expected, &err); \ - XCTAssertEqual(err, 0); \ -\ - actual = bsgkv_get##FUNC(name, &err); \ - XCTAssertEqual(err, 0); \ - XCTAssertEqual(actual, expected); \ -} - -DECLARE_SCALAR_TEST(Boolean, bool, true, false) -DECLARE_SCALAR_TEST(Int, int64_t, 1000, 2000) -DECLARE_SCALAR_TEST(Float, double, 123.456, 7.89e50) - - -- (void)testString { - const char* name = "test-string"; - const char* expected = "Blah blah blah blah blah"; - const char* expected2 = "Something else"; - char actual[100]; - int err = 0; - - bsgkv_delete(name, &err); - XCTAssertEqual(err, 0); - - bsgkv_delete(name, &err); - XCTAssertEqual(err, 0); - - bsgkv_setString(name, expected, &err); - XCTAssertEqual(err, 0); - - bsgkv_getString(name, actual, sizeof(actual), &err); - XCTAssertEqual(err, 0); - AssertEqualCString(actual, expected); - - bsgkv_setString(name, expected2, &err); - XCTAssertEqual(err, 0); - - bsgkv_getString(name, actual, sizeof(actual), &err); - XCTAssertEqual(err, 0); - AssertEqualCString(actual, expected2); - - bsgkv_delete(name, &err); - XCTAssertEqual(err, 0); - - bsgkv_getString(name, actual, sizeof(actual), &err); - XCTAssertEqual(err, ENOENT); - - bsgkv_setString(name, expected, &err); - XCTAssertEqual(err, 0); - - bsgkv_getString(name, actual, sizeof(actual), &err); - XCTAssertEqual(err, 0); - AssertEqualCString(actual, expected); -} - -- (void)testBytes { - const char* name = "test-bytes"; - const uint8_t expected[] = {0x01, 0x02, 0x03, 0x04, 0x05}; - const uint8_t expected2[] = {0xff, 0xfe, 0x0d}; - uint8_t actual[100]; - int err = 0; - int length = 0; - - bsgkv_delete(name, &err); - XCTAssertEqual(err, 0); - - bsgkv_delete(name, &err); - XCTAssertEqual(err, 0); - - bsgkv_setBytes(name, expected, sizeof(expected), &err); - XCTAssertEqual(err, 0); - - length = sizeof(actual); - bsgkv_getBytes(name, actual, &length, &err); - XCTAssertEqual(err, 0); - XCTAssertEqual(length, sizeof(expected)); - XCTAssertEqual(memcmp(actual, expected, sizeof(expected)), 0); - - bsgkv_setBytes(name, expected2, sizeof(expected2), &err); - XCTAssertEqual(err, 0); - - length = sizeof(actual); - bsgkv_getBytes(name, actual, &length, &err); - XCTAssertEqual(err, 0); - XCTAssertEqual(length, sizeof(expected2)); - XCTAssertEqual(memcmp(actual, expected2, sizeof(expected2)), 0); - - bsgkv_delete(name, &err); - XCTAssertEqual(err, 0); - - length = sizeof(actual); - bsgkv_getBytes(name, actual, &length, &err); - XCTAssertEqual(err, ENOENT); - - bsgkv_setBytes(name, expected, sizeof(expected), &err); - XCTAssertEqual(err, 0); - - length = sizeof(actual); - bsgkv_getBytes(name, actual, &length, &err); - XCTAssertEqual(err, 0); - XCTAssertEqual(length, sizeof(expected)); - XCTAssertEqual(memcmp(actual, expected, sizeof(expected)), 0); -} - -- (void)testFileDescriptorLeak { - struct rlimit limit = {0}; - if (getrlimit(RLIMIT_NOFILE, &limit) < 0) { - perror("getrlimit"); - } - rlim_t originalLimit = limit.rlim_cur; - limit.rlim_cur = 256; - if (setrlimit(RLIMIT_NOFILE, &limit) < 0) { - perror("setrlimit"); - } - - for (int i = 0; i < limit.rlim_cur; i++) { - bsgkv_close(); - - if (![self openKVStore]) { - goto testFileDescriptorLeak_end; - } - } - - const char *key = "password"; - - int err = 0; - - for (int i = 0; i < limit.rlim_cur; i++) { - uint8_t value[] = "secret"; - bsgkv_setBytes(key, value, sizeof(value), &err); - if (err != 0) { - XCTFail("bsgkv_setBytes err: %d", err); - goto testFileDescriptorLeak_end; - } - } - - for (int i = 0; i < limit.rlim_cur; i++) { - uint8_t value[24]; - int length = sizeof(value); - bsgkv_getBytes(key, value, &length, &err); - if (err != 0) { - XCTFail("bsgkv_getBytes err: %d", err); - goto testFileDescriptorLeak_end; - } - } - - for (int i = 0; i < limit.rlim_cur; i++) { - bsgkv_delete(key, &err); - if (err != 0) { - XCTFail("bsgkv_delete err: %d", err); - goto testFileDescriptorLeak_end; - } - } - -testFileDescriptorLeak_end: - - limit.rlim_cur = originalLimit; - if (setrlimit(RLIMIT_NOFILE, &limit) < 0) { - perror("setrlimit"); - } -} - -@end diff --git a/Tests/BugsnagTests/BugsnagSessionTrackerStopTest.m b/Tests/BugsnagTests/BugsnagSessionTrackerStopTest.m index e015e8485..7e1628330 100644 --- a/Tests/BugsnagTests/BugsnagSessionTrackerStopTest.m +++ b/Tests/BugsnagTests/BugsnagSessionTrackerStopTest.m @@ -23,7 +23,7 @@ - (void)setUp { [super setUp]; self.configuration = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; self.configuration.autoTrackSessions = NO; - self.tracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:nil callback:^(BugsnagSession *session) {}]; + self.tracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:nil]; } /** diff --git a/Tests/BugsnagTests/BugsnagSessionTrackerTest.m b/Tests/BugsnagTests/BugsnagSessionTrackerTest.m index 2bb9ddd5c..642d92f53 100644 --- a/Tests/BugsnagTests/BugsnagSessionTrackerTest.m +++ b/Tests/BugsnagTests/BugsnagSessionTrackerTest.m @@ -26,7 +26,7 @@ - (void)setUp { [super setUp]; self.configuration = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; [self.configuration deletePersistedUserData]; - self.sessionTracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:nil callback:^(BugsnagSession *session) {}]; + self.sessionTracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:nil]; } - (void)testStartNewSession { @@ -123,7 +123,7 @@ - (void)testOnSendBlockFalse { [self.configuration addOnSessionBlock:^BOOL(BugsnagSession *sessionPayload) { return NO; }]; - self.sessionTracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:nil callback:^(BugsnagSession *session) {}]; + self.sessionTracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:nil]; [self.sessionTracker startNewSession]; XCTAssertNil(self.sessionTracker.currentSession); } @@ -136,7 +136,7 @@ - (void)testOnSendBlockTrue { [expectation fulfill]; return YES; }]; - self.sessionTracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:nil callback:^(BugsnagSession *session) {}]; + self.sessionTracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:nil]; [self.sessionTracker startNewSession]; [self waitForExpectations:@[expectation] timeout:2]; XCTAssertNotNil(self.sessionTracker.currentSession); diff --git a/Tests/BugsnagTests/BugsnagTests.m b/Tests/BugsnagTests/BugsnagTests.m index 8e9b593ef..f7c80bc27 100644 --- a/Tests/BugsnagTests/BugsnagTests.m +++ b/Tests/BugsnagTests/BugsnagTests.m @@ -235,6 +235,7 @@ - (void)testRemoveOnSessionBlock { expectation2.inverted = YES; BugsnagConfiguration *configuration = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + configuration.autoTrackSessions = NO; // non-sending bugsnag [configuration addOnSendErrorBlock:^BOOL(BugsnagEvent *_Nonnull event) { @@ -257,6 +258,7 @@ - (void)testRemoveOnSessionBlock { BugsnagClient *client = [[BugsnagClient alloc] initWithConfiguration:configuration]; [client start]; + [client startSession]; [self waitForExpectations:@[expectation1] timeout:1.0]; [client pauseSession]; diff --git a/Tests/BugsnagTests/Info.plist b/Tests/BugsnagTests/Info.plist index 6796a2627..0b8795ab1 100644 --- a/Tests/BugsnagTests/Info.plist +++ b/Tests/BugsnagTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 6.16.8 + 6.17.0 CFBundleVersion 1 diff --git a/Tests/BugsnagTests/TestSupport.m b/Tests/BugsnagTests/TestSupport.m index 4c9a15032..48d2cb579 100644 --- a/Tests/BugsnagTests/TestSupport.m +++ b/Tests/BugsnagTests/TestSupport.m @@ -11,6 +11,7 @@ #import "BSG_KSCrashC.h" #import "BSG_KSCrashState.h" #import "BSGFileLocations.h" +#import "BSGRunContext.h" #import "BSGUtils.h" #import "Bugsnag+Private.h" @@ -29,11 +30,7 @@ + (void) purgePersistentData { [Bugsnag purge]; - // bsg_kscrash_install() will refuse to install itself twice, so reinit kscrashstate to avoid leaking state between tests - NSString *path = [BSGFileLocations current].state; - NSString *dir = [path stringByDeletingLastPathComponent]; - [NSFileManager.defaultManager createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil]; - bsg_kscrashstate_init(path.fileSystemRepresentation, &crashContext()->state); + bsg_lastRunContext = NULL; } @end diff --git a/Tests/KSCrashTests/KSCrashState_Tests.m b/Tests/KSCrashTests/KSCrashState_Tests.m index e1d0c3a70..62d541688 100755 --- a/Tests/KSCrashTests/KSCrashState_Tests.m +++ b/Tests/KSCrashTests/KSCrashState_Tests.m @@ -27,13 +27,10 @@ #import "FileBasedTestCase.h" +#import "BSGRunContext.h" #import "BSG_KSCrashState.h" #import "BSG_KSCrashC.h" -#if TARGET_OS_TV -#import "UIApplicationStub.h" -#endif - @interface bsg_kscrashstate_Tests : FileBasedTestCase @end @@ -41,11 +38,18 @@ @interface bsg_kscrashstate_Tests : FileBasedTestCase @implementation bsg_kscrashstate_Tests -#if TARGET_OS_TV // Not needed on iOS because there the tests are injected into a host app +#if TARGET_OS_OSX || TARGET_OS_TV // Not needed on iOS because there the tests are injected into a host app - (void)setUp { - [self setUpUIApplicationStub]; // These tests assume applicationState == .active + + struct BSGRunContext *oldContext = bsg_runContext; + static struct BSGRunContext context = {0}; + context.isForeground = YES; // These tests assume applicationState == .active + bsg_runContext = &context; + [self addTeardownBlock:^{ + bsg_runContext = oldContext; + }]; [super setUp]; } diff --git a/Tests/TestHost-iOS/Info.plist b/Tests/TestHost-iOS/Info.plist index e684e16eb..2c24f5fd7 100644 --- a/Tests/TestHost-iOS/Info.plist +++ b/Tests/TestHost-iOS/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 6.16.8 + 6.17.0 CFBundleVersion 1 LSRequiresIPhoneOS diff --git a/VERSION b/VERSION index 2b06ab849..1980a78f2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.16.8 +6.17.0 diff --git a/features/app_and_device_attributes.feature b/features/app_and_device_attributes.feature index da7672ae7..8f8352667 100644 --- a/features/app_and_device_attributes.feature +++ b/features/app_and_device_attributes.feature @@ -31,9 +31,7 @@ Feature: App and Device attributes present And the error payload field "events.0.device.freeDisk" is an integer And the error payload field "events.0.device.freeMemory" is an integer - And the error payload field "events.0.device.orientation" equals the platform-dependent string: - | ios | @not_null | - | macos | @null | + And on iOS, the event "device.orientation" matches "(face(down|up)|landscape(left|right)|portrait(upsidedown)?)" And the error payload field "events.0.device.time" is a date And the event "device.freeMemory" is less than the event "device.totalMemory" diff --git a/features/app_hangs.feature b/features/app_hangs.feature index 77327be2a..b4960a3ea 100644 --- a/features/app_hangs.feature +++ b/features/app_hangs.feature @@ -58,9 +58,7 @@ Feature: App hangs And the error payload field "events.0.device.freeDisk" is an integer And the error payload field "events.0.device.freeMemory" is an integer - And the error payload field "events.0.device.orientation" equals the platform-dependent string: - | ios | @not_null | - | macos | @null | + And on iOS, the event "device.orientation" matches "(face(down|up)|landscape(left|right)|portrait(upsidedown)?)" And the error payload field "events.0.device.time" is a date # App @@ -111,11 +109,12 @@ Feature: App hangs And I configure Bugsnag for "AppHangFatalOnlyScenario" And I wait to receive an error And I clear the error queue + # Wait for fixture to receive the response and save the payload + And I wait for 2 seconds And I relaunch the app And I set the HTTP status code to 200 And I configure Bugsnag for "AppHangFatalOnlyScenario" And I wait to receive an error - And the event "severity" equals "error" And the event "severityReason.type" equals "appHang" And the event "threads.0.errorReportingThread" is true diff --git a/features/barebone_tests.feature b/features/barebone_tests.feature index b6a34ee2d..0dc94e459 100644 --- a/features/barebone_tests.feature +++ b/features/barebone_tests.feature @@ -37,6 +37,7 @@ Feature: Barebone tests And the event "device.modelNumber" equals the platform-dependent string: | ios | @not_null | | macos | @null | + And on iOS, the event "device.orientation" matches "(face(down|up)|landscape(left|right)|portrait(upsidedown)?)" And the event "device.osName" equals the platform-dependent string: | ios | iOS | | macos | Mac OS | @@ -44,15 +45,8 @@ Feature: Barebone tests And the event "device.runtimeVersions.clangVersion" is not null And the event "device.runtimeVersions.osBuild" is not null And the event "device.time" is a timestamp - And the event "metaData.device.batteryLevel" equals the platform-dependent string: - | ios | @not_null | - | macos | @null | - And the event "metaData.device.charging" equals the platform-dependent string: - | ios | @not_null | - | macos | @null | - And the event "metaData.device.orientation" equals the platform-dependent string: - | ios | @not_null | - | macos | @null | + And on iOS, the event "metaData.device.batteryLevel" is a number + And on iOS, the event "metaData.device.charging" is a boolean And the event "metaData.device.simulator" is false And the event "metaData.device.timezone" is not null And the event "metaData.device.wordSize" is not null @@ -63,6 +57,10 @@ Feature: Barebone tests And the event "metaData.user.group" equals "users" And the event "metaData.user.id" is null And the event "metaData.user.name" is null + And the event "session.id" is not null + And the event "session.startedAt" is not null + And the event "session.events.handled" equals 0 + And the event "session.events.unhandled" equals 1 And the event "severity" equals "warning" And the event "severityReason.type" equals "handledException" And the event "severityReason.unhandledOverridden" is true @@ -140,6 +138,7 @@ Feature: Barebone tests And the event "device.jailbroken" is false And the event "device.locale" is not null And the event "device.manufacturer" equals "Apple" + And on iOS, the event "device.orientation" matches "(face(down|up)|landscape(left|right)|portrait(upsidedown)?)" And the event "device.osName" equals the platform-dependent string: | ios | iOS | | macos | Mac OS | @@ -147,6 +146,9 @@ Feature: Barebone tests And the event "device.runtimeVersions.clangVersion" is not null And the event "device.runtimeVersions.osBuild" is not null And the event "device.time" is a timestamp + And on iOS, the event "metaData.device.batteryLevel" is a number + And on iOS, the event "metaData.device.charging" is a boolean + And the event "metaData.device.simulator" is false And the event "metaData.error.mach.code_name" equals "KERN_INVALID_ADDRESS" And the event "metaData.error.mach.code" equals "0x1" And the event "metaData.error.mach.exception_name" is not null @@ -157,6 +159,10 @@ Feature: Barebone tests And the event "metaData.user.group" equals "users" And the event "metaData.user.id" is null And the event "metaData.user.name" is null + And the event "session.id" is not null + And the event "session.startedAt" is not null + And the event "session.events.handled" equals 0 + And the event "session.events.unhandled" equals 1 And the event "severity" equals "error" And the event "severityReason.type" equals "unhandledException" And the event "severityReason.unhandledOverridden" is null @@ -230,28 +236,21 @@ Feature: Barebone tests And the event "device.modelNumber" equals the platform-dependent string: | ios | @not_null | | macos | @null | + And on iOS 13 and later, the event "device.freeMemory" is an integer And the event "device.osName" equals the platform-dependent string: | ios | iOS | | macos | Mac OS | + And the event "device.orientation" matches "(face(down|up)|landscape(left|right)|portrait(upsidedown)?)" And the event "device.osVersion" matches "\d+\.\d+" And the event "device.runtimeVersions.clangVersion" is not null And the event "device.runtimeVersions.osBuild" is not null - And the event "device.time" is null - And the event "device.totalMemory" is not null + And the event "device.time" is a timestamp + And the event "device.totalMemory" is an integer And the event "metaData.app.name" equals "iOSTestApp" And the event "metaData.custom.bar" equals "foo" - And the event "metaData.device.batteryLevel" equals the platform-dependent string: - | ios | @not_null | - | macos | @null | - And the event "metaData.device.charging" equals the platform-dependent string: - | ios | @not_null | - | macos | @null | - And the event "metaData.device.orientation" equals the platform-dependent string: - | ios | @not_null | - | macos | @null | - And the event "metaData.device.lowMemoryWarning" equals the platform-dependent string: - | ios | @not_null | - | macos | @null | + And the event "metaData.device.batteryLevel" is a number + And the event "metaData.device.charging" is a boolean + And the event "metaData.device.lowMemoryWarning" is true And the event "metaData.device.simulator" is false And the event "metaData.device.timezone" is not null And the event "metaData.device.wordSize" is not null @@ -280,7 +279,5 @@ Feature: Barebone tests And the error payload field "events.0.app.duration" is null And the error payload field "events.0.app.durationInForeground" is null And the error payload field "events.0.device.freeDisk" is null - And the error payload field "events.0.device.freeMemory" is null And the error payload field "events.0.device.model" matches the test device model - And the error payload field "events.0.device.totalMemory" is an integer And the error payload field "events.0.threads" is an array with 0 elements diff --git a/features/fixtures/macos/macOSTestApp/main.m b/features/fixtures/macos/macOSTestApp/main.m index 413f1121e..eec4420f8 100644 --- a/features/fixtures/macos/macOSTestApp/main.m +++ b/features/fixtures/macos/macOSTestApp/main.m @@ -9,6 +9,12 @@ #import int main(int argc, const char * argv[]) { + NSString *tmpdir = [[[NSProcessInfo processInfo] environment] objectForKey:@"TMPDIR"]; + [[NSFileManager defaultManager] removeItemAtPath: + // Avoids a crash observed in -[NSPersistentUICrashHandler inspectCrashDataWithModification:handler:] + // that seems to occur if "$TMPDIR/com.bugsnag.macOSTestApp.savedState" is corrupted + [tmpdir stringByAppendingPathComponent:@"com.bugsnag.macOSTestApp.savedState"] error:nil]; + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ // Disable state restoration to prevent the following dialog being shown after crashes // "The last time you opened macOSTestApp, it unexpectedly quit while reopening windows. diff --git a/features/fixtures/shared/scenarios/BareboneTestScenarios.swift b/features/fixtures/shared/scenarios/BareboneTestScenarios.swift index e8002a8de..1f5a3d650 100644 --- a/features/fixtures/shared/scenarios/BareboneTestScenarios.swift +++ b/features/fixtures/shared/scenarios/BareboneTestScenarios.swift @@ -109,7 +109,6 @@ class BareboneTestUnhandledErrorScenario: Scenario { private var payload: Payload! override func startBugsnag() { - config.autoTrackSessions = false if eventMode == "report" { // The version of the app at report time. config.appVersion = "23.4" diff --git a/features/fixtures/shared/scenarios/OOMScenario.m b/features/fixtures/shared/scenarios/OOMScenario.m index a0ab9a37f..d395e844a 100644 --- a/features/fixtures/shared/scenarios/OOMScenario.m +++ b/features/fixtures/shared/scenarios/OOMScenario.m @@ -20,9 +20,14 @@ @implementation OOMScenario +void onCrashHandler(const BSG_KSCrashReportWriter *writer) { + assert(!"onCrashHandler should not be called for OOMs"); +} + - (void)startBugsnag { self.config.autoTrackSessions = YES; self.config.enabledErrorTypes.ooms = YES; + self.config.onCrashHandler = onCrashHandler; self.config.launchDurationMillis = 0; // Ensure isLaunching will be true for the OOM, no matter how long it takes to occur. self.config.appType = @"vanilla"; self.config.appVersion = @"3.2.1"; diff --git a/features/out_of_memory.feature b/features/out_of_memory.feature index 31e3e8a8e..1aa93bb8d 100644 --- a/features/out_of_memory.feature +++ b/features/out_of_memory.feature @@ -33,9 +33,8 @@ Feature: Out of memory errors # Ensure the basic data from OOMs are present And the event "device.jailbroken" is false - And the event "metaData.device.batteryLevel" is not null - And the event "metaData.device.charging" is not null - And the event "metaData.device.orientation" is not null + And the event "metaData.device.batteryLevel" is a number + And the event "metaData.device.charging" is a boolean And the event "metaData.device.timezone" is not null And the event "metaData.device.simulator" is false And the event "metaData.device.wordSize" is not null @@ -46,9 +45,12 @@ Feature: Out of memory errors And the event "app.bundleVersion" is not null And the event "app.dsymUUIDs" is not null And the event "app.version" is not null + And on iOS 13 and later, the event "device.freeMemory" is an integer And the event "device.manufacturer" equals "Apple" + And the event "device.orientation" matches "(face(down|up)|landscape(left|right)|portrait(upsidedown)?)" And the event "device.runtimeVersions" is not null - And the event "device.totalMemory" is not null + And the event "device.time" is a timestamp + And the event "device.totalMemory" is an integer And the event "metaData.custom.bar" equals "foo" And the event "session.id" is not null And the event "session.startedAt" is not null diff --git a/features/steps/app_steps.rb b/features/steps/app_steps.rb index 555dad954..86e97eaea 100644 --- a/features/steps/app_steps.rb +++ b/features/steps/app_steps.rb @@ -142,10 +142,13 @@ def wait_for_true end def run_macos_app - Process.kill('KILL', $fixture_pid) if $fixture_pid + if $fixture_pid + Process.kill 'KILL', $fixture_pid + Process.waitpid $fixture_pid + end $fixture_pid = Process.spawn( { 'MAZE_RUNNER' => 'TRUE' }, 'features/fixtures/macos/output/macOSTestApp.app/Contents/MacOS/macOSTestApp', - %i[err out] => ['macOSTestApp.log', File::APPEND | File::CREAT | File::RDWR] + %i[err out] => :close ) end diff --git a/features/steps/reusable_steps.rb b/features/steps/reusable_steps.rb index b9600a24a..19ddee9c0 100644 --- a/features/steps/reusable_steps.rb +++ b/features/steps/reusable_steps.rb @@ -1,10 +1,35 @@ +# frozen_string_literal: true + # A collection of steps that could be added to Maze Runner +Then(/^on (iOS|macOS), (.+)/) do |platform, step_text| + step(step_text) if platform.downcase == Maze::Helper.get_current_platform +end + +Then(/^on (iOS|macOS) (\d+) and later, (.+)/) do |platform, version, step_text| + step(step_text) if platform.downcase == Maze::Helper.get_current_platform && Maze.config.os_version >= version +end + Then('the event {string} equals one of:') do |field, possible_values| value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") Maze.check.includes(possible_values.raw.flatten, value) end +Then('the event {string} is a boolean') do |field| + value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") + Maze.check.include [true, false], value +end + +Then('the event {string} is a number') do |field| + value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") + Maze.check.kind_of Numeric, value +end + +Then('the event {string} is an integer') do |field| + value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") + Maze.check.kind_of Integer, value +end + Then('the event {string} is within {int} seconds of the current timestamp') do |field, threshold_secs| value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") Maze.check.not_nil(value, 'Expected a timestamp') diff --git a/features/support/env.rb b/features/support/env.rb index 47d1b6cc2..d6537f757 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -51,6 +51,20 @@ def skip_below(os, version) skip_this_scenario('Skipping: Run is not configured for stress tests') if ENV['STRESS_TEST'].nil? end +Maze.hooks.before do |_scenario| + $started_at = Time.now + if Maze.config.os == 'ios' && Maze.config.farm == :local + begin + $logger_pid = Process.spawn( + 'idevicesyslog', '-u', Maze.config.device_id, '-m', 'iOSTestApp', + %i[err out] => File.open('device.log', 'w') + ) + rescue Errno::ENOENT + p 'Install libimobiledevice to capture iOS device logs' + end + end +end + Maze.hooks.after do |scenario| folder1 = File.join(Dir.pwd, 'maze_output') folder2 = scenario.failed? ? 'failed' : 'passed' @@ -61,14 +75,32 @@ def skip_below(os, version) FileUtils.makedirs(path) if Maze.config.os == 'macos' - FileUtils.mv('/tmp/kscrash.log', path) - FileUtils.mv('macOSTestApp.log', path) - Process.kill('KILL', $fixture_pid) if $fixture_pid - $fixture_pid = nil + if $fixture_pid # will be nil if scenario was skipped + Process.kill 'KILL', $fixture_pid + Process.waitpid $fixture_pid + $fixture_pid = nil + sleep 1 # prevent log bleed between scenarios due to second precision of --start + log = Process.spawn( + '/usr/bin/log', 'show', '--style', 'syslog', '--predicate', + 'eventMessage contains "macOSTestApp" OR process == "macOSTestApp"', + '--start', $started_at.strftime('%Y-%m-%d %H:%M:%S%z'), + out: File.open(File.join(path, 'device.log'), 'w') + ) + Process.wait log + FileUtils.mv '/tmp/kscrash.log', path + end else - data = Maze.driver.pull_file '@com.bugsnag.iOSTestApp/Documents/kscrash.log' - File.open(File.join(path, 'kscrash.log'), 'wb') { |file| file << data } + if $logger_pid + Process.kill 'TERM', $logger_pid + Process.waitpid $logger_pid + $logger_pid = nil + FileUtils.mv 'device.log', path + end + begin + data = Maze.driver.pull_file '@com.bugsnag.iOSTestApp/Documents/kscrash.log' + File.open(File.join(path, 'kscrash.log'), 'wb') { |file| file << data } + rescue StandardError + p "Maze.driver.pull_file failed: #{$ERROR_INFO}" + end end -rescue - # pull_file can fail on BrowserStack iOS 10 with "Error: Command 'umount' not found" end diff --git a/features/thermal_state.feature b/features/thermal_state.feature index d038c1d05..3aea1ad50 100644 --- a/features/thermal_state.feature +++ b/features/thermal_state.feature @@ -25,6 +25,7 @@ Feature: Thermal State And the exception "message" equals "The app was terminated by the operating system due to a critical thermal state" And the event "metaData.device.thermalState" matches "critical" And the event has a critical thermal state breadcrumb + And the event "device.time" is a timestamp And the event "session.events.handled" equals 0 And the event "session.events.unhandled" equals 1 And the event "severity" equals "error"