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