Commit c34049f1 authored by siggi's avatar siggi Committed by Commit bot

Implements a monitor to watch for the browser hanging.

BUG=

Review URL: https://codereview.chromium.org/1036623002

Cr-Commit-Position: refs/heads/master@{#322395}
parent 6f9ccd4f
......@@ -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',
......
......@@ -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 = [
......
// 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
// 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_
// 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
......@@ -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',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment