1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/chrome_browser_application_mac.h"
7 #import "base/auto_reset.h"
8 #include "base/debug/crash_logging.h"
9 #include "base/debug/stack_trace.h"
10 #import "base/logging.h"
11 #import "base/mac/scoped_nsexception_enabler.h"
12 #import "base/mac/scoped_nsobject.h"
13 #import "base/metrics/histogram.h"
14 #include "base/strings/stringprintf.h"
15 #import "base/strings/sys_string_conversions.h"
16 #import "chrome/browser/app_controller_mac.h"
17 #include "chrome/browser/ui/tab_contents/tab_contents_iterator.h"
18 #include "chrome/common/crash_keys.h"
19 #import "chrome/common/mac/objc_method_swizzle.h"
20 #import "chrome/common/mac/objc_zombie.h"
21 #include "content/public/browser/browser_accessibility_state.h"
22 #include "content/public/browser/render_view_host.h"
23 #include "content/public/browser/web_contents.h"
25 // The implementation of NSExceptions break various assumptions in the
26 // Chrome code. This category defines a replacement for
27 // -initWithName:reason:userInfo: for purposes of forcing a break in
28 // the debugger when an exception is raised. -raise sounds more
29 // obvious to intercept, but it doesn't catch the original throw
30 // because the objc runtime doesn't use it.
31 @interface NSException (CrNSExceptionSwizzle)
32 - (id)crInitWithName:(NSString*)aName
33 reason:(NSString*)aReason
34 userInfo:(NSDictionary*)someUserInfo;
37 static IMP gOriginalInitIMP = NULL;
39 @implementation NSException (CrNSExceptionSwizzle)
40 - (id)crInitWithName:(NSString*)aName
41 reason:(NSString*)aReason
42 userInfo:(NSDictionary*)someUserInfo {
43 // Method only called when swizzled.
44 DCHECK(_cmd == @selector(initWithName:reason:userInfo:));
46 // Parts of Cocoa rely on creating and throwing exceptions. These are not
47 // worth bugging-out over. It is very important that there be zero chance that
48 // any Chromium code is on the stack; these must be created by Apple code and
49 // then immediately consumed by Apple code.
50 static NSString* const kAcceptableNSExceptionNames[] = {
51 // If an object does not support an accessibility attribute, this will
53 NSAccessibilityException,
59 for (int i = 0; kAcceptableNSExceptionNames[i]; ++i) {
60 if (aName == kAcceptableNSExceptionNames[i]) {
66 // Update breakpad with the exception info.
67 std::string value = base::StringPrintf("%s reason %s",
68 [aName UTF8String], [aReason UTF8String]);
69 base::debug::SetCrashKeyValue(crash_keys::mac::kNSException, value);
70 base::debug::SetCrashKeyToStackTrace(crash_keys::mac::kNSExceptionTrace,
71 base::debug::StackTrace());
73 // Force crash for selected exceptions to generate crash dumps.
75 if (aName == NSInternalInconsistencyException) {
76 NSString* const kNSMenuItemArrayBoundsCheck =
77 @"Invalid parameter not satisfying: (index >= 0) && "
78 @"(index < [_itemArray count])";
79 if ([aReason isEqualToString:kNSMenuItemArrayBoundsCheck]) {
83 NSString* const kNoWindowCheck = @"View is not in any window";
84 if ([aReason isEqualToString:kNoWindowCheck]) {
89 // Mostly "unrecognized selector sent to (instance|class)". A
90 // very small number of things like inappropriate nil being passed.
91 if (aName == NSInvalidArgumentException) {
94 // TODO(shess): http://crbug.com/85463 throws this exception
95 // from ImageKit. Our code is not on the stack, so it needs to
96 // be whitelisted for now.
97 NSString* const kNSURLInitNilCheck =
98 @"*** -[NSURL initFileURLWithPath:isDirectory:]: "
99 @"nil string parameter";
100 if ([aReason isEqualToString:kNSURLInitNilCheck]) {
105 // Dear reader: Something you just did provoked an NSException.
106 // NSException is implemented in terms of setjmp()/longjmp(),
107 // which does poor things when combined with C++ scoping
108 // (destructors are skipped). Chrome should be NSException-free,
109 // please check your backtrace and see if you can't file a bug
110 // with a repro case.
111 const bool allow = base::mac::GetNSExceptionsAllowed();
112 if (fatal && !allow) {
113 LOG(FATAL) << "Someone is trying to raise an exception! "
116 // Make sure that developers see when their code throws
118 DCHECK(allow) << "Someone is trying to raise an exception! "
123 // Forward to the original version.
124 return gOriginalInitIMP(self, _cmd, aName, aReason, someUserInfo);
128 namespace chrome_browser_application_mac {
130 // Maximum number of known named exceptions we'll support. There is
131 // no central registration, but I only find about 75 possibilities in
132 // the system frameworks, and many of them are probably not
133 // interesting to track in aggregate (those relating to distributed
134 // objects, for instance).
135 const size_t kKnownNSExceptionCount = 25;
137 const size_t kUnknownNSException = kKnownNSExceptionCount;
139 size_t BinForException(NSException* exception) {
140 // A list of common known exceptions. The list position will
141 // determine where they live in the histogram, so never move them
142 // around, only add to the end.
143 static NSString* const kKnownNSExceptionNames[] = {
144 // Grab-bag exception, not very common. CFArray (or other
145 // container) mutated while being enumerated is one case seen in
149 // Out-of-range on NSString or NSArray. Quite common.
152 // Invalid arg to method, unrecognized selector. Quite common.
153 NSInvalidArgumentException,
155 // malloc() returned null in object creation, I think. Turns out
156 // to be very uncommon in production, because of the OOM killer.
159 // This contains things like windowserver errors, trying to draw
160 // views which aren't in windows, unable to read nib files. By
161 // far the most common exception seen on the crash server.
162 NSInternalInconsistencyException,
167 // Make sure our array hasn't outgrown our abilities to track it.
168 DCHECK_LE(arraysize(kKnownNSExceptionNames), kKnownNSExceptionCount);
170 NSString* name = [exception name];
171 for (int i = 0; kKnownNSExceptionNames[i]; ++i) {
172 if (name == kKnownNSExceptionNames[i]) {
176 return kUnknownNSException;
179 void RecordExceptionWithUma(NSException* exception) {
180 UMA_HISTOGRAM_ENUMERATION("OSX.NSException",
181 BinForException(exception), kUnknownNSException);
184 void RegisterBrowserCrApp() {
185 [BrowserCrApplication sharedApplication];
189 [NSApp terminate:nil];
192 void CancelTerminate() {
193 [NSApp cancelTerminate:nil];
196 } // namespace chrome_browser_application_mac
201 // Do-nothing wrapper so that we can arrange to only swizzle
202 // -[NSException raise] when DCHECK() is turned on (as opposed to
203 // replicating the preprocess logic which turns DCHECK() on).
204 gOriginalInitIMP = ObjcEvilDoers::SwizzleImplementedInstanceMethods(
206 @selector(initWithName:reason:userInfo:),
207 @selector(crInitWithName:reason:userInfo:));
212 // These methods are being exposed for the purposes of overriding.
213 // Used to determine when a Panel window can become the key window.
214 @interface NSApplication (PanelsCanBecomeKey)
215 - (void)_cycleWindowsReversed:(BOOL)arg1;
216 - (id)_removeWindow:(NSWindow*)window;
217 - (id)_setKeyWindow:(NSWindow*)window;
220 @interface BrowserCrApplication (PrivateInternal)
222 // This must be called under the protection of previousKeyWindowsLock_.
223 - (void)removePreviousKeyWindow:(NSWindow*)window;
227 @implementation BrowserCrApplication
230 // Turn all deallocated Objective-C objects into zombies, keeping
231 // the most recent 10,000 of them on the treadmill.
232 ObjcEvilDoers::ZombieEnable(true, 10000);
239 // Sanity check to alert if overridden methods are not supported.
240 DCHECK([NSApplication
241 instancesRespondToSelector:@selector(_cycleWindowsReversed:)]);
242 DCHECK([NSApplication
243 instancesRespondToSelector:@selector(_removeWindow:)]);
244 DCHECK([NSApplication
245 instancesRespondToSelector:@selector(_setKeyWindow:)]);
250 // Initialize NSApplication using the custom subclass. Check whether NSApp
251 // was already initialized using another class, because that would break
253 + (NSApplication*)sharedApplication {
254 NSApplication* app = [super sharedApplication];
256 // +sharedApplication initializes the global NSApp, so if a specific
257 // NSApplication subclass is requested, require that to be the one
258 // delivered. The practical effect is to require a consistent NSApp
259 // across the executable.
260 CHECK([NSApp isKindOfClass:self])
261 << "NSApp must be of type " << [[self className] UTF8String]
262 << ", not " << [[NSApp className] UTF8String];
264 // If the message loop was initialized before NSApp is setup, the
265 // message pump will be setup incorrectly. Failing this implies
266 // that RegisterBrowserCrApp() should be called earlier.
267 CHECK(base::MessagePumpMac::UsingCrApp())
268 << "MessagePumpMac::Create() is using the wrong pump implementation"
269 << " for " << [[self className] UTF8String];
274 ////////////////////////////////////////////////////////////////////////////////
275 // HISTORICAL COMMENT (by viettrungluu, from
276 // http://codereview.chromium.org/1520006 with mild editing):
278 // A quick summary of the state of things (before the changes to shutdown):
280 // Currently, we are totally hosed (put in a bad state in which Cmd-W does the
281 // wrong thing, and which will probably eventually lead to a crash) if we begin
282 // quitting but termination is aborted for some reason.
284 // I currently know of two ways in which termination can be aborted:
285 // (1) Common case: a window has an onbeforeunload handler which pops up a
286 // "leave web page" dialog, and the user answers "no, don't leave".
287 // (2) Uncommon case: popups are enabled (in Content Settings, i.e., the popup
288 // blocker is disabled), and some nasty web page pops up a new window on
291 // I don't know of other ways in which termination can be aborted, but they may
292 // exist (or may be added in the future, for that matter).
294 // My CL [see above] does the following:
295 // a. Should prevent being put in a bad state (which breaks Cmd-W and leads to
296 // crash) under all circumstances.
297 // b. Should completely handle (1) properly.
298 // c. Doesn't (yet) handle (2) properly and puts it in a weird state (but not
300 // d. Any other ways of aborting termination would put it in that weird state.
302 // c. can be fixed by having the global flag reset on browser creation or
303 // similar (and doing so might also fix some possible d.'s as well). I haven't
304 // done this yet since I haven't thought about it carefully and since it's a
307 // The weird state: a state in which closing the last window quits the browser.
308 // This might be a bit annoying, but it's not dangerous in any way.
309 ////////////////////////////////////////////////////////////////////////////////
311 // |-terminate:| is the entry point for orderly "quit" operations in Cocoa. This
312 // includes the application menu's quit menu item and keyboard equivalent, the
313 // application's dock icon menu's quit menu item, "quit" (not "force quit") in
314 // the Activity Monitor, and quits triggered by user logout and system restart
317 // The default |-terminate:| implementation ends the process by calling exit(),
318 // and thus never leaves the main run loop. This is unsuitable for Chrome since
319 // Chrome depends on leaving the main run loop to perform an orderly shutdown.
320 // We support the normal |-terminate:| interface by overriding the default
321 // implementation. Our implementation, which is very specific to the needs of
322 // Chrome, works by asking the application delegate to terminate using its
323 // |-tryToTerminateApplication:| method.
325 // |-tryToTerminateApplication:| differs from the standard
326 // |-applicationShouldTerminate:| in that no special event loop is run in the
327 // case that immediate termination is not possible (e.g., if dialog boxes
328 // allowing the user to cancel have to be shown). Instead, this method sets a
329 // flag and tries to close all browsers. This flag causes the closure of the
330 // final browser window to begin actual tear-down of the application.
331 // Termination is cancelled by resetting this flag. The standard
332 // |-applicationShouldTerminate:| is not supported, and code paths leading to it
333 // must be redirected.
335 // When the last browser has been destroyed, the BrowserList calls
336 // chrome::OnAppExiting(), which is the point of no return. That will cause
337 // the NSApplicationWillTerminateNotification to be posted, which ends the
338 // NSApplication event loop, so final post- MessageLoop::Run() work is done
340 - (void)terminate:(id)sender {
341 AppController* appController = static_cast<AppController*>([NSApp delegate]);
342 [appController tryToTerminateApplication:self];
343 // Return, don't exit. The application is responsible for exiting on its own.
346 - (void)cancelTerminate:(id)sender {
347 AppController* appController = static_cast<AppController*>([NSApp delegate]);
348 [appController stopTryingToTerminateApplication:self];
351 - (BOOL)sendAction:(SEL)anAction to:(id)aTarget from:(id)sender {
352 // The Dock menu contains an automagic section where you can select
353 // amongst open windows. If a window is closed via JavaScript while
354 // the menu is up, the menu item for that window continues to exist.
355 // When a window is selected this method is called with the
356 // now-freed window as |aTarget|. Short-circuit the call if
357 // |aTarget| is not a valid window.
358 if (anAction == @selector(_selectWindow:)) {
359 // Not using -[NSArray containsObject:] because |aTarget| may be a
362 for (NSWindow* window in [self windows]) {
363 if (window == aTarget) {
373 // When a Cocoa control is wired to a freed object, we get crashers
374 // in the call to |super| with no useful information in the
375 // backtrace. Attempt to add some useful information.
377 // If the action is something generic like -commandDispatch:, then
378 // the tag is essential.
380 if ([sender isKindOfClass:[NSControl class]]) {
382 if (tag == 0 || tag == -1) {
383 tag = [sender selectedTag];
385 } else if ([sender isKindOfClass:[NSMenuItem class]]) {
389 NSString* actionString = NSStringFromSelector(anAction);
390 std::string value = base::StringPrintf("%s tag %ld sending %s to %p",
391 [[sender className] UTF8String],
392 static_cast<long>(tag),
393 [actionString UTF8String],
396 base::debug::ScopedCrashKey key(crash_keys::mac::kSendAction, value);
398 // Certain third-party code, such as print drivers, can still throw
399 // exceptions and Chromium cannot fix them. This provides a way to
400 // work around those on a spot basis.
401 bool enableNSExceptions = false;
403 // http://crbug.com/80686 , an Epson printer driver.
404 if (anAction == @selector(selectPDE:)) {
405 enableNSExceptions = true;
408 // Minimize the window by keeping this close to the super call.
409 scoped_ptr<base::mac::ScopedNSExceptionEnabler> enabler;
410 if (enableNSExceptions)
411 enabler.reset(new base::mac::ScopedNSExceptionEnabler());
412 return [super sendAction:anAction to:aTarget from:sender];
415 - (BOOL)isHandlingSendEvent {
416 return handlingSendEvent_;
419 - (void)setHandlingSendEvent:(BOOL)handlingSendEvent {
420 handlingSendEvent_ = handlingSendEvent;
423 - (void)sendEvent:(NSEvent*)event {
424 base::mac::ScopedSendingEvent sendingEventScoper;
425 [super sendEvent:event];
428 // NSExceptions which are caught by the event loop are logged here.
429 // NSException uses setjmp/longjmp, which can be very bad for C++, so
430 // we attempt to track and report them.
431 - (void)reportException:(NSException *)anException {
432 // If we throw an exception in this code, we can create an infinite
433 // loop. If we throw out of the if() without resetting
434 // |reportException|, we'll stop reporting exceptions for this run.
435 static BOOL reportingException = NO;
436 DCHECK(!reportingException);
437 if (!reportingException) {
438 reportingException = YES;
439 chrome_browser_application_mac::RecordExceptionWithUma(anException);
441 // http://crbug.com/45928 is a bug about needing to double-close
442 // windows sometimes. One theory is that |-isHandlingSendEvent|
443 // gets latched to always return |YES|. Since scopers are used to
444 // manipulate that value, that should not be possible. One way to
445 // sidestep scopers is setjmp/longjmp (see above). The following
446 // is to "fix" this while the more fundamental concern is
447 // addressed elsewhere.
448 [self setHandlingSendEvent:NO];
450 // If |ScopedNSExceptionEnabler| is used to allow exceptions, and an
451 // uncaught exception is thrown, it will throw past all of the scopers.
452 // Reset the flag so that future exceptions are not masked.
453 base::mac::SetNSExceptionsAllowed(false);
455 // Store some human-readable information in breakpad keys in case
456 // there is a crash. Since breakpad does not provide infinite
457 // storage, we track two exceptions. The first exception thrown
458 // is tracked because it may be the one which caused the system to
459 // go off the rails. The last exception thrown is tracked because
460 // it may be the one most directly associated with the crash.
461 static BOOL trackedFirstException = NO;
463 const char* const kExceptionKey =
464 trackedFirstException ? crash_keys::mac::kLastNSException
465 : crash_keys::mac::kFirstNSException;
466 NSString* value = [NSString stringWithFormat:@"%@ reason %@",
467 [anException name], [anException reason]];
468 base::debug::SetCrashKeyValue(kExceptionKey, [value UTF8String]);
470 // Encode the callstack from point of throw.
471 // TODO(shess): Our swizzle plus the 23-frame limit plus Cocoa
472 // overhead may make this less than useful. If so, perhaps skip
473 // some items and/or use two keys.
474 const char* const kExceptionBtKey =
475 trackedFirstException ? crash_keys::mac::kLastNSExceptionTrace
476 : crash_keys::mac::kFirstNSExceptionTrace;
477 NSArray* addressArray = [anException callStackReturnAddresses];
478 NSUInteger addressCount = [addressArray count];
480 // SetCrashKeyFromAddresses() only encodes 23, so that's a natural limit.
481 const NSUInteger kAddressCountMax = 23;
482 void* addresses[kAddressCountMax];
483 if (addressCount > kAddressCountMax)
484 addressCount = kAddressCountMax;
486 for (NSUInteger i = 0; i < addressCount; ++i) {
487 addresses[i] = reinterpret_cast<void*>(
488 [[addressArray objectAtIndex:i] unsignedIntegerValue]);
490 base::debug::SetCrashKeyFromAddresses(
491 kExceptionBtKey, addresses, static_cast<size_t>(addressCount));
493 base::debug::ClearCrashKey(kExceptionBtKey);
495 trackedFirstException = YES;
497 reportingException = NO;
500 [super reportException:anException];
503 - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
504 if ([attribute isEqualToString:@"AXEnhancedUserInterface"] &&
505 [value intValue] == 1) {
506 content::BrowserAccessibilityState::GetInstance()->OnScreenReaderDetected();
507 for (TabContentsIterator it; !it.done(); it.Next()) {
508 if (content::WebContents* contents = *it)
509 if (content::RenderViewHost* rvh = contents->GetRenderViewHost())
510 rvh->EnableFullAccessibilityMode();
513 return [super accessibilitySetValue:value forAttribute:attribute];
516 - (void)_cycleWindowsReversed:(BOOL)arg1 {
517 base::AutoReset<BOOL> pin(&cyclingWindows_, YES);
518 [super _cycleWindowsReversed:arg1];
521 - (BOOL)isCyclingWindows {
522 return cyclingWindows_;
525 - (id)_removeWindow:(NSWindow*)window {
527 base::AutoLock lock(previousKeyWindowsLock_);
528 [self removePreviousKeyWindow:window];
530 id result = [super _removeWindow:window];
532 // Ensure app has a key window after a window is removed.
533 // OS wants to make a panel browser window key after closing an app window
534 // because panels use a higher priority window level, but panel windows may
535 // refuse to become key, leaving the app with no key window. The OS does
536 // not seem to consider other windows after the first window chosen refuses
537 // to become key. Force consideration of other windows here.
538 if ([self isActive] && [self keyWindow] == nil) {
540 [self makeWindowsPerform:@selector(canBecomeKeyWindow) inOrder:YES];
544 // Return result from the super class. It appears to be the app that
545 // owns the removed window (determined via experimentation).
549 - (id)_setKeyWindow:(NSWindow*)window {
550 // |window| is nil when the current key window is being closed.
551 // A separate call follows with a new value when a new key window is set.
552 // Closed windows are not tracked in previousKeyWindows_.
554 base::AutoLock lock(previousKeyWindowsLock_);
555 [self removePreviousKeyWindow:window];
556 NSWindow* currentKeyWindow = [self keyWindow];
557 if (currentKeyWindow != nil && currentKeyWindow != window)
558 previousKeyWindows_.push_back(currentKeyWindow);
561 return [super _setKeyWindow:window];
564 - (NSWindow*)previousKeyWindow {
565 base::AutoLock lock(previousKeyWindowsLock_);
566 return previousKeyWindows_.empty() ? nil : previousKeyWindows_.back();
569 - (void)removePreviousKeyWindow:(NSWindow*)window {
570 previousKeyWindowsLock_.AssertAcquired();
571 std::vector<NSWindow*>::iterator window_iterator =
572 std::find(previousKeyWindows_.begin(),
573 previousKeyWindows_.end(),
575 if (window_iterator != previousKeyWindows_.end()) {
576 previousKeyWindows_.erase(window_iterator);