| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- // Copyright 2021 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- #import <Foundation/Foundation.h>
- #import "Crashlytics/Crashlytics/Controllers/FIRCLSMetricKitManager.h"
- #if CLS_METRICKIT_SUPPORTED
- #import "Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.h"
- #include "Crashlytics/Crashlytics/Handlers/FIRCLSMachException.h"
- #include "Crashlytics/Crashlytics/Handlers/FIRCLSSignal.h"
- #import "Crashlytics/Crashlytics/Helpers/FIRCLSCallStackTree.h"
- #import "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h"
- #import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
- #import "Crashlytics/Crashlytics/Models/FIRCLSExecutionIdentifierModel.h"
- #import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
- #import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlytics.h"
- #import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h"
- @interface FIRCLSMetricKitManager ()
- @property FBLPromise *metricKitDataAvailable;
- @property FIRCLSExistingReportManager *existingReportManager;
- @property FIRCLSFileManager *fileManager;
- @property FIRCLSManagerData *managerData;
- @property BOOL metricKitPromiseFulfilled;
- @end
- @implementation FIRCLSMetricKitManager
- - (instancetype)initWithManagerData:(FIRCLSManagerData *)managerData
- existingReportManager:(FIRCLSExistingReportManager *)existingReportManager
- fileManager:(FIRCLSFileManager *)fileManager {
- _existingReportManager = existingReportManager;
- _fileManager = fileManager;
- _managerData = managerData;
- _metricKitPromiseFulfilled = NO;
- return self;
- }
- /*
- * Registers the MetricKit manager to receive MetricKit reports by adding self to the
- * MXMetricManager subscribers. Also initializes the promise that we'll use to ensure that any
- * MetricKit report files are included in Crashylytics fatal reports. If no crash occurred on the
- * last run of the app, this promise is immediately resolved so that the upload of any nonfatal
- * events can proceed.
- */
- - (void)registerMetricKitManager API_AVAILABLE(ios(14)) {
- [[MXMetricManager sharedManager] addSubscriber:self];
- self.metricKitDataAvailable = [FBLPromise pendingPromise];
- // If there was no crash on the last run of the app or there's no diagnostic report in the
- // MetricKit directory, then we aren't expecting a MetricKit diagnostic report and should resolve
- // the promise immediately. If MetricKit captured a fatal event and Crashlytics did not, then
- // we'll still process the MetricKit crash but won't upload it until the app restarts again.
- if (![self.fileManager didCrashOnPreviousExecution] ||
- ![self.fileManager metricKitDiagnosticFileExists]) {
- @synchronized(self) {
- [self fulfillMetricKitPromise];
- }
- }
- // If we haven't resolved this promise within three seconds, resolve it now so that we're not
- // waiting indefinitely for MetricKit payloads that won't arrive.
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), self.managerData.dispatchQueue,
- ^{
- @synchronized(self) {
- if (!self.metricKitPromiseFulfilled) {
- FIRCLSDebugLog(@"Resolving MetricKit promise after three seconds");
- [self fulfillMetricKitPromise];
- }
- }
- });
- FIRCLSDebugLog(@"Finished registering metrickit manager");
- }
- /*
- * This method receives diagnostic payloads from MetricKit whenever a fatal or nonfatal MetricKit
- * event occurs. If a fatal event, this method will be called when the app restarts. Since we're
- * including a MetricKit report file in the Crashlytics report to be sent to the backend, we need
- * to make sure that we process the payloads and write the included information to file before
- * the report is sent up. If this method is called due to a nonfatal event, it will be called
- * immediately after the event. Since we send nonfatal events on the next run of the app, we can
- * write out the information but won't need to resolve the promise.
- */
- - (void)didReceiveDiagnosticPayloads:(NSArray<MXDiagnosticPayload *> *)payloads
- API_AVAILABLE(ios(14)) {
- BOOL processedFatalPayload = NO;
- for (MXDiagnosticPayload *diagnosticPayload in payloads) {
- if (!diagnosticPayload) {
- continue;
- }
- BOOL processedPayload = [self processMetricKitPayload:diagnosticPayload
- skipCrashEvent:processedFatalPayload];
- if (processedPayload && ([diagnosticPayload.crashDiagnostics count] > 0)) {
- processedFatalPayload = YES;
- }
- }
- // Once we've processed all the payloads, resolve the promise so that reporting uploading
- // continues. If there was not a crash on the previous run of the app, the promise will already
- // have been resolved.
- @synchronized(self) {
- [self fulfillMetricKitPromise];
- }
- }
- // Helper method to write a MetricKit payload's data to file.
- - (BOOL)processMetricKitPayload:(MXDiagnosticPayload *)diagnosticPayload
- skipCrashEvent:(BOOL)skipCrashEvent API_AVAILABLE(ios(14)) {
- BOOL writeFailed = NO;
- // Write out each type of diagnostic if it exists in the report
- BOOL hasCrash = [diagnosticPayload.crashDiagnostics count] > 0;
- BOOL hasHang = [diagnosticPayload.hangDiagnostics count] > 0;
- BOOL hasCPUException = [diagnosticPayload.cpuExceptionDiagnostics count] > 0;
- BOOL hasDiskWriteException = [diagnosticPayload.diskWriteExceptionDiagnostics count] > 0;
- // If there are no diagnostics in the report, return before writing out any files.
- if (!hasCrash && !hasHang && !hasCPUException && !hasDiskWriteException) {
- return false;
- }
- // MXDiagnosticPayload have both a timeStampBegin and timeStampEnd. Now that these events are
- // real-time, both refer to the same time - record both values anyway.
- NSTimeInterval beginSecondsSince1970 = [diagnosticPayload.timeStampBegin timeIntervalSince1970];
- NSTimeInterval endSecondsSince1970 = [diagnosticPayload.timeStampEnd timeIntervalSince1970];
- // Get file path for the active reports directory.
- NSString *activePath = [[self.fileManager activePath] stringByAppendingString:@"/"];
- // If there is a crash diagnostic in the payload, then this method was called for a fatal event.
- // Also ensure that there is a report from the last run of the app that we can write to.
- NSString *metricKitFatalReportFile;
- NSString *metricKitNonfatalReportFile;
- NSString *newestUnsentReportID =
- self.existingReportManager.newestUnsentReport.reportID
- ? [self.existingReportManager.newestUnsentReport.reportID stringByAppendingString:@"/"]
- : nil;
- NSString *currentReportID =
- [_managerData.executionIDModel.executionID stringByAppendingString:@"/"];
- BOOL crashlyticsFatalReported =
- ([diagnosticPayload.crashDiagnostics count] > 0) && (newestUnsentReportID != nil) &&
- ([self.fileManager
- fileExistsAtPath:[activePath stringByAppendingString:newestUnsentReportID]]);
- // Set the MetricKit fatal path appropriately depending on whether we also captured a Crashlytics
- // fatal event and whether the diagnostic report came from a fatal or nonfatal event.
- if (crashlyticsFatalReported) {
- metricKitFatalReportFile = [[activePath stringByAppendingString:newestUnsentReportID]
- stringByAppendingString:FIRCLSMetricKitFatalReportFile];
- } else {
- metricKitFatalReportFile = [[activePath stringByAppendingString:currentReportID]
- stringByAppendingString:FIRCLSMetricKitFatalReportFile];
- }
- metricKitNonfatalReportFile = [[activePath stringByAppendingString:currentReportID]
- stringByAppendingString:FIRCLSMetricKitNonfatalReportFile];
- if (!metricKitFatalReportFile || !metricKitNonfatalReportFile) {
- FIRCLSDebugLog(@"Error finding MetricKit files");
- return NO;
- }
- FIRCLSDebugLog(@"File paths for MetricKit report: %@, %@", metricKitFatalReportFile,
- metricKitNonfatalReportFile);
- if (hasCrash && ![_fileManager fileExistsAtPath:metricKitFatalReportFile]) {
- [_fileManager createFileAtPath:metricKitFatalReportFile contents:nil attributes:nil];
- }
- if ((hasHang | hasCPUException | hasDiskWriteException) &&
- ![_fileManager fileExistsAtPath:metricKitNonfatalReportFile]) {
- [_fileManager createFileAtPath:metricKitNonfatalReportFile contents:nil attributes:nil];
- }
- NSFileHandle *nonfatalFile =
- [NSFileHandle fileHandleForUpdatingAtPath:metricKitNonfatalReportFile];
- if ((hasHang | hasCPUException | hasDiskWriteException) && nonfatalFile == nil) {
- FIRCLSDebugLog(@"Unable to create or open nonfatal MetricKit file.");
- return false;
- }
- NSFileHandle *fatalFile = [NSFileHandle fileHandleForUpdatingAtPath:metricKitFatalReportFile];
- if (hasCrash && fatalFile == nil) {
- FIRCLSDebugLog(@"Unable to create or open fatal MetricKit file.");
- return false;
- }
- NSData *newLineData = [@"\n" dataUsingEncoding:NSUTF8StringEncoding];
- // For each diagnostic type, write out a section in the MetricKit report file. This section will
- // have subsections for threads, metadata, and event specific metadata.
- if (hasCrash && !skipCrashEvent) {
- // Write out time information to the MetricKit report file. Time needs to be a value for
- // backend serialization, so we write out end_time separately.
- MXCrashDiagnostic *crashDiagnostic = [diagnosticPayload.crashDiagnostics objectAtIndex:0];
- NSArray *threadArray = [self convertThreadsToArray:crashDiagnostic.callStackTree];
- NSDictionary *metadataDict = [self convertMetadataToDictionary:crashDiagnostic.metaData];
- NSString *nilString = @"";
- // On the backend, we process name, code name, and address into the subtitle of an issue.
- // Mach exception name and code should be preferred over signal name and code if available.
- const char *signalName = NULL;
- const char *signalCodeName = NULL;
- FIRCLSSignalNameLookup([crashDiagnostic.signal intValue], 0, &signalName, &signalCodeName);
- // signalName is the default name, so should never be NULL
- if (signalName == NULL) {
- signalName = "UNKNOWN";
- }
- if (signalCodeName == NULL) {
- signalCodeName = "";
- }
- const char *machExceptionName = NULL;
- const char *machExceptionCodeName = NULL;
- #if CLS_MACH_EXCEPTION_SUPPORTED
- FIRCLSMachExceptionNameLookup(
- [crashDiagnostic.exceptionType intValue],
- (mach_exception_data_type_t)[crashDiagnostic.exceptionCode intValue], &machExceptionName,
- &machExceptionCodeName);
- #endif
- if (machExceptionCodeName == NULL) {
- machExceptionCodeName = "";
- }
- NSString *name = machExceptionName != NULL ? [NSString stringWithUTF8String:machExceptionName]
- : [NSString stringWithUTF8String:signalName];
- NSString *codeName = machExceptionName != NULL
- ? [NSString stringWithUTF8String:machExceptionCodeName]
- : [NSString stringWithUTF8String:signalCodeName];
- NSDictionary *crashDictionary = @{
- @"metric_kit_fatal" : @{
- @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
- @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
- @"metadata" : metadataDict,
- @"termination_reason" :
- (crashDiagnostic.terminationReason) ? crashDiagnostic.terminationReason : nilString,
- @"virtual_memory_region_info" : (crashDiagnostic.virtualMemoryRegionInfo)
- ? crashDiagnostic.virtualMemoryRegionInfo
- : nilString,
- @"exception_type" : crashDiagnostic.exceptionType,
- @"exception_code" : crashDiagnostic.exceptionCode,
- @"signal" : crashDiagnostic.signal,
- @"app_version" : crashDiagnostic.applicationVersion,
- @"code_name" : codeName,
- @"name" : name
- }
- };
- writeFailed = ![self writeDictionaryToFile:crashDictionary
- file:fatalFile
- newLineData:newLineData];
- writeFailed = writeFailed | ![self writeDictionaryToFile:@{@"threads" : threadArray}
- file:fatalFile
- newLineData:newLineData];
- }
- if (hasHang) {
- MXHangDiagnostic *hangDiagnostic = [diagnosticPayload.hangDiagnostics objectAtIndex:0];
- NSArray *threadArray = [self convertThreadsToArray:hangDiagnostic.callStackTree];
- NSDictionary *metadataDict = [self convertMetadataToDictionary:hangDiagnostic.metaData];
- NSDictionary *hangDictionary = @{
- @"exception" : @{
- @"type" : @"metrickit_nonfatal",
- @"name" : @"hang_event",
- @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
- @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
- @"threads" : threadArray,
- @"metadata" : metadataDict,
- @"hang_duration" : [NSNumber numberWithDouble:[hangDiagnostic.hangDuration doubleValue]],
- @"app_version" : hangDiagnostic.applicationVersion
- }
- };
- writeFailed = ![self writeDictionaryToFile:hangDictionary
- file:nonfatalFile
- newLineData:newLineData];
- }
- if (hasCPUException) {
- MXCPUExceptionDiagnostic *cpuExceptionDiagnostic =
- [diagnosticPayload.cpuExceptionDiagnostics objectAtIndex:0];
- NSArray *threadArray = [self convertThreadsToArray:cpuExceptionDiagnostic.callStackTree];
- NSDictionary *metadataDict = [self convertMetadataToDictionary:cpuExceptionDiagnostic.metaData];
- NSDictionary *cpuDictionary = @{
- @"exception" : @{
- @"type" : @"metrickit_nonfatal",
- @"name" : @"cpu_exception_event",
- @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
- @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
- @"threads" : threadArray,
- @"metadata" : metadataDict,
- @"total_cpu_time" :
- [NSNumber numberWithDouble:[cpuExceptionDiagnostic.totalCPUTime doubleValue]],
- @"total_sampled_time" :
- [NSNumber numberWithDouble:[cpuExceptionDiagnostic.totalSampledTime doubleValue]],
- @"app_version" : cpuExceptionDiagnostic.applicationVersion
- }
- };
- writeFailed = ![self writeDictionaryToFile:cpuDictionary
- file:nonfatalFile
- newLineData:newLineData];
- }
- if (hasDiskWriteException) {
- MXDiskWriteExceptionDiagnostic *diskWriteExceptionDiagnostic =
- [diagnosticPayload.diskWriteExceptionDiagnostics objectAtIndex:0];
- NSArray *threadArray = [self convertThreadsToArray:diskWriteExceptionDiagnostic.callStackTree];
- NSDictionary *metadataDict =
- [self convertMetadataToDictionary:diskWriteExceptionDiagnostic.metaData];
- NSDictionary *diskWriteDictionary = @{
- @"exception" : @{
- @"type" : @"metrickit_nonfatal",
- @"name" : @"disk_write_exception_event",
- @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
- @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
- @"threads" : threadArray,
- @"metadata" : metadataDict,
- @"app_version" : diskWriteExceptionDiagnostic.applicationVersion,
- @"total_writes_caused" :
- [NSNumber numberWithDouble:[diskWriteExceptionDiagnostic.totalWritesCaused doubleValue]]
- }
- };
- writeFailed = ![self writeDictionaryToFile:diskWriteDictionary
- file:nonfatalFile
- newLineData:newLineData];
- }
- return !writeFailed;
- }
- /*
- * Required for MXMetricManager subscribers. Since we aren't currently collecting any MetricKit
- * metrics, this method is left empty.
- */
- - (void)didReceiveMetricPayloads:(NSArray<MXMetricPayload *> *)payloads API_AVAILABLE(ios(13)) {
- }
- - (FBLPromise *)waitForMetricKitDataAvailable {
- FBLPromise *result = nil;
- @synchronized(self) {
- result = self.metricKitDataAvailable;
- }
- return result;
- }
- /*
- * Helper method to convert threads for a MetricKit fatal diagnostic event to an array of threads.
- */
- - (NSArray *)convertThreadsToArray:(MXCallStackTree *)mxCallStackTree API_AVAILABLE(ios(14)) {
- FIRCLSCallStackTree *tree = [[FIRCLSCallStackTree alloc] initWithMXCallStackTree:mxCallStackTree];
- return [tree getArrayRepresentation];
- }
- /*
- * Helper method to convert threads for a MetricKit nonfatal diagnostic event to an array of frames.
- */
- - (NSArray *)convertThreadsToArrayForNonfatal:(MXCallStackTree *)mxCallStackTree
- API_AVAILABLE(ios(14)) {
- FIRCLSCallStackTree *tree = [[FIRCLSCallStackTree alloc] initWithMXCallStackTree:mxCallStackTree];
- return [tree getFramesOfBlamedThread];
- }
- /*
- * Helper method to convert metadata for a MetricKit diagnostic event to a dictionary. MXMetadata
- * has a dictionaryRepresentation method but it is deprecated.
- */
- - (NSDictionary *)convertMetadataToDictionary:(MXMetaData *)metadata API_AVAILABLE(ios(14)) {
- NSError *error = nil;
- NSDictionary *metadataDictionary =
- [NSJSONSerialization JSONObjectWithData:[metadata JSONRepresentation] options:0 error:&error];
- return metadataDictionary;
- }
- /*
- * Helper method to fulfill the metricKitDataAvailable promise and track that it has been fulfilled.
- */
- - (void)fulfillMetricKitPromise {
- if (self.metricKitPromiseFulfilled) return;
- [self.metricKitDataAvailable fulfill:nil];
- self.metricKitPromiseFulfilled = YES;
- }
- /*
- * Helper method to write a dictionary of event information to file. Returns whether it succeeded.
- */
- - (BOOL)writeDictionaryToFile:(NSDictionary *)dictionary
- file:(NSFileHandle *)file
- newLineData:(NSData *)newLineData {
- NSError *dataError = nil;
- NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&dataError];
- if (dataError) {
- FIRCLSDebugLog(@"Unable to write out dictionary.");
- return NO;
- }
- [file seekToEndOfFile];
- [file writeData:data];
- [file writeData:newLineData];
- return YES;
- }
- - (NSString *)getSignalName:(NSNumber *)signalCode {
- int signal = [signalCode intValue];
- switch (signal) {
- case SIGABRT:
- return @"SIGABRT";
- case SIGBUS:
- return @"SIGBUS";
- case SIGFPE:
- return @"SIGFPE";
- case SIGILL:
- return @"SIGILL";
- case SIGSEGV:
- return @"SIGSEGV";
- case SIGSYS:
- return @"SIGSYS";
- case SIGTRAP:
- return @"SIGTRAP";
- default:
- return @"UNKNOWN";
- }
- return @"UNKNOWN";
- }
- @end
- #endif // CLS_METRICKIT_SUPPORTED
|