1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <windows.h>
6
7#include "chrome/browser/hang_monitor/hung_plugin_action.h"
8
9#include "base/metrics/histogram.h"
10#include "base/version.h"
11#include "chrome/browser/ui/simple_message_box.h"
12#include "chrome/common/logging_chrome.h"
13#include "content/public/browser/plugin_service.h"
14#include "content/public/common/webplugininfo.h"
15#include "grit/generated_resources.h"
16#include "ui/base/l10n/l10n_util.h"
17#include "ui/gfx/win/hwnd_util.h"
18
19namespace {
20
21const wchar_t kGTalkPluginName[] = L"Google Talk Plugin";
22const int kGTalkPluginLogMinVersion = 26;  // For version 2.6 and below.
23
24enum GTalkPluginLogVersion {
25  GTALK_PLUGIN_VERSION_MIN = 0,
26  GTALK_PLUGIN_VERSION_27,
27  GTALK_PLUGIN_VERSION_28,
28  GTALK_PLUGIN_VERSION_29,
29  GTALK_PLUGIN_VERSION_30,
30  GTALK_PLUGIN_VERSION_31,
31  GTALK_PLUGIN_VERSION_32,
32  GTALK_PLUGIN_VERSION_33,
33  GTALK_PLUGIN_VERSION_34,
34  GTALK_PLUGIN_VERSION_MAX
35};
36
37// Converts the version string of Google Talk Plugin to a version enum. The
38// version format is "major(1 digit).minor(1 digit).sub(1 or 2 digits)",
39// for example, "2.7.10" and "2.8.1". Converts the string to a number as
40// 10 * major + minor - kGTalkPluginLogMinVersion.
41GTalkPluginLogVersion GetGTalkPluginVersion(const base::string16& version) {
42  int gtalk_plugin_version = GTALK_PLUGIN_VERSION_MIN;
43  Version plugin_version;
44  content::WebPluginInfo::CreateVersionFromString(version, &plugin_version);
45  if (plugin_version.IsValid() && plugin_version.components().size() >= 2) {
46    gtalk_plugin_version = 10 * plugin_version.components()[0] +
47        plugin_version.components()[1] - kGTalkPluginLogMinVersion;
48  }
49
50  if (gtalk_plugin_version < GTALK_PLUGIN_VERSION_MIN)
51    return GTALK_PLUGIN_VERSION_MIN;
52  if (gtalk_plugin_version > GTALK_PLUGIN_VERSION_MAX)
53    return GTALK_PLUGIN_VERSION_MAX;
54  return static_cast<GTalkPluginLogVersion>(gtalk_plugin_version);
55}
56
57}  // namespace
58
59HungPluginAction::HungPluginAction() : current_hung_plugin_window_(NULL) {
60}
61
62HungPluginAction::~HungPluginAction() {
63}
64
65bool HungPluginAction::OnHungWindowDetected(HWND hung_window,
66                                            HWND top_level_window,
67                                            ActionOnHungWindow* action) {
68  if (NULL == action) {
69    return false;
70  }
71  if (!IsWindow(hung_window)) {
72    return false;
73  }
74
75  bool continue_hang_detection = true;
76
77  DWORD hung_window_process_id = 0;
78  DWORD top_level_window_process_id = 0;
79  GetWindowThreadProcessId(hung_window, &hung_window_process_id);
80  GetWindowThreadProcessId(top_level_window, &top_level_window_process_id);
81
82  *action = HungWindowNotification::HUNG_WINDOW_IGNORE;
83  if (top_level_window_process_id != hung_window_process_id) {
84    base::string16 plugin_name;
85    base::string16 plugin_version;
86    GetPluginNameAndVersion(hung_window,
87                            top_level_window_process_id,
88                            &plugin_name,
89                            &plugin_version);
90    if (plugin_name.empty()) {
91      plugin_name = l10n_util::GetStringUTF16(IDS_UNKNOWN_PLUGIN_NAME);
92    } else if (kGTalkPluginName == plugin_name) {
93      UMA_HISTOGRAM_ENUMERATION("GTalkPlugin.Hung",
94          GetGTalkPluginVersion(plugin_version),
95          GTALK_PLUGIN_VERSION_MAX + 1);
96    }
97
98    if (logging::DialogsAreSuppressed()) {
99      NOTREACHED() << "Terminated a hung plugin process.";
100      *action = HungWindowNotification::HUNG_WINDOW_TERMINATE_PROCESS;
101    } else {
102      const base::string16 title = l10n_util::GetStringUTF16(
103          IDS_BROWSER_HANGMONITOR_TITLE);
104      const base::string16 message = l10n_util::GetStringFUTF16(
105          IDS_BROWSER_HANGMONITOR, plugin_name);
106      // Before displaying the message box, invoke SendMessageCallback on the
107      // hung window. If the callback ever hits, the window is not hung anymore
108      // and we can dismiss the message box.
109      SendMessageCallback(hung_window,
110                          WM_NULL,
111                          0,
112                          0,
113                          HungWindowResponseCallback,
114                          reinterpret_cast<ULONG_PTR>(this));
115      current_hung_plugin_window_ = hung_window;
116      if (chrome::ShowMessageBox(
117              NULL, title, message, chrome::MESSAGE_BOX_TYPE_QUESTION) ==
118          chrome::MESSAGE_BOX_RESULT_YES) {
119        *action = HungWindowNotification::HUNG_WINDOW_TERMINATE_PROCESS;
120      } else {
121        // If the user choses to ignore the hung window warning, the
122        // message timeout for this window should be doubled. We only
123        // double the timeout property on the window if the property
124        // exists. The property is deleted if the window becomes
125        // responsive.
126        continue_hang_detection = false;
127#pragma warning(disable:4311)
128        int child_window_message_timeout =
129            reinterpret_cast<int>(GetProp(
130                hung_window, HungWindowDetector::kHungChildWindowTimeout));
131#pragma warning(default:4311)
132        if (child_window_message_timeout) {
133          child_window_message_timeout *= 2;
134#pragma warning(disable:4312)
135          SetProp(hung_window, HungWindowDetector::kHungChildWindowTimeout,
136                  reinterpret_cast<HANDLE>(child_window_message_timeout));
137#pragma warning(default:4312)
138        }
139      }
140      current_hung_plugin_window_ = NULL;
141    }
142  }
143  if (HungWindowNotification::HUNG_WINDOW_TERMINATE_PROCESS == *action) {
144    // Enable the top-level window just in case the plugin had been
145    // displaying a modal box that had disabled the top-level window
146    EnableWindow(top_level_window, TRUE);
147  }
148  return continue_hang_detection;
149}
150
151void HungPluginAction::OnWindowResponsive(HWND window) {
152  if (window == current_hung_plugin_window_) {
153    // The message timeout for this window should fallback to the default
154    // timeout as this window is now responsive.
155    RemoveProp(window, HungWindowDetector::kHungChildWindowTimeout);
156    // The monitored plugin recovered. Let's dismiss the message box.
157    EnumThreadWindows(GetCurrentThreadId(),
158                      reinterpret_cast<WNDENUMPROC>(DismissMessageBox),
159                      NULL);
160  }
161}
162
163bool HungPluginAction::GetPluginNameAndVersion(HWND plugin_window,
164                                               DWORD browser_process_id,
165                                               base::string16* plugin_name,
166                                               base::string16* plugin_version) {
167  DCHECK(plugin_name);
168  DCHECK(plugin_version);
169  HWND window_to_check = plugin_window;
170  while (NULL != window_to_check) {
171    DWORD process_id = 0;
172    GetWindowThreadProcessId(window_to_check, &process_id);
173    if (process_id == browser_process_id) {
174      // If we have reached a window the that belongs to the browser process
175      // we have gone too far.
176      return false;
177    }
178    if (content::PluginService::GetInstance()->GetPluginInfoFromWindow(
179            window_to_check, plugin_name, plugin_version)) {
180      return true;
181    }
182    window_to_check = GetParent(window_to_check);
183  }
184  return false;
185}
186
187// static
188BOOL CALLBACK HungPluginAction::DismissMessageBox(HWND window, LPARAM ignore) {
189  base::string16 class_name = gfx::GetClassName(window);
190  // #32770 is the dialog window class which is the window class of
191  // the message box being displayed.
192  if (class_name == L"#32770") {
193    EndDialog(window, IDNO);
194    return FALSE;
195  }
196  return TRUE;
197}
198
199// static
200void CALLBACK HungPluginAction::HungWindowResponseCallback(HWND target_window,
201                                                           UINT message,
202                                                           ULONG_PTR data,
203                                                           LRESULT result) {
204  HungPluginAction* instance = reinterpret_cast<HungPluginAction*>(data);
205  DCHECK(NULL != instance);
206  if (NULL != instance) {
207    instance->OnWindowResponsive(target_window);
208  }
209}
210