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