diff options
author | siggi <siggi@chromium.org> | 2015-03-26 09:09:31 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-03-26 16:10:44 +0000 |
commit | c34049f14ab8d830ccd1b4ad5b7a508b1ad9de27 (patch) | |
tree | 43ffd2ee8ff25401835f8f7964fb90408dbf88f9 /components | |
parent | 6f9ccd4f30c31527f2a8f38878842b414b61c5fe (diff) | |
download | chromium_src-c34049f14ab8d830ccd1b4ad5b7a508b1ad9de27.zip chromium_src-c34049f14ab8d830ccd1b4ad5b7a508b1ad9de27.tar.gz chromium_src-c34049f14ab8d830ccd1b4ad5b7a508b1ad9de27.tar.bz2 |
Implements a monitor to watch for the browser hanging.
BUG=
Review URL: https://codereview.chromium.org/1036623002
Cr-Commit-Position: refs/heads/master@{#322395}
Diffstat (limited to 'components')
-rw-r--r-- | components/browser_watcher.gypi | 2 | ||||
-rw-r--r-- | components/browser_watcher/BUILD.gn | 3 | ||||
-rw-r--r-- | components/browser_watcher/window_hang_monitor_win.cc | 182 | ||||
-rw-r--r-- | components/browser_watcher/window_hang_monitor_win.h | 102 | ||||
-rw-r--r-- | components/browser_watcher/window_hang_monitor_win_unittest.cc | 221 | ||||
-rw-r--r-- | components/components_tests.gyp | 1 |
6 files changed, 511 insertions, 0 deletions
diff --git a/components/browser_watcher.gypi b/components/browser_watcher.gypi index e1997ac..b65d463 100644 --- a/components/browser_watcher.gypi +++ b/components/browser_watcher.gypi @@ -18,6 +18,8 @@ 'browser_watcher/exit_code_watcher_win.h', 'browser_watcher/exit_funnel_win.cc', 'browser_watcher/exit_funnel_win.h', + 'browser_watcher/window_hang_monitor_win.cc', + 'browser_watcher/window_hang_monitor_win.h', ], 'dependencies': [ '../base/base.gyp:base', diff --git a/components/browser_watcher/BUILD.gn b/components/browser_watcher/BUILD.gn index 1f782c2..6720895 100644 --- a/components/browser_watcher/BUILD.gn +++ b/components/browser_watcher/BUILD.gn @@ -12,6 +12,8 @@ source_set("browser_watcher") { "exit_code_watcher_win.h", "exit_funnel_win.cc", "exit_funnel_win.h", + "window_hang_monitor_win.cc", + "window_hang_monitor_win.h", ] deps = [ "//base", @@ -38,6 +40,7 @@ source_set("unit_tests") { "exit_funnel_win_unittest.cc", "watcher_client_win_unittest.cc", "watcher_metrics_provider_win_unittest.cc", + "window_hang_monitor_win_unittest.cc", ] configs += [ "//build/config/compiler:no_size_t_to_int_warning" ] deps = [ diff --git a/components/browser_watcher/window_hang_monitor_win.cc b/components/browser_watcher/window_hang_monitor_win.cc new file mode 100644 index 0000000..1218fc5 --- /dev/null +++ b/components/browser_watcher/window_hang_monitor_win.cc @@ -0,0 +1,182 @@ +// 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 "base/callback.h" +#include "base/logging.h" +#include "base/message_loop/message_loop.h" +#include "base/win/message_window.h" + +namespace browser_watcher { + +namespace { + +const size_t kPingIntervalSeconds = 60; +const size_t kHangTimeoutSeconds = 20; + +bool IsWindowValid(HWND window, + const base::string16& window_name, + base::ProcessId pid) { + // Validate the Window in two respects: + // 1. The window handle might have been re-assigned to a different window + // from the time we found it to the point where we query for its owning + // process. + // 2. The window handle might have been re-assigned to a different process + // at any point after we found it. + if (window != base::win::MessageWindow::FindWindow(window_name)) { + // The window handle has been reassigned, bail. + return false; + } + + // Re-do the process ID lookup. + DWORD new_process_id = 0; + DWORD thread_id = ::GetWindowThreadProcessId(window, &new_process_id); + if (thread_id == 0 || pid != new_process_id) { + // Another process has taken over the handle. + return false; + } + + return true; +} + +} // namespace + +WindowHangMonitor::WindowHangMonitor(const WindowEventCallback& callback) + : callback_(callback), + window_(NULL), + outstanding_ping_(nullptr), + timer_(false /* don't retain user task */, false /* don't repeat */), + ping_interval_(base::TimeDelta::FromSeconds(kPingIntervalSeconds)), + hang_timeout_(base::TimeDelta::FromSeconds(kHangTimeoutSeconds)) { +} + +WindowHangMonitor::~WindowHangMonitor() { + if (outstanding_ping_) { + // We have an outstanding ping, disable it and leak it intentionally as + // if the callback arrives eventually, it'll cause a use-after-free. + outstanding_ping_->monitor = nullptr; + outstanding_ping_ = nullptr; + } +} + +bool WindowHangMonitor::Initialize(const base::string16& window_name) { + window_name_ = window_name; + timer_.SetTaskRunner(base::MessageLoop::current()->task_runner()); + + // This code is fraught with all kinds of races. As the purpose here is + // only monitoring, this code simply bails if any kind of race is encountered. + // Find the window to monitor by name. + window_ = base::win::MessageWindow::FindWindow(window_name); + if (window_ == NULL) + return false; + + // Find and open the process owning this window. + DWORD process_id = 0; + DWORD thread_id = ::GetWindowThreadProcessId(window_, &process_id); + if (thread_id == 0 || process_id == 0) { + // The window has vanished or there was some other problem with the handle. + return false; + } + + // Keep an open handle on the process to make sure the PID isn't reused. + window_process_ = base::Process::Open(process_id); + if (!window_process_.IsValid()) { + // The process may be at a different security level. + return false; + } + + return MaybeSendPing(); +} + +void WindowHangMonitor::SetPingIntervalForTesting( + base::TimeDelta ping_interval) { + ping_interval_ = ping_interval; +} + +void WindowHangMonitor::SetHangTimeoutForTesting(base::TimeDelta hang_timeout) { + hang_timeout_ = hang_timeout; +} + +void CALLBACK WindowHangMonitor::OnPongReceived(HWND window, + UINT msg, + ULONG_PTR data, + LRESULT lresult) { + OutstandingPing* outstanding = reinterpret_cast<OutstandingPing*>(data); + + // If the monitor is still around, clear its pointer. + if (outstanding->monitor) + outstanding->monitor->outstanding_ping_ = nullptr; + + delete outstanding; +} + +bool WindowHangMonitor::MaybeSendPing() { + DCHECK(window_process_.IsValid()); + DCHECK(window_); + DCHECK(!outstanding_ping_); + + if (!IsWindowValid(window_, window_name_, window_process_.Pid())) { + // The window is no longer valid, issue the callback. + callback_.Run(WINDOW_VANISHED); + return false; + } + + // The window checks out, issue a ping against it. Set up all state ahead of + // time to allow for the possibility of the callback being invoked from within + // SendMessageCallback. + outstanding_ping_ = new OutstandingPing; + outstanding_ping_->monitor = this; + + // Note that this is still racy to |window_| having been re-assigned, but + // the race is as small as we can make it, and the next attempt will re-try. + if (!::SendMessageCallback(window_, WM_NULL, 0, 0, &OnPongReceived, + reinterpret_cast<ULONG_PTR>(outstanding_ping_))) { + // Message sending failed, assume the window is no longer valid, + // issue the callback and stop the polling. + delete outstanding_ping_; + outstanding_ping_ = nullptr; + + callback_.Run(WINDOW_VANISHED); + return false; + } + + // Issue the count-out callback. + timer_.Start( + FROM_HERE, hang_timeout_, + base::Bind(&WindowHangMonitor::OnHangTimeout, base::Unretained(this))); + + return true; +} + +void WindowHangMonitor::OnHangTimeout() { + DCHECK(window_process_.IsValid()); + DCHECK(window_); + + if (outstanding_ping_) { + // The ping is still outstanding, the window is hung or has vanished. + // Orphan the outstanding ping. If the callback arrives late, it will + // delete it, or if the callback never arrives it'll leak. + outstanding_ping_->monitor = NULL; + outstanding_ping_ = NULL; + + if (!IsWindowValid(window_, window_name_, window_process_.Pid())) { + // The window vanished. + callback_.Run(WINDOW_VANISHED); + } else { + // The window hung. + callback_.Run(WINDOW_HUNG); + } + } else { + // No ping outstanding, window is not yet hung. Schedule the next retry. + timer_.Start( + FROM_HERE, hang_timeout_ - ping_interval_, + base::Bind(&WindowHangMonitor::OnRetryTimeout, base::Unretained(this))); + } +} + +void WindowHangMonitor::OnRetryTimeout() { + MaybeSendPing(); +} + +} // namespace browser_watcher diff --git a/components/browser_watcher/window_hang_monitor_win.h b/components/browser_watcher/window_hang_monitor_win.h new file mode 100644 index 0000000..47c24bc --- /dev/null +++ b/components/browser_watcher/window_hang_monitor_win.h @@ -0,0 +1,102 @@ +// 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. +#ifndef COMPONENTS_BROWSER_WATCHER_WINDOW_HANG_MONITOR_WIN_H_ +#define COMPONENTS_BROWSER_WATCHER_WINDOW_HANG_MONITOR_WIN_H_ + +#include <windows.h> + +#include "base/callback_forward.h" +#include "base/macros.h" +#include "base/process/process.h" +#include "base/strings/string16.h" +#include "base/time/time.h" +#include "base/timer/timer.h" + +namespace browser_watcher { + +// Monitors a window for hanging by periodically sending it a WM_NULL message +// and timing the response. +class WindowHangMonitor { + public: + enum WindowEvent { + WINDOW_HUNG, + WINDOW_VANISHED, + }; + // Called when a hang is detected or when the window has gone away. + // Called precisely zero or one time(s). + typedef base::Callback<void(WindowEvent)> WindowEventCallback; + + // Initialize the monitor with an event callback. + explicit WindowHangMonitor(const WindowEventCallback& callback); + ~WindowHangMonitor(); + + // Initializes the watcher to monitor the window answering to |window_name|. + // Returns true on success. + bool Initialize(const base::string16& window_name); + + // Testing accessors. + bool IsIdleForTesting() const { return !timer_.IsRunning(); } + void SetPingIntervalForTesting(base::TimeDelta ping_interval); + void SetHangTimeoutForTesting(base::TimeDelta hang_timeout); + + HWND window() const { return window_; } + const base::Process& window_process() const { return window_process_; } + + private: + struct OutstandingPing { + WindowHangMonitor* monitor; + }; + + static void CALLBACK + OnPongReceived(HWND window, UINT msg, ULONG_PTR data, LRESULT lresult); + + // Checks that |window_| is still valid, and sends it a ping. + // Issues a |WINDOW_VANISHED| callback if the window's no longer valid. + // Schedules OnHangTimeout in case of success. + // Returns true on success, false if the window is no longer valid or other + // failure. + bool MaybeSendPing(); + + // Runs after a |hang_timeout_| delay after sending a ping. Checks whether + // a pong was received. Either issues a callback or schedules OnRetryTimeout. + void OnHangTimeout(); + + // Runs periodically at |ping_interval_| interval, as long as the window is + // still valid and not hung. + void OnRetryTimeout(); + + // Invoked on significant window events. + WindowEventCallback callback_; + + // The name of the (message) window to monitor. + base::string16 window_name_; + + // The monitored window handle. + HWND window_; + + // The process that owned |window_| when Initialize was called. + base::Process window_process_; + + // The time the last message was sent. + base::Time last_ping_; + + // The ping interval, must be larger than |hang_timeout_|. + base::TimeDelta ping_interval_; + + // The time after which |window_| is assumed hung. + base::TimeDelta hang_timeout_; + + // The timer used to schedule polls. + base::Timer timer_; + + // Non-null when there is an outstanding ping. + // This is intentionally leaked when a hang is detected. + OutstandingPing* outstanding_ping_; + + DISALLOW_COPY_AND_ASSIGN(WindowHangMonitor); +}; + +} // namespace browser_watcher + +#endif // COMPONENTS_BROWSER_WATCHER_WINDOW_HANG_MONITOR_WIN_H_ diff --git a/components/browser_watcher/window_hang_monitor_win_unittest.cc b/components/browser_watcher/window_hang_monitor_win_unittest.cc new file mode 100644 index 0000000..236c18d --- /dev/null +++ b/components/browser_watcher/window_hang_monitor_win_unittest.cc @@ -0,0 +1,221 @@ +// 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/bind.h" +#include "base/callback.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/process/process_handle.h" +#include "base/run_loop.h" +#include "base/strings/string16.h" +#include "base/strings/stringprintf.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/thread.h" +#include "base/win/message_window.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace browser_watcher { + +namespace { + +class WindowHangMonitorTest : public testing::Test { + public: + typedef std::vector<WindowHangMonitor::WindowEvent> WindowEventVector; + + WindowHangMonitorTest() + : monitor_(base::Bind(&WindowHangMonitorTest::OnWindowEvent, + base::Unretained(this))), + message_loop_(base::MessageLoop::TYPE_UI), + run_loop_(nullptr), + pings_(0), + worker_thread_("HangMan") {} + + // Callback from the hang detector. + void OnWindowEvent(WindowHangMonitor::WindowEvent event) { + // Record the event and terminate the message loop. + events_.push_back(event); + run_loop_->Quit(); + } + + void SetUp() override { + // Pick a window name unique to this process. + window_name_ = base::StringPrintf(L"WindowHanMonitorTest-%d", + base::GetCurrentProcId()); + ASSERT_TRUE(worker_thread_.StartWithOptions( + base::Thread::Options(base::MessageLoop::TYPE_UI, 0))); + + // Set relatively short hang detection and ping intervals. + monitor_.SetHangTimeoutForTesting(base::TimeDelta::FromMilliseconds(50)); + monitor_.SetPingIntervalForTesting(base::TimeDelta::FromMilliseconds(200)); + } + + void TearDown() override { + DeleteMessageWindow(); + worker_thread_.Stop(); + } + + void CreateMessageWindow() { + bool succeeded = false; + base::WaitableEvent created(true, false); + ASSERT_TRUE(worker_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); + worker_thread_.task_runner()->PostTask( + FROM_HERE, + base::Bind(&WindowHangMonitorTest::DeleteMessageWindowInWorkerThread, + base::Unretained(this), &deleted)); + deleted.Wait(); + } + + bool MessageCallback(UINT message, + WPARAM wparam, + LPARAM lparam, + LRESULT* result) { + EXPECT_EQ(worker_thread_.message_loop(), base::MessageLoop::current()); + if (message == WM_NULL) + ++pings_; + + return false; // Pass through to DefWindowProc. + } + + void RunMessageLoop() { + ASSERT_FALSE(run_loop_); + + base::RunLoop loop; + + run_loop_ = &loop; + loop.Run(); + run_loop_ = nullptr; + } + + WindowHangMonitor* monitor() { return &monitor_; } + const WindowEventVector& events() const { return events_; } + const base::win::MessageWindow* message_window() const { + return message_window_.get(); + } + size_t pings() const { return pings_; } + const base::string16& window_name() const { return window_name_; } + base::Thread* worker_thread() { return &worker_thread_; } + + private: + 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(); + } + + WindowHangMonitor monitor_; + WindowEventVector events_; + + // Message and run loops for the main thread. + base::MessageLoop message_loop_; + base::RunLoop* run_loop_; + scoped_ptr<base::win::MessageWindow> message_window_; + base::string16 window_name_; + size_t pings_; + base::Thread worker_thread_; +}; + +} // namespace + +TEST_F(WindowHangMonitorTest, InitFailsWhenNoWindow) { + ASSERT_FALSE(monitor()->Initialize(window_name())); + EXPECT_TRUE(monitor()->IsIdleForTesting()); + EXPECT_EQ(0, pings()); + EXPECT_EQ(0, events().size()); +} + +TEST_F(WindowHangMonitorTest, InitSucceedsWhenWindow) { + CreateMessageWindow(); + + ASSERT_TRUE(monitor()->Initialize(window_name())); + EXPECT_FALSE(monitor()->IsIdleForTesting()); + + // Delete the window to synchronize against any pending message pings. + DeleteMessageWindow(); + + EXPECT_EQ(1, pings()); + EXPECT_EQ(0, events().size()); +} + +TEST_F(WindowHangMonitorTest, DetectsWindowDisappearance) { + CreateMessageWindow(); + + EXPECT_TRUE(monitor()->Initialize(window_name())); + EXPECT_EQ(0, events().size()); + + DeleteMessageWindow(); + + RunMessageLoop(); + + EXPECT_TRUE(monitor()->IsIdleForTesting()); + ASSERT_EQ(1, events().size()); + EXPECT_EQ(WindowHangMonitor::WINDOW_VANISHED, events()[0]); +} + +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(); + + ASSERT_TRUE(monitor()->Initialize(window_name())); + EXPECT_EQ(0, events().size()); + + ASSERT_TRUE(::SetWindowText(message_window()->hwnd(), L"Gonsky")); + + RunMessageLoop(); + + EXPECT_TRUE(monitor()->IsIdleForTesting()); + ASSERT_EQ(1, events().size()); + EXPECT_EQ(WindowHangMonitor::WINDOW_VANISHED, events()[0]); +} + +TEST_F(WindowHangMonitorTest, DetectsWindowHang) { + CreateMessageWindow(); + + ASSERT_TRUE(monitor()->Initialize(window_name())); + EXPECT_EQ(0, events().size()); + + // Block the worker thread. + base::WaitableEvent hang(true, false); + + worker_thread()->message_loop_proxy()->PostTask( + FROM_HERE, + base::Bind(&base::WaitableEvent::Wait, base::Unretained(&hang))); + + RunMessageLoop(); + + // Unblock the worker thread. + hang.Signal(); + + EXPECT_TRUE(monitor()->IsIdleForTesting()); + ASSERT_EQ(1, events().size()); + EXPECT_EQ(WindowHangMonitor::WINDOW_HUNG, events()[0]); +} + +} // namespace browser_watcher diff --git a/components/components_tests.gyp b/components/components_tests.gyp index aba49d4..7a8045c 100644 --- a/components/components_tests.gyp +++ b/components/components_tests.gyp @@ -78,6 +78,7 @@ 'browser_watcher/exit_funnel_win_unittest.cc', 'browser_watcher/watcher_client_win_unittest.cc', 'browser_watcher/watcher_metrics_provider_win_unittest.cc', + 'browser_watcher/window_hang_monitor_win_unittest.cc', ], 'captive_portal_unittest_sources': [ 'captive_portal/captive_portal_detector_unittest.cc', |