| // 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 |