| // Copyright 2015 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 "components/browser_watcher/window_hang_monitor_win.h" |
| |
| #include <vector> |
| |
| #include "base/base_switches.h" |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/callback.h" |
| #include "base/callback_helpers.h" |
| #include "base/command_line.h" |
| #include "base/location.h" |
| #include "base/macros.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/process/launch.h" |
| #include "base/process/process.h" |
| #include "base/process/process_handle.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/test/multiprocess_test.h" |
| #include "base/threading/thread.h" |
| #include "base/win/message_window.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "testing/multiprocess_func_list.h" |
| |
| namespace browser_watcher { |
| |
| namespace { |
| |
| // Simulates a process that never opens a window. |
| MULTIPROCESS_TEST_MAIN(NoWindowChild) { |
| ::Sleep(INFINITE); |
| return 0; |
| } |
| |
| // Manages a WindowHangMonitor that lives on a background thread. |
| class HangMonitorThread { |
| public: |
| // Instantiates the background thread. |
| HangMonitorThread() |
| : event_(WindowHangMonitor::WINDOW_NOT_FOUND), |
| event_received_(false, false), |
| thread_("HangMonitorThread") {} |
| |
| ~HangMonitorThread() { |
| if (hang_monitor_.get()) |
| DestroyWatcher(); |
| } |
| |
| // Starts the background thread and the monitor to observe the window named |
| // |window_name| in |process|. Blocks until the monitor has been initialized. |
| bool Start(base::Process process, const base::string16& window_name) { |
| if (!thread_.StartWithOptions( |
| base::Thread::Options(base::MessageLoop::TYPE_UI, 0))) { |
| return false; |
| } |
| |
| base::WaitableEvent complete(false, false); |
| if (!thread_.task_runner()->PostTask( |
| FROM_HERE, base::Bind(&HangMonitorThread::StartupOnThread, |
| base::Unretained(this), window_name, |
| base::Passed(process.Pass()), |
| base::Unretained(&complete)))) { |
| return false; |
| } |
| |
| complete.Wait(); |
| |
| return true; |
| } |
| |
| // Returns true if a window event is detected within |timeout|. |
| bool TimedWaitForEvent(base::TimeDelta timeout) { |
| return event_received_.TimedWait(timeout); |
| } |
| |
| // Blocks indefinitely for a window event and returns it. |
| WindowHangMonitor::WindowEvent WaitForEvent() { |
| event_received_.Wait(); |
| return event_; |
| } |
| |
| // Destroys the monitor and stops the background thread. Blocks until the |
| // operation completes. |
| void DestroyWatcher() { |
| thread_.task_runner()->PostTask( |
| FROM_HERE, base::Bind(&HangMonitorThread::ShutdownOnThread, |
| base::Unretained(this))); |
| // This will block until the above-posted task completes. |
| thread_.Stop(); |
| } |
| |
| private: |
| // Invoked when the monitor signals an event. Unblocks a call to |
| // TimedWaitForEvent or WaitForEvent. |
| void EventCallback(WindowHangMonitor::WindowEvent event) { |
| if (event_received_.IsSignaled()) |
| ADD_FAILURE() << "Multiple calls to EventCallback."; |
| event_ = event; |
| event_received_.Signal(); |
| } |
| |
| // Initializes the WindowHangMonitor to observe the window named |window_name| |
| // in |process|. Signals |complete| when done. |
| void StartupOnThread(const base::string16& window_name, |
| base::Process process, |
| base::WaitableEvent* complete) { |
| hang_monitor_.reset(new WindowHangMonitor( |
| base::TimeDelta::FromMilliseconds(100), |
| base::TimeDelta::FromMilliseconds(100), |
| base::Bind(&HangMonitorThread::EventCallback, base::Unretained(this)))); |
| hang_monitor_->Initialize(process.Pass(), window_name); |
| complete->Signal(); |
| } |
| |
| // Destroys the WindowHangMonitor. |
| void ShutdownOnThread() { hang_monitor_.reset(); } |
| |
| // The detected event. Invalid if |event_received_| has not been signaled. |
| WindowHangMonitor::WindowEvent event_; |
| // Indicates that |event_| has been assigned in response to a callback from |
| // the WindowHangMonitor. |
| base::WaitableEvent event_received_; |
| // The WindowHangMonitor under test. |
| scoped_ptr<WindowHangMonitor> hang_monitor_; |
| // The background thread. |
| base::Thread thread_; |
| |
| DISALLOW_COPY_AND_ASSIGN(HangMonitorThread); |
| }; |
| |
| class WindowHangMonitorTest : public testing::Test { |
| public: |
| WindowHangMonitorTest() |
| : ping_event_(false, false), |
| pings_(0), |
| window_thread_("WindowHangMonitorTest window_thread") {} |
| |
| void SetUp() override { |
| // Pick a window name unique to this process. |
| window_name_ = base::StringPrintf(L"WindowHanMonitorTest-%d", |
| base::GetCurrentProcId()); |
| ASSERT_TRUE(window_thread_.StartWithOptions( |
| base::Thread::Options(base::MessageLoop::TYPE_UI, 0))); |
| } |
| |
| void TearDown() override { |
| DeleteMessageWindow(); |
| window_thread_.Stop(); |
| } |
| |
| void CreateMessageWindow() { |
| bool succeeded = false; |
| base::WaitableEvent created(true, false); |
| ASSERT_TRUE(window_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::Bind(&WindowHangMonitorTest::CreateMessageWindowInWorkerThread, |
| base::Unretained(this), window_name_, &succeeded, |
| &created))); |
| created.Wait(); |
| ASSERT_TRUE(succeeded); |
| } |
| |
| void DeleteMessageWindow() { |
| base::WaitableEvent deleted(true, false); |
| window_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::Bind(&WindowHangMonitorTest::DeleteMessageWindowInWorkerThread, |
| base::Unretained(this), &deleted)); |
| deleted.Wait(); |
| } |
| |
| void WaitForPing() { |
| while (true) { |
| { |
| base::AutoLock auto_lock(ping_lock_); |
| if (pings_) { |
| ping_event_.Reset(); |
| --pings_; |
| return; |
| } |
| } |
| ping_event_.Wait(); |
| } |
| } |
| |
| HangMonitorThread& monitor_thread() { return monitor_thread_; } |
| |
| const base::win::MessageWindow* message_window() const { |
| return message_window_.get(); |
| } |
| |
| const base::string16& window_name() const { return window_name_; } |
| |
| base::Thread* window_thread() { return &window_thread_; } |
| |
| private: |
| bool MessageCallback(UINT message, |
| WPARAM wparam, |
| LPARAM lparam, |
| LRESULT* result) { |
| EXPECT_EQ(window_thread_.message_loop(), base::MessageLoop::current()); |
| if (message == WM_NULL) { |
| base::AutoLock auto_lock(ping_lock_); |
| ++pings_; |
| ping_event_.Signal(); |
| } |
| |
| return false; // Pass through to DefWindowProc. |
| } |
| |
| void CreateMessageWindowInWorkerThread(const base::string16& name, |
| bool* success, |
| base::WaitableEvent* created) { |
| message_window_.reset(new base::win::MessageWindow); |
| *success = message_window_->CreateNamed( |
| base::Bind(&WindowHangMonitorTest::MessageCallback, |
| base::Unretained(this)), |
| name); |
| created->Signal(); |
| } |
| |
| void DeleteMessageWindowInWorkerThread(base::WaitableEvent* deleted) { |
| message_window_.reset(); |
| if (deleted) |
| deleted->Signal(); |
| } |
| |
| HangMonitorThread monitor_thread_; |
| scoped_ptr<base::win::MessageWindow> message_window_; |
| base::string16 window_name_; |
| base::Lock ping_lock_; |
| base::WaitableEvent ping_event_; |
| size_t pings_; |
| base::Thread window_thread_; |
| |
| DISALLOW_COPY_AND_ASSIGN(WindowHangMonitorTest); |
| }; |
| |
| } // namespace |
| |
| TEST_F(WindowHangMonitorTest, NoWindow) { |
| base::CommandLine child_command_line = |
| base::GetMultiProcessTestChildBaseCommandLine(); |
| child_command_line.AppendSwitchASCII(switches::kTestChildProcess, |
| "NoWindowChild"); |
| base::Process process = |
| base::LaunchProcess(child_command_line, base::LaunchOptions()); |
| ASSERT_TRUE(process.IsValid()); |
| |
| base::ScopedClosureRunner terminate_process_runner( |
| base::Bind(base::IgnoreResult(&base::Process::Terminate), |
| base::Unretained(&process), 1, true)); |
| |
| monitor_thread().Start(process.Duplicate(), window_name()); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| |
| terminate_process_runner.Reset(); |
| |
| ASSERT_EQ(WindowHangMonitor::WINDOW_NOT_FOUND, |
| monitor_thread().WaitForEvent()); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| } |
| |
| TEST_F(WindowHangMonitorTest, WindowBeforeWatcher) { |
| CreateMessageWindow(); |
| |
| monitor_thread().Start(base::Process::Current(), window_name()); |
| |
| WaitForPing(); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| } |
| |
| TEST_F(WindowHangMonitorTest, WindowBeforeDestroy) { |
| CreateMessageWindow(); |
| |
| monitor_thread().Start(base::Process::Current(), window_name()); |
| |
| WaitForPing(); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| |
| monitor_thread().DestroyWatcher(); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent(base::TimeDelta())); |
| } |
| |
| TEST_F(WindowHangMonitorTest, NoWindowBeforeDestroy) { |
| monitor_thread().Start(base::Process::Current(), window_name()); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| monitor_thread().DestroyWatcher(); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent(base::TimeDelta())); |
| } |
| |
| TEST_F(WindowHangMonitorTest, WatcherBeforeWindow) { |
| monitor_thread().Start(base::Process::Current(), window_name()); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| |
| CreateMessageWindow(); |
| |
| WaitForPing(); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| } |
| |
| TEST_F(WindowHangMonitorTest, DetectsWindowDisappearance) { |
| CreateMessageWindow(); |
| |
| monitor_thread().Start(base::Process::Current(), window_name()); |
| |
| WaitForPing(); |
| |
| DeleteMessageWindow(); |
| |
| ASSERT_EQ(WindowHangMonitor::WINDOW_VANISHED, |
| monitor_thread().WaitForEvent()); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| } |
| |
| TEST_F(WindowHangMonitorTest, DetectsWindowNameChange) { |
| // This test changes the title of the message window as a proxy for what |
| // happens if the window handle is reused for a different purpose. The latter |
| // is impossible to test in a deterministic fashion. |
| CreateMessageWindow(); |
| |
| monitor_thread().Start(base::Process::Current(), window_name()); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| |
| ASSERT_TRUE(::SetWindowText(message_window()->hwnd(), L"Gonsky")); |
| |
| ASSERT_EQ(WindowHangMonitor::WINDOW_VANISHED, |
| monitor_thread().WaitForEvent()); |
| } |
| |
| TEST_F(WindowHangMonitorTest, DetectsWindowHang) { |
| CreateMessageWindow(); |
| |
| monitor_thread().Start(base::Process::Current(), window_name()); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| |
| // Block the worker thread. |
| base::WaitableEvent hang(true, false); |
| |
| window_thread()->task_runner()->PostTask( |
| FROM_HERE, |
| base::Bind(&base::WaitableEvent::Wait, base::Unretained(&hang))); |
| |
| EXPECT_EQ(WindowHangMonitor::WINDOW_HUNG, |
| monitor_thread().WaitForEvent()); |
| |
| // Unblock the worker thread. |
| hang.Signal(); |
| |
| ASSERT_FALSE(monitor_thread().TimedWaitForEvent( |
| base::TimeDelta::FromMilliseconds(150))); |
| } |
| |
| } // namespace browser_watcher |