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>
25 * @class AIExceptionController
26 * @brief Catches application exceptions and forwards them to the crash reporter application
28 * Once configured, sets itself as the NSExceptionHandler delegate to decode the stack traces
29 * generated via NSExceptionHandler, write them to a file, and launch the crash reporter.
31 @implementation AIExceptionController
33 //Enable exception catching for the crash reporter
34 static BOOL catchExceptions = NO;
36 //These exceptions can be safely ignored.
37 static NSSet *safeExceptionReasons = nil, *safeExceptionNames = nil;
39 + (void)enableExceptionCatching
41 //Log and Handle all exceptions
42 NSExceptionHandler *exceptionHandler = [NSExceptionHandler defaultExceptionHandler];
43 [exceptionHandler setExceptionHandlingMask:(NSHandleUncaughtExceptionMask |
44 NSHandleUncaughtSystemExceptionMask |
45 NSHandleUncaughtRuntimeErrorMask |
46 NSHandleTopLevelExceptionMask /*|
47 NSHandleOtherExceptionMask*/)];
48 [exceptionHandler setDelegate:self];
50 catchExceptions = YES;
52 //Remove any existing exception logs
53 [[NSFileManager defaultManager] trashFileAtPath:EXCEPTIONS_PATH];
55 //Set up exceptions to except
56 //More of these (matched by substring) can be found in -raise
57 if (!safeExceptionReasons) {
58 safeExceptionReasons = [[NSSet alloc] initWithObjects:
59 @"_sharedInstance is invalid.", //Address book framework is weird sometimes
60 @"No text was found", //ICeCoffEE is an APE haxie which would crash us whenever a user pasted, or something like that
61 @"No URL is selected", //ICeCoffEE also crashes us when clicking links. How obnoxious. Release software should not use NSAssert like this.
62 @"Error (1000) creating CGSWindow", //This looks like an odd NSImage error... it occurs sporadically, seems harmless, and doesn't appear avoidable
63 @"Error (1007) creating CGSWindow", //This looks like an odd NSImage error... it occurs sporadically, seems harmless, and doesn't appear avoidable
64 @"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
65 @"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.
66 @"Invalid parameter not satisfying: (index >= 0) && (index <= (_itemArray ? CFArrayGetCount(_itemArray) : 0))", //Like the above, but <= instead of <
67 @"Invalid parameter not satisfying: entry", //NSOutlineView throws this, particularly if it gets clicked while reloading or the computer sleeps while reloading
68 @"Invalid parameter not satisfying: aString != nil", //The Find command can throw this, as can other AppKit methods
71 if (!safeExceptionNames) {
72 safeExceptionNames = [[NSSet alloc] initWithObjects:
73 @"GIFReadingException", //GIF reader sucks
74 @"NSPortTimeoutException", //Harmless - it timed out for a reason
75 @"NSInvalidReceivePortException", //Same story as NSPortTimeoutException
76 @"NSAccessibilityException", //Harmless - one day we should figure out how we aren't accessible, but not today
77 @"NSImageCacheException", //NSImage is silly
78 @"NSArchiverArchiveInconsistency", //Odd system hacks can lead to this one
79 @"NSUnknownKeyException", //No reason to crash on invalid Applescript syntax
80 @"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.
81 @"NSCharacterConversionException", //We can't help it if a character can't be converted...
82 @"NSRTFException", //Better to ignore than to crash
87 // mask is NSHandle<exception type>Mask, exception's userInfo has stack trace for key NSStackTraceKey
88 + (BOOL)exceptionHandler:(NSExceptionHandler *)sender shouldHandleException:(NSException *)exception mask:(unsigned int)aMask
90 BOOL shouldLaunchCrashReporter = YES;
91 if (catchExceptions) {
92 NSString *theReason = [exception reason];
93 NSString *theName = [exception name];
96 //Ignore various known harmless or unavoidable exceptions (From the system or system hacks)
97 if ((!theReason) || //Harmless
98 [safeExceptionReasons containsObject:theReason] ||
99 [theReason rangeOfString:@"NSRunStorage, _NSBlockNumberForIndex()"].location != NSNotFound || //NSLayoutManager throws this for fun in a purely-AppKit stack trace
100 [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
101 [theReason rangeOfString:@"incomprehensible archive"].location != NSNotFound || //NSKeyedUnarchiver can get confused and throw this; it's out of our control
102 [theReason rangeOfString:@"-whiteComponent not defined"].location != NSNotFound || //Random NSColor exception for certain coded color values
103 [theReason rangeOfString:@"Failed to get fache"].location != NSNotFound || //Thrown by NSFontManager when availableFontFamilies is called if it runs into a corrupt font
104 [theReason rangeOfString:@"NSWindow: -_newFirstResponderAfterResigining"].location != NSNotFound || //NSAssert within system code, harmless
105 [theReason rangeOfString:@"-patternImage not defined"].location != NSNotFound || //Painters Color Picker throws an exception during the normal course of operation. Don't you hate that?
106 [theReason rangeOfString:@"Failed to set font"].location != NSNotFound || //Corrupt fonts
107 [theReason rangeOfString:@"Delete invalid attribute range"].location != NSNotFound || //NSAttributedString's initWithCoder can throw this
108 [theReason rangeOfString:@"NSMutableRLEArray objectAtIndex:effectiveRange:: Out of bounds"].location != NSNotFound || //-[NSLayoutManager textContainerForGlyphAtIndex:effectiveRange:] as of 10.4 can throw this
109 [theReason rangeOfString:@"TSMProcessRawKeyCode failed"].location != NSNotFound || //May be raised by -[NSEvent charactersIgnoringModifiers]
110 [theReason rangeOfString:@"Invalid PMPrintSettings in print info"].location != NSNotFound || //Invalid saved print settings can make the print dialogue throw this
111 [theReason rangeOfString:@"-[NSConcreteTextStorage attribute:atIndex:effectiveRange:]: Range or index out of bounds"].location != NSNotFound || //Can't find the source of this, but it seems to happen randomly and not provide a stack trace.
112 [theReason rangeOfString:@"SketchUpColor"].location != NSNotFound || //NSColorSwatch addition which can yield an exception
113 [theReason rangeOfString:@"-[NSConcreteFileHandle dealloc]: Bad file descriptor"].location != NSNotFound || // NSFileHandle on an invalid file descriptor should log but not crash
114 (!theName) || //Harmless
115 [theName rangeOfString:@"RSS"].location != NSNotFound || //Sparkle's RSS handling whines sometimes, but we don't care.
116 [safeExceptionNames containsObject:theName])
118 shouldLaunchCrashReporter = NO;
121 //Check the stack trace for a third set of known offenders
122 backtrace = (shouldLaunchCrashReporter ? [exception decodedExceptionStackTrace] : nil);
124 [backtrace rangeOfString:@"-[NSFontPanel setPanelFont:isMultiple:] (in AppKit)"].location != NSNotFound || //NSFontPanel likes to create exceptions
125 [backtrace rangeOfString:@"-[NSScrollView(NSScrollViewAccessibility) accessibilityChildrenAttribute]"].location != NSNotFound || //Perhaps we aren't implementing an accessibility method properly? No idea what though :(
126 [backtrace rangeOfString:@"-[WebBridge objectLoadedFromCacheWithURL:response:data:]"].location != NSNotFound || //WebBridge throws this randomly it seems
127 [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.
128 [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.
129 [backtrace rangeOfString:@"[NSSpellChecker sharedSpellChecker]"].location != NSNotFound //The spell checker screws up and starts throwing an exception on every word on many people's systems.
132 shouldLaunchCrashReporter = NO;
135 if (shouldLaunchCrashReporter) {
136 NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
137 NSString *crashReporterPath = [bundlePath stringByAppendingPathComponent:RELATIVE_PATH_TO_CRASH_REPORTER];
138 NSString *versionString = [[NSProcessInfo processInfo] operatingSystemVersionString];
139 NSString *preferredLocalization = [[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0];
141 NSLog(@"Launching the Adium Crash Reporter because an exception of type %@ occurred:\n%@", theName,theReason);
143 //Pass the exception to the crash reporter and close this application
144 [[NSString stringWithFormat:@"OS Version:\t%@\nLanguage:\t%@\nException:\t%@\nReason:\t%@\nStack trace:\n%@",
145 versionString,preferredLocalization,theName,theReason,backtrace] writeToFile:EXCEPTIONS_PATH atomically:YES];
147 [[NSWorkspace sharedWorkspace] openFile:bundlePath withApplication:crashReporterPath];
152 NSLog(@"The following unhandled exception was ignored: %@ (%@)\nStack trace:\n%@",
155 (backtrace ? backtrace : @"(Unavailable)"));
156 AILog(@"The following unhandled exception was ignored: %@ (%@)\nStack trace:\n%@",
159 (backtrace ? backtrace : @"(Unavailable)"));
164 return shouldLaunchCrashReporter;
169 @implementation NSException (AIExceptionControllerAdditions)
170 //Decode the stack trace within [self userInfo] and return it
171 - (NSString *)decodedExceptionStackTrace
173 NSDictionary *dict = [self userInfo];
174 NSString *stackTrace = nil;
176 //Turn the nonsense of memory addresses into a human-readable backtrace complete with line numbers
177 if (dict && (stackTrace = [dict objectForKey:NSStackTraceKey])) {
178 NSMutableString *processedStackTrace;
181 /*We use several command line apps to decode our exception:
182 * * atos -p PID addresses...: converts addresses (hex numbers) to symbol names that we can read.
183 * * tail -n +3: strip the first three lines.
184 * * head -n +NUM: reduces to the first NUM lines. we pass NUM as the number of addresses minus 4.
185 * * c++filt: de-mangles C++ names.
187 * __ZNK12CApplication23CreateClipboardTextViewERsR12CViewManager (in TextWrangler)
189 * CApplication::CreateClipboardTextView(short&, CViewManager&) const (in TextWrangler)
190 * * cat -n: adds line numbers. fairly meaningless, but fun.
192 str = [NSString stringWithFormat:@"%s -p %d %@ | tail -n +3 | head -n +%d | %s | cat -n",
193 [[[[NSBundle mainBundle] pathForResource:@"atos" ofType:nil] stringByEscapingForShell] fileSystemRepresentation], //atos arg 0
194 [[NSProcessInfo processInfo] processIdentifier], //atos arg 2 (argument to -p)
195 stackTrace, //atos arg 3..inf
196 ([[stackTrace componentsSeparatedByString:@" "] count] - 4), //head arg 3
197 [[[[NSBundle mainBundle] pathForResource:@"c++filt" ofType:nil] stringByEscapingForShell] fileSystemRepresentation]]; //c++filt arg 0
199 FILE *file = popen( [str UTF8String], "r" );
200 NSMutableData *data = [[NSMutableData alloc] init];
203 NSZone *zone = [self zone];
205 size_t bufferSize = getpagesize();
206 char *buffer = NSZoneMalloc(zone, bufferSize);
208 buffer = alloca(bufferSize = 512);
214 while ((amountRead = fread(buffer, sizeof(char), bufferSize, file))) {
215 [data appendBytes:buffer length:amountRead];
218 if (zone) NSZoneFree(zone, buffer);
223 //we use ISO 8859-1 because it preserves all bytes. UTF-8 doesn't (beacuse
224 // certain sequences of bytes may get added together or cause the string to be rejected).
225 //and it shouldn't matter; we shouldn't be getting high-ASCII in the backtrace anyway. :)
226 processedStackTrace = [[[NSMutableString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] autorelease];
229 //Clear out a useless string inserted into some stack traces as of 10.4 to improve crashlog readability
230 [processedStackTrace replaceOccurrencesOfString:@"task_start_peeking: can't suspend failed (ipc/send) invalid destination port"
232 options:NSLiteralSearch
233 range:NSMakeRange(0, [processedStackTrace length])];
235 return processedStackTrace;
238 //If we are unable to decode the stack trace, return the best we have