Permalink
Browse files

Add support for submitting crash reports.

Based on SFBCrashReporter from https://github.com/sbooth/SFBCrashReporter
  • Loading branch information...
martinh committed Mar 17, 2012
1 parent c993e92 commit 967aca5d7574cd37017c53b443eed2aea57f49c1
View
@@ -338,8 +338,23 @@ RESOURCES = \
par/par.doc \
Credits.txt
-ifneq ($(CONFIGURATION),DEBUG)
+ifeq ($(CONFIGURATION),BETA)
+CRASH_REPORTER = 1
+endif
+ifeq ($(CONFIGURATION),SNAPSHOT)
+CRASH_REPORTER = 1
+endif
+
+ifeq ($(CRASH_REPORTER),1)
+OBJC_SRCS += \
+ GenerateFormData.m \
+ SFBCrashReporter.m \
+ SFBCrashReporterWindowController.m \
+ SFBSystemInformation.m
+XIBS += SFBCrashReporterWindow.xib
+endif
+ifneq ($(CONFIGURATION),DEBUG)
BUNDLE_USERS = vicoapp textmate kswedberg
BUNDLE_REPOS = $(addprefix $(RESDIR)/,$(addsuffix -bundles.json,$(BUNDLE_USERS)))
.PHONY: $(BUNDLE_REPOS)
@@ -455,18 +470,21 @@ CPPFLAGS = -Iapp -Ijson -Ioniguruma -Iuniversalchardet -I$(DERIVEDDIR) -F. -Iplb
LDFLAGS += -F.
TOOL_LDLIBS = -framework ApplicationServices -framework Foundation
-APP_LDLIBS = -lcrypto -lresolv -lffi -framework Carbon -framework WebKit -framework Cocoa
+APP_LDLIBS = -lcrypto -lresolv -lffi
+APP_FRAMEWORKS = Carbon WebKit Cocoa
PAR_LDLIBS =
XORKEY_LDLIBS =
# Use Sparkle updates for all but release builds
ifneq ($(CONFIGURATION),RELEASE)
CFLAGS += -DUSE_SPARKLE
-FRAMEWORKS = \
- Sparkle.framework
-APP_LDLIBS += -framework Sparkle
-else
-FRAMEWORKS =
+APP_FRAMEWORKS += Sparkle
+endif
+
+# Crash Reporter requires AddressBook framework for getting the users email address
+ifeq ($(CRASH_REPORTER),1)
+APP_FRAMEWORKS += AddressBook
+CFLAGS += -DCRASH_REPORTER=1
endif
# paths
@@ -541,7 +559,9 @@ app: $(NIBS) $(RESOURCES) $(BUNDLE_REPOS) $(INFOPLIST) help $(APPDIR)/Contents/P
dsymutil $(BINDIR)/Vico -o $(BUILDDIR)/Vico.app.dSYM; \
fi
rsync -a --delete --exclude ".git" --exclude ".DS_Store" $(RESOURCES) $(RESDIR)
- if test -n "$(FRAMEWORKS)"; then rsync -a --delete --exclude ".git" --exclude ".DS_Store" $(FRAMEWORKS) $(FWDIR); fi
+ifneq ($(CONFIGURATION),RELEASE)
+ rsync -a --delete --exclude ".git" --exclude ".DS_Store" Sparkle.framework $(FWDIR)
+endif
cp -f app/en.lproj/Credits.rtf $(RESDIR)/en.lproj/Credits.rtf
cp -f app/en.lproj/InfoPlist.strings $(RESDIR)/en.lproj/InfoPlist.strings
# find $(RESDIR)/Bundles \( -iname "*.plist" -or -iname "*.tmCommand" -or -iname "*.tmSnippet" -or -iname "*.tmPreferences" \) -exec /usr/bin/plutil -convert binary1 "{}" \;
@@ -550,6 +570,7 @@ app: $(NIBS) $(RESOURCES) $(BUNDLE_REPOS) $(INFOPLIST) help $(APPDIR)/Contents/P
ifeq ($(CONFIGURATION),RELEASE)
/usr/libexec/PlistBuddy -c "Delete :SUPublicDSAKeyFile" $(INFOPLIST)
/usr/libexec/PlistBuddy -c "Delete :SUFeedURL" $(INFOPLIST)
+ /usr/libexec/PlistBuddy -c "Delete :SFBCrashReporterCrashSubmissionURL" $(INFOPLIST)
endif
binaries: $(OBJDIR)/Vico $(OBJDIR)/vicotool $(OBJDIR)/par
@@ -606,7 +627,7 @@ $(OBJDIR)/NSString-scopeSelector.o: app/scope_selector.h
$(OBJDIR)/Vico: $(OBJS)
mkdir -p $(OBJDIR)
- $(CXX) $(LDFLAGS) $(LDLIBS) $(APP_LDLIBS) $^ -o $@
+ $(CXX) $(LDFLAGS) $(LDLIBS) $(APP_LDLIBS) $(addprefix -framework ,$(APP_FRAMEWORKS)) $^ -o $@
$(OBJDIR)/vicotool: $(TOOL_OBJS)
mkdir -p $(OBJDIR)
View
@@ -0,0 +1,13 @@
+/*
+ * Copyright (C) 2009 Stephen F. Booth <me@sbooth.org>
+ * All Rights Reserved
+ */
+
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// ========================================
+// Generates multipart/form-data from the given dictionary using the specified boundary
+// ========================================
+NSData * GenerateFormData(NSDictionary *formValues, NSString *boundary);
View
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2009 Stephen F. Booth <me@sbooth.org>
+ * All Rights Reserved
+ */
+
+#import "GenerateFormData.h"
+
+NSData *
+GenerateFormData(NSDictionary *formValues, NSString *boundary)
+{
+ NSCParameterAssert(nil != formValues);
+ NSCParameterAssert(nil != boundary);
+
+ NSMutableData *result = [[NSMutableData alloc] init];
+
+ // Iterate over the form elements' keys and append their values
+ NSArray *keys = [formValues allKeys];
+ for(NSString *key in keys) {
+ id value = [formValues valueForKey:key];
+
+ [result appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSASCIIStringEncoding]];
+
+ // String value
+ if([value isKindOfClass:[NSString class]]) {
+ NSString *string = (NSString *)value;
+ [result appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n", key] dataUsingEncoding:NSASCIIStringEncoding]];
+ [result appendData:[@"Content-Type: text/plain; charset=utf-8\r\n" dataUsingEncoding:NSASCIIStringEncoding]];
+ [result appendData:[@"\r\n" dataUsingEncoding:NSASCIIStringEncoding]];
+ [result appendData:[string dataUsingEncoding:NSUTF8StringEncoding]];
+ }
+ // Number value
+ else if([value isKindOfClass:[NSNumber class]]) {
+ NSNumber *number = (NSNumber *)value;
+ [result appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n", key] dataUsingEncoding:NSASCIIStringEncoding]];
+ [result appendData:[@"Content-Type: text/plain; charset=utf-8\r\n" dataUsingEncoding:NSASCIIStringEncoding]];
+ [result appendData:[@"\r\n" dataUsingEncoding:NSASCIIStringEncoding]];
+ [result appendData:[[number stringValue] dataUsingEncoding:NSUTF8StringEncoding]];
+ }
+ // URL value (only file URLs are supported)
+ else if([value isKindOfClass:[NSURL class]] && [(NSURL *)value isFileURL]) {
+ NSURL *url = (NSURL *)value;
+ [result appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, [[url path] lastPathComponent]] dataUsingEncoding:NSASCIIStringEncoding]];
+ [result appendData:[@"Content-Type: application/octet-stream\r\n" dataUsingEncoding:NSASCIIStringEncoding]];
+ [result appendData:[@"\r\n" dataUsingEncoding:NSASCIIStringEncoding]];
+ [result appendData:[NSData dataWithContentsOfURL:url]];
+ }
+ // Illegal class
+ else
+ NSLog(@"SFBCrashReporterError: formValues contained illegal object %@ of class %@", value, [value class]);
+
+ [result appendData:[@"\r\n" dataUsingEncoding:NSASCIIStringEncoding]];
+ }
+
+ [result appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSASCIIStringEncoding]];
+
+ return [result autorelease];
+}
View
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2009 Stephen F. Booth <me@sbooth.org>
+ * All Rights Reserved
+ */
+
+#import <Cocoa/Cocoa.h>
+
+// ========================================
+// The main interface
+// ========================================
+@interface SFBCrashReporter : NSObject
+{
+}
+
+// Ensure that SFBCrashReporterCrashSubmissionURL is set to a string in either your application's Info.plist
+// or NSUserDefaults and call this
++ (void) checkForNewCrashes;
+
+@end
View
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2009, 2010 Stephen F. Booth <me@sbooth.org>
+ * All Rights Reserved
+ */
+
+#import "SFBCrashReporter.h"
+#import "SFBCrashReporterWindowController.h"
+
+@interface SFBCrashReporter (Private)
++ (NSArray *) crashLogPaths;
+@end
+
+@implementation SFBCrashReporter
+
++ (void) checkForNewCrashes
+{
+ // If no URL is found for the submission, we can't do anything
+ NSString *crashSubmissionURLString = [[NSUserDefaults standardUserDefaults] stringForKey:@"SFBCrashReporterCrashSubmissionURL"];
+ if(!crashSubmissionURLString) {
+ crashSubmissionURLString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"SFBCrashReporterCrashSubmissionURL"];
+ if(!crashSubmissionURLString)
+ [NSException raise:@"Missing SFBCrashReporterCrashSubmissionURL" format:@"You must specify the URL for crash log submission as the SFBCrashReporterCrashSubmissionURL in either Info.plist or the user defaults!"];
+ }
+
+ // Determine when the last crash was reported
+ NSDate *lastCrashReportDate = [[NSUserDefaults standardUserDefaults] objectForKey:@"SFBCrashReporterLastCrashReportDate"];
+
+ // If a crash was never reported, use now as the starting point
+ if(!lastCrashReportDate) {
+ lastCrashReportDate = [NSDate date];
+ [[NSUserDefaults standardUserDefaults] setObject:lastCrashReportDate forKey:@"SFBCrashReporterLastCrashReportDate"];
+ }
+
+ // Determine if it is even necessary to show the window (by comparing file modification dates to the last time a crash was reported)
+ NSArray *crashLogPaths = [self crashLogPaths];
+ for(NSString *path in crashLogPaths) {
+ NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[path stringByResolvingSymlinksInPath] error:nil];
+ NSDate *fileModificationDate = [fileAttributes fileModificationDate];
+
+ // If the last time a crash was reported is earlier than the file's modification date, allow the user to report the crash
+ if(NSOrderedAscending == [lastCrashReportDate compare:fileModificationDate]) {
+ [SFBCrashReporterWindowController showWindowForCrashLogPath:path submissionURL:[NSURL URLWithString:crashSubmissionURLString]];
+
+ // Don't prompt more than once
+ break;
+ }
+ }
+}
+
+@end
+
+@implementation SFBCrashReporter (Private)
+
++ (NSArray *) crashLogDirectories
+{
+ // Determine which directories contain crash logs based on the OS version
+ // See http://developer.apple.com/technotes/tn2004/tn2123.html
+
+ // Determine the OS version
+ SInt32 versionMajor = 0;
+ OSErr err = Gestalt(gestaltSystemVersionMajor, &versionMajor);
+ if(noErr != err)
+ NSLog(@"SFBCrashReporter: Unable to determine major system version (%i)", err);
+
+ SInt32 versionMinor = 0;
+ err = Gestalt(gestaltSystemVersionMinor, &versionMinor);
+ if(noErr != err)
+ NSLog(@"SFBCrashReporter: Unable to determine minor system version (%i)", err);
+
+ NSArray *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask | NSLocalDomainMask, YES);
+ NSString *crashLogDirectory = nil;
+
+ // Snow Leopard (10.6) or later
+ // Snow Leopard crash logs are located in ~/Library/Logs/DiagnosticReports with aliases placed in the Leopard location
+ if(10 == versionMajor && 6 <= versionMinor)
+ crashLogDirectory = @"Logs/DiagnosticReports";
+ // Leopard (10.5) or earlier
+ // Leopard crash logs have the form APPNAME_YYYY-MM-DD-hhmm_MACHINE.crash and are located in ~/Library/Logs/CrashReporter
+ else if(10 == versionMajor && 5 >= versionMinor)
+ crashLogDirectory = @"Logs/CrashReporter";
+
+ NSMutableArray *crashFolderPaths = [[NSMutableArray alloc] init];
+
+ for(NSString *libraryPath in libraryPaths) {
+ NSString *path = [libraryPath stringByAppendingPathComponent:crashLogDirectory];
+
+ BOOL isDir = NO;
+ if([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir] && isDir) {
+ [crashFolderPaths addObject:path];
+ break;
+ }
+ }
+
+ return [crashFolderPaths autorelease];
+}
+
++ (NSArray *) crashLogPaths
+{
+ NSString *applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
+ NSArray *crashLogDirectories = [self crashLogDirectories];
+
+ NSMutableArray *paths = [[NSMutableArray alloc] init];
+
+ for(NSString *crashLogDirectory in crashLogDirectories) {
+ NSString *file = nil;
+ NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:crashLogDirectory];
+ while((file = [dirEnum nextObject]))
+ if([file hasPrefix:applicationName])
+ [paths addObject:[crashLogDirectory stringByAppendingPathComponent:file]];
+ }
+
+ return [paths autorelease];
+}
+
+@end
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2009 Stephen F. Booth <me@sbooth.org>
+ * All Rights Reserved
+ */
+
+#import <Cocoa/Cocoa.h>
+
+// ========================================
+// The main class for SFBCrashReporter
+// ========================================
+@interface SFBCrashReporterWindowController : NSWindowController
+{
+ IBOutlet NSTextView *_commentsTextView;
+ IBOutlet NSButton *_reportButton;
+ IBOutlet NSButton *_ignoreButton;
+ IBOutlet NSButton *_discardButton;
+ IBOutlet NSProgressIndicator *_progressIndicator;
+
+@private
+ NSString *_emailAddress;
+ NSString *_crashLogPath;
+ NSURL *_submissionURL;
+
+ NSURLConnection *_urlConnection;
+ NSMutableData *_responseData;
+}
+
+// ========================================
+// Properties
+@property (copy) NSString * emailAddress;
+@property (copy) NSString * crashLogPath;
+@property (copy) NSURL * submissionURL;
+
+// ========================================
+// Always use this to show the window- do not alloc/init directly
++ (void) showWindowForCrashLogPath:(NSString *)path submissionURL:(NSURL *)submissionURL;
+
+// ========================================
+// Action methods
+- (IBAction) sendReport:(id)sender;
+- (IBAction) ignoreReport:(id)sender;
+- (IBAction) discardReport:(id)sender;
+
+@end
Oops, something went wrong.

0 comments on commit 967aca5

Please sign in to comment.