another.im-ios/Monal/Classes/MLCrashReporter.m
2024-11-18 15:53:52 +01:00

387 lines
18 KiB
Objective-C

//
// MLCrashReporter.m
// Monal
//
// Created by admin on 21.06.23.
// Copyright © 2023 monal-im.org. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <KSCrash/KSCrash.h>
#import <KSCrash/KSCrashReportFilterBasic.h>
#import <KSCrash/KSCrashReportFilterJSON.h>
#import <KSCrash/KSCrashReportFilterAppleFmt.h>
#import <KSCrash/KSCrashReportFilterGZip.h>
#import <KSCrash/KSCrashReportFields.h>
#import <KSCrash/NSError+SimpleConstructor.h>
#import <MessageUI/MessageUI.h>
#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 <KSCrashReportFilter>
+(instancetype) filter;
@end
@interface KSCrashReportFilterEmpty: NSObject <KSCrashReportFilter>
+(instancetype) filter;
@end
@interface KSCrashReportFilterAddAuxInfo : NSObject <KSCrashReportFilter>
+(instancetype) filter;
@end
@interface KSCrashReportFilterAddMLLogfile : NSObject <KSCrashReportFilter>
+(instancetype) filter;
@end
@interface KSCrashReportFilterAddProfraw : NSObject <KSCrashReportFilter>
+(instancetype) filter;
@end
@interface MLCrashReporter() <KSCrashReportFilter, MFMailComposeViewControllerDelegate>
@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<KSCrashReportFilter> dummyFilter = [KSCrashReportFilterEmpty filter];
NSString* dummyFilterName = @"dummy not printed";
id<KSCrashReportFilter> auxInfoFilter = [KSCrashReportFilterAddAuxInfo filter];
NSString* auxInfoName = @"AUX Info (*.txt)";
id<KSCrashReportFilter> appleFilter = [KSCrashReportFilterAppleFmt filterWithReportStyle:KSAppleReportStyleSymbolicatedSideBySide];
NSString* appleName = @"Apple Report (*.crash)";
NSArray<id<KSCrashReportFilter>>* jsonFilter = @[[KSCrashReportFilterJSONEncode filterWithOptions:KSJSONEncodeOptionPretty], [KSCrashReportFilterDataToString filter]];
NSString* jsonName = @"JSON Report (*.json)";
id<KSCrashReportFilter> logfileFilter = [KSCrashReportFilterAddMLLogfile filter];
NSString* logfileName = @"Logfile (*.rawlog.gz)";
id<KSCrashReportFilter> 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