2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AICrashReporter.h"
18 #import "AIExceptionController.h"
19 #import <AIUtilities/AIFileManagerAdditions.h>
20 #import <AIUtilities/AIStringAdditions.h>
21 #import <ExceptionHandling/NSExceptionHandler.h>
24 * @class AIExceptionController
25 * @brief Catches application exceptions and forwards them to the crash reporter application
27 * Once configured, poses as NSException to decode the stack traces generated via NSExceptionHandler,
28 * write them to a file, and launch the crash reporter.
30 @implementation AIExceptionController
32 //Enable exception catching for the crash reporter
33 static BOOL catchExceptions = NO;
35 //These exceptions can be safely ignored.
36 static NSSet *safeExceptionReasons = nil, *safeExceptionNames = nil;
38 + (void)enableExceptionCatching
40 //Log and Handle all exceptions
41 NSExceptionHandler *exceptionHandler = [NSExceptionHandler defaultExceptionHandler];
42 [exceptionHandler setExceptionHandlingMask:(NSHandleUncaughtExceptionMask |
43 NSHandleUncaughtSystemExceptionMask |
44 NSHandleUncaughtRuntimeErrorMask |
45 NSHandleTopLevelExceptionMask |
46 NSHandleOtherExceptionMask)];
47 [exceptionHandler setDelegate:self];
49 catchExceptions = YES;
51 //Remove any existing exception logs
52 [[NSFileManager defaultManager] trashFileAtPath:EXCEPTIONS_PATH];
54 //Set up exceptions to except
55 //More of these (matched by substring) can be found in -raise
56 if(!safeExceptionReasons) {
57 safeExceptionReasons = [[NSSet alloc] initWithObjects:
58 @"_sharedInstance is invalid.", //Address book framework is weird sometimes
59 @"No text was found", //ICeCoffEE is an APE haxie which would crash us whenever a user pasted, or something like that
60 @"No URL is selected", //ICeCoffEE also crashes us when clicking links. How obnoxious. Release software should not use NSAssert like this.
61 @"Error (1000) creating CGSWindow", //This looks like an odd NSImage error... it occurs sporadically, seems harmless, and doesn't appear avoidable
62 @"Access invalid attribute location 0 (length 0)", //The undo manager can throw this one when restoring a large amount of attributed text... doesn't appear avoidable
63 @"Invalid parameter not satisfying: (index >= 0) && (index < (_itemArray ? CFArrayGetCount(_itemArray) : 0))", //A couple AppKit methods, particularly NSSpellChecker, seem to expect this exception to be happily thrown in the normal course of operation. Lovely. Also needed for FontSight compatibility.
64 @"Invalid parameter not satisfying: (index >= 0) && (index <= (_itemArray ? CFArrayGetCount(_itemArray) : 0))", //Like the above, but <= instead of <
65 @"Invalid parameter not satisfying: entry", //NSOutlineView throws this, particularly if it gets clicked while reloading or the computer sleeps while reloading
66 @"Invalid parameter not satisfying: aString != nil", //The Find command can throw this, as can other AppKit methods
69 if(!safeExceptionNames) {
70 safeExceptionNames = [[NSSet alloc] initWithObjects:
71 @"GIFReadingException", //GIF reader sucks
72 @"NSPortTimeoutException", //Harmless - it timed out for a reason
73 @"NSInvalidReceivePortException", //Similar to NSPortTimeoutException
74 @"NSAccessibilityException", //Harmless - one day we should figure out how we aren't accessible, but not today
75 @"NSImageCacheException", //NSImage is silly
76 @"NSArchiverArchiveInconsistency", //Odd system hacks can lead to this one
77 @"NSUnknownKeyException", //No reason to crash on invalid Applescript syntax
78 @"NSObjectInaccessibleException", //We don't use DO, but spell checking does; AppleScript execution requires multiple run loops, and the HIToolbox can get confused and try to spellcheck in the applescript thread. Silly Apple.
83 // mask is NSHandle<exception type>Mask, exception's userInfo has stack trace for key NSStackTraceKey
84 + (BOOL)exceptionHandler:(NSExceptionHandler *)sender shouldHandleException:(NSException *)exception mask:(unsigned int)aMask
86 BOOL shouldLaunchCrashReporter = YES;
88 if (catchExceptions) {
89 NSString *theReason = [exception reason];
90 NSString *theName = [exception name];
91 NSString *backtrace = [exception decodedExceptionStackTrace];
93 NSLog(@"Caught exception: %@ - %@",theName,theReason);
95 //Ignore various known harmless or unavoidable exceptions (From the system or system hacks)
96 if((!theReason) || //Harmless
97 [safeExceptionReasons containsObject:theReason] ||
98 [theReason rangeOfString:@"NSRunStorage, _NSBlockNumberForIndex()"].location != NSNotFound || //NSLayoutManager throws this for fun in a purely-AppKit stack trace
99 [theReason rangeOfString:@"Broken pipe"].location != NSNotFound || //libezv throws broken pipes as NSFileHandleOperationException with this in the reason; I'd rather we watched for "broken pipe" than ignore all file handle errors
100 [theReason rangeOfString:@"incomprehensible archive"].location != NSNotFound || //NSKeyedUnarchiver can get confused and throw this; it's out of our control
101 [theReason rangeOfString:@"-whiteComponent not defined"].location != NSNotFound || //Random NSColor exception for certain coded color values
102 [theReason rangeOfString:@"Failed to get fache"].location != NSNotFound || //Thrown by NSFontManager when availableFontFamilies is called if it runs into a corrupt font
103 [theReason rangeOfString:@"NSWindow: -_newFirstResponderAfterResigining"].location != NSNotFound || //NSAssert within system code, harmless
104 [theReason rangeOfString:@"-patternImage not defined"].location != NSNotFound || //Painters Color Picker throws an exception during the normal course of operation. Don't you hate that?
105 [theReason rangeOfString:@"Failed to set font"].location != NSNotFound || //Corrupt fonts
106 [theReason rangeOfString:@"Delete invalid attribute range"].location != NSNotFound || //NSAttributedString's initWithCoder can throw this
107 [theReason rangeOfString:@"NSMutableRLEArray objectAtIndex:effectiveRange:: Out of bounds"].location != NSNotFound || //-[NSLayoutManager textContainerForGlyphAtIndex:effectiveRange:] as of 10.4 can throw this
108 [theReason rangeOfString:@"TSMProcessRawKeyCode failed"].location != NSNotFound || //May be raised by -[NSEvent charactersIgnoringModifiers]
109 (!theName) || //Harmless
110 [safeExceptionNames containsObject:theName])
112 shouldLaunchCrashReporter = NO;
115 //Check the stack trace for a third set of known offenders
117 [backtrace rangeOfString:@"-[NSFontPanel setPanelFont:isMultiple:] (in AppKit)"].location != NSNotFound || //NSFontPanel likes to create exceptions
118 [backtrace rangeOfString:@"-[NSScrollView(NSScrollViewAccessibility) accessibilityChildrenAttribute]"].location != NSNotFound || //Perhaps we aren't implementing an accessibility method properly? No idea what though :(
119 [backtrace rangeOfString:@"-[WebBridge objectLoadedFromCacheWithURL:response:data:]"].location != NSNotFound || //WebBridge throws this randomly it seems
120 [backtrace rangeOfString:@"-[NSTextView(NSSharing) _preflightSpellChecker:]"].location != NSNotFound || //Systemwide spell checker gets corrupted on some systems; other apps just end up logging to console, and we should do the same.
121 [backtrace rangeOfString:@"-[NSFontManager(NSFontManagerCollectionAdditions) _collectionsChanged:]"].location != NSNotFound //Deleting an empty collection in 10.4.3 (and possibly other versions) throws an NSRangeException with this in the backtrace.
124 shouldLaunchCrashReporter = NO;
127 if(shouldLaunchCrashReporter){
128 NSString *bundlePath = [[[NSBundle mainBundle] bundlePath] stringByExpandingTildeInPath];
129 NSString *crashReporterPath = [bundlePath stringByAppendingPathComponent:RELATIVE_PATH_TO_CRASH_REPORTER];
130 NSString *versionString = [[NSProcessInfo processInfo] operatingSystemVersionString];
131 NSString *preferredLocalization = [[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0];
133 NSLog(@"Launching the Adium Crash Reporter because an exception of type %@ occurred:\n%@", theName,theReason);
135 //Pass the exception to the crash reporter and close this application
136 [[NSString stringWithFormat:@"OS Version:\t%@\nLanguage:\t%@\nException:\t%@\nReason:\t%@\nStack trace:\n%@",
137 versionString,preferredLocalization,theName,theReason,backtrace] writeToFile:EXCEPTIONS_PATH atomically:YES];
139 [[NSWorkspace sharedWorkspace] openFile:bundlePath withApplication:crashReporterPath];
143 NSLog(@"The following unhandled exception was ignored: %@ (%@)\nStack trace:\n%@",
146 (backtrace ? backtrace : @"(Unavailable)"));
147 AILog(@"The following unhandled exception was ignored: %@ (%@)\nStack trace:\n%@",
150 (backtrace ? backtrace : @"(Unavailable)"));
154 return shouldLaunchCrashReporter;
159 @implementation NSException (AIExceptionControllerAdditions)
160 //Decode the stack trace within [self userInfo] and return it
161 - (NSString *)decodedExceptionStackTrace
163 NSDictionary *dict = [self userInfo];
164 NSString *stackTrace = nil;
166 //Turn the nonsense of memory addresses into a human-readable backtrace complete with line numbers
167 if(dict && (stackTrace = [dict objectForKey:NSStackTraceKey])) {
168 NSMutableString *processedStackTrace;
171 //We use two command line apps to decode our exception
172 str = [NSString stringWithFormat:@"%s -p %d %@ | tail -n +3 | head -n +%d | %s | cat -n",
173 [[[[NSBundle mainBundle] pathForResource:@"atos" ofType:nil] stringByEscapingForShell] fileSystemRepresentation], //atos arg 0
174 [[NSProcessInfo processInfo] processIdentifier], //atos arg 2 (argument to -p)
175 stackTrace, //atos arg 3..inf
176 ([[stackTrace componentsSeparatedByString:@" "] count] - 4), //head arg 3
177 [[[[NSBundle mainBundle] pathForResource:@"c++filt" ofType:nil] stringByEscapingForShell] fileSystemRepresentation]]; //c++filt arg 0
179 FILE *file = popen( [str UTF8String], "r" );
180 NSMutableData *data = [[NSMutableData alloc] init];
183 NSZone *zone = [self zone];
185 size_t bufferSize = getpagesize();
186 char *buffer = NSZoneMalloc(zone, bufferSize);
188 buffer = alloca(bufferSize = 512);
194 while((amountRead = fread(buffer, sizeof(char), bufferSize, file))) {
195 [data appendBytes:buffer length:amountRead];
198 if(zone) NSZoneFree(zone, buffer);
203 //we use ISO 8859-1 because it preserves all bytes. UTF-8 doesn't (beacuse
204 // certain sequences of bytes may get added together or cause the string to be rejected).
205 //and it shouldn't matter; we shouldn't be getting high-ASCII in the backtrace anyway. :)
206 processedStackTrace = [[[NSMutableString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] autorelease];
209 //Clear out a useless string inserted into some stack traces as of 10.4 to improve crashlog readability
210 [processedStackTrace replaceOccurrencesOfString:@"task_start_peeking: can't suspend failed (ipc/send) invalid destination port"
212 options:NSLiteralSearch
213 range:NSMakeRange(0, [processedStackTrace length])];
215 return(processedStackTrace);
218 //If we are unable to decode the stack trace, return the best we have