| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/message_loop/message_pump_mac.h" |
| |
| #include "base/bind.h" |
| #include "base/cancelable_callback.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #import "base/mac/scoped_nsobject.h" |
| #include "base/macros.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/message_loop/message_loop_current.h" |
| #include "base/test/bind_test_util.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| @interface TestModalAlertCloser : NSObject |
| - (void)runTestThenCloseAlert:(NSAlert*)alert; |
| @end |
| |
| namespace { |
| |
| // Internal constants from message_pump_mac.mm. |
| constexpr int kAllModesMask = 0xf; |
| constexpr int kNSApplicationModalSafeModeMask = 0x3; |
| |
| } // namespace |
| |
| namespace base { |
| |
| class TestMessagePumpCFRunLoopBase { |
| public: |
| bool TestCanInvalidateTimers() { |
| return MessagePumpCFRunLoopBase::CanInvalidateCFRunLoopTimers(); |
| } |
| static void SetTimerValid(CFRunLoopTimerRef timer, bool valid) { |
| MessagePumpCFRunLoopBase::ChromeCFRunLoopTimerSetValid(timer, valid); |
| } |
| |
| static void PerformTimerCallback(CFRunLoopTimerRef timer, void* info) { |
| TestMessagePumpCFRunLoopBase* self = |
| static_cast<TestMessagePumpCFRunLoopBase*>(info); |
| self->timer_callback_called_ = true; |
| |
| if (self->invalidate_timer_in_callback_) { |
| SetTimerValid(timer, false); |
| } |
| } |
| |
| bool invalidate_timer_in_callback_; |
| |
| bool timer_callback_called_; |
| }; |
| |
| TEST(MessagePumpMacTest, TestCanInvalidateTimers) { |
| TestMessagePumpCFRunLoopBase message_pump_test; |
| |
| // Catch whether or not the use of private API ever starts failing. |
| EXPECT_TRUE(message_pump_test.TestCanInvalidateTimers()); |
| } |
| |
| TEST(MessagePumpMacTest, TestInvalidatedTimerReuse) { |
| TestMessagePumpCFRunLoopBase message_pump_test; |
| |
| CFRunLoopTimerContext timer_context = CFRunLoopTimerContext(); |
| timer_context.info = &message_pump_test; |
| const CFTimeInterval kCFTimeIntervalMax = |
| std::numeric_limits<CFTimeInterval>::max(); |
| ScopedCFTypeRef<CFRunLoopTimerRef> test_timer(CFRunLoopTimerCreate( |
| NULL, // allocator |
| kCFTimeIntervalMax, // fire time |
| kCFTimeIntervalMax, // interval |
| 0, // flags |
| 0, // priority |
| TestMessagePumpCFRunLoopBase::PerformTimerCallback, &timer_context)); |
| CFRunLoopAddTimer(CFRunLoopGetCurrent(), test_timer, |
| kMessageLoopExclusiveRunLoopMode); |
| |
| // Sanity check. |
| EXPECT_TRUE(CFRunLoopTimerIsValid(test_timer)); |
| |
| // Confirm that the timer fires as expected, and that it's not a one-time-use |
| // timer (those timers are invalidated after they fire). |
| CFAbsoluteTime next_fire_time = CFAbsoluteTimeGetCurrent() + 0.01; |
| CFRunLoopTimerSetNextFireDate(test_timer, next_fire_time); |
| message_pump_test.timer_callback_called_ = false; |
| message_pump_test.invalidate_timer_in_callback_ = false; |
| CFRunLoopRunInMode(kMessageLoopExclusiveRunLoopMode, 0.02, true); |
| EXPECT_TRUE(message_pump_test.timer_callback_called_); |
| EXPECT_TRUE(CFRunLoopTimerIsValid(test_timer)); |
| |
| // As a repeating timer, the timer should have a new fire date set in the |
| // future. |
| EXPECT_GT(CFRunLoopTimerGetNextFireDate(test_timer), next_fire_time); |
| |
| // Try firing the timer, and invalidating it within its callback. |
| next_fire_time = CFAbsoluteTimeGetCurrent() + 0.01; |
| CFRunLoopTimerSetNextFireDate(test_timer, next_fire_time); |
| message_pump_test.timer_callback_called_ = false; |
| message_pump_test.invalidate_timer_in_callback_ = true; |
| CFRunLoopRunInMode(kMessageLoopExclusiveRunLoopMode, 0.02, true); |
| EXPECT_TRUE(message_pump_test.timer_callback_called_); |
| EXPECT_FALSE(CFRunLoopTimerIsValid(test_timer)); |
| |
| // The CFRunLoop believes the timer is invalid, so it should not have a |
| // fire date. |
| EXPECT_EQ(0, CFRunLoopTimerGetNextFireDate(test_timer)); |
| |
| // Now mark the timer as valid and confirm that it still fires correctly. |
| TestMessagePumpCFRunLoopBase::SetTimerValid(test_timer, true); |
| EXPECT_TRUE(CFRunLoopTimerIsValid(test_timer)); |
| next_fire_time = CFAbsoluteTimeGetCurrent() + 0.01; |
| CFRunLoopTimerSetNextFireDate(test_timer, next_fire_time); |
| message_pump_test.timer_callback_called_ = false; |
| message_pump_test.invalidate_timer_in_callback_ = false; |
| CFRunLoopRunInMode(kMessageLoopExclusiveRunLoopMode, 0.02, true); |
| EXPECT_TRUE(message_pump_test.timer_callback_called_); |
| EXPECT_TRUE(CFRunLoopTimerIsValid(test_timer)); |
| |
| // Confirm that the run loop again gave it a new fire date in the future. |
| EXPECT_GT(CFRunLoopTimerGetNextFireDate(test_timer), next_fire_time); |
| |
| CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), test_timer, |
| kMessageLoopExclusiveRunLoopMode); |
| } |
| |
| namespace { |
| |
| // PostedTasks are only executed while the message pump has a delegate. That is, |
| // when a base::RunLoop is running, so in order to test whether posted tasks |
| // are run by CFRunLoopRunInMode and *not* by the regular RunLoop, we need to |
| // be inside a task that is also calling CFRunLoopRunInMode. |
| // This function posts |task| and runs the given |mode|. |
| void RunTaskInMode(CFRunLoopMode mode, OnceClosure task) { |
| // Since this task is "ours" rather than a system task, allow nesting. |
| MessageLoopCurrent::ScopedNestableTaskAllower allow; |
| CancelableOnceClosure cancelable(std::move(task)); |
| ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, cancelable.callback()); |
| while (CFRunLoopRunInMode(mode, 0, true) == kCFRunLoopRunHandledSource) |
| ; |
| } |
| |
| } // namespace |
| |
| // Tests the correct behavior of ScopedPumpMessagesInPrivateModes. |
| TEST(MessagePumpMacTest, ScopedPumpMessagesInPrivateModes) { |
| MessageLoopForUI message_loop; |
| |
| CFRunLoopMode kRegular = kCFRunLoopDefaultMode; |
| CFRunLoopMode kPrivate = CFSTR("NSUnhighlightMenuRunLoopMode"); |
| |
| // Work is seen when running in the default mode. |
| ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| BindOnce(&RunTaskInMode, kRegular, MakeExpectedRunClosure(FROM_HERE))); |
| EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); |
| |
| // But not seen when running in a private mode. |
| ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| BindOnce(&RunTaskInMode, kPrivate, MakeExpectedNotRunClosure(FROM_HERE))); |
| EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); |
| |
| { |
| ScopedPumpMessagesInPrivateModes allow_private; |
| // Now the work should be seen. |
| ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| BindOnce(&RunTaskInMode, kPrivate, MakeExpectedRunClosure(FROM_HERE))); |
| EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); |
| |
| // The regular mode should also work the same. |
| ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| BindOnce(&RunTaskInMode, kRegular, MakeExpectedRunClosure(FROM_HERE))); |
| EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); |
| } |
| |
| // And now the scoper is out of scope, private modes should no longer see it. |
| ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| BindOnce(&RunTaskInMode, kPrivate, MakeExpectedNotRunClosure(FROM_HERE))); |
| EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); |
| |
| // Only regular modes see it. |
| ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| BindOnce(&RunTaskInMode, kRegular, MakeExpectedRunClosure(FROM_HERE))); |
| EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); |
| } |
| |
| // Tests that private message loop modes are not pumped while a modal dialog is |
| // present. |
| TEST(MessagePumpMacTest, ScopedPumpMessagesAttemptWithModalDialog) { |
| MessageLoopForUI message_loop; |
| |
| { |
| base::ScopedPumpMessagesInPrivateModes allow_private; |
| // No modal window, so all modes should be pumped. |
| EXPECT_EQ(kAllModesMask, allow_private.GetModeMaskForTest()); |
| } |
| |
| base::scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]); |
| [alert addButtonWithTitle:@"OK"]; |
| base::scoped_nsobject<TestModalAlertCloser> closer( |
| [[TestModalAlertCloser alloc] init]); |
| [closer performSelector:@selector(runTestThenCloseAlert:) |
| withObject:alert |
| afterDelay:0 |
| inModes:@[ NSModalPanelRunLoopMode ]]; |
| NSInteger result = [alert runModal]; |
| EXPECT_EQ(NSAlertFirstButtonReturn, result); |
| } |
| |
| // This is a regression test for a scenario where the invalidation of the |
| // delayed work timer (using non-public APIs) causes a nested native run loop to |
| // hang. The exact root cause of the hang is unknown since it involves the |
| // closed-source Core Foundation runtime, but the steps needed to trigger it |
| // are: |
| // |
| // 1. Post a delayed task that will run some time after step #4. |
| // 2. Allow Chrome tasks to run in nested run loops (with |
| // ScopedNestableTaskAllower). |
| // 3. Allow running Chrome tasks during private run loop modes (with |
| // ScopedPumpMessagesInPrivateModes). |
| // 4. Open a pop-up menu via [NSMenu popupContextMenu]. This will start a |
| // private native run loop to process menu interaction. |
| // 5. In a posted task, close the menu with [NSMenu cancelTracking]. |
| // |
| // At this point the menu closes visually but the nested run loop (flakily) |
| // hangs forever in a live-lock, i.e., Chrome tasks keep executing but the |
| // NSMenu call in #4 never returns. |
| // |
| // The workaround is to avoid timer invalidation during nested native run loops. |
| // |
| // DANGER: As the pop-up menu captures keyboard input, the bug will make the |
| // machine's keyboard inoperable during the live-lock. Use a TTY-based remote |
| // terminal such as SSH (as opposed to Chromoting) to investigate the issue. |
| // |
| TEST(MessagePumpMacTest, DontInvalidateTimerInNativeRunLoop) { |
| MessageLoopForUI message_loop; |
| NSWindow* window = |
| [[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100) |
| styleMask:NSBorderlessWindowMask |
| backing:NSBackingStoreBuffered |
| defer:NO] autorelease]; |
| NSMenu* menu = [[NSMenu alloc] initWithTitle:@"Test menu"]; |
| [menu insertItemWithTitle:@"Dummy item" |
| action:@selector(dummy) |
| keyEquivalent:@"a" |
| atIndex:0]; |
| NSEvent* event = [NSEvent otherEventWithType:NSApplicationDefined |
| location:NSZeroPoint |
| modifierFlags:0 |
| timestamp:0 |
| windowNumber:0 |
| context:nil |
| subtype:0 |
| data1:0 |
| data2:0]; |
| |
| // Post a task to open the menu. This needs to be a separate task so that |
| // nested task execution can be allowed. |
| ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindOnce( |
| [](NSWindow* window, NSMenu* menu, NSEvent* event) { |
| MessageLoopCurrent::ScopedNestableTaskAllower allow; |
| ScopedPumpMessagesInPrivateModes pump_private; |
| // When the bug triggers, this call never returns. |
| [NSMenu popUpContextMenu:menu |
| withEvent:event |
| forView:[window contentView]]; |
| }, |
| window, menu, event)); |
| |
| // Post another task to close the menu. The 100ms delay was determined |
| // experimentally on a 2013 Mac Pro. |
| RunLoop run_loop; |
| ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](RunLoop* run_loop, NSMenu* menu) { |
| [menu cancelTracking]; |
| run_loop->Quit(); |
| }, |
| &run_loop, menu), |
| base::TimeDelta::FromMilliseconds(100)); |
| |
| EXPECT_NO_FATAL_FAILURE(run_loop.Run()); |
| } |
| |
| TEST(MessagePumpMacTest, QuitWithModalWindow) { |
| MessageLoopForUI message_loop; |
| NSWindow* window = |
| [[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100) |
| styleMask:NSBorderlessWindowMask |
| backing:NSBackingStoreBuffered |
| defer:NO] autorelease]; |
| |
| // Check that quitting the run loop while a modal window is shown applies to |
| // |run_loop| rather than the internal NSApplication modal run loop. |
| RunLoop run_loop; |
| ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindLambdaForTesting([&] { |
| MessageLoopCurrent::ScopedNestableTaskAllower allow; |
| ScopedPumpMessagesInPrivateModes pump_private; |
| [NSApp runModalForWindow:window]; |
| })); |
| ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, |
| base::BindLambdaForTesting([&] { |
| [NSApp stopModal]; |
| run_loop.Quit(); |
| })); |
| |
| EXPECT_NO_FATAL_FAILURE(run_loop.Run()); |
| } |
| |
| } // namespace base |
| |
| @implementation TestModalAlertCloser |
| |
| - (void)runTestThenCloseAlert:(NSAlert*)alert { |
| EXPECT_TRUE([NSApp modalWindow]); |
| { |
| base::ScopedPumpMessagesInPrivateModes allow_private; |
| // With a modal window, only safe modes should be pumped. |
| EXPECT_EQ(kNSApplicationModalSafeModeMask, |
| allow_private.GetModeMaskForTest()); |
| } |
| [[alert buttons][0] performClick:nil]; |
| } |
| |
| @end |