FIRCLSMetricKitManager.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import <Foundation/Foundation.h>
  15. #import "Crashlytics/Crashlytics/Controllers/FIRCLSMetricKitManager.h"
  16. #if CLS_METRICKIT_SUPPORTED
  17. #import "Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.h"
  18. #include "Crashlytics/Crashlytics/Handlers/FIRCLSMachException.h"
  19. #include "Crashlytics/Crashlytics/Handlers/FIRCLSSignal.h"
  20. #import "Crashlytics/Crashlytics/Helpers/FIRCLSCallStackTree.h"
  21. #import "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h"
  22. #import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
  23. #import "Crashlytics/Crashlytics/Models/FIRCLSExecutionIdentifierModel.h"
  24. #import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
  25. #import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlytics.h"
  26. #import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h"
  27. @interface FIRCLSMetricKitManager ()
  28. @property FBLPromise *metricKitDataAvailable;
  29. @property FIRCLSExistingReportManager *existingReportManager;
  30. @property FIRCLSFileManager *fileManager;
  31. @property FIRCLSManagerData *managerData;
  32. @property BOOL metricKitPromiseFulfilled;
  33. @end
  34. @implementation FIRCLSMetricKitManager
  35. - (instancetype)initWithManagerData:(FIRCLSManagerData *)managerData
  36. existingReportManager:(FIRCLSExistingReportManager *)existingReportManager
  37. fileManager:(FIRCLSFileManager *)fileManager {
  38. _existingReportManager = existingReportManager;
  39. _fileManager = fileManager;
  40. _managerData = managerData;
  41. _metricKitPromiseFulfilled = NO;
  42. return self;
  43. }
  44. /*
  45. * Registers the MetricKit manager to receive MetricKit reports by adding self to the
  46. * MXMetricManager subscribers. Also initializes the promise that we'll use to ensure that any
  47. * MetricKit report files are included in Crashylytics fatal reports. If no crash occurred on the
  48. * last run of the app, this promise is immediately resolved so that the upload of any nonfatal
  49. * events can proceed.
  50. */
  51. - (void)registerMetricKitManager API_AVAILABLE(ios(14)) {
  52. [[MXMetricManager sharedManager] addSubscriber:self];
  53. self.metricKitDataAvailable = [FBLPromise pendingPromise];
  54. // If there was no crash on the last run of the app or there's no diagnostic report in the
  55. // MetricKit directory, then we aren't expecting a MetricKit diagnostic report and should resolve
  56. // the promise immediately. If MetricKit captured a fatal event and Crashlytics did not, then
  57. // we'll still process the MetricKit crash but won't upload it until the app restarts again.
  58. if (![self.fileManager didCrashOnPreviousExecution] ||
  59. ![self.fileManager metricKitDiagnosticFileExists]) {
  60. @synchronized(self) {
  61. [self fulfillMetricKitPromise];
  62. }
  63. }
  64. // If we haven't resolved this promise within three seconds, resolve it now so that we're not
  65. // waiting indefinitely for MetricKit payloads that won't arrive.
  66. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), self.managerData.dispatchQueue,
  67. ^{
  68. @synchronized(self) {
  69. if (!self.metricKitPromiseFulfilled) {
  70. FIRCLSDebugLog(@"Resolving MetricKit promise after three seconds");
  71. [self fulfillMetricKitPromise];
  72. }
  73. }
  74. });
  75. FIRCLSDebugLog(@"Finished registering metrickit manager");
  76. }
  77. /*
  78. * This method receives diagnostic payloads from MetricKit whenever a fatal or nonfatal MetricKit
  79. * event occurs. If a fatal event, this method will be called when the app restarts. Since we're
  80. * including a MetricKit report file in the Crashlytics report to be sent to the backend, we need
  81. * to make sure that we process the payloads and write the included information to file before
  82. * the report is sent up. If this method is called due to a nonfatal event, it will be called
  83. * immediately after the event. Since we send nonfatal events on the next run of the app, we can
  84. * write out the information but won't need to resolve the promise.
  85. */
  86. - (void)didReceiveDiagnosticPayloads:(NSArray<MXDiagnosticPayload *> *)payloads
  87. API_AVAILABLE(ios(14)) {
  88. BOOL processedFatalPayload = NO;
  89. for (MXDiagnosticPayload *diagnosticPayload in payloads) {
  90. if (!diagnosticPayload) {
  91. continue;
  92. }
  93. BOOL processedPayload = [self processMetricKitPayload:diagnosticPayload
  94. skipCrashEvent:processedFatalPayload];
  95. if (processedPayload && ([diagnosticPayload.crashDiagnostics count] > 0)) {
  96. processedFatalPayload = YES;
  97. }
  98. }
  99. // Once we've processed all the payloads, resolve the promise so that reporting uploading
  100. // continues. If there was not a crash on the previous run of the app, the promise will already
  101. // have been resolved.
  102. @synchronized(self) {
  103. [self fulfillMetricKitPromise];
  104. }
  105. }
  106. // Helper method to write a MetricKit payload's data to file.
  107. - (BOOL)processMetricKitPayload:(MXDiagnosticPayload *)diagnosticPayload
  108. skipCrashEvent:(BOOL)skipCrashEvent API_AVAILABLE(ios(14)) {
  109. BOOL writeFailed = NO;
  110. // Write out each type of diagnostic if it exists in the report
  111. BOOL hasCrash = [diagnosticPayload.crashDiagnostics count] > 0;
  112. BOOL hasHang = [diagnosticPayload.hangDiagnostics count] > 0;
  113. BOOL hasCPUException = [diagnosticPayload.cpuExceptionDiagnostics count] > 0;
  114. BOOL hasDiskWriteException = [diagnosticPayload.diskWriteExceptionDiagnostics count] > 0;
  115. // If there are no diagnostics in the report, return before writing out any files.
  116. if (!hasCrash && !hasHang && !hasCPUException && !hasDiskWriteException) {
  117. return false;
  118. }
  119. // MXDiagnosticPayload have both a timeStampBegin and timeStampEnd. Now that these events are
  120. // real-time, both refer to the same time - record both values anyway.
  121. NSTimeInterval beginSecondsSince1970 = [diagnosticPayload.timeStampBegin timeIntervalSince1970];
  122. NSTimeInterval endSecondsSince1970 = [diagnosticPayload.timeStampEnd timeIntervalSince1970];
  123. // Get file path for the active reports directory.
  124. NSString *activePath = [[self.fileManager activePath] stringByAppendingString:@"/"];
  125. // If there is a crash diagnostic in the payload, then this method was called for a fatal event.
  126. // Also ensure that there is a report from the last run of the app that we can write to.
  127. NSString *metricKitFatalReportFile;
  128. NSString *metricKitNonfatalReportFile;
  129. NSString *newestUnsentReportID =
  130. self.existingReportManager.newestUnsentReport.reportID
  131. ? [self.existingReportManager.newestUnsentReport.reportID stringByAppendingString:@"/"]
  132. : nil;
  133. NSString *currentReportID =
  134. [_managerData.executionIDModel.executionID stringByAppendingString:@"/"];
  135. BOOL crashlyticsFatalReported =
  136. ([diagnosticPayload.crashDiagnostics count] > 0) && (newestUnsentReportID != nil) &&
  137. ([self.fileManager
  138. fileExistsAtPath:[activePath stringByAppendingString:newestUnsentReportID]]);
  139. // Set the MetricKit fatal path appropriately depending on whether we also captured a Crashlytics
  140. // fatal event and whether the diagnostic report came from a fatal or nonfatal event.
  141. if (crashlyticsFatalReported) {
  142. metricKitFatalReportFile = [[activePath stringByAppendingString:newestUnsentReportID]
  143. stringByAppendingString:FIRCLSMetricKitFatalReportFile];
  144. } else {
  145. metricKitFatalReportFile = [[activePath stringByAppendingString:currentReportID]
  146. stringByAppendingString:FIRCLSMetricKitFatalReportFile];
  147. }
  148. metricKitNonfatalReportFile = [[activePath stringByAppendingString:currentReportID]
  149. stringByAppendingString:FIRCLSMetricKitNonfatalReportFile];
  150. if (!metricKitFatalReportFile || !metricKitNonfatalReportFile) {
  151. FIRCLSDebugLog(@"Error finding MetricKit files");
  152. return NO;
  153. }
  154. FIRCLSDebugLog(@"File paths for MetricKit report: %@, %@", metricKitFatalReportFile,
  155. metricKitNonfatalReportFile);
  156. if (hasCrash && ![_fileManager fileExistsAtPath:metricKitFatalReportFile]) {
  157. [_fileManager createFileAtPath:metricKitFatalReportFile contents:nil attributes:nil];
  158. }
  159. if ((hasHang | hasCPUException | hasDiskWriteException) &&
  160. ![_fileManager fileExistsAtPath:metricKitNonfatalReportFile]) {
  161. [_fileManager createFileAtPath:metricKitNonfatalReportFile contents:nil attributes:nil];
  162. }
  163. NSFileHandle *nonfatalFile =
  164. [NSFileHandle fileHandleForUpdatingAtPath:metricKitNonfatalReportFile];
  165. if ((hasHang | hasCPUException | hasDiskWriteException) && nonfatalFile == nil) {
  166. FIRCLSDebugLog(@"Unable to create or open nonfatal MetricKit file.");
  167. return false;
  168. }
  169. NSFileHandle *fatalFile = [NSFileHandle fileHandleForUpdatingAtPath:metricKitFatalReportFile];
  170. if (hasCrash && fatalFile == nil) {
  171. FIRCLSDebugLog(@"Unable to create or open fatal MetricKit file.");
  172. return false;
  173. }
  174. NSData *newLineData = [@"\n" dataUsingEncoding:NSUTF8StringEncoding];
  175. // For each diagnostic type, write out a section in the MetricKit report file. This section will
  176. // have subsections for threads, metadata, and event specific metadata.
  177. if (hasCrash && !skipCrashEvent) {
  178. // Write out time information to the MetricKit report file. Time needs to be a value for
  179. // backend serialization, so we write out end_time separately.
  180. MXCrashDiagnostic *crashDiagnostic = [diagnosticPayload.crashDiagnostics objectAtIndex:0];
  181. NSArray *threadArray = [self convertThreadsToArray:crashDiagnostic.callStackTree];
  182. NSDictionary *metadataDict = [self convertMetadataToDictionary:crashDiagnostic.metaData];
  183. NSString *nilString = @"";
  184. // On the backend, we process name, code name, and address into the subtitle of an issue.
  185. // Mach exception name and code should be preferred over signal name and code if available.
  186. const char *signalName = NULL;
  187. const char *signalCodeName = NULL;
  188. FIRCLSSignalNameLookup([crashDiagnostic.signal intValue], 0, &signalName, &signalCodeName);
  189. // signalName is the default name, so should never be NULL
  190. if (signalName == NULL) {
  191. signalName = "UNKNOWN";
  192. }
  193. if (signalCodeName == NULL) {
  194. signalCodeName = "";
  195. }
  196. const char *machExceptionName = NULL;
  197. const char *machExceptionCodeName = NULL;
  198. #if CLS_MACH_EXCEPTION_SUPPORTED
  199. FIRCLSMachExceptionNameLookup(
  200. [crashDiagnostic.exceptionType intValue],
  201. (mach_exception_data_type_t)[crashDiagnostic.exceptionCode intValue], &machExceptionName,
  202. &machExceptionCodeName);
  203. #endif
  204. if (machExceptionCodeName == NULL) {
  205. machExceptionCodeName = "";
  206. }
  207. NSString *name = machExceptionName != NULL ? [NSString stringWithUTF8String:machExceptionName]
  208. : [NSString stringWithUTF8String:signalName];
  209. NSString *codeName = machExceptionName != NULL
  210. ? [NSString stringWithUTF8String:machExceptionCodeName]
  211. : [NSString stringWithUTF8String:signalCodeName];
  212. NSDictionary *crashDictionary = @{
  213. @"metric_kit_fatal" : @{
  214. @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
  215. @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
  216. @"metadata" : metadataDict,
  217. @"termination_reason" :
  218. (crashDiagnostic.terminationReason) ? crashDiagnostic.terminationReason : nilString,
  219. @"virtual_memory_region_info" : (crashDiagnostic.virtualMemoryRegionInfo)
  220. ? crashDiagnostic.virtualMemoryRegionInfo
  221. : nilString,
  222. @"exception_type" : crashDiagnostic.exceptionType,
  223. @"exception_code" : crashDiagnostic.exceptionCode,
  224. @"signal" : crashDiagnostic.signal,
  225. @"app_version" : crashDiagnostic.applicationVersion,
  226. @"code_name" : codeName,
  227. @"name" : name
  228. }
  229. };
  230. writeFailed = ![self writeDictionaryToFile:crashDictionary
  231. file:fatalFile
  232. newLineData:newLineData];
  233. writeFailed = writeFailed | ![self writeDictionaryToFile:@{@"threads" : threadArray}
  234. file:fatalFile
  235. newLineData:newLineData];
  236. }
  237. if (hasHang) {
  238. MXHangDiagnostic *hangDiagnostic = [diagnosticPayload.hangDiagnostics objectAtIndex:0];
  239. NSArray *threadArray = [self convertThreadsToArray:hangDiagnostic.callStackTree];
  240. NSDictionary *metadataDict = [self convertMetadataToDictionary:hangDiagnostic.metaData];
  241. NSDictionary *hangDictionary = @{
  242. @"exception" : @{
  243. @"type" : @"metrickit_nonfatal",
  244. @"name" : @"hang_event",
  245. @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
  246. @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
  247. @"threads" : threadArray,
  248. @"metadata" : metadataDict,
  249. @"hang_duration" : [NSNumber numberWithDouble:[hangDiagnostic.hangDuration doubleValue]],
  250. @"app_version" : hangDiagnostic.applicationVersion
  251. }
  252. };
  253. writeFailed = ![self writeDictionaryToFile:hangDictionary
  254. file:nonfatalFile
  255. newLineData:newLineData];
  256. }
  257. if (hasCPUException) {
  258. MXCPUExceptionDiagnostic *cpuExceptionDiagnostic =
  259. [diagnosticPayload.cpuExceptionDiagnostics objectAtIndex:0];
  260. NSArray *threadArray = [self convertThreadsToArray:cpuExceptionDiagnostic.callStackTree];
  261. NSDictionary *metadataDict = [self convertMetadataToDictionary:cpuExceptionDiagnostic.metaData];
  262. NSDictionary *cpuDictionary = @{
  263. @"exception" : @{
  264. @"type" : @"metrickit_nonfatal",
  265. @"name" : @"cpu_exception_event",
  266. @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
  267. @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
  268. @"threads" : threadArray,
  269. @"metadata" : metadataDict,
  270. @"total_cpu_time" :
  271. [NSNumber numberWithDouble:[cpuExceptionDiagnostic.totalCPUTime doubleValue]],
  272. @"total_sampled_time" :
  273. [NSNumber numberWithDouble:[cpuExceptionDiagnostic.totalSampledTime doubleValue]],
  274. @"app_version" : cpuExceptionDiagnostic.applicationVersion
  275. }
  276. };
  277. writeFailed = ![self writeDictionaryToFile:cpuDictionary
  278. file:nonfatalFile
  279. newLineData:newLineData];
  280. }
  281. if (hasDiskWriteException) {
  282. MXDiskWriteExceptionDiagnostic *diskWriteExceptionDiagnostic =
  283. [diagnosticPayload.diskWriteExceptionDiagnostics objectAtIndex:0];
  284. NSArray *threadArray = [self convertThreadsToArray:diskWriteExceptionDiagnostic.callStackTree];
  285. NSDictionary *metadataDict =
  286. [self convertMetadataToDictionary:diskWriteExceptionDiagnostic.metaData];
  287. NSDictionary *diskWriteDictionary = @{
  288. @"exception" : @{
  289. @"type" : @"metrickit_nonfatal",
  290. @"name" : @"disk_write_exception_event",
  291. @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
  292. @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
  293. @"threads" : threadArray,
  294. @"metadata" : metadataDict,
  295. @"app_version" : diskWriteExceptionDiagnostic.applicationVersion,
  296. @"total_writes_caused" :
  297. [NSNumber numberWithDouble:[diskWriteExceptionDiagnostic.totalWritesCaused doubleValue]]
  298. }
  299. };
  300. writeFailed = ![self writeDictionaryToFile:diskWriteDictionary
  301. file:nonfatalFile
  302. newLineData:newLineData];
  303. }
  304. return !writeFailed;
  305. }
  306. /*
  307. * Required for MXMetricManager subscribers. Since we aren't currently collecting any MetricKit
  308. * metrics, this method is left empty.
  309. */
  310. - (void)didReceiveMetricPayloads:(NSArray<MXMetricPayload *> *)payloads API_AVAILABLE(ios(13)) {
  311. }
  312. - (FBLPromise *)waitForMetricKitDataAvailable {
  313. FBLPromise *result = nil;
  314. @synchronized(self) {
  315. result = self.metricKitDataAvailable;
  316. }
  317. return result;
  318. }
  319. /*
  320. * Helper method to convert threads for a MetricKit fatal diagnostic event to an array of threads.
  321. */
  322. - (NSArray *)convertThreadsToArray:(MXCallStackTree *)mxCallStackTree API_AVAILABLE(ios(14)) {
  323. FIRCLSCallStackTree *tree = [[FIRCLSCallStackTree alloc] initWithMXCallStackTree:mxCallStackTree];
  324. return [tree getArrayRepresentation];
  325. }
  326. /*
  327. * Helper method to convert threads for a MetricKit nonfatal diagnostic event to an array of frames.
  328. */
  329. - (NSArray *)convertThreadsToArrayForNonfatal:(MXCallStackTree *)mxCallStackTree
  330. API_AVAILABLE(ios(14)) {
  331. FIRCLSCallStackTree *tree = [[FIRCLSCallStackTree alloc] initWithMXCallStackTree:mxCallStackTree];
  332. return [tree getFramesOfBlamedThread];
  333. }
  334. /*
  335. * Helper method to convert metadata for a MetricKit diagnostic event to a dictionary. MXMetadata
  336. * has a dictionaryRepresentation method but it is deprecated.
  337. */
  338. - (NSDictionary *)convertMetadataToDictionary:(MXMetaData *)metadata API_AVAILABLE(ios(14)) {
  339. NSError *error = nil;
  340. NSDictionary *metadataDictionary =
  341. [NSJSONSerialization JSONObjectWithData:[metadata JSONRepresentation] options:0 error:&error];
  342. return metadataDictionary;
  343. }
  344. /*
  345. * Helper method to fulfill the metricKitDataAvailable promise and track that it has been fulfilled.
  346. */
  347. - (void)fulfillMetricKitPromise {
  348. if (self.metricKitPromiseFulfilled) return;
  349. [self.metricKitDataAvailable fulfill:nil];
  350. self.metricKitPromiseFulfilled = YES;
  351. }
  352. /*
  353. * Helper method to write a dictionary of event information to file. Returns whether it succeeded.
  354. */
  355. - (BOOL)writeDictionaryToFile:(NSDictionary *)dictionary
  356. file:(NSFileHandle *)file
  357. newLineData:(NSData *)newLineData {
  358. NSError *dataError = nil;
  359. NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&dataError];
  360. if (dataError) {
  361. FIRCLSDebugLog(@"Unable to write out dictionary.");
  362. return NO;
  363. }
  364. [file seekToEndOfFile];
  365. [file writeData:data];
  366. [file writeData:newLineData];
  367. return YES;
  368. }
  369. - (NSString *)getSignalName:(NSNumber *)signalCode {
  370. int signal = [signalCode intValue];
  371. switch (signal) {
  372. case SIGABRT:
  373. return @"SIGABRT";
  374. case SIGBUS:
  375. return @"SIGBUS";
  376. case SIGFPE:
  377. return @"SIGFPE";
  378. case SIGILL:
  379. return @"SIGILL";
  380. case SIGSEGV:
  381. return @"SIGSEGV";
  382. case SIGSYS:
  383. return @"SIGSYS";
  384. case SIGTRAP:
  385. return @"SIGTRAP";
  386. default:
  387. return @"UNKNOWN";
  388. }
  389. return @"UNKNOWN";
  390. }
  391. @end
  392. #endif // CLS_METRICKIT_SUPPORTED