summaryrefslogtreecommitdiffstats
path: root/components/browser_watcher
diff options
context:
space:
mode:
authorsiggi <siggi@chromium.org>2015-03-26 09:09:31 -0700
committerCommit bot <commit-bot@chromium.org>2015-03-26 16:10:44 +0000
commitc34049f14ab8d830ccd1b4ad5b7a508b1ad9de27 (patch)
tree43ffd2ee8ff25401835f8f7964fb90408dbf88f9 /components/browser_watcher
parent6f9ccd4f30c31527f2a8f38878842b414b61c5fe (diff)
downloadchromium_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/browser_watcher')
-rw-r--r--components/browser_watcher/BUILD.gn3
-rw-r--r--components/browser_watcher/window_hang_monitor_win.cc182
-rw-r--r--components/browser_watcher/window_hang_monitor_win.h102
-rw-r--r--components/browser_watcher/window_hang_monitor_win_unittest.cc221
4 files changed, 508 insertions, 0 deletions
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