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 "chrome/browser/ui/views/hung_renderer_view.h"
6
7#include "base/i18n/rtl.h"
8#include "base/memory/scoped_vector.h"
9#include "base/strings/utf_string_conversions.h"
10#include "chrome/browser/favicon/favicon_tab_helper.h"
11#include "chrome/browser/platform_util.h"
12#include "chrome/browser/ui/browser_dialogs.h"
13#include "chrome/browser/ui/browser_finder.h"
14#include "chrome/browser/ui/chrome_web_modal_dialog_manager_delegate.h"
15#include "chrome/browser/ui/tab_contents/core_tab_helper.h"
16#include "chrome/browser/ui/tab_contents/tab_contents_iterator.h"
17#include "chrome/browser/ui/views/constrained_window_views.h"
18#include "chrome/common/chrome_constants.h"
19#include "chrome/common/logging_chrome.h"
20#include "chrome/grit/generated_resources.h"
21#include "components/web_modal/web_contents_modal_dialog_host.h"
22#include "content/public/browser/render_process_host.h"
23#include "content/public/browser/render_view_host.h"
24#include "content/public/browser/web_contents.h"
25#include "content/public/common/result_codes.h"
26#include "grit/theme_resources.h"
27#include "ui/aura/window.h"
28#include "ui/base/l10n/l10n_util.h"
29#include "ui/base/resource/resource_bundle.h"
30#include "ui/gfx/canvas.h"
31#include "ui/views/controls/button/label_button.h"
32#include "ui/views/controls/image_view.h"
33#include "ui/views/controls/label.h"
34#include "ui/views/layout/grid_layout.h"
35#include "ui/views/layout/layout_constants.h"
36#include "ui/views/widget/widget.h"
37#include "ui/views/window/client_view.h"
38
39#if defined(OS_WIN)
40#include "chrome/browser/hang_monitor/hang_crash_dump_win.h"
41#include "chrome/browser/profiles/profile.h"
42#include "chrome/browser/shell_integration.h"
43#include "ui/base/win/shell.h"
44#include "ui/views/win/hwnd_util.h"
45#endif
46
47using content::WebContents;
48
49HungRendererDialogView* HungRendererDialogView::g_instance_ = NULL;
50
51///////////////////////////////////////////////////////////////////////////////
52// HungPagesTableModel, public:
53
54HungPagesTableModel::HungPagesTableModel(Delegate* delegate)
55    : observer_(NULL),
56      delegate_(delegate) {
57}
58
59HungPagesTableModel::~HungPagesTableModel() {
60}
61
62content::RenderProcessHost* HungPagesTableModel::GetRenderProcessHost() {
63  return tab_observers_.empty() ? NULL :
64      tab_observers_[0]->web_contents()->GetRenderProcessHost();
65}
66
67content::RenderViewHost* HungPagesTableModel::GetRenderViewHost() {
68  return tab_observers_.empty() ? NULL :
69      tab_observers_[0]->web_contents()->GetRenderViewHost();
70}
71
72void HungPagesTableModel::InitForWebContents(WebContents* hung_contents) {
73  tab_observers_.clear();
74  if (hung_contents) {
75    // Force hung_contents to be first.
76    if (hung_contents) {
77      tab_observers_.push_back(new WebContentsObserverImpl(this,
78                                                           hung_contents));
79    }
80    for (TabContentsIterator it; !it.done(); it.Next()) {
81      if (*it != hung_contents &&
82          it->GetRenderProcessHost() == hung_contents->GetRenderProcessHost())
83        tab_observers_.push_back(new WebContentsObserverImpl(this, *it));
84    }
85  }
86  // The world is different.
87  if (observer_)
88    observer_->OnModelChanged();
89}
90
91///////////////////////////////////////////////////////////////////////////////
92// HungPagesTableModel, ui::TableModel implementation:
93
94int HungPagesTableModel::RowCount() {
95  return static_cast<int>(tab_observers_.size());
96}
97
98base::string16 HungPagesTableModel::GetText(int row, int column_id) {
99  DCHECK(row >= 0 && row < RowCount());
100  base::string16 title = tab_observers_[row]->web_contents()->GetTitle();
101  if (title.empty())
102    title = CoreTabHelper::GetDefaultTitle();
103  // TODO(xji): Consider adding a special case if the title text is a URL,
104  // since those should always have LTR directionality. Please refer to
105  // http://crbug.com/6726 for more information.
106  base::i18n::AdjustStringForLocaleDirection(&title);
107  return title;
108}
109
110gfx::ImageSkia HungPagesTableModel::GetIcon(int row) {
111  DCHECK(row >= 0 && row < RowCount());
112  return FaviconTabHelper::FromWebContents(
113      tab_observers_[row]->web_contents())->GetFavicon().AsImageSkia();
114}
115
116void HungPagesTableModel::SetObserver(ui::TableModelObserver* observer) {
117  observer_ = observer;
118}
119
120void HungPagesTableModel::GetGroupRange(int model_index,
121                                        views::GroupRange* range) {
122  DCHECK(range);
123  range->start = 0;
124  range->length = RowCount();
125}
126
127void HungPagesTableModel::TabDestroyed(WebContentsObserverImpl* tab) {
128  // Clean up tab_observers_ and notify our observer.
129  TabObservers::iterator i = std::find(
130      tab_observers_.begin(), tab_observers_.end(), tab);
131  DCHECK(i != tab_observers_.end());
132  int index = static_cast<int>(i - tab_observers_.begin());
133  tab_observers_.erase(i);
134  if (observer_)
135    observer_->OnItemsRemoved(index, 1);
136
137  // Notify the delegate.
138  delegate_->TabDestroyed();
139  // WARNING: we've likely been deleted.
140}
141
142HungPagesTableModel::WebContentsObserverImpl::WebContentsObserverImpl(
143    HungPagesTableModel* model, WebContents* tab)
144    : content::WebContentsObserver(tab),
145      model_(model) {
146}
147
148void HungPagesTableModel::WebContentsObserverImpl::RenderProcessGone(
149    base::TerminationStatus status) {
150  model_->TabDestroyed(this);
151}
152
153void HungPagesTableModel::WebContentsObserverImpl::WebContentsDestroyed() {
154  model_->TabDestroyed(this);
155}
156
157///////////////////////////////////////////////////////////////////////////////
158// HungRendererDialogView
159
160// static
161gfx::ImageSkia* HungRendererDialogView::frozen_icon_ = NULL;
162
163// The dimensions of the hung pages list table view, in pixels.
164static const int kTableViewWidth = 300;
165static const int kTableViewHeight = 100;
166
167// Padding space in pixels between frozen icon to the info label, hung pages
168// list table view and the Kill pages button.
169static const int kCentralColumnPadding =
170    views::kUnrelatedControlLargeHorizontalSpacing;
171
172///////////////////////////////////////////////////////////////////////////////
173// HungRendererDialogView, public:
174
175// static
176HungRendererDialogView* HungRendererDialogView::Create(
177    gfx::NativeView context) {
178  if (!g_instance_) {
179    g_instance_ = new HungRendererDialogView;
180    views::DialogDelegate::CreateDialogWidget(g_instance_, context, NULL);
181  }
182  return g_instance_;
183}
184
185// static
186HungRendererDialogView* HungRendererDialogView::GetInstance() {
187  return g_instance_;
188}
189
190// static
191bool HungRendererDialogView::IsFrameActive(WebContents* contents) {
192  gfx::NativeView frame_view =
193      platform_util::GetTopLevel(contents->GetNativeView());
194  return platform_util::IsWindowActive(frame_view);
195}
196
197// static
198void HungRendererDialogView::KillRendererProcess(
199    base::ProcessHandle process_handle) {
200#if defined(OS_WIN)
201  // Try to generate a crash report for the hung process.
202  CrashDumpAndTerminateHungChildProcess(process_handle);
203#else
204  base::KillProcess(process_handle, content::RESULT_CODE_HUNG, false);
205#endif
206}
207
208
209HungRendererDialogView::HungRendererDialogView()
210    : hung_pages_table_(NULL),
211      kill_button_(NULL),
212      initialized_(false) {
213  InitClass();
214}
215
216HungRendererDialogView::~HungRendererDialogView() {
217  hung_pages_table_->SetModel(NULL);
218}
219
220void HungRendererDialogView::ShowForWebContents(WebContents* contents) {
221  DCHECK(contents && GetWidget());
222
223  // Don't show the warning unless the foreground window is the frame, or this
224  // window (but still invisible). If the user has another window or
225  // application selected, activating ourselves is rude.
226  if (!IsFrameActive(contents) &&
227      !platform_util::IsWindowActive(GetWidget()->GetNativeWindow()))
228    return;
229
230  if (!GetWidget()->IsActive()) {
231    // Place the dialog over content's browser window, similar to modal dialogs.
232    Browser* browser = chrome::FindBrowserWithWebContents(contents);
233    if (browser) {
234      ChromeWebModalDialogManagerDelegate* manager = browser;
235      UpdateBrowserModalDialogPosition(
236          GetWidget(), manager->GetWebContentsModalDialogHost());
237    }
238
239    gfx::NativeView frame_view =
240        platform_util::GetTopLevel(contents->GetNativeView());
241    views::Widget* insert_after =
242        views::Widget::GetWidgetForNativeView(frame_view);
243    if (insert_after)
244      GetWidget()->StackAboveWidget(insert_after);
245
246#if defined(OS_WIN)
247    // Group the hung renderer dialog with the browsers with the same profile.
248    Profile* profile =
249        Profile::FromBrowserContext(contents->GetBrowserContext());
250    ui::win::SetAppIdForWindow(
251        ShellIntegration::GetChromiumModelIdForProfile(profile->GetPath()),
252        views::HWNDForWidget(GetWidget()));
253#endif
254
255    // We only do this if the window isn't active (i.e. hasn't been shown yet,
256    // or is currently shown but deactivated for another WebContents). This is
257    // because this window is a singleton, and it's possible another active
258    // renderer may hang while this one is showing, and we don't want to reset
259    // the list of hung pages for a potentially unrelated renderer while this
260    // one is showing.
261    hung_pages_table_model_->InitForWebContents(contents);
262    GetWidget()->Show();
263  }
264}
265
266void HungRendererDialogView::EndForWebContents(WebContents* contents) {
267  DCHECK(contents);
268  if (hung_pages_table_model_->RowCount() == 0 ||
269      hung_pages_table_model_->GetRenderProcessHost() ==
270      contents->GetRenderProcessHost()) {
271    GetWidget()->Close();
272    // Close is async, make sure we drop our references to the tab immediately
273    // (it may be going away).
274    hung_pages_table_model_->InitForWebContents(NULL);
275  }
276}
277
278///////////////////////////////////////////////////////////////////////////////
279// HungRendererDialogView, views::DialogDelegate implementation:
280
281base::string16 HungRendererDialogView::GetWindowTitle() const {
282  return l10n_util::GetStringUTF16(IDS_BROWSER_HANGMONITOR_RENDERER_TITLE);
283}
284
285void HungRendererDialogView::WindowClosing() {
286  // We are going to be deleted soon, so make sure our instance is destroyed.
287  g_instance_ = NULL;
288}
289
290int HungRendererDialogView::GetDialogButtons() const {
291  // We specifically don't want a CANCEL button here because that code path is
292  // also called when the window is closed by the user clicking the X button in
293  // the window's titlebar, and also if we call Window::Close. Rather, we want
294  // the OK button to wait for responsiveness (and close the dialog) and our
295  // additional button (which we create) to kill the process (which will result
296  // in the dialog being destroyed).
297  return ui::DIALOG_BUTTON_OK;
298}
299
300base::string16 HungRendererDialogView::GetDialogButtonLabel(
301    ui::DialogButton button) const {
302  if (button == ui::DIALOG_BUTTON_OK)
303    return l10n_util::GetStringUTF16(IDS_BROWSER_HANGMONITOR_RENDERER_WAIT);
304  return views::DialogDelegateView::GetDialogButtonLabel(button);
305}
306
307views::View* HungRendererDialogView::CreateExtraView() {
308  DCHECK(!kill_button_);
309  kill_button_ = new views::LabelButton(this,
310      l10n_util::GetStringUTF16(IDS_BROWSER_HANGMONITOR_RENDERER_END));
311  kill_button_->SetStyle(views::Button::STYLE_BUTTON);
312  return kill_button_;
313}
314
315bool HungRendererDialogView::Accept(bool window_closing) {
316  // Don't do anything if we're being called only because the dialog is being
317  // destroyed and we don't supply a Cancel function...
318  if (window_closing)
319    return true;
320
321  // Start waiting again for responsiveness.
322  if (hung_pages_table_model_->GetRenderViewHost())
323    hung_pages_table_model_->GetRenderViewHost()->RestartHangMonitorTimeout();
324  return true;
325}
326
327
328bool HungRendererDialogView::UseNewStyleForThisDialog() const {
329#if defined(OS_WIN)
330  // Use the old dialog style without Aero glass, otherwise the dialog will be
331  // visually constrained to browser window bounds. See http://crbug.com/323278
332  return ui::win::IsAeroGlassEnabled();
333#else
334  return views::DialogDelegateView::UseNewStyleForThisDialog();
335#endif
336}
337
338///////////////////////////////////////////////////////////////////////////////
339// HungRendererDialogView, views::ButtonListener implementation:
340
341void HungRendererDialogView::ButtonPressed(
342    views::Button* sender, const ui::Event& event) {
343  if (sender == kill_button_ &&
344      hung_pages_table_model_->GetRenderProcessHost()) {
345    base::ProcessHandle process_handle =
346        hung_pages_table_model_->GetRenderProcessHost()->GetHandle();
347
348    KillRendererProcess(process_handle);
349  }
350}
351
352///////////////////////////////////////////////////////////////////////////////
353// HungRendererDialogView, HungPagesTableModel::Delegate overrides:
354
355void HungRendererDialogView::TabDestroyed() {
356  GetWidget()->Close();
357}
358
359///////////////////////////////////////////////////////////////////////////////
360// HungRendererDialogView, views::View overrides:
361
362void HungRendererDialogView::ViewHierarchyChanged(
363    const ViewHierarchyChangedDetails& details) {
364  if (!initialized_ && details.is_add && details.child == this && GetWidget())
365    Init();
366}
367
368///////////////////////////////////////////////////////////////////////////////
369// HungRendererDialogView, private:
370
371void HungRendererDialogView::Init() {
372  views::ImageView* frozen_icon_view = new views::ImageView;
373  frozen_icon_view->SetImage(frozen_icon_);
374
375  views::Label* info_label = new views::Label(
376      l10n_util::GetStringUTF16(IDS_BROWSER_HANGMONITOR_RENDERER));
377  info_label->SetMultiLine(true);
378  info_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
379
380  hung_pages_table_model_.reset(new HungPagesTableModel(this));
381  std::vector<ui::TableColumn> columns;
382  columns.push_back(ui::TableColumn());
383  hung_pages_table_ = new views::TableView(
384      hung_pages_table_model_.get(), columns, views::ICON_AND_TEXT, true);
385  hung_pages_table_->SetGrouper(hung_pages_table_model_.get());
386
387  using views::GridLayout;
388  using views::ColumnSet;
389
390  GridLayout* layout = GridLayout::CreatePanel(this);
391  SetLayoutManager(layout);
392
393  const int double_column_set_id = 0;
394  ColumnSet* column_set = layout->AddColumnSet(double_column_set_id);
395  column_set->AddColumn(GridLayout::LEADING, GridLayout::LEADING, 0,
396                        GridLayout::FIXED, frozen_icon_->width(), 0);
397  column_set->AddPaddingColumn(0, kCentralColumnPadding);
398  column_set->AddColumn(GridLayout::FILL, GridLayout::FILL, 1,
399                        GridLayout::USE_PREF, 0, 0);
400
401  layout->StartRow(0, double_column_set_id);
402  layout->AddView(frozen_icon_view, 1, 3);
403  // Add the label with a preferred width of 1, this way it doesn't effect the
404  // overall preferred size of the dialog.
405  layout->AddView(
406      info_label, 1, 1, GridLayout::FILL, GridLayout::LEADING, 1, 0);
407
408  layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
409
410  layout->StartRow(0, double_column_set_id);
411  layout->SkipColumns(1);
412  layout->AddView(hung_pages_table_->CreateParentIfNecessary(), 1, 1,
413                  views::GridLayout::FILL,
414                  views::GridLayout::FILL, kTableViewWidth, kTableViewHeight);
415
416  initialized_ = true;
417}
418
419// static
420void HungRendererDialogView::InitClass() {
421  static bool initialized = false;
422  if (!initialized) {
423    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
424    frozen_icon_ = rb.GetImageSkiaNamed(IDR_FROZEN_TAB_ICON);
425    initialized = true;
426  }
427}
428
429namespace chrome {
430
431void ShowHungRendererDialog(WebContents* contents) {
432  if (logging::DialogsAreSuppressed())
433    return;
434
435  gfx::NativeView toplevel_view =
436      platform_util::GetTopLevel(contents->GetNativeView());
437  // Don't show the dialog if there is no root window for the renderer, because
438  // it's invisible to the user (happens when the renderer is for prerendering
439  // for example).
440  if (!toplevel_view->GetRootWindow())
441    return;
442  HungRendererDialogView* view = HungRendererDialogView::Create(toplevel_view);
443  view->ShowForWebContents(contents);
444}
445
446void HideHungRendererDialog(WebContents* contents) {
447  if (!logging::DialogsAreSuppressed() && HungRendererDialogView::GetInstance())
448    HungRendererDialogView::GetInstance()->EndForWebContents(contents);
449}
450
451}  // namespace chrome
452