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