AIHyperlinks universal building
[adiumx.git] / Source / AIExceptionController.m
blob5573069a1135fe611346008b86739b6f23360cda
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>
24  * @class AIExceptionController
25  * @brief Catches application exceptions and forwards them to the crash reporter application
26  *
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.
29  */
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
67                         nil];
68         }
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.
79                         nil];
80         }
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);
94                 
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])
111                 {
112                         shouldLaunchCrashReporter = NO;
113                 }
114                 
115                 //Check the stack trace for a third set of known offenders
116                 if(!backtrace ||
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.
122                    )
123                 {
124                            shouldLaunchCrashReporter = NO;
125                 }
126                            
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];
132         
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];
141                         exit(-1);
142                 }else{
143                         NSLog(@"The following unhandled exception was ignored: %@ (%@)\nStack trace:\n%@",
144                                   theName,
145                                   theReason,
146                                   (backtrace ? backtrace : @"(Unavailable)"));
147                         AILog(@"The following unhandled exception was ignored: %@ (%@)\nStack trace:\n%@",
148                                   theName,
149                                   theReason,
150                                   (backtrace ? backtrace : @"(Unavailable)"));
151                 }
152         }
154         return shouldLaunchCrashReporter;
157 @end
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;
169                 NSString                        *str;
170                 
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];
182                 if(file) {
183                         NSZone  *zone = [self zone];
185                         size_t   bufferSize = getpagesize();
186                         char    *buffer = NSZoneMalloc(zone, bufferSize);
187                         if(!buffer) {
188                                 buffer = alloca(bufferSize = 512);
189                                 zone = NULL;
190                         }
192                         size_t   amountRead;
194                         while((amountRead = fread(buffer, sizeof(char), bufferSize, file))) {
195                                 [data appendBytes:buffer length:amountRead];
196                         }
198                         if(zone) NSZoneFree(zone, buffer);
200                         pclose(file);
201                 }
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];
207                 [data release];
208                 
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"
211                                                                                          withString:@""
212                                                                                                 options:NSLiteralSearch
213                                                                                                   range:NSMakeRange(0, [processedStackTrace length])];
214                 
215                 return(processedStackTrace);
216         }
217         
218         //If we are unable to decode the stack trace, return the best we have
219         return(stackTrace);
222 @end