From f4dd2726f62430f790666fad59fb059dd18b8e9d Mon Sep 17 00:00:00 2001 From: Rameez Qurashi <75379021+rqbacktrace@users.noreply.github.com> Date: Wed, 14 Apr 2021 19:38:12 -0400 Subject: [PATCH] Attachment support for crash reports (#59) Attachment support for crash reports --- Backtrace.podspec | 2 +- Backtrace.xcodeproj/project.pbxproj | 86 ++++++++++++++++--- Examples/Example-iOS-ObjC/AppDelegate.m | 5 +- Examples/Example-iOS/AppDelegate.swift | 29 ++++++- Examples/Example-macOS-ObjC/AppDelegate.m | 7 +- Examples/Example-tvOS/AppDelegate.swift | 2 +- .../Attributes/AttributesProvider.swift | 5 ++ .../Attributes/AttributesStorage.swift | 66 +++++--------- .../Features/Client/BacktraceReporter.swift | 12 ++- .../Model/AttachmentBookmarkHandler.swift | 42 +++++++++ .../Client/Model/AttachmentsStorage.swift | 72 ++++++++++++++++ Sources/Public/BacktraceClient.swift | 9 ++ .../Public/BacktraceClientConfiguration.swift | 1 + .../Public/BacktraceClientCustomizing.swift | 11 +++ Sources/Public/BacktraceCrashReporter.swift | 51 ++++++++++- .../Public/BacktraceDatabaseSettings.swift | 4 +- .../Internal/ReportMetadataStorage.swift | 56 ++++++++++++ Sources/Public/Internal/SignalContext.swift | 4 + Tests/AttachmentStorageTests.swift | 77 +++++++++++++++++ Tests/BacktraceClientTests.swift | 4 +- .../Mocks/AttachmentBookmarkHandlerMock.swift | 21 +++++ Tests/Mocks/ReportMetadataStorageMock.swift | 19 ++++ 22 files changed, 512 insertions(+), 73 deletions(-) create mode 100644 Sources/Features/Client/Model/AttachmentBookmarkHandler.swift create mode 100644 Sources/Features/Client/Model/AttachmentsStorage.swift create mode 100644 Sources/Public/Internal/ReportMetadataStorage.swift create mode 100644 Tests/AttachmentStorageTests.swift create mode 100644 Tests/Mocks/AttachmentBookmarkHandlerMock.swift create mode 100644 Tests/Mocks/ReportMetadataStorageMock.swift diff --git a/Backtrace.podspec b/Backtrace.podspec index 3d2994df..0ca22da2 100644 --- a/Backtrace.podspec +++ b/Backtrace.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.name = "Backtrace" - s.version = "1.6.0" + s.version = "1.6.1" s.summary = "Backtrace's integration with iOS, macOS and tvOS" s.description = "Reliable crash and hang reporting for iOS, macOS and tvOS." s.homepage = "https://backtrace.io/" diff --git a/Backtrace.xcodeproj/project.pbxproj b/Backtrace.xcodeproj/project.pbxproj index bb376f92..f6df4a62 100644 --- a/Backtrace.xcodeproj/project.pbxproj +++ b/Backtrace.xcodeproj/project.pbxproj @@ -74,6 +74,24 @@ 81B51B136E9684981F70E9B8 /* Pods_Backtrace_tvOSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3EEF3FB2BE5FB7CD0332FDE7 /* Pods_Backtrace_tvOSTests.framework */; }; 99DD1F3F781FA819ECA75324 /* Pods_Backtrace_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8493CFBC65BA4978D35038FD /* Pods_Backtrace_tvOS.framework */; }; A8BE9BEC461DB224950FF9F2 /* Pods_Example_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9E84E683CE60F74196C77AC /* Pods_Example_tvOS.framework */; }; + AF1D0F3C2618304100C02F81 /* AttachmentStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1D0F3B2618304100C02F81 /* AttachmentStorageTests.swift */; }; + AF1D0F3D2618304200C02F81 /* AttachmentStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1D0F3B2618304100C02F81 /* AttachmentStorageTests.swift */; }; + AF1D0F3E2618304200C02F81 /* AttachmentStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1D0F3B2618304100C02F81 /* AttachmentStorageTests.swift */; }; + AF5AB03A26261A4E0003698C /* AttachmentsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7833BA2613D1B400530A10 /* AttachmentsStorage.swift */; }; + AF5AB04726261A760003698C /* AttachmentBookmarkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCCCEC126260BC400B83A28 /* AttachmentBookmarkHandler.swift */; }; + AF5AB05526261BDD0003698C /* AttachmentBookmarkHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5AB05426261BDD0003698C /* AttachmentBookmarkHandlerMock.swift */; }; + AF5AB05626261BDD0003698C /* AttachmentBookmarkHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5AB05426261BDD0003698C /* AttachmentBookmarkHandlerMock.swift */; }; + AF5AB05726261BDD0003698C /* AttachmentBookmarkHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5AB05426261BDD0003698C /* AttachmentBookmarkHandlerMock.swift */; }; + AF5AB0A02626226C0003698C /* AttachmentsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7833BA2613D1B400530A10 /* AttachmentsStorage.swift */; }; + AF5AB0A12626226D0003698C /* AttachmentsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7833BA2613D1B400530A10 /* AttachmentsStorage.swift */; }; + AF5AB0BB262622730003698C /* AttachmentBookmarkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCCCEC126260BC400B83A28 /* AttachmentBookmarkHandler.swift */; }; + AF5AB0BC262622730003698C /* AttachmentBookmarkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCCCEC126260BC400B83A28 /* AttachmentBookmarkHandler.swift */; }; + AF7477592620C6B200DEE7D1 /* ReportMetadataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7477582620C6B200DEE7D1 /* ReportMetadataStorage.swift */; }; + AF74775A2620C6B200DEE7D1 /* ReportMetadataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7477582620C6B200DEE7D1 /* ReportMetadataStorage.swift */; }; + AF74775B2620C6B200DEE7D1 /* ReportMetadataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7477582620C6B200DEE7D1 /* ReportMetadataStorage.swift */; }; + AFCCCE232625392300B83A28 /* ReportMetadataStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCCCE222625392300B83A28 /* ReportMetadataStorageMock.swift */; }; + AFCCCE242625392300B83A28 /* ReportMetadataStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCCCE222625392300B83A28 /* ReportMetadataStorageMock.swift */; }; + AFCCCE252625392300B83A28 /* ReportMetadataStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCCCE222625392300B83A28 /* ReportMetadataStorageMock.swift */; }; DC873DB19BD91A1268112804 /* Pods_Backtrace_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F832896FDD11162B84E86E23 /* Pods_Backtrace_iOS.framework */; }; DE46BAA3E4497194057A99FE /* Pods_Example_iOS_ObjC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A51056C8548E16A5E320976D /* Pods_Example_iOS_ObjC.framework */; }; F21211A5222348AC000B3692 /* BacktraceCrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21211A4222348AC000B3692 /* BacktraceCrashReporter.swift */; }; @@ -343,6 +361,12 @@ A51056C8548E16A5E320976D /* Pods_Example_iOS_ObjC.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Example_iOS_ObjC.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A79CCB26F3CBD94DC01CC462 /* Pods_Example_macOS_ObjC.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Example_macOS_ObjC.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AE06F243F57768157DE89AC2 /* Pods-Backtrace-macOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Backtrace-macOS.release.xcconfig"; path = "Target Support Files/Pods-Backtrace-macOS/Pods-Backtrace-macOS.release.xcconfig"; sourceTree = ""; }; + AF1D0F3B2618304100C02F81 /* AttachmentStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentStorageTests.swift; sourceTree = ""; }; + AF5AB05426261BDD0003698C /* AttachmentBookmarkHandlerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentBookmarkHandlerMock.swift; sourceTree = ""; }; + AF7477582620C6B200DEE7D1 /* ReportMetadataStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportMetadataStorage.swift; sourceTree = ""; }; + AF7833BA2613D1B400530A10 /* AttachmentsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsStorage.swift; sourceTree = ""; }; + AFCCCE222625392300B83A28 /* ReportMetadataStorageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportMetadataStorageMock.swift; sourceTree = ""; }; + AFCCCEC126260BC400B83A28 /* AttachmentBookmarkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentBookmarkHandler.swift; sourceTree = ""; }; B4F2A98EC9FE9A5E7BE2A6B9 /* Pods-Backtrace-tvOSTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Backtrace-tvOSTests.release.xcconfig"; path = "Target Support Files/Pods-Backtrace-tvOSTests/Pods-Backtrace-tvOSTests.release.xcconfig"; sourceTree = ""; }; B723C7EAC07E861B99FC1A4C /* Pods-Backtrace-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Backtrace-iOS.release.xcconfig"; path = "Target Support Files/Pods-Backtrace-iOS/Pods-Backtrace-iOS.release.xcconfig"; sourceTree = ""; }; C495D179EFBB711373441639 /* Pods-Backtrace-tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Backtrace-tvOS.release.xcconfig"; path = "Target Support Files/Pods-Backtrace-tvOS/Pods-Backtrace-tvOS.release.xcconfig"; sourceTree = ""; }; @@ -579,6 +603,15 @@ path = "Backtrace-tvOSTests"; sourceTree = ""; }; + AF7833B92613D0E300530A10 /* Model */ = { + isa = PBXGroup; + children = ( + AF7833BA2613D1B400530A10 /* AttachmentsStorage.swift */, + AFCCCEC126260BC400B83A28 /* AttachmentBookmarkHandler.swift */, + ); + path = Model; + sourceTree = ""; + }; E1CB76ADFD3A1D9326B4E46D /* Pods */ = { isa = PBXGroup; children = ( @@ -674,6 +707,7 @@ F2AB638C22470E3500939BC9 /* BacktraceFileManagerTests.swift */, F2981BC822901B1400DFE098 /* AttributesTests.swift */, F2A81B5023F02279007C63E4 /* BacktraceRateLimiterTests.swift */, + AF1D0F3B2618304100C02F81 /* AttachmentStorageTests.swift */, ); path = Tests; sourceTree = ""; @@ -703,6 +737,7 @@ F28F164121E28421008E4B96 /* Client */ = { isa = PBXGroup; children = ( + AF7833B92613D0E300530A10 /* Model */, F28F164521E28441008E4B96 /* BacktraceReporter.swift */, F28162F921EFD6AD00A12B7A /* BacktraceResponse.swift */, 0B6B4CFC25CD8331002DA15C /* BacktraceOomWatcher.swift */, @@ -762,6 +797,8 @@ F2AB63742246484100939BC9 /* WatcherRepositoryMock.swift */, F229D789223A56ED008EC851 /* UrlSessionMock.swift */, F2AB637D22464FD500939BC9 /* DebuggerCheckerMock.swift */, + AFCCCE222625392300B83A28 /* ReportMetadataStorageMock.swift */, + AF5AB05426261BDD0003698C /* AttachmentBookmarkHandlerMock.swift */, ); path = Mocks; sourceTree = ""; @@ -829,6 +866,7 @@ F259E4D52229A40C00F282C7 /* Result.swift */, F2AB63802246E16400939BC9 /* ReportingPolicy.swift */, F28635392283685100F45412 /* Map+KeyPath.swift */, + AF7477582620C6B200DEE7D1 /* ReportMetadataStorage.swift */, ); path = Internal; sourceTree = ""; @@ -1761,6 +1799,7 @@ files = ( F29959AB225539960085B5C3 /* BacktraceCrashReporter.swift in Sources */, 28F95BCF22526061003936E0 /* BacktraceLogger.swift in Sources */, + AF5AB0BB262622730003698C /* AttachmentBookmarkHandler.swift in Sources */, 28F95BD022526064003936E0 /* BacktraceClient.swift in Sources */, 28F95BCD2252605A003936E0 /* BacktraceClientDelegate.swift in Sources */, 28F95BC92252602C003936E0 /* Foundation+Extensions.swift in Sources */, @@ -1771,6 +1810,7 @@ 28F95BD32252606F003936E0 /* BacktraceClientConfiguration.swift in Sources */, 28F95BD422526072003936E0 /* BacktraceCredentials.swift in Sources */, 28F95BE4225260A7003936E0 /* MultipartRequest.swift in Sources */, + AF5AB0A12626226D0003698C /* AttachmentsStorage.swift in Sources */, 28F95BCE2252605E003936E0 /* BacktraceClientCustomizing.swift in Sources */, 28F95BE9225260B6003936E0 /* BacktraceFileManager.swift in Sources */, 28F95BEF225260D8003936E0 /* BluetoothStatusListener.swift in Sources */, @@ -1786,6 +1826,7 @@ 28F95BE7225260B0003936E0 /* Attachment.swift in Sources */, 0B6B4CFF25CD8331002DA15C /* BacktraceOomWatcher.swift in Sources */, 28F95BCB22526045003936E0 /* Dispatching.swift in Sources */, + AF74775B2620C6B200DEE7D1 /* ReportMetadataStorage.swift in Sources */, 28F95BDD2252608E003936E0 /* BacktraceReportStatus.swift in Sources */, 28F95BDE22526091003936E0 /* ReportingPolicy.swift in Sources */, 28F95BEC225260C9003936E0 /* AttributesStorage.swift in Sources */, @@ -1820,13 +1861,16 @@ F21DD3A62255666F00404CC3 /* BacktraceApiTests.swift in Sources */, F21DD3A72255666F00404CC3 /* BacktraceWatcherTests.swift in Sources */, F21DD3A82255666F00404CC3 /* AttachmentTests.swift in Sources */, + AFCCCE252625392300B83A28 /* ReportMetadataStorageMock.swift in Sources */, F2981BCB22901B1400DFE098 /* AttributesTests.swift in Sources */, F21DD3A92255666F00404CC3 /* ReportingPolicyTests.swift in Sources */, F2A81B5623F02297007C63E4 /* BacktraceRateLimiterTests.swift in Sources */, F21DD3AA2255666F00404CC3 /* DispatcherTests.swift in Sources */, F21DD3AB2255666F00404CC3 /* CrashReporterTests.swift in Sources */, + AF5AB05726261BDD0003698C /* AttachmentBookmarkHandlerMock.swift in Sources */, F21DD3AC2255666F00404CC3 /* BacktraceFileManagerTests.swift in Sources */, 28F95BBE22525DCC003936E0 /* Backtrace_tvOSTests.swift in Sources */, + AF1D0F3E2618304200C02F81 /* AttachmentStorageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1839,6 +1883,7 @@ F28162FB21EFD6AD00A12B7A /* BacktraceResponse.swift in Sources */, F29CD79621FDD9DC00216C59 /* BacktraceClientDelegate.swift in Sources */, F21771BE21E341CA0059896E /* Dispatcher.swift in Sources */, + AF5AB0A02626226C0003698C /* AttachmentsStorage.swift in Sources */, 28AC773D21FA5A8900FED661 /* BacktraceDatabaseSettings.swift in Sources */, F266B83821C77B9600D14417 /* BacktraceLogger.swift in Sources */, F2AFB59E22274EDA00AAA1D7 /* Dispatching.swift in Sources */, @@ -1856,6 +1901,8 @@ F25F9E9B21EE84EA00236E04 /* BacktraceResult.swift in Sources */, F21D302C224A18D60013B5D7 /* Store.swift in Sources */, F266B83421C77B9600D14417 /* BacktraceError.swift in Sources */, + AF5AB0BC262622730003698C /* AttachmentBookmarkHandler.swift in Sources */, + AF74775A2620C6B200DEE7D1 /* ReportMetadataStorage.swift in Sources */, 28966EFB2214BBDC00E6E891 /* AttributesStorage.swift in Sources */, F259E4D72229A41400F282C7 /* Result.swift in Sources */, F28635482288958C00F45412 /* System.swift in Sources */, @@ -1896,13 +1943,16 @@ F229D7862239A172008EC851 /* BacktraceClientTests.swift in Sources */, 282C85EB22419C600014FE75 /* BacktraceWatcherTests.swift in Sources */, F229D78C223A591F008EC851 /* UrlSessionMock.swift in Sources */, + AFCCCE242625392300B83A28 /* ReportMetadataStorageMock.swift in Sources */, F29CD78B21FC5F8600216C59 /* BacktraceDatabaseTests.swift in Sources */, F2AB638B22470CD800939BC9 /* CrashReporterTests.swift in Sources */, F2A81B5523F02297007C63E4 /* BacktraceRateLimiterTests.swift in Sources */, F2981BCA22901B1400DFE098 /* AttributesTests.swift in Sources */, F2AB63882247075100939BC9 /* DispatcherTests.swift in Sources */, + AF5AB05626261BDD0003698C /* AttachmentBookmarkHandlerMock.swift in Sources */, F2AB63732246481100939BC9 /* AttachmentTests.swift in Sources */, F229D790223A5F6A008EC851 /* BacktraceApiTests.swift in Sources */, + AF1D0F3D2618304200C02F81 /* AttachmentStorageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1939,6 +1989,8 @@ F2AB639C22479A3500939BC9 /* Model.xcdatamodeld in Sources */, F28F165121E2A08F008E4B96 /* HttpMethod.swift in Sources */, F22EB87721BBD36800DEE94E /* BacktraceClient.swift in Sources */, + AF5AB04726261A760003698C /* AttachmentBookmarkHandler.swift in Sources */, + AF5AB03A26261A4E0003698C /* AttachmentsStorage.swift in Sources */, 28966EFA2214BBD200E6E891 /* AttributesStorage.swift in Sources */, F259E4D62229A40C00F282C7 /* Result.swift in Sources */, 2846E1FE223070CB0035F98C /* Attachment.swift in Sources */, @@ -1947,6 +1999,7 @@ F21211A5222348AC000B3692 /* BacktraceCrashReporter.swift in Sources */, 0B6B4CFD25CD8331002DA15C /* BacktraceOomWatcher.swift in Sources */, F2C1514221F7D8E30014F1B3 /* PersistentRepository.swift in Sources */, + AF7477592620C6B200DEE7D1 /* ReportMetadataStorage.swift in Sources */, F2D8BE3821BD7894007CFEFA /* BacktraceError.swift in Sources */, F282075821CEA31F0017367F /* BacktraceReport.swift in Sources */, F28635472288958C00F45412 /* System.swift in Sources */, @@ -1981,13 +2034,16 @@ F229D7852239A172008EC851 /* BacktraceClientTests.swift in Sources */, 282C85EA22419C560014FE75 /* BacktraceWatcherTests.swift in Sources */, F229D78D223A5920008EC851 /* UrlSessionMock.swift in Sources */, + AFCCCE232625392300B83A28 /* ReportMetadataStorageMock.swift in Sources */, F29CD78A21FC5F8500216C59 /* BacktraceDatabaseTests.swift in Sources */, F2AB638A22470CD700939BC9 /* CrashReporterTests.swift in Sources */, F2A81B5423F02297007C63E4 /* BacktraceRateLimiterTests.swift in Sources */, F2981BC922901B1400DFE098 /* AttributesTests.swift in Sources */, F2AB63872247075100939BC9 /* DispatcherTests.swift in Sources */, + AF5AB05526261BDD0003698C /* AttachmentBookmarkHandlerMock.swift in Sources */, F2AB63722246481100939BC9 /* AttachmentTests.swift in Sources */, F229D78F223A5F6A008EC851 /* BacktraceApiTests.swift in Sources */, + AF1D0F3C2618304100C02F81 /* AttachmentStorageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2128,12 +2184,13 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = ""; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -2211,12 +2268,13 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = ""; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -2438,7 +2496,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -2525,7 +2583,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -2603,7 +2661,7 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -2682,7 +2740,7 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2754,7 +2812,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -2833,7 +2891,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3248,7 +3306,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -3327,7 +3385,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3400,7 +3458,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -3475,7 +3533,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3545,7 +3603,7 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -3620,7 +3678,7 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = K952H3J75D; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; diff --git a/Examples/Example-iOS-ObjC/AppDelegate.m b/Examples/Example-iOS-ObjC/AppDelegate.m index ee1fe9b0..1f856cc1 100644 --- a/Examples/Example-iOS-ObjC/AppDelegate.m +++ b/Examples/Example-iOS-ObjC/AppDelegate.m @@ -19,13 +19,14 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( backtraceDatabaseSettings.retryInterval = 5; backtraceDatabaseSettings.retryLimit = 3; backtraceDatabaseSettings.retryBehaviour = RetryBehaviourInterval; - backtraceDatabaseSettings.retryOrder = RetryOderStack; + backtraceDatabaseSettings.retryOrder = RetryOrderStack; BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc] initWithCredentials: credentials dbSettings: backtraceDatabaseSettings reportsPerMin: 3 - allowsAttachingDebugger: TRUE]; + allowsAttachingDebugger: TRUE + detectOOM: FALSE]; BacktraceClient.shared = [[BacktraceClient alloc] initWithConfiguration: configuration error: nil]; BacktraceClient.shared.delegate = self; diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift index 4352367d..dbc19faa 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -18,13 +18,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!, token: Keys.backtraceToken as String) + let backtraceDatabaseSettings = BacktraceDatabaseSettings() backtraceDatabaseSettings.maxRecordCount = 1000 backtraceDatabaseSettings.maxDatabaseSize = 10 backtraceDatabaseSettings.retryInterval = 5 backtraceDatabaseSettings.retryLimit = 3 backtraceDatabaseSettings.retryBehaviour = RetryBehaviour.interval - backtraceDatabaseSettings.retryOrder = RetryOder.queue + backtraceDatabaseSettings.retryOrder = RetryOrder.queue let backtraceConfiguration = BacktraceClientConfiguration(credentials: backtraceCredentials, dbSettings: backtraceDatabaseSettings, reportsPerMin: 10, @@ -34,6 +35,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { BacktraceClient.shared?.delegate = self BacktraceClient.shared?.attributes = ["foo": "bar", "testing": true] + let fileName = "sample.txt" + let fileUrl = try? createAndWriteFile(fileName) + var crashAttachments = Attachments() + crashAttachments[fileName] = fileUrl + BacktraceClient.shared?.attachments = crashAttachments + BacktraceClient.shared?.loggingDestinations = [BacktraceBaseDestination(level: .debug)] do { try throwingFunc() @@ -46,6 +53,26 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { return true } + + func createAndWriteFile(_ fileName: String) throws -> URL { + let dirName = "directory" + guard let libraryDirectoryUrl = try? FileManager.default.url( + for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else { + throw CustomError.runtimeError + } + let directoryUrl = libraryDirectoryUrl.appendingPathComponent(dirName) + try? FileManager().createDirectory( + at: directoryUrl, + withIntermediateDirectories: false, + attributes: nil + ) + let fileUrl = directoryUrl.appendingPathComponent(fileName) + let formatter = DateFormatter() + formatter.timeStyle = .medium + let myData = formatter.string(from: Date()) + try myData.write(to: fileUrl, atomically: true, encoding: .utf8) + return fileUrl + } } extension AppDelegate: BacktraceClientDelegate { diff --git a/Examples/Example-macOS-ObjC/AppDelegate.m b/Examples/Example-macOS-ObjC/AppDelegate.m index 93677103..86267287 100644 --- a/Examples/Example-macOS-ObjC/AppDelegate.m +++ b/Examples/Example-macOS-ObjC/AppDelegate.m @@ -18,13 +18,14 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { backtraceDatabaseSettings.retryInterval = 5; backtraceDatabaseSettings.retryLimit = 3; backtraceDatabaseSettings.retryBehaviour = RetryBehaviourInterval; - backtraceDatabaseSettings.retryOrder = RetryOderStack; - + backtraceDatabaseSettings.retryOrder = RetryOrderStack; + BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc] initWithCredentials: credentials dbSettings: backtraceDatabaseSettings reportsPerMin: 3 - allowsAttachingDebugger: TRUE]; + allowsAttachingDebugger: TRUE + detectOOM: FALSE]; BacktraceClient.shared = [[BacktraceClient alloc] initWithConfiguration: configuration error: nil]; [BacktraceClient.shared setAttributes: @{@"foo": @"bar"}]; BacktraceClient.shared.delegate = self; diff --git a/Examples/Example-tvOS/AppDelegate.swift b/Examples/Example-tvOS/AppDelegate.swift index 84979a39..d873e73d 100644 --- a/Examples/Example-tvOS/AppDelegate.swift +++ b/Examples/Example-tvOS/AppDelegate.swift @@ -24,7 +24,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { backtraceDatabaseSettings.retryInterval = 5 backtraceDatabaseSettings.retryLimit = 3 backtraceDatabaseSettings.retryBehaviour = RetryBehaviour.interval - backtraceDatabaseSettings.retryOrder = RetryOder.queue + backtraceDatabaseSettings.retryOrder = RetryOrder.queue let backtraceConfiguration = BacktraceClientConfiguration(credentials: backtraceCredentials, dbSettings: backtraceDatabaseSettings, reportsPerMin: 10, diff --git a/Sources/Features/Attributes/AttributesProvider.swift b/Sources/Features/Attributes/AttributesProvider.swift index 737b366d..ce9d37b2 100644 --- a/Sources/Features/Attributes/AttributesProvider.swift +++ b/Sources/Features/Attributes/AttributesProvider.swift @@ -14,6 +14,7 @@ final class AttributesProvider { // attributes can be modified on runtime var attributes: Attributes = [:] + var attachments: Attachments = [:] private let attributesSources: [AttributesSource] private let faultInfo: FaultInfo @@ -43,6 +44,10 @@ extension AttributesProvider: SignalContext { self.attributes["error.type"] = errorType } + var attachmentPaths: [String] { + return attachments.map(\.value.path) + } + var allAttributes: Attributes { return attributes + defaultAttributes } diff --git a/Sources/Features/Attributes/AttributesStorage.swift b/Sources/Features/Attributes/AttributesStorage.swift index b1afa2df..fa74912d 100644 --- a/Sources/Features/Attributes/AttributesStorage.swift +++ b/Sources/Features/Attributes/AttributesStorage.swift @@ -1,7 +1,7 @@ import Foundation -final class AttributesStorage { - struct Config { +enum AttributesStorage { + struct AttributesConfig: Config { let cacheUrl: URL let directoryUrl: URL let fileUrl: URL @@ -17,58 +17,38 @@ final class AttributesStorage { } } - private static let directoryName = Bundle(for: AttributesStorage.self).bundleIdentifier ?? "BacktraceCache" + private static let directoryName = Bundle.main.bundleIdentifier ?? "BacktraceCache" static func store(_ attributes: Attributes, fileName: String) throws { - let config = try Config(fileName: fileName) - - if !FileManager.default.fileExists(atPath: config.directoryUrl.path) { - try FileManager.default.createDirectory(atPath: config.directoryUrl.path, - withIntermediateDirectories: false, - attributes: nil) - } - - if #available(iOS 11.0, tvOS 11.0, macOS 10.13, *) { - try (attributes as NSDictionary).write(to: config.fileUrl) - } else { - guard (attributes as NSDictionary).write(to: config.fileUrl, atomically: true) else { - throw FileError.fileNotWritten - } - } + try store(attributes, fileName: fileName, storage: ReportMetadataStorageImpl.self) + } + + static func store(_ attributes: Attributes, fileName: String, storage: T.Type) throws { + let config = try AttributesConfig(fileName: fileName) + try T.storeToFile(attributes, config: config) BacktraceLogger.debug("Stored attributes at path: \(config.fileUrl)") } static func retrieve(fileName: String) throws -> Attributes { - let config = try Config(fileName: fileName) - guard FileManager.default.fileExists(atPath: config.fileUrl.path) else { - throw FileError.fileNotExists - } - // load file to NSDictionary - let dictionary: NSDictionary - if #available(iOS 11.0, tvOS 11.0, macOS 10.13, *) { - dictionary = try NSDictionary(contentsOf: config.fileUrl, error: ()) - } else { - guard let dictionaryFromFile = NSDictionary(contentsOf: config.fileUrl) else { - throw FileError.invalidPropertyList - } - dictionary = dictionaryFromFile - } - // cast safety to AttributesType - guard let attributes: Attributes = dictionary as? Attributes else { - throw FileError.invalidPropertyList - } + try retrieve(fileName: fileName, storage: ReportMetadataStorageImpl.self) + } + + static func retrieve(fileName: String, storage: T.Type) throws -> Attributes { + let config = try AttributesConfig(fileName: fileName) + let dictionary = try T.retrieveFromFile(config: config) + // cast safely to AttributesType + let attributes: Attributes = dictionary as Attributes BacktraceLogger.debug("Retrieved attributes from path: \(config.fileUrl)") return attributes } static func remove(fileName: String) throws { - let config = try Config(fileName: fileName) - // check file exists - guard FileManager.default.fileExists(atPath: config.fileUrl.path) else { - throw FileError.fileNotExists - } - // remove file - try FileManager.default.removeItem(at: config.fileUrl) + try remove(fileName: fileName, storage: ReportMetadataStorageImpl.self) + } + + static func remove(fileName: String, storage: T.Type) throws { + let config = try AttributesConfig(fileName: fileName) + try T.removeFile(config: config) BacktraceLogger.debug("Removed attributes at path: \(config.fileUrl)") } } diff --git a/Sources/Features/Client/BacktraceReporter.swift b/Sources/Features/Client/BacktraceReporter.swift index f0851848..279fcf75 100644 --- a/Sources/Features/Client/BacktraceReporter.swift +++ b/Sources/Features/Client/BacktraceReporter.swift @@ -78,6 +78,14 @@ extension BacktraceReporter: BacktraceClientCustomizing { attributesProvider.attributes = newValue } } + + var attachments: Attachments { + get { + return attributesProvider.attachments + } set { + attributesProvider.attachments = newValue + } + } } extension BacktraceReporter { @@ -96,7 +104,7 @@ extension BacktraceReporter { attributesProvider.set(faultMessage: faultMessage) let resource = try reporter.generateLiveReport(exception: exception, attributes: attributesProvider.allAttributes, - attachmentPaths: attachmentPaths) + attachmentPaths: attachmentPaths + attributesProvider.attachmentPaths) return send(resource: resource) } @@ -106,7 +114,7 @@ extension BacktraceReporter { attributesProvider.set(errorType: "Exception") let resource = try reporter.generateLiveReport(exception: exception, attributes: attributesProvider.allAttributes, - attachmentPaths: attachmentPaths) + attachmentPaths: attachmentPaths + attributesProvider.attachmentPaths) return resource } } diff --git a/Sources/Features/Client/Model/AttachmentBookmarkHandler.swift b/Sources/Features/Client/Model/AttachmentBookmarkHandler.swift new file mode 100644 index 00000000..bc58bf66 --- /dev/null +++ b/Sources/Features/Client/Model/AttachmentBookmarkHandler.swift @@ -0,0 +1,42 @@ +import Foundation + +protocol AttachmentBookmarkHandler { + static func convertAttachmentUrlsToBookmarks(_ attachments: Attachments) throws -> Bookmarks + static func extractAttachmentUrls(_ bookmarks: Bookmarks) throws -> Attachments +} + +enum AttachmentBookmarkHandlerImpl: AttachmentBookmarkHandler { + static func convertAttachmentUrlsToBookmarks(_ attachments: Attachments) throws -> Bookmarks { + var attachmentsBookmarksDict = Bookmarks() + for attachment in attachments { + do { + let bookmark = try attachment.value.bookmarkData(options: URL.BookmarkCreationOptions.minimalBookmark) + attachmentsBookmarksDict[attachment.key] = bookmark + } catch { + BacktraceLogger.error("Could not bookmark attachment file URL. Error: \(error)") + continue + } + } + return attachmentsBookmarksDict + } + + static func extractAttachmentUrls(_ bookmarks: Bookmarks) throws -> Attachments { + var attachments = Attachments() + for bookmark in bookmarks { + var stale = Bool(false) + guard let fileUrl = try? URL(resolvingBookmarkData: bookmark.value, + options: URL.BookmarkResolutionOptions(), + relativeTo: nil, + bookmarkDataIsStale: &stale) else { + BacktraceLogger.error("Could not resolve file URL from bookmark") + continue + } + if stale { + BacktraceLogger.error("Bookmark data is stale. This should not happen") + continue + } + attachments[bookmark.key] = fileUrl + } + return attachments + } +} diff --git a/Sources/Features/Client/Model/AttachmentsStorage.swift b/Sources/Features/Client/Model/AttachmentsStorage.swift new file mode 100644 index 00000000..db5e6af0 --- /dev/null +++ b/Sources/Features/Client/Model/AttachmentsStorage.swift @@ -0,0 +1,72 @@ +import Foundation + +enum AttachmentsStorageError: Error { + case invalidDictionary + case invalidBookmark +} + +enum AttachmentsStorage { + struct AttachmentsConfig: Config { + let cacheUrl: URL + let directoryUrl: URL + let fileUrl: URL + + init(fileName: String) throws { + guard let cacheDirectoryURL = + FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { + throw FileError.noCacheDirectory + } + self.cacheUrl = cacheDirectoryURL + self.directoryUrl = cacheDirectoryURL.appendingPathComponent(directoryName) + self.fileUrl = directoryUrl.appendingPathComponent("\(fileName)_attachments.plist") + } + } + + private static let directoryName = Bundle.main.bundleIdentifier ?? "BacktraceCache" + + static func store(_ attachments: Attachments, fileName: String) throws { + try store(attachments, fileName: fileName, storage: ReportMetadataStorageImpl.self, + bookmarkHandler: AttachmentBookmarkHandlerImpl.self) + } + + static func store + (_ attachments: Attachments, fileName: String, storage: T.Type, bookmarkHandler: U.Type) throws { + let config = try AttachmentsConfig(fileName: fileName) + let attachmentBookmarks = try U.convertAttachmentUrlsToBookmarks(attachments) + try T.storeToFile(attachmentBookmarks, config: config) + BacktraceLogger.debug("Stored attachments paths at path: \(config.fileUrl)") + } + + static func retrieve(fileName: String) throws -> Attachments { + try retrieve(fileName: fileName, storage: ReportMetadataStorageImpl.self, + bookmarkHandler: AttachmentBookmarkHandlerImpl.self) + } + + static func retrieve + (fileName: String, storage: T.Type, bookmarkHandler: U.Type) throws -> Attachments { + let config = try AttachmentsConfig(fileName: fileName) + let dictionary = try T.retrieveFromFile(config: config) + + guard let bookmarks = dictionary as? Bookmarks else { + BacktraceLogger.debug("Could not convert stored dictionary to Bookmarks type") + throw AttachmentsStorageError.invalidDictionary + } + guard let attachments = try? U.extractAttachmentUrls(bookmarks) else { + BacktraceLogger.debug("Could not extract attachment URLs from stored attachments Bookmarks") + throw AttachmentsStorageError.invalidBookmark + } + + BacktraceLogger.debug("Retrieved attachment paths at path: \(config.fileUrl)") + return attachments + } + + static func remove(fileName: String) throws { + try remove(fileName: fileName, storage: ReportMetadataStorageImpl.self) + } + + static func remove(fileName: String, storage: T.Type) throws { + let config = try AttachmentsConfig(fileName: fileName) + try T.removeFile(config: config) + BacktraceLogger.debug("Removed attachments paths at path: \(config.fileUrl)") + } +} diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index a1341035..0176c05a 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -98,6 +98,15 @@ extension BacktraceClient: BacktraceClientCustomizing { reporter.attributes = newValue } } + + /// Additional file attachments which are automatically added to each report. + @objc public var attachments: Attachments { + get { + return reporter.attachments + } set { + reporter.attachments = newValue + } + } } // MARK: - BacktraceReporting diff --git a/Sources/Public/BacktraceClientConfiguration.swift b/Sources/Public/BacktraceClientConfiguration.swift index 4649cc75..598a3e51 100644 --- a/Sources/Public/BacktraceClientConfiguration.swift +++ b/Sources/Public/BacktraceClientConfiguration.swift @@ -35,6 +35,7 @@ import Foundation /// - reportsPerMin: Maximum number of records sent to Backtrace services in 1 minute. Default: `30`. /// - allowsAttachingDebugger: if set to `true` BacktraceClient will report reports even when the debugger /// is attached. Default: `false`. + /// - detectOOM: if set to `true` BacktraceClient will detect when the app is out of memory. Default: `false`. @objc public init(credentials: BacktraceCredentials, dbSettings: BacktraceDatabaseSettings = BacktraceDatabaseSettings(), reportsPerMin: Int = 30, diff --git a/Sources/Public/BacktraceClientCustomizing.swift b/Sources/Public/BacktraceClientCustomizing.swift index f01b152b..a926ba3e 100644 --- a/Sources/Public/BacktraceClientCustomizing.swift +++ b/Sources/Public/BacktraceClientCustomizing.swift @@ -6,12 +6,23 @@ public typealias BacktraceClientProtocol = BacktraceReporting & BacktraceClientC /// Type-alias of passing attributes to library. public typealias Attributes = [String: Any] +/// Type-alias of passing file attachments to library. +/// Expected format: Filename, File URL bookmark +public typealias Attachments = [String: URL] + +/// Type-alias of storing file attachments on disk (as a bookmark) +/// Expected format: Filename, File URL bookmark +public typealias Bookmarks = [String: Data] + /// Provides customization functionality to `BacktraceClient`. @objc public protocol BacktraceClientCustomizing { /// Additional attributes which are automatically added to each report. @objc var attributes: Attributes { get set } + /// Additional file attachments which are automatically added to each report. + @objc var attachments: Attachments { get set } + /// The object that acts as the delegate object of the `BacktraceClient` instance. @objc var delegate: BacktraceClientDelegate? { get set } } diff --git a/Sources/Public/BacktraceCrashReporter.swift b/Sources/Public/BacktraceCrashReporter.swift index b9c2b577..a1365066 100644 --- a/Sources/Public/BacktraceCrashReporter.swift +++ b/Sources/Public/BacktraceCrashReporter.swift @@ -5,6 +5,7 @@ import Backtrace_PLCrashReporter @objc public class BacktraceCrashReporter: NSObject { private let reporter: PLCrashReporter static private let crashName = "live_report" + private let copiedFileAttachments: [URL] /// Creates an instance of a crash reporter. /// - Parameter config: A `PLCrashReporterConfig` configuration to use. @@ -16,6 +17,8 @@ import Backtrace_PLCrashReporter /// - Parameter reporter: An instance of `PLCrashReporter` to use. @objc public init(reporter: PLCrashReporter) { self.reporter = reporter + self.copiedFileAttachments = BacktraceCrashReporter.copyFileAttachmentsFromPendingCrashes() + super.init() } } @@ -32,6 +35,7 @@ extension BacktraceCrashReporter: CrashReporting { attributesProvider.set(faultMessage: "siginfo_t.si_signo: \(signalInfo.si_signo)") BacktraceOomWatcher.clean() try? AttributesStorage.store(attributesProvider.allAttributes, fileName: BacktraceCrashReporter.crashName) + try? AttachmentsStorage.store(attributesProvider.attachments, fileName: BacktraceCrashReporter.crashName) } var callbacks = withUnsafeMutableBytes(of: &mutableContext) { rawMutablePointer in @@ -52,19 +56,62 @@ extension BacktraceCrashReporter: CrashReporting { try reporter.enableAndReturnError() } + // This function retrieves, constructs, and sends the pending crash report func pendingCrashReport() throws -> BacktraceReport { let reportData = try reporter.loadPendingCrashReportDataAndReturnError() let attributes = (try? AttributesStorage.retrieve(fileName: BacktraceCrashReporter.crashName)) ?? [:] - // NOTE: - no attachments in crash reports - return try BacktraceReport(report: reportData, attributes: attributes, attachmentPaths: []) + let attachmentPaths = copiedFileAttachments.map(\.path) + return try BacktraceReport(report: reportData, attributes: attributes, attachmentPaths: attachmentPaths) } + // This function is called to copy stored file attachments + // from pending crashes so that they are not overwritten by the + // new app session + static func copyFileAttachmentsFromPendingCrashes() -> [URL] { + guard let directoryUrl = try? AttachmentsStorage.AttachmentsConfig(fileName: "").directoryUrl else { + BacktraceLogger.error("Could not get cache directory URL") + return [URL]() + } + let attachments = (try? AttachmentsStorage.retrieve(fileName: BacktraceCrashReporter.crashName)) ?? [:] + var copiedFileAttachments = [URL]() + for attachment in attachments { + let fileManager = FileManager.default + let copiedAttachmentPath = directoryUrl.appendingPathComponent(attachment.key) + do { + if !fileManager.fileExists(atPath: attachment.value.path) { + BacktraceLogger.error("File attachment from previous session does not exist") + continue + } + if fileManager.fileExists(atPath: copiedAttachmentPath.path) { + try fileManager.removeItem(atPath: copiedAttachmentPath.path) + } + try fileManager.copyItem(at: attachment.value, to: copiedAttachmentPath) + copiedFileAttachments.append(copiedAttachmentPath) + } catch { + BacktraceLogger.error("Could not copy bookmarked attachment file from previous session. Error: \(error)") + continue + } + } + return copiedFileAttachments + } + func hasPendingCrashes() -> Bool { return reporter.hasPendingCrashReport() } func purgePendingCrashReport() throws { try AttributesStorage.remove(fileName: BacktraceCrashReporter.crashName) + try AttachmentsStorage.remove(fileName: BacktraceCrashReporter.crashName) + try deleteCopiedFileAttachments() try reporter.purgePendingCrashReportAndReturnError() } + + func deleteCopiedFileAttachments() throws { + let fileManager = FileManager.default + for attachment in copiedFileAttachments { + if fileManager.fileExists(atPath: attachment.path) { + try fileManager.removeItem(atPath: attachment.path) + } + } + } } diff --git a/Sources/Public/BacktraceDatabaseSettings.swift b/Sources/Public/BacktraceDatabaseSettings.swift index 723606d1..739e2ea5 100644 --- a/Sources/Public/BacktraceDatabaseSettings.swift +++ b/Sources/Public/BacktraceDatabaseSettings.swift @@ -19,7 +19,7 @@ import Foundation @objc public var retryBehaviour: RetryBehaviour = .interval /// Retry order. Default `RetryOder.queue`. - @objc public var retryOrder: RetryOder = .queue + @objc public var retryOrder: RetryOrder = .queue internal var maxDatabaseSizeInBytes: Int { return maxDatabaseSize * 1024 * 1024 @@ -37,7 +37,7 @@ import Foundation } /// Backtrace retrying order for not successfully sent reports. -@objc public enum RetryOder: Int { +@objc public enum RetryOrder: Int { /// Library will retry sending oldest reports first (FIFO). case queue /// Library will retry sending youngest reports first (LIFO). diff --git a/Sources/Public/Internal/ReportMetadataStorage.swift b/Sources/Public/Internal/ReportMetadataStorage.swift new file mode 100644 index 00000000..1dd1ea36 --- /dev/null +++ b/Sources/Public/Internal/ReportMetadataStorage.swift @@ -0,0 +1,56 @@ +import Foundation + +protocol Config { + var cacheUrl: URL { get } + var directoryUrl: URL { get } + var fileUrl: URL { get } +} + +protocol ReportMetadataStorage { + static func storeToFile(_ dictionary: [String: Any], config: Config) throws + static func retrieveFromFile(config: Config) throws -> [String: Any] + static func removeFile(config: Config) throws +} + +enum ReportMetadataStorageImpl: ReportMetadataStorage { + static func storeToFile(_ dictionary: [String: Any], config: Config) throws { + if !FileManager.default.fileExists(atPath: config.directoryUrl.path) { + try FileManager.default.createDirectory(atPath: config.directoryUrl.path, + withIntermediateDirectories: false, + attributes: nil) + } + + if #available(iOS 11.0, tvOS 11.0, macOS 10.13, *) { + try (dictionary as NSDictionary).write(to: config.fileUrl) + } else { + guard (dictionary as NSDictionary).write(to: config.fileUrl, atomically: true) else { + throw FileError.fileNotWritten + } + } + } + + static func retrieveFromFile(config: Config) throws -> [String: Any] { + guard FileManager.default.fileExists(atPath: config.fileUrl.path) else { + throw FileError.fileNotExists + } + // load file to NSDictionary + let nsDictionary: NSDictionary + if #available(iOS 11.0, tvOS 11.0, macOS 10.13, *) { + nsDictionary = try NSDictionary(contentsOf: config.fileUrl, error: ()) + } else { + guard let dictionaryFromFile = NSDictionary(contentsOf: config.fileUrl) else { + throw FileError.invalidPropertyList + } + nsDictionary = dictionaryFromFile + } + let dictionary = nsDictionary as? [String: Any] ?? [String: Any]() + return dictionary + } + + static func removeFile(config: Config) throws { + guard FileManager.default.fileExists(atPath: config.fileUrl.path) else { + throw FileError.fileNotExists + } + try FileManager.default.removeItem(at: config.fileUrl) + } +} diff --git a/Sources/Public/Internal/SignalContext.swift b/Sources/Public/Internal/SignalContext.swift index e72771d2..8e7b62d7 100644 --- a/Sources/Public/Internal/SignalContext.swift +++ b/Sources/Public/Internal/SignalContext.swift @@ -3,6 +3,10 @@ import Foundation protocol SignalContext: CustomStringConvertible { var allAttributes: Attributes { get } var attributes: Attributes { get set } + // File attachments are stored to disk as URLs + var attachments: Attachments { get set } + // File attachments are used in `BacktraceReport` as string paths + var attachmentPaths: [String] { get } func set(faultMessage: String?) func set(errorType: String?) } diff --git a/Tests/AttachmentStorageTests.swift b/Tests/AttachmentStorageTests.swift new file mode 100644 index 00000000..2d919d0c --- /dev/null +++ b/Tests/AttachmentStorageTests.swift @@ -0,0 +1,77 @@ +import XCTest + +import Nimble +import Quick +@testable import Backtrace + +final class AttachmentStorageTests: QuickSpec { + + override func spec() { + describe("AttachmentStorage") { + it("can save attachments as a plist") { + var crashAttachments = Attachments() + let storage = ReportMetadataStorageMock.self + let bookmarkHandler = AttachmentBookmarkHandlerMock.self + + guard let fileUrl = try? self.createAFile() else { + throw FileError.fileNotWritten + } + crashAttachments["myFile"] = fileUrl + + let attachmentsFileName = "attachments" + try? AttachmentsStorage.store(crashAttachments, + fileName: attachmentsFileName, + storage: storage, + bookmarkHandler: bookmarkHandler) + + let attachments = + (try? AttachmentsStorage.retrieve(fileName: attachmentsFileName, + storage: storage, + bookmarkHandler: bookmarkHandler)) ?? Attachments() + let attachmentPaths = attachments.map(\.value.path) + + expect(attachmentPaths).toNot(beNil()) + expect(attachmentPaths.count).to(be(1)) + expect(attachmentPaths[0]).to(equal(fileUrl.path)) + } + it("can work with empty attachments") { + let crashAttachments = Attachments() + let storage = ReportMetadataStorageMock.self + let bookmarkHandler = AttachmentBookmarkHandlerMock.self + + let attachmentsFileName = "attachments" + try? AttachmentsStorage.store(crashAttachments, + fileName: attachmentsFileName, + storage: storage, + bookmarkHandler: bookmarkHandler) + + let attachments = + (try? AttachmentsStorage.retrieve(fileName: attachmentsFileName, + storage: storage, + bookmarkHandler: bookmarkHandler)) ?? Attachments() + let attachmentPaths = attachments.map(\.value.path) + + expect(attachmentPaths).toNot(beNil()) + expect(attachmentPaths.count).to(be(0)) + } + } + } + + func createAFile() throws -> URL { + let fileName = "sample" + let dirName = "directory" + guard let libraryDirectoryUrl = try? FileManager.default.url( + for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else { + throw FileError.fileNotWritten + } + let directoryUrl = libraryDirectoryUrl.appendingPathComponent(dirName) + try? FileManager().createDirectory( + at: directoryUrl, + withIntermediateDirectories: false, + attributes: nil + ) + let fileUrl = directoryUrl.appendingPathComponent(fileName).appendingPathExtension("txt") + + return fileUrl + } +} diff --git a/Tests/BacktraceClientTests.swift b/Tests/BacktraceClientTests.swift index ebb4b16e..f2458e6c 100644 --- a/Tests/BacktraceClientTests.swift +++ b/Tests/BacktraceClientTests.swift @@ -23,7 +23,7 @@ final class BacktraceClientTests: QuickSpec { expect(defaultDbSettings.retryInterval).to(be(5)) expect(defaultDbSettings.retryLimit).to(be(3)) expect(defaultDbSettings.retryBehaviour.rawValue).to(be(RetryBehaviour.interval.rawValue)) - expect(defaultDbSettings.retryOrder.rawValue).to(be(RetryOder.queue.rawValue)) + expect(defaultDbSettings.retryOrder.rawValue).to(be(RetryOrder.queue.rawValue)) expect(defaultDbSettings.maxDatabaseSizeInBytes).to(be(0)) } @@ -47,7 +47,7 @@ final class BacktraceClientTests: QuickSpec { let maxDatabaseSize = 10 let retryInterval = 10 let retryBehaviour = RetryBehaviour.interval - let retryOrder = RetryOder.stack + let retryOrder = RetryOrder.stack let retryLimit = 10 customDbSettings.maxRecordCount = maxRecordCount diff --git a/Tests/Mocks/AttachmentBookmarkHandlerMock.swift b/Tests/Mocks/AttachmentBookmarkHandlerMock.swift new file mode 100644 index 00000000..d1dbe478 --- /dev/null +++ b/Tests/Mocks/AttachmentBookmarkHandlerMock.swift @@ -0,0 +1,21 @@ +import Foundation +import XCTest +@testable import Backtrace + +enum AttachmentBookmarkHandlerMock: AttachmentBookmarkHandler { + static func convertAttachmentUrlsToBookmarks(_ attachments: Attachments) throws -> Bookmarks { + var attachmentsBookmarksDict = Bookmarks() + for attachment in attachments { + attachmentsBookmarksDict[attachment.key] = attachment.value.path.data(using: .utf8) + } + return attachmentsBookmarksDict + } + + static func extractAttachmentUrls(_ bookmarks: Bookmarks) throws -> Attachments { + var attachments = Attachments() + for bookmark in bookmarks { + attachments[bookmark.key] = URL(string: String(data: bookmark.value, encoding: .utf8) ?? String()) + } + return attachments + } +} diff --git a/Tests/Mocks/ReportMetadataStorageMock.swift b/Tests/Mocks/ReportMetadataStorageMock.swift new file mode 100644 index 00000000..122e7046 --- /dev/null +++ b/Tests/Mocks/ReportMetadataStorageMock.swift @@ -0,0 +1,19 @@ +import Foundation +import XCTest +@testable import Backtrace + +struct ReportMetadataStorageMock: ReportMetadataStorage { + static var fileSystemMock = [String: [String: Any]]() + + static func storeToFile(_ dictionary: [String: Any], config: Config) throws { + fileSystemMock[config.fileUrl.path] = dictionary + } + + static func retrieveFromFile(config: Config) throws -> [String: Any] { + return fileSystemMock[config.fileUrl.path]! + } + + static func removeFile(config: Config) throws { + fileSystemMock.removeValue(forKey: config.fileUrl.path) + } +}