Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 376 lines (312 sloc) 14.06 kb
dc5be1c Initial check-in.
uli authored
1 //
2 // UKCrashReporter.m
3 // NiftyFeatures
4 //
5 // Created by Uli Kusterer on Sat Feb 04 2006.
720b0e7 @uliwitness Added licensing information to the source files.
authored
6 // Copyright (c) 2006 Uli Kusterer.
7 //
8 // This software is provided 'as-is', without any express or implied
9 // warranty. In no event will the authors be held liable for any damages
10 // arising from the use of this software.
11 //
12 // Permission is granted to anyone to use this software for any purpose,
13 // including commercial applications, and to alter it and redistribute it
14 // freely, subject to the following restrictions:
15 //
16 // 1. The origin of this software must not be misrepresented; you must not
17 // claim that you wrote the original software. If you use this software
18 // in a product, an acknowledgment in the product documentation would be
19 // appreciated but is not required.
20 //
21 // 2. Altered source versions must be plainly marked as such, and must not be
22 // misrepresented as being the original software.
23 //
24 // 3. This notice may not be removed or altered from any source
25 // distribution.
dc5be1c Initial check-in.
uli authored
26 //
27
28 // -----------------------------------------------------------------------------
29 // Headers:
30 // -----------------------------------------------------------------------------
31
32 #import "UKCrashReporter.h"
33 #import "UKSystemInfo.h"
34 #import <AddressBook/AddressBook.h>
35
36
37 NSString* UKCrashReporterFindTenFiveCrashReportPath( NSString* appName, NSString* crashLogsFolder );
38
39 // -----------------------------------------------------------------------------
40 // UKCrashReporterCheckForCrash:
41 // This submits the crash report to a CGI form as a POST request by
42 // passing it as the request variable "crashlog".
43 //
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.
48 //
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
53 // application.
54 // -----------------------------------------------------------------------------
55
56 void UKCrashReporterCheckForCrash()
57 {
58 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
59
60 NS_DURING
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 )
65 {
66 [pool release];
67 NS_VOIDRETURN;
68 }
69
70 long sysvMajor = 0, sysvMinor = 0, sysvBugfix = 0;
71 UKGetSystemVersionComponents( &sysvMajor, &sysvMinor, &sysvBugfix );
72 BOOL isTenFiveOrBetter = sysvMajor >= 10 && sysvMinor >= 5;
73
74 // Get the log file, its last change date and last report date:
75 NSString* appName = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"CFBundleExecutable"];
76 NSString* crashLogsFolder = [@"~/Library/Logs/CrashReporter/" stringByExpandingTildeInPath];
77 NSString* crashLogName = [appName stringByAppendingString: @".crash.log"];
78 NSString* crashLogPath = nil;
79 if( !isTenFiveOrBetter )
80 crashLogPath = [crashLogsFolder stringByAppendingPathComponent: crashLogName];
81 else
82 crashLogPath = UKCrashReporterFindTenFiveCrashReportPath( appName, crashLogsFolder );
83 NSDictionary* fileAttrs = [[NSFileManager defaultManager] fileAttributesAtPath: crashLogPath traverseLink: YES];
84 NSDate* lastTimeCrashLogged = (fileAttrs == nil) ? nil : [fileAttrs fileModificationDate];
85 NSTimeInterval lastCrashReportInterval = [[NSUserDefaults standardUserDefaults] floatForKey: @"UKCrashReporterLastCrashReportDate"];
86 NSDate* lastTimeCrashReported = [NSDate dateWithTimeIntervalSince1970: lastCrashReportInterval];
87
88 if( lastTimeCrashLogged ) // We have a crash log file and its mod date? Means we crashed sometime in the past.
89 {
90 // If we never before reported a crash or the last report lies before the last crash:
91 if( [lastTimeCrashReported compare: lastTimeCrashLogged] == NSOrderedAscending )
92 {
93 // Fetch the newest report from the log:
20f6e31 @uliwitness Shut up two deprecation warnings (may raise system requirements).
authored
94 NSString* crashLog = [NSString stringWithContentsOfFile: crashLogPath encoding: NSUTF8StringEncoding error: nil]; // +++ Check error.
dc5be1c Initial check-in.
uli authored
95 NSArray* separateReports = [crashLog componentsSeparatedByString: @"\n\n**********\n\n"];
96 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).
97 unsigned numCores = UKCountCores();
98 NSString* numCPUsString = (numCores == 1) ? @"" : [NSString stringWithFormat: @"%dx ",numCores];
99
100 // Create a string containing Mac and CPU info, crash log and prefs:
101 currentReport = [NSString stringWithFormat:
102 @"Model: %@\nCPU Speed: %@%.2f GHz\n%@\n\nPreferences:\n%@",
103 UKMachineName(), numCPUsString, ((float)UKClockSpeed()) / 1000.0f,
104 currentReport,
105 [[NSUserDefaults standardUserDefaults] persistentDomainForName: [[NSBundle mainBundle] bundleIdentifier]]];
106
107 // Now show a crash reporter window so the user can edit the info to send:
108 [[UKCrashReporter alloc] initWithLogString: currentReport];
109 }
110 }
111 NS_HANDLER
112 NSLog(@"Error during check for crash: %@",localException);
113 NS_ENDHANDLER
114
115 [pool release];
116 }
117
118 NSString* UKCrashReporterFindTenFiveCrashReportPath( NSString* appName, NSString* crashLogsFolder )
119 {
120 NSDirectoryEnumerator* enny = [[NSFileManager defaultManager] enumeratorAtPath: crashLogsFolder];
121 NSString* currName = nil;
122 NSString* crashLogPrefix = [NSString stringWithFormat: @"%@_",appName];
123 NSString* crashLogSuffix = @".crash";
124 NSString* foundName = nil;
125 NSDate* foundDate = nil;
126
127 // Find the newest of our crash log files:
128 while(( currName = [enny nextObject] ))
129 {
130 if( [currName hasPrefix: crashLogPrefix] && [currName hasSuffix: crashLogSuffix] )
131 {
132 NSDate* currDate = [[enny fileAttributes] fileModificationDate];
133 if( foundName )
134 {
135 if( [currDate isGreaterThan: foundDate] )
136 {
137 foundName = currName;
138 foundDate = currDate;
139 }
140 }
141 else
142 {
143 foundName = currName;
144 foundDate = currDate;
145 }
146 }
147 }
148
149 if( !foundName )
150 return nil;
151 else
152 return [crashLogsFolder stringByAppendingPathComponent: foundName];
153 }
154
155
156 NSString* gCrashLogString = nil;
157
158
159 @implementation UKCrashReporter
160
161 -(id) initWithLogString: (NSString*)theLog
162 {
163 // In super init the awakeFromNib method gets called, so we can not
164 // use ivars to transfer the log, and use a global instead:
165 gCrashLogString = [theLog retain];
166
167 self = [super init];
168 return self;
169 }
170
171
172 -(id) init
173 {
174 self = [super init];
175 if( self )
176 {
177 feedbackMode = YES;
178 }
179 return self;
180 }
181
182
183 -(void) dealloc
184 {
185 [connection release];
186 connection = nil;
187
188 [super dealloc];
189 }
190
191
192 -(void) awakeFromNib
193 {
194 // Insert the app name into the explanation message:
195 NSString* appName = [[NSFileManager defaultManager] displayNameAtPath: [[NSBundle mainBundle] bundlePath]];
196 NSMutableString* expl = nil;
197 if( gCrashLogString )
198 expl = [[[explanationField stringValue] mutableCopy] autorelease];
199 else
200 expl = [[NSLocalizedStringFromTable(@"FEEDBACK_EXPLANATION_TEXT",@"UKCrashReporter",@"") mutableCopy] autorelease];
201 [expl replaceOccurrencesOfString: @"%%APPNAME" withString: appName
202 options: 0 range: NSMakeRange(0, [expl length])];
203 [explanationField setStringValue: expl];
204
205 // Insert user name and e-mail address into the information field:
206 NSMutableString* userMessage = nil;
207 if( gCrashLogString )
208 userMessage = [[[informationField string] mutableCopy] autorelease];
209 else
210 userMessage = [[NSLocalizedStringFromTable(@"FEEDBACK_MESSAGE_TEXT",@"UKCrashReporter",@"") mutableCopy] autorelease];
211 [userMessage replaceOccurrencesOfString: @"%%LONGUSERNAME" withString: NSFullUserName()
212 options: 0 range: NSMakeRange(0, [userMessage length])];
213 ABMultiValue* emailAddresses = [[[ABAddressBook sharedAddressBook] me] valueForProperty: kABEmailProperty];
214 NSString* emailAddr = NSLocalizedStringFromTable(@"MISSING_EMAIL_ADDRESS",@"UKCrashReporter",@"");
215 if( emailAddresses )
216 {
217 NSString* defaultKey = [emailAddresses primaryIdentifier];
218 if( defaultKey )
219 {
220 unsigned int defaultIndex = [emailAddresses indexForIdentifier: defaultKey];
dde81ed @uliwitness Shut up some warnings.
authored
221 if( defaultIndex != NSNotFound )
dc5be1c Initial check-in.
uli authored
222 emailAddr = [emailAddresses valueAtIndex: defaultIndex];
223 }
224 }
225 [userMessage replaceOccurrencesOfString: @"%%EMAILADDRESS" withString: emailAddr
226 options: 0 range: NSMakeRange(0, [userMessage length])];
227 [informationField setString: userMessage];
228
229 // Show the crash log to the user:
230 if( gCrashLogString )
231 {
232 [crashLogField setString: gCrashLogString];
233 [gCrashLogString release];
234 gCrashLogString = nil;
235 }
236 else
237 {
238 [remindButton setHidden: YES];
239
240 int itemIndex = [switchTabView indexOfTabViewItemWithIdentifier: @"de.zathras.ukcrashreporter.crashlog-tab"];
241 NSTabViewItem* crashLogItem = [switchTabView tabViewItemAtIndex: itemIndex];
242 unsigned numCores = UKCountCores();
243 NSString* numCPUsString = (numCores == 1) ? @"" : [NSString stringWithFormat: @"%dx ",numCores];
244 [crashLogItem setLabel: NSLocalizedStringFromTable(@"SYSTEM_INFO_TAB_NAME",@"UKCrashReporter",@"")];
245
246 NSString* systemInfo = [NSString stringWithFormat: @"Application: %@ %@\nModel: %@\nCPU Speed: %@%.2f GHz\nSystem Version: %@\n\nPreferences:\n%@",
247 appName, [[[NSBundle mainBundle] infoDictionary] objectForKey: @"CFBundleVersion"],
248 UKMachineName(), numCPUsString, ((float)UKClockSpeed()) / 1000.0f,
249 UKSystemVersionString(),
250 [[NSUserDefaults standardUserDefaults] persistentDomainForName: [[NSBundle mainBundle] bundleIdentifier]]];
251 [crashLogField setString: systemInfo];
252 }
253
254 // Show the window:
255 [reportWindow makeKeyAndOrderFront: self];
256 }
257
258
259 -(IBAction) sendCrashReport: (id)sender
260 {
261 NSString *boundary = @"0xKhTmLbOuNdArY";
262 NSMutableString* crashReportString = [NSMutableString string];
263 [crashReportString appendString: [informationField string]];
264 [crashReportString appendString: @"\n==========\n"];
265 [crashReportString appendString: [crashLogField string]];
266 [crashReportString replaceOccurrencesOfString: boundary withString: @"USED_TO_BE_KHTMLBOUNDARY" options: 0 range: NSMakeRange(0, [crashReportString length])];
267 NSData* crashReport = [crashReportString dataUsingEncoding: NSUTF8StringEncoding];
268
269 // Prepare a request:
270 NSMutableURLRequest *postRequest = [NSMutableURLRequest requestWithURL: [NSURL URLWithString: NSLocalizedStringFromTable( @"CRASH_REPORT_CGI_URL", @"UKCrashReporter", @"" )]];
271 NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundary];
272 NSString *agent = @"UKCrashReporter";
273
274 // Add form trappings to crashReport:
275 NSData* header = [[NSString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"crashlog\"\r\n\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding];
276 NSMutableData* formData = [[header mutableCopy] autorelease];
277 [formData appendData: crashReport];
278 [formData appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];
279
280 // setting the headers:
281 [postRequest setHTTPMethod: @"POST"];
282 [postRequest setValue: contentType forHTTPHeaderField: @"Content-Type"];
283 [postRequest setValue: agent forHTTPHeaderField: @"User-Agent"];
dde81ed @uliwitness Shut up some warnings.
authored
284 NSString *contentLength = [NSString stringWithFormat:@"%u", [formData length]];
dc5be1c Initial check-in.
uli authored
285 [postRequest setValue: contentLength forHTTPHeaderField: @"Content-Length"];
286 [postRequest setHTTPBody: formData];
287
288 // Go into progress mode and kick off the HTTP post:
289 [progressIndicator startAnimation: self];
290 [sendButton setEnabled: NO];
291 [remindButton setEnabled: NO];
292 [discardButton setEnabled: NO];
293
294 connection = [[NSURLConnection connectionWithRequest: postRequest delegate: self] retain];
295 }
296
297
298 -(IBAction) remindMeLater: (id)sender
299 {
300 [reportWindow orderOut: self];
301 }
302
303
304 -(IBAction) discardCrashReport: (id)sender
305 {
306 // Remember we already did this crash, so we don't ask twice:
307 if( !feedbackMode )
308 {
309 [[NSUserDefaults standardUserDefaults] setFloat: [[NSDate date] timeIntervalSince1970] forKey: @"UKCrashReporterLastCrashReportDate"];
310 [[NSUserDefaults standardUserDefaults] synchronize];
311 }
312
313 [reportWindow orderOut: self];
314 }
315
316
317 -(void) showFinishedMessage: (NSError*)errMsg
318 {
319 if( errMsg )
320 {
321 NSString* errTitle = nil;
322 if( feedbackMode )
323 errTitle = NSLocalizedStringFromTable( @"COULDNT_SEND_FEEDBACK_ERROR",@"UKCrashReporter",@"");
324 else
325 errTitle = NSLocalizedStringFromTable( @"COULDNT_SEND_CRASH_REPORT_ERROR",@"UKCrashReporter",@"");
326
327 NSRunAlertPanel( errTitle, @"%@", NSLocalizedStringFromTable( @"COULDNT_SEND_CRASH_REPORT_ERROR_OK",@"UKCrashReporter",@""), @"", @"",
328 [errMsg localizedDescription] );
329 }
330
331 [reportWindow orderOut: self];
332 [self autorelease];
333 }
334
335
336 -(void) connectionDidFinishLoading:(NSURLConnection *)conn
337 {
338 [connection release];
339 connection = nil;
340
341 // Now that we successfully sent this crash, don't report it again:
342 if( !feedbackMode )
343 {
344 [[NSUserDefaults standardUserDefaults] setFloat: [[NSDate date] timeIntervalSince1970] forKey: @"UKCrashReporterLastCrashReportDate"];
345 [[NSUserDefaults standardUserDefaults] synchronize];
346 }
347
348 [self performSelectorOnMainThread: @selector(showFinishedMessage:) withObject: nil waitUntilDone: NO];
349 }
350
351
352 -(void) connection:(NSURLConnection *)conn didFailWithError:(NSError *)error
353 {
354 [connection release];
355 connection = nil;
356
357 [self performSelectorOnMainThread: @selector(showFinishedMessage:) withObject: error waitUntilDone: NO];
358 }
359
360 @end
361
362
363 @implementation UKFeedbackProvider
364
365 -(IBAction) orderFrontFeedbackWindow: (id)sender
366 {
367 [[UKCrashReporter alloc] init];
368 }
369
370
371 -(IBAction) orderFrontBugReportWindow: (id)sender
372 {
373 [[UKCrashReporter alloc] init];
374 }
375
376 @end
Something went wrong with that request. Please try again.