Libpurple.framework [484]: libpurple 2.2.0
[adiumx.git] / Source / AIExceptionController.m
blob3efbda0611518e200a4051655181a7a36714c4e5
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
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.
8  * 
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.
12  * 
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.
15  */
17 #import "AICrashReporter.h"
18 #import "AIExceptionController.h"
19 #import <AIUtilities/AIFileManagerAdditions.h>
20 #import <AIUtilities/AIStringAdditions.h>
21 #import <ExceptionHandling/NSExceptionHandler.h>
22 #include <unistd.h>
24 /*!
25  * @class AIExceptionController
26  * @brief Catches application exceptions and forwards them to the crash reporter application
27  *
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.
30  */
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
69                         nil];
70         }
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
83                         nil];
84         }
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];
94                 NSString        *backtrace;
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])
117                 {
118                         shouldLaunchCrashReporter = NO;
119                 }
120                 
121                 //Check the stack trace for a third set of known offenders
122                 backtrace = (shouldLaunchCrashReporter ? [exception decodedExceptionStackTrace] : nil);
123                 if (!backtrace ||
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.
130                    )
131                 {
132                            shouldLaunchCrashReporter = NO;
133                 }
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];
140         
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];
149                         exit(-1);
150                 } else {
151                         /*
152                         NSLog(@"The following unhandled exception was ignored: %@ (%@)\nStack trace:\n%@",
153                                   theName,
154                                   theReason,
155                                   (backtrace ? backtrace : @"(Unavailable)"));
156                         AILog(@"The following unhandled exception was ignored: %@ (%@)\nStack trace:\n%@",
157                                   theName,
158                                   theReason,
159                                   (backtrace ? backtrace : @"(Unavailable)"));
160                          */
161                 }
162         }
164         return shouldLaunchCrashReporter;
167 @end
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;
179                 NSString                        *str;
180                 
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.
186                  *              example, before:
187                  *                      __ZNK12CApplication23CreateClipboardTextViewERsR12CViewManager (in TextWrangler)
188                  *              example, after:
189                  *                      CApplication::CreateClipboardTextView(short&, CViewManager&) const (in TextWrangler)
190                  *      * cat -n: adds line numbers. fairly meaningless, but fun.
191                  */
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];
202                 if (file) {
203                         NSZone  *zone = [self zone];
205                         size_t   bufferSize = getpagesize();
206                         char    *buffer = NSZoneMalloc(zone, bufferSize);
207                         if (!buffer) {
208                                 buffer = alloca(bufferSize = 512);
209                                 zone = NULL;
210                         }
212                         size_t   amountRead;
214                         while ((amountRead = fread(buffer, sizeof(char), bufferSize, file))) {
215                                 [data appendBytes:buffer length:amountRead];
216                         }
218                         if (zone) NSZoneFree(zone, buffer);
220                         pclose(file);
221                 }
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];
227                 [data release];
228                 
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"
231                                                                                          withString:@""
232                                                                                                 options:NSLiteralSearch
233                                                                                                   range:NSMakeRange(0, [processedStackTrace length])];
234                 
235                 return processedStackTrace;
236         }
237         
238         //If we are unable to decode the stack trace, return the best we have
239         return stackTrace;
242 @end