blob: f373e11d3cfe1edcfd86876d212299444711e517 [file] [log] [blame]
// 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