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.
4
5#import "chrome/browser/chrome_browser_application_mac.h"
6
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/mac/scoped_objc_class_swizzler.h"
14#import "base/metrics/histogram.h"
15#include "base/strings/stringprintf.h"
16#import "base/strings/sys_string_conversions.h"
17#import "chrome/browser/app_controller_mac.h"
18#include "chrome/browser/ui/tab_contents/tab_contents_iterator.h"
19#include "chrome/common/crash_keys.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"
24
25namespace {
26
27// Tracking for cases being hit by -crInitWithName:reason:userInfo:.
28enum ExceptionEventType {
29  EXCEPTION_ACCESSIBILITY = 0,
30  EXCEPTION_MENU_ITEM_BOUNDS_CHECK,
31  EXCEPTION_VIEW_NOT_IN_WINDOW,
32  EXCEPTION_NSURL_INIT_NIL,
33  EXCEPTION_NSDATADETECTOR_NIL_STRING,
34
35  // Always keep this at the end.
36  EXCEPTION_MAX,
37};
38
39void RecordExceptionEvent(ExceptionEventType event_type) {
40  UMA_HISTOGRAM_ENUMERATION("OSX.ExceptionHandlerEvents",
41                            event_type, EXCEPTION_MAX);
42}
43
44}  // namespace
45
46// The implementation of NSExceptions break various assumptions in the
47// Chrome code.  This category defines a replacement for
48// -initWithName:reason:userInfo: for purposes of forcing a break in
49// the debugger when an exception is raised.  -raise sounds more
50// obvious to intercept, but it doesn't catch the original throw
51// because the objc runtime doesn't use it.
52@interface NSException (CrNSExceptionSwizzle)
53- (id)crInitWithName:(NSString*)aName
54              reason:(NSString*)aReason
55            userInfo:(NSDictionary*)someUserInfo;
56@end
57
58static IMP gOriginalInitIMP = NULL;
59
60@implementation NSException (CrNSExceptionSwizzle)
61- (id)crInitWithName:(NSString*)aName
62              reason:(NSString*)aReason
63            userInfo:(NSDictionary*)someUserInfo {
64  // Method only called when swizzled.
65  DCHECK(_cmd == @selector(initWithName:reason:userInfo:));
66  DCHECK(gOriginalInitIMP);
67
68  // Parts of Cocoa rely on creating and throwing exceptions. These are not
69  // worth bugging-out over. It is very important that there be zero chance that
70  // any Chromium code is on the stack; these must be created by Apple code and
71  // then immediately consumed by Apple code.
72  static NSString* const kAcceptableNSExceptionNames[] = {
73    // If an object does not support an accessibility attribute, this will
74    // get thrown.
75    NSAccessibilityException,
76  };
77
78  BOOL found = NO;
79  for (size_t i = 0; i < arraysize(kAcceptableNSExceptionNames); ++i) {
80    if (aName == kAcceptableNSExceptionNames[i]) {
81      found = YES;
82      RecordExceptionEvent(EXCEPTION_ACCESSIBILITY);
83      break;
84    }
85  }
86
87  if (!found) {
88    // Update breakpad with the exception info.
89    std::string value = base::StringPrintf("%s reason %s",
90        [aName UTF8String], [aReason UTF8String]);
91    base::debug::SetCrashKeyValue(crash_keys::mac::kNSException, value);
92    base::debug::SetCrashKeyToStackTrace(crash_keys::mac::kNSExceptionTrace,
93                                         base::debug::StackTrace());
94
95    // Force crash for selected exceptions to generate crash dumps.
96    BOOL fatal = NO;
97    if (aName == NSInternalInconsistencyException) {
98      NSString* const kNSMenuItemArrayBoundsCheck =
99          @"Invalid parameter not satisfying: (index >= 0) && "
100          @"(index < [_itemArray count])";
101      if ([aReason isEqualToString:kNSMenuItemArrayBoundsCheck]) {
102        RecordExceptionEvent(EXCEPTION_MENU_ITEM_BOUNDS_CHECK);
103        fatal = YES;
104      }
105
106      NSString* const kNoWindowCheck = @"View is not in any window";
107      if ([aReason isEqualToString:kNoWindowCheck]) {
108        RecordExceptionEvent(EXCEPTION_VIEW_NOT_IN_WINDOW);
109        fatal = YES;
110      }
111    }
112
113    // Mostly "unrecognized selector sent to (instance|class)".  A
114    // very small number of things like inappropriate nil being passed.
115    if (aName == NSInvalidArgumentException) {
116      fatal = YES;
117
118      // TODO(shess): http://crbug.com/85463 throws this exception
119      // from ImageKit.  Our code is not on the stack, so it needs to
120      // be whitelisted for now.
121      NSString* const kNSURLInitNilCheck =
122          @"*** -[NSURL initFileURLWithPath:isDirectory:]: "
123          @"nil string parameter";
124      if ([aReason isEqualToString:kNSURLInitNilCheck]) {
125        RecordExceptionEvent(EXCEPTION_NSURL_INIT_NIL);
126        fatal = NO;
127      }
128
129      // TODO(shess): <http://crbug.com/316759> OSX 10.9 is failing
130      // trying to extract structure from a string.
131      NSString* const kNSDataDetectorNilCheck =
132          @"*** -[NSDataDetector enumerateMatchesInString:"
133          @"options:range:usingBlock:]: nil argument";
134      if ([aReason isEqualToString:kNSDataDetectorNilCheck]) {
135        RecordExceptionEvent(EXCEPTION_NSDATADETECTOR_NIL_STRING);
136        fatal = NO;
137      }
138    }
139
140    // Dear reader: Something you just did provoked an NSException.
141    // NSException is implemented in terms of setjmp()/longjmp(),
142    // which does poor things when combined with C++ scoping
143    // (destructors are skipped).  Chrome should be NSException-free,
144    // please check your backtrace and see if you can't file a bug
145    // with a repro case.
146    const bool allow = base::mac::GetNSExceptionsAllowed();
147    if (fatal && !allow) {
148      LOG(FATAL) << "Someone is trying to raise an exception!  "
149                 << value;
150    } else {
151      // Make sure that developers see when their code throws
152      // exceptions.
153      DCHECK(allow) << "Someone is trying to raise an exception!  "
154                    << value;
155    }
156  }
157
158  // Forward to the original version.
159  return gOriginalInitIMP(self, _cmd, aName, aReason, someUserInfo);
160}
161@end
162
163namespace chrome_browser_application_mac {
164
165// Maximum number of known named exceptions we'll support.  There is
166// no central registration, but I only find about 75 possibilities in
167// the system frameworks, and many of them are probably not
168// interesting to track in aggregate (those relating to distributed
169// objects, for instance).
170const size_t kKnownNSExceptionCount = 25;
171
172const size_t kUnknownNSException = kKnownNSExceptionCount;
173
174size_t BinForException(NSException* exception) {
175  // A list of common known exceptions.  The list position will
176  // determine where they live in the histogram, so never move them
177  // around, only add to the end.
178  static NSString* const kKnownNSExceptionNames[] = {
179    // Grab-bag exception, not very common.  CFArray (or other
180    // container) mutated while being enumerated is one case seen in
181    // production.
182    NSGenericException,
183
184    // Out-of-range on NSString or NSArray.  Quite common.
185    NSRangeException,
186
187    // Invalid arg to method, unrecognized selector.  Quite common.
188    NSInvalidArgumentException,
189
190    // malloc() returned null in object creation, I think.  Turns out
191    // to be very uncommon in production, because of the OOM killer.
192    NSMallocException,
193
194    // This contains things like windowserver errors, trying to draw
195    // views which aren't in windows, unable to read nib files.  By
196    // far the most common exception seen on the crash server.
197    NSInternalInconsistencyException,
198
199    nil
200  };
201
202  // Make sure our array hasn't outgrown our abilities to track it.
203  DCHECK_LE(arraysize(kKnownNSExceptionNames), kKnownNSExceptionCount);
204
205  NSString* name = [exception name];
206  for (int i = 0; kKnownNSExceptionNames[i]; ++i) {
207    if (name == kKnownNSExceptionNames[i]) {
208      return i;
209    }
210  }
211  return kUnknownNSException;
212}
213
214void RecordExceptionWithUma(NSException* exception) {
215  UMA_HISTOGRAM_ENUMERATION("OSX.NSException",
216      BinForException(exception), kUnknownNSException);
217}
218
219void RegisterBrowserCrApp() {
220  [BrowserCrApplication sharedApplication];
221};
222
223void Terminate() {
224  [NSApp terminate:nil];
225}
226
227void CancelTerminate() {
228  [NSApp cancelTerminate:nil];
229}
230
231}  // namespace chrome_browser_application_mac
232
233namespace {
234
235void SwizzleInit() {
236  // Do-nothing wrapper so that we can arrange to only swizzle
237  // -[NSException raise] when DCHECK() is turned on (as opposed to
238  // replicating the preprocess logic which turns DCHECK() on).
239  CR_DEFINE_STATIC_LOCAL(base::mac::ScopedObjCClassSwizzler,
240                         swizzle_exception,
241                         ([NSException class],
242                          @selector(initWithName:reason:userInfo:),
243                          @selector(crInitWithName:reason:userInfo:)));
244  gOriginalInitIMP = swizzle_exception.GetOriginalImplementation();
245}
246
247}  // namespace
248
249// These methods are being exposed for the purposes of overriding.
250// Used to determine when a Panel window can become the key window.
251@interface NSApplication (PanelsCanBecomeKey)
252- (void)_cycleWindowsReversed:(BOOL)arg1;
253- (id)_removeWindow:(NSWindow*)window;
254- (id)_setKeyWindow:(NSWindow*)window;
255@end
256
257@interface BrowserCrApplication (PrivateInternal)
258
259// This must be called under the protection of previousKeyWindowsLock_.
260- (void)removePreviousKeyWindow:(NSWindow*)window;
261
262@end
263
264@implementation BrowserCrApplication
265
266+ (void)initialize {
267  // Turn all deallocated Objective-C objects into zombies, keeping
268  // the most recent 10,000 of them on the treadmill.
269  ObjcEvilDoers::ZombieEnable(true, 10000);
270}
271
272- (id)init {
273  SwizzleInit();
274  self = [super init];
275
276  // Sanity check to alert if overridden methods are not supported.
277  DCHECK([NSApplication
278      instancesRespondToSelector:@selector(_cycleWindowsReversed:)]);
279  DCHECK([NSApplication
280      instancesRespondToSelector:@selector(_removeWindow:)]);
281  DCHECK([NSApplication
282      instancesRespondToSelector:@selector(_setKeyWindow:)]);
283
284  return self;
285}
286
287// Initialize NSApplication using the custom subclass.  Check whether NSApp
288// was already initialized using another class, because that would break
289// some things.
290+ (NSApplication*)sharedApplication {
291  NSApplication* app = [super sharedApplication];
292
293  // +sharedApplication initializes the global NSApp, so if a specific
294  // NSApplication subclass is requested, require that to be the one
295  // delivered.  The practical effect is to require a consistent NSApp
296  // across the executable.
297  CHECK([NSApp isKindOfClass:self])
298      << "NSApp must be of type " << [[self className] UTF8String]
299      << ", not " << [[NSApp className] UTF8String];
300
301  // If the message loop was initialized before NSApp is setup, the
302  // message pump will be setup incorrectly.  Failing this implies
303  // that RegisterBrowserCrApp() should be called earlier.
304  CHECK(base::MessagePumpMac::UsingCrApp())
305      << "MessagePumpMac::Create() is using the wrong pump implementation"
306      << " for " << [[self className] UTF8String];
307
308  return app;
309}
310
311////////////////////////////////////////////////////////////////////////////////
312// HISTORICAL COMMENT (by viettrungluu, from
313// http://codereview.chromium.org/1520006 with mild editing):
314//
315// A quick summary of the state of things (before the changes to shutdown):
316//
317// Currently, we are totally hosed (put in a bad state in which Cmd-W does the
318// wrong thing, and which will probably eventually lead to a crash) if we begin
319// quitting but termination is aborted for some reason.
320//
321// I currently know of two ways in which termination can be aborted:
322// (1) Common case: a window has an onbeforeunload handler which pops up a
323//     "leave web page" dialog, and the user answers "no, don't leave".
324// (2) Uncommon case: popups are enabled (in Content Settings, i.e., the popup
325//     blocker is disabled), and some nasty web page pops up a new window on
326//     closure.
327//
328// I don't know of other ways in which termination can be aborted, but they may
329// exist (or may be added in the future, for that matter).
330//
331// My CL [see above] does the following:
332// a. Should prevent being put in a bad state (which breaks Cmd-W and leads to
333//    crash) under all circumstances.
334// b. Should completely handle (1) properly.
335// c. Doesn't (yet) handle (2) properly and puts it in a weird state (but not
336//    that bad).
337// d. Any other ways of aborting termination would put it in that weird state.
338//
339// c. can be fixed by having the global flag reset on browser creation or
340// similar (and doing so might also fix some possible d.'s as well). I haven't
341// done this yet since I haven't thought about it carefully and since it's a
342// corner case.
343//
344// The weird state: a state in which closing the last window quits the browser.
345// This might be a bit annoying, but it's not dangerous in any way.
346////////////////////////////////////////////////////////////////////////////////
347
348// |-terminate:| is the entry point for orderly "quit" operations in Cocoa. This
349// includes the application menu's quit menu item and keyboard equivalent, the
350// application's dock icon menu's quit menu item, "quit" (not "force quit") in
351// the Activity Monitor, and quits triggered by user logout and system restart
352// and shutdown.
353//
354// The default |-terminate:| implementation ends the process by calling exit(),
355// and thus never leaves the main run loop. This is unsuitable for Chrome since
356// Chrome depends on leaving the main run loop to perform an orderly shutdown.
357// We support the normal |-terminate:| interface by overriding the default
358// implementation. Our implementation, which is very specific to the needs of
359// Chrome, works by asking the application delegate to terminate using its
360// |-tryToTerminateApplication:| method.
361//
362// |-tryToTerminateApplication:| differs from the standard
363// |-applicationShouldTerminate:| in that no special event loop is run in the
364// case that immediate termination is not possible (e.g., if dialog boxes
365// allowing the user to cancel have to be shown). Instead, this method sets a
366// flag and tries to close all browsers. This flag causes the closure of the
367// final browser window to begin actual tear-down of the application.
368// Termination is cancelled by resetting this flag. The standard
369// |-applicationShouldTerminate:| is not supported, and code paths leading to it
370// must be redirected.
371//
372// When the last browser has been destroyed, the BrowserList calls
373// chrome::OnAppExiting(), which is the point of no return. That will cause
374// the NSApplicationWillTerminateNotification to be posted, which ends the
375// NSApplication event loop, so final post- MessageLoop::Run() work is done
376// before exiting.
377- (void)terminate:(id)sender {
378  AppController* appController = static_cast<AppController*>([NSApp delegate]);
379  [appController tryToTerminateApplication:self];
380  // Return, don't exit. The application is responsible for exiting on its own.
381}
382
383- (void)cancelTerminate:(id)sender {
384  AppController* appController = static_cast<AppController*>([NSApp delegate]);
385  [appController stopTryingToTerminateApplication:self];
386}
387
388- (BOOL)sendAction:(SEL)anAction to:(id)aTarget from:(id)sender {
389  // The Dock menu contains an automagic section where you can select
390  // amongst open windows.  If a window is closed via JavaScript while
391  // the menu is up, the menu item for that window continues to exist.
392  // When a window is selected this method is called with the
393  // now-freed window as |aTarget|.  Short-circuit the call if
394  // |aTarget| is not a valid window.
395  if (anAction == @selector(_selectWindow:)) {
396    // Not using -[NSArray containsObject:] because |aTarget| may be a
397    // freed object.
398    BOOL found = NO;
399    for (NSWindow* window in [self windows]) {
400      if (window == aTarget) {
401        found = YES;
402        break;
403      }
404    }
405    if (!found) {
406      return NO;
407    }
408  }
409
410  // When a Cocoa control is wired to a freed object, we get crashers
411  // in the call to |super| with no useful information in the
412  // backtrace.  Attempt to add some useful information.
413
414  // If the action is something generic like -commandDispatch:, then
415  // the tag is essential.
416  NSInteger tag = 0;
417  if ([sender isKindOfClass:[NSControl class]]) {
418    tag = [sender tag];
419    if (tag == 0 || tag == -1) {
420      tag = [sender selectedTag];
421    }
422  } else if ([sender isKindOfClass:[NSMenuItem class]]) {
423    tag = [sender tag];
424  }
425
426  NSString* actionString = NSStringFromSelector(anAction);
427  std::string value = base::StringPrintf("%s tag %ld sending %s to %p",
428      [[sender className] UTF8String],
429      static_cast<long>(tag),
430      [actionString UTF8String],
431      aTarget);
432
433  base::debug::ScopedCrashKey key(crash_keys::mac::kSendAction, value);
434
435  // Certain third-party code, such as print drivers, can still throw
436  // exceptions and Chromium cannot fix them.  This provides a way to
437  // work around those on a spot basis.
438  bool enableNSExceptions = false;
439
440  // http://crbug.com/80686 , an Epson printer driver.
441  if (anAction == @selector(selectPDE:)) {
442    enableNSExceptions = true;
443  }
444
445  // Minimize the window by keeping this close to the super call.
446  scoped_ptr<base::mac::ScopedNSExceptionEnabler> enabler;
447  if (enableNSExceptions)
448    enabler.reset(new base::mac::ScopedNSExceptionEnabler());
449  return [super sendAction:anAction to:aTarget from:sender];
450}
451
452- (BOOL)isHandlingSendEvent {
453  return handlingSendEvent_;
454}
455
456- (void)setHandlingSendEvent:(BOOL)handlingSendEvent {
457  handlingSendEvent_ = handlingSendEvent;
458}
459
460- (void)sendEvent:(NSEvent*)event {
461  base::mac::ScopedSendingEvent sendingEventScoper;
462  [super sendEvent:event];
463}
464
465// NSExceptions which are caught by the event loop are logged here.
466// NSException uses setjmp/longjmp, which can be very bad for C++, so
467// we attempt to track and report them.
468- (void)reportException:(NSException *)anException {
469  // If we throw an exception in this code, we can create an infinite
470  // loop.  If we throw out of the if() without resetting
471  // |reportException|, we'll stop reporting exceptions for this run.
472  static BOOL reportingException = NO;
473  DCHECK(!reportingException);
474  if (!reportingException) {
475    reportingException = YES;
476    chrome_browser_application_mac::RecordExceptionWithUma(anException);
477
478    // http://crbug.com/45928 is a bug about needing to double-close
479    // windows sometimes.  One theory is that |-isHandlingSendEvent|
480    // gets latched to always return |YES|.  Since scopers are used to
481    // manipulate that value, that should not be possible.  One way to
482    // sidestep scopers is setjmp/longjmp (see above).  The following
483    // is to "fix" this while the more fundamental concern is
484    // addressed elsewhere.
485    [self setHandlingSendEvent:NO];
486
487    // If |ScopedNSExceptionEnabler| is used to allow exceptions, and an
488    // uncaught exception is thrown, it will throw past all of the scopers.
489    // Reset the flag so that future exceptions are not masked.
490    base::mac::SetNSExceptionsAllowed(false);
491
492    // Store some human-readable information in breakpad keys in case
493    // there is a crash.  Since breakpad does not provide infinite
494    // storage, we track two exceptions.  The first exception thrown
495    // is tracked because it may be the one which caused the system to
496    // go off the rails.  The last exception thrown is tracked because
497    // it may be the one most directly associated with the crash.
498    static BOOL trackedFirstException = NO;
499
500    const char* const kExceptionKey =
501        trackedFirstException ? crash_keys::mac::kLastNSException
502                              : crash_keys::mac::kFirstNSException;
503    NSString* value = [NSString stringWithFormat:@"%@ reason %@",
504                                [anException name], [anException reason]];
505    base::debug::SetCrashKeyValue(kExceptionKey, [value UTF8String]);
506
507    // Encode the callstack from point of throw.
508    // TODO(shess): Our swizzle plus the 23-frame limit plus Cocoa
509    // overhead may make this less than useful.  If so, perhaps skip
510    // some items and/or use two keys.
511    const char* const kExceptionBtKey =
512        trackedFirstException ? crash_keys::mac::kLastNSExceptionTrace
513                              : crash_keys::mac::kFirstNSExceptionTrace;
514    NSArray* addressArray = [anException callStackReturnAddresses];
515    NSUInteger addressCount = [addressArray count];
516    if (addressCount) {
517      // SetCrashKeyFromAddresses() only encodes 23, so that's a natural limit.
518      const NSUInteger kAddressCountMax = 23;
519      void* addresses[kAddressCountMax];
520      if (addressCount > kAddressCountMax)
521        addressCount = kAddressCountMax;
522
523      for (NSUInteger i = 0; i < addressCount; ++i) {
524        addresses[i] = reinterpret_cast<void*>(
525            [[addressArray objectAtIndex:i] unsignedIntegerValue]);
526      }
527      base::debug::SetCrashKeyFromAddresses(
528          kExceptionBtKey, addresses, static_cast<size_t>(addressCount));
529    } else {
530      base::debug::ClearCrashKey(kExceptionBtKey);
531    }
532    trackedFirstException = YES;
533
534    reportingException = NO;
535  }
536
537  [super reportException:anException];
538}
539
540- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
541  if ([attribute isEqualToString:@"AXEnhancedUserInterface"] &&
542      [value intValue] == 1) {
543    content::BrowserAccessibilityState::GetInstance()->OnScreenReaderDetected();
544  }
545  return [super accessibilitySetValue:value forAttribute:attribute];
546}
547
548- (void)_cycleWindowsReversed:(BOOL)arg1 {
549  base::AutoReset<BOOL> pin(&cyclingWindows_, YES);
550  [super _cycleWindowsReversed:arg1];
551}
552
553- (BOOL)isCyclingWindows {
554  return cyclingWindows_;
555}
556
557- (id)_removeWindow:(NSWindow*)window {
558  {
559    base::AutoLock lock(previousKeyWindowsLock_);
560    [self removePreviousKeyWindow:window];
561  }
562  id result = [super _removeWindow:window];
563
564  // Ensure app has a key window after a window is removed.
565  // OS wants to make a panel browser window key after closing an app window
566  // because panels use a higher priority window level, but panel windows may
567  // refuse to become key, leaving the app with no key window. The OS does
568  // not seem to consider other windows after the first window chosen refuses
569  // to become key. Force consideration of other windows here.
570  if ([self isActive] && [self keyWindow] == nil) {
571    NSWindow* key =
572        [self makeWindowsPerform:@selector(canBecomeKeyWindow) inOrder:YES];
573    [key makeKeyWindow];
574  }
575
576  // Return result from the super class. It appears to be the app that
577  // owns the removed window (determined via experimentation).
578  return result;
579}
580
581- (id)_setKeyWindow:(NSWindow*)window {
582  // |window| is nil when the current key window is being closed.
583  // A separate call follows with a new value when a new key window is set.
584  // Closed windows are not tracked in previousKeyWindows_.
585  if (window != nil) {
586    base::AutoLock lock(previousKeyWindowsLock_);
587    [self removePreviousKeyWindow:window];
588    NSWindow* currentKeyWindow = [self keyWindow];
589    if (currentKeyWindow != nil && currentKeyWindow != window)
590      previousKeyWindows_.push_back(currentKeyWindow);
591  }
592
593  return [super _setKeyWindow:window];
594}
595
596- (NSWindow*)previousKeyWindow {
597  base::AutoLock lock(previousKeyWindowsLock_);
598  return previousKeyWindows_.empty() ? nil : previousKeyWindows_.back();
599}
600
601- (void)removePreviousKeyWindow:(NSWindow*)window {
602  previousKeyWindowsLock_.AssertAcquired();
603  std::vector<NSWindow*>::iterator window_iterator =
604      std::find(previousKeyWindows_.begin(),
605                previousKeyWindows_.end(),
606                window);
607  if (window_iterator != previousKeyWindows_.end()) {
608    previousKeyWindows_.erase(window_iterator);
609  }
610}
611
612@end
613