// // UKCrashReporter.m // NiftyFeatures // // Created by Uli Kusterer on Sat Feb 04 2006. // Copyright (c) 2006 M. Uli Kusterer. All rights reserved. // // ----------------------------------------------------------------------------- // Headers: // ----------------------------------------------------------------------------- #import "UKCrashReporter.h" #import "UKSystemInfo.h" #import <AddressBook/AddressBook.h> NSString* UKCrashReporterFindTenFiveCrashReportPath( NSString* appName, NSString* crashLogsFolder ); // ----------------------------------------------------------------------------- // UKCrashReporterCheckForCrash: // This submits the crash report to a CGI form as a POST request by // passing it as the request variable "crashlog". // // KNOWN LIMITATION: If the app crashes several times in a row, only the // last crash report will be sent because this doesn't // walk through the log files to try and determine the // dates of all reports. // // This is written so it works back to OS X 10.2, or at least gracefully // fails by just doing nothing on such older OSs. This also should never // throw exceptions or anything on failure. This is an additional service // for the developer and *mustn't* interfere with regular operation of the // application. // ----------------------------------------------------------------------------- void UKCrashReporterCheckForCrash() { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; NS_DURING // Try whether the classes we need to talk to the CGI are present: Class NSMutableURLRequestClass = NSClassFromString( @"NSMutableURLRequest" ); Class NSURLConnectionClass = NSClassFromString( @"NSURLConnection" ); if( NSMutableURLRequestClass == Nil || NSURLConnectionClass == Nil ) { [pool release]; NS_VOIDRETURN; } long sysvMajor = 0, sysvMinor = 0, sysvBugfix = 0; UKGetSystemVersionComponents( &sysvMajor, &sysvMinor, &sysvBugfix ); BOOL isTenFiveOrBetter = sysvMajor >= 10 && sysvMinor >= 5; // Get the log file, its last change date and last report date: NSString* appName = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"CFBundleExecutable"]; NSString* crashLogsFolder = [@"~/Library/Logs/CrashReporter/" stringByExpandingTildeInPath]; NSString* crashLogName = [appName stringByAppendingString: @".crash.log"]; NSString* crashLogPath = nil; if( !isTenFiveOrBetter ) crashLogPath = [crashLogsFolder stringByAppendingPathComponent: crashLogName]; else crashLogPath = UKCrashReporterFindTenFiveCrashReportPath( appName, crashLogsFolder ); NSDictionary* fileAttrs = [[NSFileManager defaultManager] fileAttributesAtPath: crashLogPath traverseLink: YES]; NSDate* lastTimeCrashLogged = (fileAttrs == nil) ? nil : [fileAttrs fileModificationDate]; NSTimeInterval lastCrashReportInterval = [[NSUserDefaults standardUserDefaults] floatForKey: @"UKCrashReporterLastCrashReportDate"]; NSDate* lastTimeCrashReported = [NSDate dateWithTimeIntervalSince1970: lastCrashReportInterval]; if( lastTimeCrashLogged ) // We have a crash log file and its mod date? Means we crashed sometime in the past. { // If we never before reported a crash or the last report lies before the last crash: if( [lastTimeCrashReported compare: lastTimeCrashLogged] == NSOrderedAscending ) { // Fetch the newest report from the log: NSString* crashLog = [NSString stringWithContentsOfFile: crashLogPath]; NSArray* separateReports = [crashLog componentsSeparatedByString: @"\n\n**********\n\n"]; NSString* currentReport = [separateReports count] > 0 ? [separateReports objectAtIndex: [separateReports count] -1] : @"*** Couldn't read Report ***"; // 1 since report 0 is empty (file has a delimiter at the top). unsigned numCores = UKCountCores(); NSString* numCPUsString = (numCores == 1) ? @"" : [NSString stringWithFormat: @"%dx ",numCores]; // Create a string containing Mac and CPU info, crash log and prefs: currentReport = [NSString stringWithFormat: @"Model: %@\nCPU Speed: %@%.2f GHz\n%@\n\nPreferences:\n%@", UKMachineName(), numCPUsString, ((float)UKClockSpeed()) / 1000.0f, currentReport, [[NSUserDefaults standardUserDefaults] persistentDomainForName: [[NSBundle mainBundle] bundleIdentifier]]]; // Now show a crash reporter window so the user can edit the info to send: [[UKCrashReporter alloc] initWithLogString: currentReport]; } } NS_HANDLER NSLog(@"Error during check for crash: %@",localException); NS_ENDHANDLER [pool release]; } NSString* UKCrashReporterFindTenFiveCrashReportPath( NSString* appName, NSString* crashLogsFolder ) { NSDirectoryEnumerator* enny = [[NSFileManager defaultManager] enumeratorAtPath: crashLogsFolder]; NSString* currName = nil; NSString* crashLogPrefix = [NSString stringWithFormat: @"%@_",appName]; NSString* crashLogSuffix = @".crash"; NSString* foundName = nil; NSDate* foundDate = nil; // Find the newest of our crash log files: while(( currName = [enny nextObject] )) { if( [currName hasPrefix: crashLogPrefix] && [currName hasSuffix: crashLogSuffix] ) { NSDate* currDate = [[enny fileAttributes] fileModificationDate]; if( foundName ) { if( [currDate isGreaterThan: foundDate] ) { foundName = currName; foundDate = currDate; } } else { foundName = currName; foundDate = currDate; } } } if( !foundName ) return nil; else return [crashLogsFolder stringByAppendingPathComponent: foundName]; } NSString* gCrashLogString = nil; @implementation UKCrashReporter -(id) initWithLogString: (NSString*)theLog { // In super init the awakeFromNib method gets called, so we can not // use ivars to transfer the log, and use a global instead: gCrashLogString = [theLog retain]; self = [super init]; return self; } -(id) init { self = [super init]; if( self ) { feedbackMode = YES; } return self; } -(void) dealloc { [connection release]; connection = nil; [super dealloc]; } -(void) awakeFromNib { // Insert the app name into the explanation message: NSString* appName = [[NSFileManager defaultManager] displayNameAtPath: [[NSBundle mainBundle] bundlePath]]; NSMutableString* expl = nil; if( gCrashLogString ) expl = [[[explanationField stringValue] mutableCopy] autorelease]; else expl = [[NSLocalizedStringFromTable(@"FEEDBACK_EXPLANATION_TEXT",@"UKCrashReporter",@"") mutableCopy] autorelease]; [expl replaceOccurrencesOfString: @"%%APPNAME" withString: appName options: 0 range: NSMakeRange(0, [expl length])]; [explanationField setStringValue: expl]; // Insert user name and e-mail address into the information field: NSMutableString* userMessage = nil; if( gCrashLogString ) userMessage = [[[informationField string] mutableCopy] autorelease]; else userMessage = [[NSLocalizedStringFromTable(@"FEEDBACK_MESSAGE_TEXT",@"UKCrashReporter",@"") mutableCopy] autorelease]; [userMessage replaceOccurrencesOfString: @"%%LONGUSERNAME" withString: NSFullUserName() options: 0 range: NSMakeRange(0, [userMessage length])]; ABMultiValue* emailAddresses = [[[ABAddressBook sharedAddressBook] me] valueForProperty: kABEmailProperty]; NSString* emailAddr = NSLocalizedStringFromTable(@"MISSING_EMAIL_ADDRESS",@"UKCrashReporter",@""); if( emailAddresses ) { NSString* defaultKey = [emailAddresses primaryIdentifier]; if( defaultKey ) { unsigned int defaultIndex = [emailAddresses indexForIdentifier: defaultKey]; if( defaultIndex >= 0 ) emailAddr = [emailAddresses valueAtIndex: defaultIndex]; } } [userMessage replaceOccurrencesOfString: @"%%EMAILADDRESS" withString: emailAddr options: 0 range: NSMakeRange(0, [userMessage length])]; [informationField setString: userMessage]; // Show the crash log to the user: if( gCrashLogString ) { [crashLogField setString: gCrashLogString]; [gCrashLogString release]; gCrashLogString = nil; } else { [remindButton setHidden: YES]; int itemIndex = [switchTabView indexOfTabViewItemWithIdentifier: @"de.zathras.ukcrashreporter.crashlog-tab"]; NSTabViewItem* crashLogItem = [switchTabView tabViewItemAtIndex: itemIndex]; unsigned numCores = UKCountCores(); NSString* numCPUsString = (numCores == 1) ? @"" : [NSString stringWithFormat: @"%dx ",numCores]; [crashLogItem setLabel: NSLocalizedStringFromTable(@"SYSTEM_INFO_TAB_NAME",@"UKCrashReporter",@"")]; NSString* systemInfo = [NSString stringWithFormat: @"Application: %@ %@\nModel: %@\nCPU Speed: %@%.2f GHz\nSystem Version: %@\n\nPreferences:\n%@", appName, [[[NSBundle mainBundle] infoDictionary] objectForKey: @"CFBundleVersion"], UKMachineName(), numCPUsString, ((float)UKClockSpeed()) / 1000.0f, UKSystemVersionString(), [[NSUserDefaults standardUserDefaults] persistentDomainForName: [[NSBundle mainBundle] bundleIdentifier]]]; [crashLogField setString: systemInfo]; } // Show the window: [reportWindow makeKeyAndOrderFront: self]; } -(IBAction) sendCrashReport: (id)sender { NSString *boundary = @"0xKhTmLbOuNdArY"; NSMutableString* crashReportString = [NSMutableString string]; [crashReportString appendString: [informationField string]]; [crashReportString appendString: @"\n==========\n"]; [crashReportString appendString: [crashLogField string]]; [crashReportString replaceOccurrencesOfString: boundary withString: @"USED_TO_BE_KHTMLBOUNDARY" options: 0 range: NSMakeRange(0, [crashReportString length])]; NSData* crashReport = [crashReportString dataUsingEncoding: NSUTF8StringEncoding]; // Prepare a request: NSMutableURLRequest *postRequest = [NSMutableURLRequest requestWithURL: [NSURL URLWithString: NSLocalizedStringFromTable( @"CRASH_REPORT_CGI_URL", @"UKCrashReporter", @"" )]]; NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundary]; NSString *agent = @"UKCrashReporter"; // Add form trappings to crashReport: NSData* header = [[NSString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"crashlog\"\r\n\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]; NSMutableData* formData = [[header mutableCopy] autorelease]; [formData appendData: crashReport]; [formData appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]]; // setting the headers: [postRequest setHTTPMethod: @"POST"]; [postRequest setValue: contentType forHTTPHeaderField: @"Content-Type"]; [postRequest setValue: agent forHTTPHeaderField: @"User-Agent"]; NSString *contentLength = [NSString stringWithFormat:@"%lu", [formData length]]; [postRequest setValue: contentLength forHTTPHeaderField: @"Content-Length"]; [postRequest setHTTPBody: formData]; // Go into progress mode and kick off the HTTP post: [progressIndicator startAnimation: self]; [sendButton setEnabled: NO]; [remindButton setEnabled: NO]; [discardButton setEnabled: NO]; connection = [[NSURLConnection connectionWithRequest: postRequest delegate: self] retain]; } -(IBAction) remindMeLater: (id)sender { [reportWindow orderOut: self]; } -(IBAction) discardCrashReport: (id)sender { // Remember we already did this crash, so we don't ask twice: if( !feedbackMode ) { [[NSUserDefaults standardUserDefaults] setFloat: [[NSDate date] timeIntervalSince1970] forKey: @"UKCrashReporterLastCrashReportDate"]; [[NSUserDefaults standardUserDefaults] synchronize]; } [reportWindow orderOut: self]; } -(void) showFinishedMessage: (NSError*)errMsg { if( errMsg ) { NSString* errTitle = nil; if( feedbackMode ) errTitle = NSLocalizedStringFromTable( @"COULDNT_SEND_FEEDBACK_ERROR",@"UKCrashReporter",@""); else errTitle = NSLocalizedStringFromTable( @"COULDNT_SEND_CRASH_REPORT_ERROR",@"UKCrashReporter",@""); NSRunAlertPanel( errTitle, @"%@", NSLocalizedStringFromTable( @"COULDNT_SEND_CRASH_REPORT_ERROR_OK",@"UKCrashReporter",@""), @"", @"", [errMsg localizedDescription] ); } [reportWindow orderOut: self]; [self autorelease]; } -(void) connectionDidFinishLoading:(NSURLConnection *)conn { [connection release]; connection = nil; // Now that we successfully sent this crash, don't report it again: if( !feedbackMode ) { [[NSUserDefaults standardUserDefaults] setFloat: [[NSDate date] timeIntervalSince1970] forKey: @"UKCrashReporterLastCrashReportDate"]; [[NSUserDefaults standardUserDefaults] synchronize]; } [self performSelectorOnMainThread: @selector(showFinishedMessage:) withObject: nil waitUntilDone: NO]; } -(void) connection:(NSURLConnection *)conn didFailWithError:(NSError *)error { [connection release]; connection = nil; [self performSelectorOnMainThread: @selector(showFinishedMessage:) withObject: error waitUntilDone: NO]; } @end @implementation UKFeedbackProvider -(IBAction) orderFrontFeedbackWindow: (id)sender { [[UKCrashReporter alloc] init]; } -(IBAction) orderFrontBugReportWindow: (id)sender { [[UKCrashReporter alloc] init]; } @end |