Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 154 additions & 16 deletions Boxer.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

160 changes: 160 additions & 0 deletions Boxer/Application Delegate/BXApplication.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,170 @@
*/

#import "BXApplication.h"
#import "BXBaseAppController.h"
#import "BXBaseAppController+BXHotKeys.h"
#import "BXEmulatorErrors.h"
#import <objc/runtime.h>

static NSString *BXApplicationCrashDumpSafeFilenameComponent(NSString *string)
{
NSMutableCharacterSet *allowedCharacters = [NSMutableCharacterSet alphanumericCharacterSet];
[allowedCharacters addCharactersInString: @"-_. "];

NSMutableString *result = [NSMutableString stringWithCapacity: string.length];
for (NSUInteger index = 0; index < string.length; index++)
{
unichar character = [string characterAtIndex: index];
if ([allowedCharacters characterIsMember: character])
[result appendFormat: @"%C", character];
else
[result appendString: @"-"];
}

return result;
}

static BOOL BXApplicationIsReportingEmulatorExceptionFromSession(void)
{
for (NSString *symbol in NSThread.callStackSymbols)
{
if ([symbol containsString: @"_reportEmulatorException:"])
return YES;
}

return NO;
}

static NSURL *BXApplicationWriteFallbackCrashDumpForException(NSException *exception)
{
NSFileManager *manager = [NSFileManager defaultManager];
NSURL *applicationSupportURL = [manager URLsForDirectory: NSApplicationSupportDirectory
inDomains: NSUserDomainMask].firstObject;
if (!applicationSupportURL)
return nil;

NSURL *dumpFolderURL = [[applicationSupportURL URLByAppendingPathComponent: @"Boxer"
isDirectory: YES] URLByAppendingPathComponent: @"Crash Dumps"
isDirectory: YES];

NSError *folderError = nil;
if (![manager createDirectoryAtURL: dumpFolderURL
withIntermediateDirectories: YES
attributes: nil
error: &folderError])
{
NSLog(@"Could not create Boxer fallback crash dump folder at %@: %@", dumpFolderURL.path, folderError);
return nil;
}

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [NSLocale localeWithLocaleIdentifier: @"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd HH.mm.ss";

NSString *safeName = BXApplicationCrashDumpSafeFilenameComponent(exception.name ?: @"Exception");
NSString *fileName = [NSString stringWithFormat: @"Boxer Fallback Crash %@ - %@.txt",
[dateFormatter stringFromDate: [NSDate date]],
safeName];
NSURL *dumpURL = [dumpFolderURL URLByAppendingPathComponent: fileName];

NSMutableString *dump = [NSMutableString string];
[dump appendString: @"Boxer Fallback Crash Dump\n"];
[dump appendString: @"==========================\n\n"];
[dump appendFormat: @"Created: %@\n", [NSDate date]];
[dump appendFormat: @"Boxer version: %@ (%@)\n",
[[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleShortVersionString"] ?: @"unknown",
[[NSBundle mainBundle] objectForInfoDictionaryKey: (NSString *)kCFBundleVersionKey] ?: @"unknown"];

[dump appendString: @"\nException\n"];
[dump appendString: @"---------\n"];
[dump appendFormat: @"Name: %@\n", exception.name ?: @"unknown"];
[dump appendFormat: @"Reason: %@\n", exception.reason ?: @"none"];
[dump appendFormat: @"Source file: %@\n", [exception.userInfo objectForKey: @"file"] ?: @"unknown"];
[dump appendFormat: @"Function: %@\n", [exception.userInfo objectForKey: @"function"] ?: @"unknown"];
[dump appendFormat: @"Line: %@\n", [exception.userInfo objectForKey: @"line"] ?: @"unknown"];

[dump appendString: @"\nException call stack symbols\n"];
[dump appendString: @"----------------------------\n"];
if (exception.callStackSymbols.count)
[dump appendFormat: @"%@\n", [exception.callStackSymbols componentsJoinedByString: @"\n"]];
else
[dump appendString: @"none\n"];

[dump appendString: @"\nCurrent thread call stack symbols\n"];
[dump appendString: @"---------------------------------\n"];
[dump appendFormat: @"%@\n", [NSThread.callStackSymbols componentsJoinedByString: @"\n"]];

NSError *writeError = nil;
if (![dump writeToURL: dumpURL
atomically: YES
encoding: NSUTF8StringEncoding
error: &writeError])
{
NSLog(@"Could not write Boxer fallback crash dump to %@: %@", dumpURL.path, writeError);
return nil;
}

NSLog(@"Saved Boxer fallback crash dump to %@", dumpURL.path);
return dumpURL;
}

static BOOL BXApplicationWriteLocalReportFromLegacyBugReportURL(NSURL *URL)
{
if (![[URL.host lowercaseString] isEqualToString: @"boxerapp.com"] || ![URL.path isEqualToString: @"/report-an-issue"])
return NO;

NSURLComponents *components = [NSURLComponents componentsWithURL: URL resolvingAgainstBaseURL: NO];
NSString *title = nil;
NSString *body = nil;
for (NSURLQueryItem *item in components.queryItems)
{
if ([item.name isEqualToString: @"title"])
title = item.value;
else if ([item.name isEqualToString: @"body"])
body = item.value;
}

BXBaseAppController *controller = (BXBaseAppController *)NSApp.delegate;
if ([controller respondsToSelector: @selector(reportIssueWithTitle:body:)])
{
[controller reportIssueWithTitle: title body: body];
return YES;
}

return NO;
}

@implementation NSWorkspace (BXLocalReports)

+ (void) load
{
Method originalMethod = class_getInstanceMethod(self, @selector(openURL:));
Method replacementMethod = class_getInstanceMethod(self, @selector(bx_openURL:));
method_exchangeImplementations(originalMethod, replacementMethod);
}

- (BOOL) bx_openURL: (NSURL *)URL
{
if (BXApplicationWriteLocalReportFromLegacyBugReportURL(URL))
return YES;

return [self bx_openURL: URL];
}

@end

@implementation BXApplication

- (void) reportException: (NSException *)exception
{
if ([exception.name isEqualToString: BXEmulatorUnrecoverableException] && !BXApplicationIsReportingEmulatorExceptionFromSession())
{
BXApplicationWriteFallbackCrashDumpForException(exception);
}

[super reportException: exception];
}

- (void) sendEvent: (NSEvent *)theEvent
{
//Dispatch media key events.
Expand Down
19 changes: 12 additions & 7 deletions Boxer/Application Delegate/BXBaseAppController.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,19 +162,24 @@
@end


/// Top-level methods for reporting fatal errors to Boxer's error reporting page.
/// Top-level methods for saving fatal error reports.
@interface BXBaseAppController (BXErrorReporting)

/// Opens an issue tracker page for a new issue, prefilling with optional issue data.
/// @param title If provided, the title field of the issue form will be prefilled with this string.
/// @param body If provided, the content field of the issue form will be prefilled with this string.
/// Saves an error report file, prefilled with optional issue data.
/// @param title If provided, the title of the issue.
/// @param body If provided, the content of the issue report.
- (void) reportIssueWithTitle: (NSString *)title
body: (NSString *)body;

/// Opens an issue tracker page prefilled with the details of the specified error.
/// @param error The error whose details should be prefilled into the issue form.
/// @param session The session that triggered the error. Details of the session will be included in the issue text.
/// Saves an error report file prefilled with the details of the specified error.
/// @param error The error whose details should be prefilled into the report.
/// @param session The session that triggered the error. Details of the session will be included in the report.
- (void) reportIssueForError: (NSError *)error
inSession: (BXSession *)session;

/// Saves an error report for the specified error and returns the file URL.
- (NSURL *) writeIssueForError: (NSError *)error
inSession: (BXSession *)session
revealInFinder: (BOOL)revealInFinder;

@end
129 changes: 109 additions & 20 deletions Boxer/Application Delegate/BXBaseAppController.m
Original file line number Diff line number Diff line change
Expand Up @@ -696,23 +696,99 @@ - (IBAction) decrementVolume: (id)sender

@implementation BXBaseAppController (BXErrorReporting)

- (void) reportIssueWithTitle: (NSString *)title body: (NSString *)body
{
NSString *issueURLString = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"BugReportURL"];
NSAssert(issueURLString.length, @"No issue URL found in Info.plist for key %@", @"BugReportURL");
- (NSURL *) _writeIssueWithTitle: (NSString *)title
body: (NSString *)body
revealInFinder: (BOOL)revealInFinder
{
NSFileManager *manager = [NSFileManager defaultManager];
NSURL *applicationSupportURL = [manager URLsForDirectory: NSApplicationSupportDirectory
inDomains: NSUserDomainMask].firstObject;
if (!applicationSupportURL)
{
NSBeep();
return nil;
}

NSURL *reportFolderURL = [[applicationSupportURL URLByAppendingPathComponent: @"Boxer"
isDirectory: YES] URLByAppendingPathComponent: @"Crash Dumps"
isDirectory: YES];

if (issueURLString.length)
NSError *folderError = nil;
if (![manager createDirectoryAtURL: reportFolderURL
withIntermediateDirectories: YES
attributes: nil
error: &folderError])
{
NSString *encodedTitle = (title) ? [title stringByAddingPercentEncodingWithAllowedCharacters: NSCharacterSet.URLQueryAllowedCharacterSet] : @"";
NSString *encodedBody = (body) ? [body stringByAddingPercentEncodingWithAllowedCharacters: NSCharacterSet.URLQueryAllowedCharacterSet] : @"";

NSString *completeURLString = [NSString stringWithFormat: @"%@?title=%@&body=%@", issueURLString, encodedTitle, encodedBody];
[[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: completeURLString]];
NSLog(@"Could not create Boxer report folder at %@: %@", reportFolderURL.path, folderError);
NSBeep();
return nil;
}

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [NSLocale localeWithLocaleIdentifier: @"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd HH.mm.ss";

NSString *reportTitle = title.length ? title : @"Boxer Error Report";
NSMutableCharacterSet *allowedCharacters = [NSMutableCharacterSet alphanumericCharacterSet];
[allowedCharacters addCharactersInString: @"-_. "];

NSMutableString *safeTitle = [NSMutableString stringWithCapacity: reportTitle.length];
for (NSUInteger index = 0; index < reportTitle.length; index++)
{
unichar character = [reportTitle characterAtIndex: index];
if ([allowedCharacters characterIsMember: character])
[safeTitle appendFormat: @"%C", character];
else
[safeTitle appendString: @"-"];
}

NSString *fileName = [NSString stringWithFormat: @"Boxer Error Report %@ - %@.md",
[dateFormatter stringFromDate: [NSDate date]],
safeTitle];
NSURL *reportURL = [reportFolderURL URLByAppendingPathComponent: fileName];

NSMutableString *report = [NSMutableString string];
[report appendFormat: @"# %@\n\n", reportTitle];
[report appendFormat: @"Created: %@\n\n", [NSDate date]];
if (body.length)
[report appendString: body];
else
[report appendString: @"No additional error details were available.\n"];

NSError *writeError = nil;
if (![report writeToURL: reportURL
atomically: YES
encoding: NSUTF8StringEncoding
error: &writeError])
{
NSLog(@"Could not write Boxer error report to %@: %@", reportURL.path, writeError);
NSBeep();
return nil;
}

NSLog(@"Saved Boxer error report to %@", reportURL.path);
if (revealInFinder)
{
[[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs: @[reportURL]];
}

return reportURL;
}

- (void) reportIssueWithTitle: (NSString *)title body: (NSString *)body
{
[self _writeIssueWithTitle: title body: body revealInFinder: YES];
}

- (void) reportIssueForError: (NSError *)error
inSession: (BXSession *)session
{
[self writeIssueForError: error inSession: session revealInFinder: YES];
}

- (NSURL *) writeIssueForError: (NSError *)error
inSession: (BXSession *)session
revealInFinder: (BOOL)revealInFinder
{
if ([error matchesDomain: BXEmulatorErrorDomain code: BXEmulatorUnrecoverableError])
{
Expand Down Expand Up @@ -741,21 +817,28 @@ - (void) reportIssueForError: (NSError *)error

//----
[issueBody appendString: @"## Error details ##\n\n"];
[issueBody appendFormat: @"**Error message:** %@\n", exception.reason];
[issueBody appendFormat: @"**Error message:** %@\n", exception.reason ?: error.localizedDescription ?: @"Unknown error"];

if (function)
{
[issueBody appendFormat: @"**In function:** `%@` (line %@)\n", function, lineNumber];
}

[issueBody appendString: @"**Full stack trace:**\n\n"];
for (NSDictionary<ADBCallstackKeys, id> *description in exception.callStackDescriptions)
if (exception)
{
NSString *libraryName = [description objectForKey: ADBCallstackLibraryName];
NSString *funcName = [description objectForKey: ADBCallstackHumanReadableFunctionName];
NSNumber *offset = [description objectForKey: ADBCallstackSymbolOffset];

[issueBody appendFormat: @" %@ -- %@ (%@)\n", libraryName, funcName, offset];
[issueBody appendString: @"**Full stack trace:**\n\n"];
for (NSDictionary<ADBCallstackKeys, id> *description in exception.callStackDescriptions)
{
NSString *libraryName = [description objectForKey: ADBCallstackLibraryName];
NSString *funcName = [description objectForKey: ADBCallstackHumanReadableFunctionName];
NSNumber *offset = [description objectForKey: ADBCallstackSymbolOffset];

[issueBody appendFormat: @" %@ -- %@ (%@)\n", libraryName, funcName, offset];
}
}
else
{
[issueBody appendString: @"**Full stack trace:** unavailable; the error did not include an exception object.\n"];
}

//----
Expand All @@ -782,12 +865,18 @@ - (void) reportIssueForError: (NSError *)error
}
}

[(BXBaseAppController *)[NSApp delegate] reportIssueWithTitle: issueTitle body: issueBody];
NSURL *crashDumpURL = [error.userInfo objectForKey: @"crashDumpURL"];
if (crashDumpURL)
{
[issueBody appendFormat: @"\n\n## Local crash dump ##\n\n%@\n", crashDumpURL.path];
}

return [(BXBaseAppController *)[NSApp delegate] _writeIssueWithTitle: issueTitle body: issueBody revealInFinder: revealInFinder];
}
//We don't yet have suitable formulations for other kinds of errors, so just open the issue page blank.
else
{
[(BXBaseAppController *)[NSApp delegate] reportIssueWithTitle: nil body: nil];
return [(BXBaseAppController *)[NSApp delegate] _writeIssueWithTitle: nil body: nil revealInFinder: revealInFinder];
}
}
@end
Loading
Loading