blob: 739a46ea054bc7eec531c45aa7c10bb44aab043c [file] [log] [blame]
// 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 <utility>
#include "base/callback.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/win/message_window.h"
namespace browser_watcher {
namespace {
// Returns true if the class name for |window| equals |str|.
bool WindowClassNameEqualsString(HWND window, base::StringPiece16 str) {
wchar_t class_name[MAX_PATH];
int str_length = ::GetClassName(window, class_name, MAX_PATH);
return str_length && str.compare(class_name) == 0;
}
// Returns true if the window text is an existing directory. Ensures that
// |window| is the right Chrome message window to ping. This could be improved
// by testing for a valid profile in the directory.
bool WindowNameIsExistingDirectory(HWND window) {
base::string16 window_name;
int str_length = ::GetWindowText(
window, base::WriteInto(&window_name, MAX_PATH), MAX_PATH);
window_name.resize(str_length);
return base::DirectoryExists(base::FilePath(window_name));
}
// Returns the Chrome message window handle for the specified |pid| or nullptr
// if not found.
HWND FindChromeMessageWindow(base::ProcessId pid) {
HWND candidate = ::FindWindowEx(HWND_MESSAGE, nullptr, nullptr, nullptr);
while (candidate) {
DWORD actual_process_id = 0;
::GetWindowThreadProcessId(candidate, &actual_process_id);
if (WindowClassNameEqualsString(candidate, L"Chrome_MessageWindow") &&
WindowNameIsExistingDirectory(candidate) && actual_process_id == pid) {
return candidate;
}
candidate = ::GetNextWindow(candidate, GW_HWNDNEXT);
}
return nullptr;
}
} // namespace
WindowHangMonitor::WindowHangMonitor(base::TimeDelta ping_interval,
base::TimeDelta timeout,
const WindowEventCallback& callback)
: callback_(callback),
ping_interval_(ping_interval),
hang_timeout_(timeout),
outstanding_ping_(nullptr) {
}
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;
}
}
void WindowHangMonitor::Initialize(base::Process process) {
window_process_ = std::move(process);
timer_.SetTaskRunner(base::ThreadTaskRunnerHandle::Get());
ScheduleFindWindow();
}
void WindowHangMonitor::ScheduleFindWindow() {
// TODO(erikwright): We could reduce the polling by using WaitForInputIdle,
// but it is hard to test (requiring a non-Console executable).
timer_.Start(
FROM_HERE, ping_interval_,
base::Bind(&WindowHangMonitor::PollForWindow, base::Unretained(this)));
}
void WindowHangMonitor::PollForWindow() {
int exit_code = 0;
if (window_process_.WaitForExitWithTimeout(base::TimeDelta(), &exit_code)) {
callback_.Run(WINDOW_NOT_FOUND);
return;
}
HWND hwnd = FindChromeMessageWindow(window_process_.Pid());
if (hwnd) {
// Sends a ping and schedules a timeout task. Upon receiving a ping response
// further pings will be scheduled ad infinitum. Will signal any failure now
// or later via the callback.
SendPing(hwnd);
} else {
ScheduleFindWindow();
}
}
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;
}
void WindowHangMonitor::SendPing(HWND hwnd) {
// 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 racy to |hwnd| having been re-assigned. If that occurs,
// we might fail to identify the disappearance of the window with this ping.
// This is acceptable, as the next ping should detect it.
if (!::SendMessageCallback(hwnd, 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;
}
// Issue the count-out callback.
timer_.Start(FROM_HERE, hang_timeout_,
base::Bind(&WindowHangMonitor::OnHangTimeout,
base::Unretained(this), hwnd));
}
void WindowHangMonitor::OnHangTimeout(HWND hwnd) {
DCHECK(window_process_.IsValid());
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 = nullptr;
outstanding_ping_ = nullptr;
if (hwnd != FindChromeMessageWindow(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() {
DCHECK(window_process_.IsValid());
DCHECK(window_process_.IsValid());
DCHECK(!outstanding_ping_);
// We can't simply hold onto the previously located HWND due to potential
// aliasing.
// 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.
HWND hwnd = FindChromeMessageWindow(window_process_.Pid());
if (hwnd) {
SendPing(hwnd);
} else {
callback_.Run(WINDOW_VANISHED);
}
}
} // namespace browser_watcher