5 // Created by Uli Kusterer on Sat Feb 04 2006.
6 // Copyright (c) 2006 M. Uli Kusterer. All rights reserved.
9 // -----------------------------------------------------------------------------
11 // -----------------------------------------------------------------------------
13 #import "UKCrashReporter.h"
14 #import "UKSystemInfo.h"
15 #import <Cocoa/Cocoa.h>
17 NSString* UKCrashReporterFindTenFiveCrashReportPath( NSString* appName, NSString* crashLogsFolder );
19 @implementation UKCrashReporter
23 if( (self = [super init]) )
39 // -----------------------------------------------------------------------------
41 // This submits the crash report to a CGI form as a POST request by
42 // passing it as the request variable "crashlog".
44 // KNOWN LIMITATION: If the app crashes several times in a row, only the
45 // last crash report will be sent because this doesn't
46 // walk through the log files to try and determine the
47 // dates of all reports.
49 // This is written so it works back to OS X 10.2, or at least gracefully
50 // fails by just doing nothing on such older OSs. This also should never
51 // throw exceptions or anything on failure. This is an additional service
52 // for the developer and *mustn't* interfere with regular operation of the
54 // -----------------------------------------------------------------------------
56 - (void) checkForCrash
58 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
61 // Try whether the classes we need to talk to the CGI are present:
62 Class NSMutableURLRequestClass = NSClassFromString( @"NSMutableURLRequest" );
63 Class NSURLConnectionClass = NSClassFromString( @"NSURLConnection" );
64 if( NSMutableURLRequestClass == Nil || NSURLConnectionClass == Nil )
70 SInt32 sysvMajor = 0, sysvMinor = 0, sysvBugfix = 0;
71 UKGetSystemVersionComponents( &sysvMajor, &sysvMinor, &sysvBugfix );
72 BOOL isTenFiveOrBetter = sysvMajor >= 10 && sysvMinor >= 5;
74 // Get the log file, its last change date and last report date:
75 NSString* appName = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"CFBundleExecutable"];
76 NSString* appRevision = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"CFBundleVersion"];
77 NSString* crashLogsFolder = [@"~/Library/Logs/DiagnosticReports/" stringByExpandingTildeInPath];
78 NSString* crashLogName = [appName stringByAppendingString: @".crash.log"];
79 NSString* crashLogPath = nil;
80 if( !isTenFiveOrBetter )
81 crashLogPath = [crashLogsFolder stringByAppendingPathComponent: crashLogName];
83 crashLogPath = UKCrashReporterFindTenFiveCrashReportPath( appName, crashLogsFolder );
84 NSDictionary* fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath: crashLogPath error:nil];
85 NSDate* lastTimeCrashLogged = (fileAttrs == nil) ? nil : [fileAttrs fileModificationDate];
86 NSTimeInterval lastCrashReportInterval = [[NSUserDefaults standardUserDefaults] floatForKey: @"crashreport.date"];
87 NSDate* lastTimeCrashReported = [NSDate dateWithTimeIntervalSince1970: lastCrashReportInterval];
89 if( lastTimeCrashLogged ) // We have a crash log file and its mod date? Means we crashed sometime in the past.
91 // If we never before reported a crash or the last report lies before the last crash:
92 if( [lastTimeCrashReported compare: lastTimeCrashLogged] == NSOrderedAscending )
94 //NSLog(@"New crash log found! Running alert panel");
95 if( NSRunAlertPanel( NSLocalizedStringFromTable( @"Do you want to report the last crash?", @"Crash", @"" ),
96 NSLocalizedStringFromTable( @"The application %@ has recently crashed. To help improve it, you can send the crash log to the author.", @"Crash", @"" ),
97 NSLocalizedStringFromTable( @"Send", @"Crash", @"" ), // NSAlertDefaultReturn
98 NSLocalizedStringFromTable( @"Don't Send", @"Crash", @"" ), // NSAlertAlternateReturn
101 // Fetch the newest report from the log:
102 NSString* crashLog = [NSString stringWithContentsOfFile: crashLogPath encoding:NSASCIIStringEncoding error:nil];
103 NSArray* separateReports = [crashLog componentsSeparatedByString: @"\n\n**********\n\n"];
104 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).
105 NSData* crashReport = [currentReport dataUsingEncoding: NSUTF8StringEncoding]; // 1 since report 0 is empty (file has a delimiter at the top).
107 NSString *boundary = @"0xKhTmLbOuNdArY";
109 // Prepare a request:
110 NSString *url = [[[@"https://crash.cyberduck.io/report" stringByAppendingString:@"?revision="] stringByAppendingString:appRevision] stringByAppendingString:@"&os=mac"];
111 NSMutableURLRequest *postRequest = [NSMutableURLRequestClass requestWithURL:[NSURL URLWithString: url]];
112 NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundary];
113 NSString *agent = [NSString stringWithFormat:@"Cyberduck (%@)", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]];
115 // Add form trappings to crashReport:
116 NSData* header = [[NSString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"crashlog\"\r\n\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding];
117 NSMutableData* formData = [[header mutableCopy] autorelease];
118 [formData appendData: crashReport];
119 [formData appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];
121 // setting the headers:
122 [postRequest setHTTPMethod: @"POST"];
123 [postRequest setValue: contentType forHTTPHeaderField: @"Content-Type"];
124 [postRequest setValue: agent forHTTPHeaderField: @"User-Agent"];
125 NSString *contentLength = [NSString stringWithFormat:@"%lu", [formData length]];
126 [postRequest setValue: contentLength forHTTPHeaderField: @"Content-Length"];
127 [postRequest setHTTPBody: formData];
129 // Go into progress mode and kick off the HTTP post:
130 connection = [[NSURLConnection connectionWithRequest: postRequest delegate: self] retain];
134 [[NSUserDefaults standardUserDefaults] setFloat: [[NSDate date] timeIntervalSince1970] forKey: @"crashreport.date"];
135 [[NSUserDefaults standardUserDefaults] synchronize];
140 NSLog(@"Error during check for crash: %@",localException);
146 NSString* UKCrashReporterFindTenFiveCrashReportPath( NSString* appName, NSString* crashLogsFolder )
148 NSDirectoryEnumerator* enny = [[NSFileManager defaultManager] enumeratorAtPath: crashLogsFolder];
149 NSString* currName = nil;
150 NSString* crashLogPrefix = [NSString stringWithFormat: @"%@_",appName];
151 NSString* crashLogSuffix = @".crash";
152 NSString* foundName = nil;
153 NSDate* foundDate = nil;
155 // Find the newest of our crash log files:
156 while(( currName = [enny nextObject] ))
158 if( [currName hasPrefix: crashLogPrefix] && [currName hasSuffix: crashLogSuffix] )
160 NSDate* currDate = [[enny fileAttributes] fileModificationDate];
163 if( [currDate isGreaterThan: foundDate] )
165 foundName = currName;
166 foundDate = currDate;
171 foundName = currName;
172 foundDate = currDate;
180 return [crashLogsFolder stringByAppendingPathComponent: foundName];
183 -(void) connectionDidFinishLoading:(NSURLConnection *)conn
185 [connection release];
188 // Now that we successfully sent this crash, don't report it again:
189 [[NSUserDefaults standardUserDefaults] setFloat: [[NSDate date] timeIntervalSince1970] forKey: @"crashreport.date"];
190 [[NSUserDefaults standardUserDefaults] synchronize];
194 -(void) connection:(NSURLConnection *)conn didFailWithError:(NSError *)error
196 [connection release];