hung_plugin_tab_helper.cc revision f2477e01787aa58f445919b809d89e252beef54f
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/hung_plugin_tab_helper.h"
6
7#include "base/bind.h"
8#include "base/files/file_path.h"
9#include "base/memory/scoped_ptr.h"
10#include "base/process/process.h"
11#include "base/rand_util.h"
12#include "build/build_config.h"
13#include "chrome/browser/chrome_notification_types.h"
14#include "chrome/browser/infobars/confirm_infobar_delegate.h"
15#include "chrome/browser/infobars/infobar.h"
16#include "chrome/browser/infobars/infobar_service.h"
17#include "chrome/common/chrome_version_info.h"
18#include "content/public/browser/browser_child_process_host_iterator.h"
19#include "content/public/browser/browser_thread.h"
20#include "content/public/browser/child_process_data.h"
21#include "content/public/browser/notification_details.h"
22#include "content/public/browser/notification_service.h"
23#include "content/public/browser/plugin_service.h"
24#include "content/public/browser/render_process_host.h"
25#include "content/public/common/process_type.h"
26#include "content/public/common/result_codes.h"
27#include "grit/chromium_strings.h"
28#include "grit/generated_resources.h"
29#include "grit/locale_settings.h"
30#include "grit/theme_resources.h"
31#include "ui/base/l10n/l10n_util.h"
32
33#if defined(OS_WIN)
34#include "base/win/scoped_handle.h"
35#include "chrome/browser/hang_monitor/hang_crash_dump_win.h"
36#endif
37
38
39namespace {
40
41#if defined(OS_WIN)
42
43// OwnedHandleVector ----------------------------------------------------------
44
45class OwnedHandleVector {
46 public:
47  typedef std::vector<HANDLE> Handles;
48  OwnedHandleVector();
49  ~OwnedHandleVector();
50
51  Handles* data() { return &data_; }
52
53 private:
54  Handles data_;
55
56  DISALLOW_COPY_AND_ASSIGN(OwnedHandleVector);
57};
58
59OwnedHandleVector::OwnedHandleVector() {
60}
61
62OwnedHandleVector::~OwnedHandleVector() {
63  for (Handles::iterator iter = data_.begin(); iter != data_.end(); ++iter)
64    ::CloseHandle(*iter);
65}
66
67
68// Helpers --------------------------------------------------------------------
69
70const char kDumpChildProcessesSequenceName[] = "DumpChildProcesses";
71
72void DumpBrowserInBlockingPool() {
73  CrashDumpForHangDebugging(::GetCurrentProcess());
74}
75
76void DumpRenderersInBlockingPool(OwnedHandleVector* renderer_handles) {
77  for (OwnedHandleVector::Handles::const_iterator iter =
78           renderer_handles->data()->begin();
79       iter != renderer_handles->data()->end(); ++iter) {
80    CrashDumpForHangDebugging(*iter);
81  }
82}
83
84void DumpAndTerminatePluginInBlockingPool(
85    base::win::ScopedHandle* plugin_handle) {
86  CrashDumpAndTerminateHungChildProcess(plugin_handle->Get());
87}
88
89#endif  // defined(OS_WIN)
90
91// Called on the I/O thread to actually kill the plugin with the given child
92// ID. We specifically don't want this to be a member function since if the
93// user chooses to kill the plugin, we want to kill it even if they close the
94// tab first.
95//
96// Be careful with the child_id. It's supplied by the renderer which might be
97// hacked.
98void KillPluginOnIOThread(int child_id) {
99  content::BrowserChildProcessHostIterator iter(
100      content::PROCESS_TYPE_PPAPI_PLUGIN);
101  while (!iter.Done()) {
102    const content::ChildProcessData& data = iter.GetData();
103    if (data.id == child_id) {
104#if defined(OS_WIN)
105      HANDLE handle = NULL;
106      HANDLE current_process = ::GetCurrentProcess();
107      ::DuplicateHandle(current_process, data.handle, current_process, &handle,
108                        0, FALSE, DUPLICATE_SAME_ACCESS);
109      // Run it in blocking pool so that it won't block the I/O thread. Besides,
110      // we would like to make sure that it happens after dumping renderers.
111      content::BrowserThread::PostBlockingPoolSequencedTask(
112          kDumpChildProcessesSequenceName, FROM_HERE,
113          base::Bind(&DumpAndTerminatePluginInBlockingPool,
114                     base::Owned(new base::win::ScopedHandle(handle))));
115#else
116      base::KillProcess(data.handle, content::RESULT_CODE_HUNG, false);
117#endif
118      break;
119    }
120    ++iter;
121  }
122  // Ignore the case where we didn't find the plugin, it may have terminated
123  // before this function could run.
124}
125
126}  // namespace
127
128
129// HungPluginInfoBarDelegate --------------------------------------------------
130
131class HungPluginInfoBarDelegate : public ConfirmInfoBarDelegate {
132 public:
133  // Creates a hung plugin infobar delegate and adds it to |infobar_service|.
134  // Returns the delegate if it was successfully added.
135  static HungPluginInfoBarDelegate* Create(InfoBarService* infobar_service,
136                                           HungPluginTabHelper* helper,
137                                           int plugin_child_id,
138                                           const string16& plugin_name);
139
140 private:
141  HungPluginInfoBarDelegate(HungPluginTabHelper* helper,
142                            InfoBarService* infobar_service,
143                            int plugin_child_id,
144                            const string16& plugin_name);
145  virtual ~HungPluginInfoBarDelegate();
146
147  // ConfirmInfoBarDelegate:
148  virtual int GetIconID() const OVERRIDE;
149  virtual string16 GetMessageText() const OVERRIDE;
150  virtual int GetButtons() const OVERRIDE;
151  virtual string16 GetButtonLabel(InfoBarButton button) const OVERRIDE;
152  virtual bool Accept() OVERRIDE;
153
154  HungPluginTabHelper* helper_;
155  int plugin_child_id_;
156
157  string16 message_;
158  string16 button_text_;
159};
160
161// static
162HungPluginInfoBarDelegate* HungPluginInfoBarDelegate::Create(
163    InfoBarService* infobar_service,
164    HungPluginTabHelper* helper,
165    int plugin_child_id,
166    const string16& plugin_name) {
167  return static_cast<HungPluginInfoBarDelegate*>(infobar_service->AddInfoBar(
168      scoped_ptr<InfoBarDelegate>(new HungPluginInfoBarDelegate(
169          helper, infobar_service, plugin_child_id, plugin_name))));
170}
171
172HungPluginInfoBarDelegate::HungPluginInfoBarDelegate(
173    HungPluginTabHelper* helper,
174    InfoBarService* infobar_service,
175    int plugin_child_id,
176    const string16& plugin_name)
177    : ConfirmInfoBarDelegate(infobar_service),
178      helper_(helper),
179      plugin_child_id_(plugin_child_id),
180      message_(l10n_util::GetStringFUTF16(
181          IDS_BROWSER_HANGMONITOR_PLUGIN_INFOBAR, plugin_name)),
182      button_text_(l10n_util::GetStringUTF16(
183          IDS_BROWSER_HANGMONITOR_PLUGIN_INFOBAR_KILLBUTTON)) {
184}
185
186HungPluginInfoBarDelegate::~HungPluginInfoBarDelegate() {
187}
188
189int HungPluginInfoBarDelegate::GetIconID() const {
190  return IDR_INFOBAR_PLUGIN_CRASHED;
191}
192
193string16 HungPluginInfoBarDelegate::GetMessageText() const {
194  return message_;
195}
196
197int HungPluginInfoBarDelegate::GetButtons() const {
198  return BUTTON_OK;
199}
200
201string16 HungPluginInfoBarDelegate::GetButtonLabel(InfoBarButton button) const {
202  return button_text_;
203}
204
205bool HungPluginInfoBarDelegate::Accept() {
206  helper_->KillPlugin(plugin_child_id_);
207  return true;
208}
209
210
211// HungPluginTabHelper::PluginState -------------------------------------------
212
213// Per-plugin state (since there could be more than one plugin hung).  The
214// integer key is the child process ID of the plugin process.  This maintains
215// the state for all plugins on this page that are currently hung, whether or
216// not we're currently showing the infobar.
217struct HungPluginTabHelper::PluginState {
218  // Initializes the plugin state to be a hung plugin.
219  PluginState(const base::FilePath& p, const string16& n);
220  ~PluginState();
221
222  base::FilePath path;
223  string16 name;
224
225  // Possibly-null if we're not showing an infobar right now.
226  InfoBarDelegate* infobar;
227
228  // Time to delay before re-showing the infobar for a hung plugin. This is
229  // increased each time the user cancels it.
230  base::TimeDelta next_reshow_delay;
231
232  // Handles calling the helper when the infobar should be re-shown.
233  base::Timer timer;
234
235 private:
236  // Initial delay in seconds before re-showing the hung plugin message.
237  static const int kInitialReshowDelaySec;
238
239  // Since the scope of the timer manages our callback, this struct should
240  // not be copied.
241  DISALLOW_COPY_AND_ASSIGN(PluginState);
242};
243
244// static
245const int HungPluginTabHelper::PluginState::kInitialReshowDelaySec = 10;
246
247HungPluginTabHelper::PluginState::PluginState(const base::FilePath& p,
248                                              const string16& n)
249    : path(p),
250      name(n),
251      infobar(NULL),
252      next_reshow_delay(base::TimeDelta::FromSeconds(kInitialReshowDelaySec)),
253      timer(false, false) {
254}
255
256HungPluginTabHelper::PluginState::~PluginState() {
257}
258
259
260// HungPluginTabHelper --------------------------------------------------------
261
262DEFINE_WEB_CONTENTS_USER_DATA_KEY(HungPluginTabHelper);
263
264HungPluginTabHelper::HungPluginTabHelper(content::WebContents* contents)
265    : content::WebContentsObserver(contents) {
266  registrar_.Add(this, chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REMOVED,
267                 content::NotificationService::AllSources());
268}
269
270HungPluginTabHelper::~HungPluginTabHelper() {
271}
272
273void HungPluginTabHelper::PluginCrashed(const base::FilePath& plugin_path,
274                                        base::ProcessId plugin_pid) {
275  // TODO(brettw) ideally this would take the child process ID. When we do this
276  // for NaCl plugins, we'll want to know exactly which process it was since
277  // the path won't be useful.
278  InfoBarService* infobar_service =
279      InfoBarService::FromWebContents(web_contents());
280  if (!infobar_service)
281    return;
282
283  // For now, just do a brute-force search to see if we have this plugin. Since
284  // we'll normally have 0 or 1, this is fast.
285  for (PluginStateMap::iterator i = hung_plugins_.begin();
286       i != hung_plugins_.end(); ++i) {
287    if (i->second->path == plugin_path) {
288      if (i->second->infobar)
289        infobar_service->RemoveInfoBar(i->second->infobar);
290      hung_plugins_.erase(i);
291      break;
292    }
293  }
294}
295
296void HungPluginTabHelper::PluginHungStatusChanged(
297    int plugin_child_id,
298    const base::FilePath& plugin_path,
299    bool is_hung) {
300  InfoBarService* infobar_service =
301      InfoBarService::FromWebContents(web_contents());
302  if (!infobar_service)
303    return;
304
305  PluginStateMap::iterator found = hung_plugins_.find(plugin_child_id);
306  if (found != hung_plugins_.end()) {
307    if (!is_hung) {
308      // Hung plugin became un-hung, close the infobar and delete our info.
309      if (found->second->infobar)
310        infobar_service->RemoveInfoBar(found->second->infobar);
311      hung_plugins_.erase(found);
312    }
313    return;
314  }
315
316  string16 plugin_name =
317      content::PluginService::GetInstance()->GetPluginDisplayNameByPath(
318          plugin_path);
319
320  linked_ptr<PluginState> state(new PluginState(plugin_path, plugin_name));
321  hung_plugins_[plugin_child_id] = state;
322  ShowBar(plugin_child_id, state.get());
323}
324
325void HungPluginTabHelper::Observe(
326    int type,
327    const content::NotificationSource& source,
328    const content::NotificationDetails& details) {
329  DCHECK_EQ(chrome::NOTIFICATION_TAB_CONTENTS_INFOBAR_REMOVED, type);
330  // Note: do not dereference. The InfoBarContainer will delete the object when
331  // it gets this notification, we only remove our tracking info, if we have
332  // any.
333  //
334  // TODO(pkasting): This comment will be incorrect and should be removed once
335  // InfoBars own their delegates.
336  InfoBarDelegate* infobar =
337      content::Details<InfoBar::RemovedDetails>(details)->first;
338  for (PluginStateMap::iterator i = hung_plugins_.begin();
339       i != hung_plugins_.end(); ++i) {
340    PluginState* state = i->second.get();
341    if (state->infobar == infobar) {
342      state->infobar = NULL;
343
344      // Schedule the timer to re-show the infobar if the plugin continues to be
345      // hung.
346      state->timer.Start(FROM_HERE, state->next_reshow_delay,
347          base::Bind(&HungPluginTabHelper::OnReshowTimer,
348                     base::Unretained(this),
349                     i->first));
350
351      // Next time we do this, delay it twice as long to avoid being annoying.
352      state->next_reshow_delay *= 2;
353      return;
354    }
355  }
356}
357
358void HungPluginTabHelper::KillPlugin(int child_id) {
359#if defined(OS_WIN)
360  // Dump renderers that are sending or receiving pepper messages, in order to
361  // diagnose inter-process deadlocks.
362  // Only do that on the Canary channel, for 20% of pepper plugin hangs.
363  if (base::RandInt(0, 100) < 20) {
364    chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
365    if (channel == chrome::VersionInfo::CHANNEL_CANARY) {
366      scoped_ptr<OwnedHandleVector> renderer_handles(new OwnedHandleVector);
367      HANDLE current_process = ::GetCurrentProcess();
368      content::RenderProcessHost::iterator renderer_iter =
369          content::RenderProcessHost::AllHostsIterator();
370      for (; !renderer_iter.IsAtEnd(); renderer_iter.Advance()) {
371        content::RenderProcessHost* host = renderer_iter.GetCurrentValue();
372        HANDLE handle = NULL;
373        ::DuplicateHandle(current_process, host->GetHandle(), current_process,
374                          &handle, 0, FALSE, DUPLICATE_SAME_ACCESS);
375        renderer_handles->data()->push_back(handle);
376      }
377      // If there are a lot of renderer processes, it is likely that we will
378      // generate too many crash dumps. They might not all be uploaded/recorded
379      // due to our crash dump uploading restrictions. So we just don't generate
380      // renderer crash dumps in that case.
381      if (renderer_handles->data()->size() > 0 &&
382          renderer_handles->data()->size() < 4) {
383        content::BrowserThread::PostBlockingPoolSequencedTask(
384            kDumpChildProcessesSequenceName, FROM_HERE,
385            base::Bind(&DumpBrowserInBlockingPool));
386        content::BrowserThread::PostBlockingPoolSequencedTask(
387            kDumpChildProcessesSequenceName, FROM_HERE,
388            base::Bind(&DumpRenderersInBlockingPool,
389                       base::Owned(renderer_handles.release())));
390      }
391    }
392  }
393#endif
394
395  PluginStateMap::iterator found = hung_plugins_.find(child_id);
396  DCHECK(found != hung_plugins_.end());
397
398  content::BrowserThread::PostTask(content::BrowserThread::IO,
399                                   FROM_HERE,
400                                   base::Bind(&KillPluginOnIOThread, child_id));
401  CloseBar(found->second.get());
402}
403
404void HungPluginTabHelper::OnReshowTimer(int child_id) {
405  // The timer should have been cancelled if the record isn't in our map
406  // anymore.
407  PluginStateMap::iterator found = hung_plugins_.find(child_id);
408  DCHECK(found != hung_plugins_.end());
409  DCHECK(!found->second->infobar);
410  ShowBar(child_id, found->second.get());
411}
412
413void HungPluginTabHelper::ShowBar(int child_id, PluginState* state) {
414  InfoBarService* infobar_service =
415      InfoBarService::FromWebContents(web_contents());
416  if (!infobar_service)
417    return;
418
419  DCHECK(!state->infobar);
420  state->infobar = HungPluginInfoBarDelegate::Create(infobar_service, this,
421                                                     child_id, state->name);
422}
423
424void HungPluginTabHelper::CloseBar(PluginState* state) {
425  InfoBarService* infobar_service =
426      InfoBarService::FromWebContents(web_contents());
427  if (infobar_service && state->infobar) {
428    infobar_service->RemoveInfoBar(state->infobar);
429    state->infobar = NULL;
430  }
431}
432