summaryrefslogtreecommitdiffstats
path: root/components/browser_watcher/window_hang_monitor_win.cc
blob: 1218fc5b17b2a6f24063ef42051b22719d9511c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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