// // MLCrashReporter.m // Monal // // Created by admin on 21.06.23. // Copyright © 2023 monal-im.org. All rights reserved. // #import #import #import #import #import #import #import #import #import #import "MLConstants.h" #import "HelperTools.h" #import "MonalAppDelegate.h" #import "MLCrashReporter.h" #define PART_SEPARATOR_FORMAT "\n\n-------- d049d576-9bf0-47dd-839f-dee6b07c1df9 -------- %@ -------- d049d576-9bf0-47dd-839f-dee6b07c1df9 --------\n\n" @interface KSCrashReportFilterAlert: NSObject +(instancetype) filter; @end @interface KSCrashReportFilterEmpty: NSObject +(instancetype) filter; @end @interface KSCrashReportFilterAddAuxInfo : NSObject +(instancetype) filter; @end @interface KSCrashReportFilterAddMLLogfile : NSObject +(instancetype) filter; @end @interface KSCrashReportFilterAddProfraw : NSObject +(instancetype) filter; @end @interface MLCrashReporter() @property (atomic, strong) NSArray* _Nullable kscrashReports; @property (atomic, strong) KSCrashReportFilterCompletion _Nullable kscrashCompletion; @end @implementation MLCrashReporter +(void) reportPendingCrashes { //send out pending KSCrash reports KSCrash* handler = [KSCrash sharedInstance]; handler.deleteBehaviorAfterSendAll = KSCDeleteAlways; //KSCDeleteNever id dummyFilter = [KSCrashReportFilterEmpty filter]; NSString* dummyFilterName = @"dummy not printed"; id auxInfoFilter = [KSCrashReportFilterAddAuxInfo filter]; NSString* auxInfoName = @"AUX Info (*.txt)"; id appleFilter = [KSCrashReportFilterAppleFmt filterWithReportStyle:KSAppleReportStyleSymbolicatedSideBySide]; NSString* appleName = @"Apple Report (*.crash)"; NSArray>* jsonFilter = @[[KSCrashReportFilterJSONEncode filterWithOptions:KSJSONEncodeOptionPretty], [KSCrashReportFilterDataToString filter]]; NSString* jsonName = @"JSON Report (*.json)"; id logfileFilter = [KSCrashReportFilterAddMLLogfile filter]; NSString* logfileName = @"Logfile (*.rawlog.gz)"; id profrawFilter = [KSCrashReportFilterAddMLLogfile filter]; NSString* profrawName = @"Profile (*.profraw)"; handler.sink = [KSCrashReportFilterPipeline filterWithFilters: [KSCrashReportFilterAlert filter], [KSCrashReportFilterCombine filterWithFiltersAndKeys: dummyFilter, dummyFilterName, //this dummy is needed to make the filter framework print the title of our aux data auxInfoFilter, auxInfoName, appleFilter, appleName, jsonFilter, jsonName, logfileFilter, logfileName, profrawFilter, profrawName, nil ], [KSCrashReportFilterConcatenate filterWithSeparatorFmt:@PART_SEPARATOR_FORMAT keys: dummyFilterName, auxInfoName, appleName, jsonName, logfileName, profrawName, nil ], [KSCrashReportFilterStringToData filter], [KSCrashReportFilterGZipCompress filterWithCompressionLevel:-1], [[self alloc] init], //add this class as filter to send out all stuff via mail nil ]; DDLogVerbose(@"Trying to send crash reports..."); [handler sendAllReportsWithCompletion:^(NSArray* reports, BOOL completed, NSError* error){ if(completed) DDLogWarn(@"Sent %d reports", (int)[reports count]); else DDLogError(@"Failed to send reports: %@", error); }]; } -(void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion { if(![MFMailComposeViewController canSendMail]) { #if TARGET_OS_SIMULATOR u_int32_t runid_raw = arc4random(); NSString* runid = [HelperTools hexadecimalString:[NSData dataWithBytes:&runid_raw length:sizeof(runid_raw)]]; int i = 1; for(NSData* report in reports) if(![report isKindOfClass:[NSData class]]) DDLogError(@"Report was of unsupported data type %@", [report class]); else { NSString* path = [[HelperTools getContainerURLForPathComponents:@[[NSString stringWithFormat:@"CrashReport-%@-%d.mcrash.gz", runid, i++]]] path]; DDLogWarn(@"Writing report %d to file: %@", i, path); [report writeToFile:path atomically:YES]; } kscrash_callCompletion(onCompletion, reports, YES, [NSError errorWithDomain:[[self class] description] code:0 description:@"Crashreports written to simulator container..."]); return; #else UIAlertController* alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Email Error", @"Crash report error dialog") message:NSLocalizedString(@"This device is not configured to send email.", @"Crash report error dialog") preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction* okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"Crash report error dialog") style:UIAlertActionStyleDefault handler:nil]; [alertController addAction:okAction]; [[(MonalAppDelegate*)[[UIApplication sharedApplication] delegate] getTopViewController] presentViewController:alertController animated:YES completion:NULL]; kscrash_callCompletion(onCompletion, reports, NO, [NSError errorWithDomain:[[self class] description] code:0 description:NSLocalizedString(@"E-Mail not enabled on device", @"Crash report error dialog")]); return; #endif } self.kscrashCompletion = onCompletion; self.kscrashReports = reports; DDLogVerbose(@"Preparing MFMailComposeViewController..."); MFMailComposeViewController* mailController = [[MFMailComposeViewController alloc] init]; mailController.mailComposeDelegate = self; [mailController setToRecipients:@[@"crash@monal-im.org"]]; [mailController setSubject:@"Crash Reports"]; [mailController setMessageBody:@"> Please fill in your last actions that led to this crash:\n" isHTML:NO]; int i = 1; for(NSData* report in reports) if(![report isKindOfClass:[NSData class]]) DDLogError(@"Report was of unsupported data type %@", [report class]); else { DDLogVerbose(@"Adding mail attachment..."); [mailController addAttachmentData:report mimeType:@"binary" fileName:[NSString stringWithFormat:@"CrashReport-%d.mcrash.gz", i++]]; } dispatch_async(dispatch_get_main_queue(), ^{ DDLogVerbose(@"Presenting MFMailComposeViewController..."); [[(MonalAppDelegate*)[[UIApplication sharedApplication] delegate] getTopViewController] presentViewController:mailController animated:YES completion:nil]; }); } -(void) mailComposeController:(__unused MFMailComposeViewController*) mailController didFinishWithResult:(MFMailComposeResult) result error:(NSError*) error { dispatch_async(dispatch_get_main_queue(), ^{ [[(MonalAppDelegate*)[[UIApplication sharedApplication] delegate] getTopViewController] dismissViewControllerAnimated:YES completion:nil]; if(self.kscrashCompletion == nil) { DDLogError(@"No kscrash completion given!"); return; } switch(result) { case MFMailComposeResultSent: DDLogInfo(@"Crash report send result: MFMailComposeResultSent"); kscrash_callCompletion(self.kscrashCompletion, self.kscrashReports, YES, nil); break; case MFMailComposeResultSaved: DDLogInfo(@"Crash report send result: MFMailComposeResultSaved"); kscrash_callCompletion(self.kscrashCompletion, self.kscrashReports, YES, nil); break; case MFMailComposeResultCancelled: DDLogInfo(@"Crash report send result: MFMailComposeResultCancelled"); kscrash_callCompletion(self.kscrashCompletion, self.kscrashReports, NO, [NSError errorWithDomain:[[self class] description] code:0 description:@"User cancelled"]); break; case MFMailComposeResultFailed: DDLogInfo(@"Crash report send result: MFMailComposeResultFailed"); kscrash_callCompletion(self.kscrashCompletion, self.kscrashReports, NO, error); break; default: { DDLogInfo(@"Crash report send result: unknown"); kscrash_callCompletion(self.kscrashCompletion, self.kscrashReports, NO, [NSError errorWithDomain:[[self class] description] code:0 description:@"Unknown MFMailComposeResult: %d", result]); } } self.kscrashCompletion = nil; self.kscrashReports = nil; }); } @end @implementation KSCrashReportFilterAlert +(instancetype) filter { return [[self alloc] init]; } -(void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion { NSString* title = NSLocalizedString(@"Crash Detected", @"Crash reporting"); NSString* message = NSLocalizedString(@"The app crashed last time it was launched. Send a crash report? This crash report will contain privacy related data. We will only use it to debug your crash and delete it afterwards!", @"Crash reporting"); NSString* yesAnswer = NSLocalizedString(@"Sure, send it!", @"Crash reporting"); NSString* noAnswer = NSLocalizedString(@"No, thanks", @"Crash reporting"); DDLogVerbose(@"KSCrashReportFilterAlert started..."); dispatch_async(dispatch_get_main_queue(), ^{ UIAlertController* alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction* yesAction = [UIAlertAction actionWithTitle:yesAnswer style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction* _Nonnull action) { kscrash_callCompletion(onCompletion, reports, YES, nil); }]; UIAlertAction* noAction = [UIAlertAction actionWithTitle:noAnswer style:UIAlertActionStyleCancel handler:^(__unused UIAlertAction* _Nonnull action) { kscrash_callCompletion(onCompletion, reports, NO, nil); }]; [alertController addAction:yesAction]; [alertController addAction:noAction]; [[(MonalAppDelegate*)[[UIApplication sharedApplication] delegate] getTopViewController] presentViewController:alertController animated:YES completion:NULL]; }); DDLogVerbose(@"KSCrashReportFilterAlert finished..."); } @end @implementation KSCrashReportFilterEmpty +(instancetype) filter { return [[self alloc] init]; } -(void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion { DDLogVerbose(@"KSCrashReportFilterEmpty started..."); NSMutableArray* filteredReports = [NSMutableArray arrayWithCapacity:[reports count]]; for(NSUInteger i = 0; i < reports.count; i++) [filteredReports addObject:@""]; DDLogVerbose(@"KSCrashReportFilterEmpty finished..."); kscrash_callCompletion(onCompletion, filteredReports, YES, nil); } @end @implementation KSCrashReportFilterAddAuxInfo +(instancetype) filter { return [[self alloc] init]; } -(void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion { DDLogVerbose(@"KSCrashReportFilterAddAuxInfo started..."); NSMutableArray* filteredReports = [NSMutableArray arrayWithCapacity:[reports count]]; for(NSDictionary* report in reports) { NSMutableString* auxData = [NSMutableString new]; //add version of monal reporting this crash [auxData appendString:[NSString stringWithFormat:@"reporterVersion: %@\n", [HelperTools appBuildVersionInfoFor:MLVersionTypeLog]]]; //add user data to aux data for(NSString* userKey in report[@"user"]) [auxData appendString:[NSString stringWithFormat:@"%@: %@\n", userKey, report[@"user"][userKey]]]; //add crash_info_message and crash_info_message2 to aux data NSMutableString* crashInfos = [NSMutableString new]; for(NSDictionary* binaryImage in report[@"binary_images"]) { if(binaryImage[@"crash_info_message"] != nil) [crashInfos appendString:[NSString stringWithFormat:@"message at %@:\n%@\n\n", binaryImage[@"name"], binaryImage[@"crash_info_message"]]]; if(binaryImage[@"crash_info_message2"] != nil) [crashInfos appendString:[NSString stringWithFormat:@"message2 at %@:\n%@\n\n", binaryImage[@"name"], binaryImage[@"crash_info_message2"]]]; if(binaryImage[@"crash_info_signature"] != nil) [crashInfos appendString:[NSString stringWithFormat:@"signature at %@:\n%@\n\n", binaryImage[@"name"], binaryImage[@"crash_info_signature"]]]; if(binaryImage[@"crash_info_backtrace"] != nil) [crashInfos appendString:[NSString stringWithFormat:@"backtrace at %@:\n%@\n\n", binaryImage[@"name"], binaryImage[@"crash_info_backtrace"]]]; } if([crashInfos length] > 0) [auxData appendString:[NSString stringWithFormat:@"\nAvailable crash info messages:\n\n%@", crashInfos]]; [filteredReports addObject:auxData]; } DDLogVerbose(@"KSCrashReportFilterAddAuxInfo finished..."); kscrash_callCompletion(onCompletion, filteredReports, YES, nil); } @end @implementation KSCrashReportFilterAddMLLogfile +(instancetype) filter { return [[self alloc] init]; } -(void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion { DDLogVerbose(@"KSCrashReportFilterAddMLLogfile started..."); NSMutableArray* filteredReports = [NSMutableArray arrayWithCapacity:[reports count]]; for(NSDictionary* report in reports) { NSString* logfileCopy = report[@"user"][@"logfileCopy"]; NSData* logfileData = [NSData new]; if(logfileCopy != nil) { DDLogDebug(@"Adding logfile copy of '%@' from '%@' to crash report...", report[@"user"][@"currentLogfile"], report[@"user"][@"logfileCopy"]); logfileData = [NSData dataWithContentsOfFile:logfileCopy]; DDLogVerbose(@"NSData of logfile copy: %@", logfileData); NSError* error = nil; [[NSFileManager defaultManager] removeItemAtPath:logfileCopy error:&error]; if(error != nil) DDLogError(@"Failed to delete logfileCopy: %@", error); if(logfileData == nil) logfileData = [NSData new]; } DDLogVerbose(@"Converting logfile data to hex..."); [filteredReports addObject:[HelperTools hexadecimalString:logfileData]]; } DDLogVerbose(@"KSCrashReportFilterAddMLLogfile finished..."); kscrash_callCompletion(onCompletion, filteredReports, YES, nil); } @end @implementation KSCrashReportFilterAddProfraw +(instancetype) filter { return [[self alloc] init]; } -(void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion { DDLogVerbose(@"KSCrashReportFilterAddProfraw started..."); NSMutableArray* filteredReports = [NSMutableArray arrayWithCapacity:[reports count]]; for(NSDictionary* report in reports) { NSString* profileCopy = report[@"user"][@"profileCopy"]; NSData* profileData = [NSData new]; if(profileCopy != nil) { DDLogDebug(@"Adding profile copy of '%@' from '%@' to crash report...", report[@"user"][@"currentProfile"], report[@"user"][@"profileCopy"]); profileData = [NSData dataWithContentsOfFile:profileCopy]; DDLogVerbose(@"NSData of profile copy: %@", profileData); NSError* error = nil; [[NSFileManager defaultManager] removeItemAtPath:profileCopy error:&error]; if(error != nil) DDLogError(@"Failed to delete profileCopy: %@", error); if(profileData == nil) profileData = [NSData new]; } DDLogVerbose(@"Converting profile data to hex..."); [filteredReports addObject:[HelperTools hexadecimalString:profileData]]; } DDLogVerbose(@"KSCrashReportFilterAddProfile finished..."); kscrash_callCompletion(onCompletion, filteredReports, YES, nil); } @end