Home | History | Annotate | Download | only in browser
      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 
     25 namespace {
     26 
     27 // Tracking for cases being hit by -crInitWithName:reason:userInfo:.
     28 enum 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 
     39 void 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 
     58 static 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 
    163 namespace 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).
    170 const size_t kKnownNSExceptionCount = 25;
    171 
    172 const size_t kUnknownNSException = kKnownNSExceptionCount;
    173 
    174 size_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 
    214 void RecordExceptionWithUma(NSException* exception) {
    215   UMA_HISTOGRAM_ENUMERATION("OSX.NSException",
    216       BinForException(exception), kUnknownNSException);
    217 }
    218 
    219 void RegisterBrowserCrApp() {
    220   [BrowserCrApplication sharedApplication];
    221 };
    222 
    223 void Terminate() {
    224   [NSApp terminate:nil];
    225 }
    226 
    227 void CancelTerminate() {
    228   [NSApp cancelTerminate:nil];
    229 }
    230 
    231 }  // namespace chrome_browser_application_mac
    232 
    233 namespace {
    234 
    235 void 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