| // Copyright (c) 2009 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/chrome_browser_application_mac.h" |
| |
| #import "base/histogram.h" |
| #import "base/logging.h" |
| #import "base/scoped_nsobject.h" |
| #import "base/sys_string_conversions.h" |
| #import "chrome/app/breakpad_mac.h" |
| #import "chrome/browser/cocoa/objc_method_swizzle.h" |
| |
| // The implementation of NSExceptions break various assumptions in the |
| // Chrome code. This category defines a replacement for |
| // -initWithName:reason:userInfo: for purposes of forcing a break in |
| // the debugger when an exception is raised. -raise sounds more |
| // obvious to intercept, but it doesn't catch the original throw |
| // because the objc runtime doesn't use it. |
| @interface NSException (NSExceptionSwizzle) |
| - (id)chromeInitWithName:(NSString *)aName |
| reason:(NSString *)aReason |
| userInfo:(NSDictionary *)someUserInfo; |
| @end |
| |
| static IMP gOriginalInitIMP = NULL; |
| |
| @implementation NSException (NSExceptionSwizzle) |
| - (id)chromeInitWithName:(NSString *)aName |
| reason:(NSString *)aReason |
| userInfo:(NSDictionary *)someUserInfo { |
| // Method only called when swizzled. |
| DCHECK(_cmd == @selector(initWithName:reason:userInfo:)); |
| |
| // Parts of Cocoa rely on creating and throwing exceptions. These are not |
| // worth bugging-out over. It is very important that there be zero chance that |
| // any Chromium code is on the stack; these must be created by Apple code and |
| // then immediately consumed by Apple code. |
| static const NSString* kAcceptableNSExceptionNames[] = { |
| // If an object does not support an accessibility attribute, this will |
| // get thrown. |
| NSAccessibilityException, |
| |
| nil |
| }; |
| |
| BOOL found = NO; |
| for (int i = 0; kAcceptableNSExceptionNames[i]; ++i) { |
| if (aName == kAcceptableNSExceptionNames[i]) { |
| found = YES; |
| } |
| } |
| |
| if (!found) { |
| // Dear reader: something you just did provoked an NSException. |
| // Please check your backtrace and see if you can't file a bug with |
| // a repro case. You should be able to safely continue past the |
| // NOTREACHED(), but feel free to comment it out locally if it is |
| // making your job hard. |
| DLOG(ERROR) << "Someone is preparing to raise an exception! " |
| << base::SysNSStringToUTF8(aName) << " *** " |
| << base::SysNSStringToUTF8(aReason); |
| NOTREACHED(); |
| } |
| |
| // Forward to the original version. |
| return gOriginalInitIMP(self, _cmd, aName, aReason, someUserInfo); |
| } |
| @end |
| |
| namespace chrome_browser_application_mac { |
| |
| // Maximum number of known named exceptions we'll support. There is |
| // no central registration, but I only find about 75 possibilities in |
| // the system frameworks, and many of them are probably not |
| // interesting to track in aggregate (those relating to distributed |
| // objects, for instance). |
| const size_t kKnownNSExceptionCount = 25; |
| |
| const size_t kUnknownNSException = kKnownNSExceptionCount; |
| |
| size_t BinForException(NSException* exception) { |
| // A list of common known exceptions. The list position will |
| // determine where they live in the histogram, so never move them |
| // around, only add to the end. |
| static const NSString* kKnownNSExceptionNames[] = { |
| // ??? |
| NSGenericException, |
| |
| // Out-of-range on NSString or NSArray. |
| NSRangeException, |
| |
| // Invalid arg to method, unrecognized selector. |
| NSInvalidArgumentException, |
| |
| // malloc() returned null in object creation, I think. |
| NSMallocException, |
| |
| nil |
| }; |
| |
| // Make sure our array hasn't outgrown our abilities to track it. |
| DCHECK_LE(arraysize(kKnownNSExceptionNames), kKnownNSExceptionCount); |
| |
| const NSString* name = [exception name]; |
| for (int i = 0; kKnownNSExceptionNames[i]; ++i) { |
| if (name == kKnownNSExceptionNames[i]) { |
| return i; |
| } |
| } |
| return kUnknownNSException; |
| } |
| |
| void RecordExceptionWithUma(NSException* exception) { |
| UMA_HISTOGRAM_ENUMERATION("OSX.NSException", |
| BinForException(exception), kUnknownNSException); |
| } |
| |
| void Terminate() { |
| [NSApp terminate:nil]; |
| } |
| |
| } // namespace chrome_browser_application_mac |
| |
| namespace { |
| |
| // Helper to make it easy to get crash keys right. |
| // TODO(shess): Find a better home for this. app/breakpad_mac.h |
| // doesn't work. |
| class ScopedCrashKey { |
| public: |
| ScopedCrashKey(NSString* key, NSString* value) |
| : crash_key_([key retain]) { |
| SetCrashKeyValue(crash_key_.get(), value); |
| } |
| ~ScopedCrashKey() { |
| ClearCrashKeyValue(crash_key_.get()); |
| } |
| |
| private: |
| scoped_nsobject<NSString> crash_key_; |
| }; |
| |
| // Do-nothing wrapper so that we can arrange to only swizzle |
| // -[NSException raise] when DCHECK() is turned on (as opposed to |
| // replicating the preprocess logic which turns DCHECK() on). |
| BOOL SwizzleNSExceptionInit() { |
| gOriginalInitIMP = ObjcEvilDoers::SwizzleImplementedInstanceMethods( |
| [NSException class], |
| @selector(initWithName:reason:userInfo:), |
| @selector(chromeInitWithName:reason:userInfo:)); |
| return YES; |
| } |
| |
| } // namespace |
| |
| @implementation BrowserCrApplication |
| |
| - init { |
| DCHECK(SwizzleNSExceptionInit()); |
| return [super init]; |
| } |
| |
| // -terminate: is the entry point for orderly "quit" operations in Cocoa. |
| // This includes the application menu's quit menu item and keyboard |
| // equivalent, the application's dock icon menu's quit menu item, "quit" (not |
| // "force quit") in the Activity Monitor, and quits triggered by user logout |
| // and system restart and shutdown. |
| // |
| // The default NSApplication -terminate: implementation will end the process |
| // by calling exit(), and thus never leave the main run loop. This is |
| // unsuitable for Chrome's purposes. Chrome depends on leaving the main |
| // run loop to perform a proper orderly shutdown. This design is ingrained |
| // in the application and the assumptions that its code makes, and is |
| // entirely reasonable and works well on other platforms, but it's not |
| // compatible with the standard Cocoa quit sequence. Quits originated from |
| // within the application can be redirected to not use -terminate:, but |
| // quits from elsewhere cannot be. |
| // |
| // To allow the Cocoa-based Chrome to support the standard Cocoa -terminate: |
| // interface, and allow all quits to cause Chrome to shut down properly |
| // regardless of their origin, -terminate: is overriden. The custom |
| // -terminate: does not end the application with exit(). Instead, it simply |
| // returns after posting the normal NSApplicationWillTerminateNotification |
| // notification. The application is responsible for exiting on its own in |
| // whatever way it deems appropriate. In Chrome's case, the main run loop will |
| // end and the applicaton will exit by returning from main(). |
| // |
| // This implementation of -terminate: is scaled back and is not as |
| // fully-featured as the implementation in NSApplication, nor is it a direct |
| // drop-in replacement -terminate: in most applications. It is |
| // purpose-specific to Chrome. |
| - (void)terminate:(id)sender { |
| NSApplicationTerminateReply shouldTerminate = NSTerminateNow; |
| SEL selector = @selector(applicationShouldTerminate:); |
| if ([[self delegate] respondsToSelector:selector]) |
| shouldTerminate = [[self delegate] applicationShouldTerminate:self]; |
| |
| // If shouldTerminate is NSTerminateLater, the application is expected to |
| // call -replyToApplicationShouldTerminate: when it knows whether or not it |
| // should terminate. If the argument is YES, |
| // -replyToApplicationShouldTerminate: will call -terminate:. This will |
| // result in another call to the delegate's -applicationShouldTerminate:, |
| // which would be expected to return NSTerminateNow at that point. |
| if (shouldTerminate != NSTerminateNow) |
| return; |
| |
| [[NSNotificationCenter defaultCenter] |
| postNotificationName:NSApplicationWillTerminateNotification |
| object:self]; |
| |
| // Return, don't exit. The application is responsible for exiting on its |
| // own. |
| } |
| |
| - (BOOL)sendAction:(SEL)anAction to:(id)aTarget from:(id)sender { |
| // The Dock menu contains an automagic section where you can select |
| // amongst open windows. If a window is closed via JavaScript while |
| // the menu is up, the menu item for that window continues to exist. |
| // When a window is selected this method is called with the |
| // now-freed window as |aTarget|. Short-circuit the call if |
| // |aTarget| is not a valid window. |
| if (anAction == @selector(_selectWindow:)) { |
| // Not using -[NSArray containsObject:] because |aTarget| may be a |
| // freed object. |
| BOOL found = NO; |
| for (NSWindow* window in [self windows]) { |
| if (window == aTarget) { |
| found = YES; |
| break; |
| } |
| } |
| if (!found) { |
| return NO; |
| } |
| } |
| |
| // When a Cocoa control is wired to a freed object, we get crashers |
| // in the call to |super| with no useful information in the |
| // backtrace. Attempt to add some useful information. |
| static const NSString* kActionKey = @"sendaction"; |
| |
| // If the action is something generic like -commandDispatch:, then |
| // the tag is essential. |
| NSInteger tag = 0; |
| if ([sender isKindOfClass:[NSControl class]]) { |
| tag = [sender tag]; |
| if (tag == 0 || tag == -1) { |
| tag = [sender selectedTag]; |
| } |
| } else if ([sender isKindOfClass:[NSMenuItem class]]) { |
| tag = [sender tag]; |
| } |
| |
| NSString* actionString = NSStringFromSelector(anAction); |
| NSString* value = |
| [NSString stringWithFormat:@"%@ tag %d sending %@ to %p", |
| [sender className], tag, actionString, aTarget]; |
| |
| ScopedCrashKey key(kActionKey, value); |
| return [super sendAction:anAction to:aTarget from:sender]; |
| } |
| |
| // NSExceptions which are caught by the event loop are logged here. |
| // NSException uses setjmp/longjmp, which can be very bad for C++, so |
| // we attempt to track and report them. |
| - (void)reportException:(NSException *)anException { |
| // If we throw an exception in this code, we can create an infinite |
| // loop. If we throw out of the if() without resetting |
| // |reportException|, we'll stop reporting exceptions for this run. |
| static BOOL reportingException = NO; |
| DCHECK(!reportingException); |
| if (!reportingException) { |
| reportingException = YES; |
| chrome_browser_application_mac::RecordExceptionWithUma(anException); |
| |
| // Store some human-readable information in breakpad keys in case |
| // there is a crash. Since breakpad does not provide infinite |
| // storage, we track two exceptions. The first exception thrown |
| // is tracked because it may be the one which caused the system to |
| // go off the rails. The last exception thrown is tracked because |
| // it may be the one most directly associated with the crash. |
| static const NSString* kFirstExceptionKey = @"firstexception"; |
| static BOOL trackedFirstException = NO; |
| static const NSString* kLastExceptionKey = @"lastexception"; |
| |
| // TODO(shess): It would be useful to post some stacktrace info |
| // from the exception. |
| // 10.6 has -[NSException callStackSymbols] |
| // 10.5 has -[NSException callStackReturnAddresses] |
| // 10.5 has backtrace_symbols(). |
| // I've tried to combine the latter two, but got nothing useful. |
| // The addresses are right, though, maybe we could train the crash |
| // server to decode them for us. |
| |
| NSString* value = [NSString stringWithFormat:@"%@ reason %@", |
| [anException name], [anException reason]]; |
| if (!trackedFirstException) { |
| SetCrashKeyValue(kFirstExceptionKey, value); |
| trackedFirstException = YES; |
| } else { |
| SetCrashKeyValue(kLastExceptionKey, value); |
| } |
| |
| reportingException = NO; |
| } |
| |
| [super reportException:anException]; |
| } |
| |
| @end |