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 39 namespace { 40 41 #if defined(OS_WIN) 42 43 // OwnedHandleVector ---------------------------------------------------------- 44 45 class 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 59 OwnedHandleVector::OwnedHandleVector() { 60 } 61 62 OwnedHandleVector::~OwnedHandleVector() { 63 for (Handles::iterator iter = data_.begin(); iter != data_.end(); ++iter) 64 ::CloseHandle(*iter); 65 } 66 67 68 // Helpers -------------------------------------------------------------------- 69 70 const char kDumpChildProcessesSequenceName[] = "DumpChildProcesses"; 71 72 void DumpBrowserInBlockingPool() { 73 CrashDumpForHangDebugging(::GetCurrentProcess()); 74 } 75 76 void 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 84 void 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. 98 void 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 131 class HungPluginInfoBarDelegate : public ConfirmInfoBarDelegate { 132 public: 133 // Creates a hung plugin infobar and delegate and adds the infobar to 134 // |infobar_service|. Returns the infobar if it was successfully added. 135 static InfoBar* Create(InfoBarService* infobar_service, 136 HungPluginTabHelper* helper, 137 int plugin_child_id, 138 const base::string16& plugin_name); 139 140 private: 141 HungPluginInfoBarDelegate(HungPluginTabHelper* helper, 142 int plugin_child_id, 143 const base::string16& plugin_name); 144 virtual ~HungPluginInfoBarDelegate(); 145 146 // ConfirmInfoBarDelegate: 147 virtual int GetIconID() const OVERRIDE; 148 virtual base::string16 GetMessageText() const OVERRIDE; 149 virtual int GetButtons() const OVERRIDE; 150 virtual base::string16 GetButtonLabel(InfoBarButton button) const OVERRIDE; 151 virtual bool Accept() OVERRIDE; 152 153 HungPluginTabHelper* helper_; 154 int plugin_child_id_; 155 156 base::string16 message_; 157 base::string16 button_text_; 158 }; 159 160 // static 161 InfoBar* HungPluginInfoBarDelegate::Create(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 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 InfoBar* infobar = content::Details<InfoBar::RemovedDetails>(details)->first; 329 for (PluginStateMap::iterator i = hung_plugins_.begin(); 330 i != hung_plugins_.end(); ++i) { 331 PluginState* state = i->second.get(); 332 if (state->infobar == infobar) { 333 state->infobar = NULL; 334 335 // Schedule the timer to re-show the infobar if the plugin continues to be 336 // hung. 337 state->timer.Start(FROM_HERE, state->next_reshow_delay, 338 base::Bind(&HungPluginTabHelper::OnReshowTimer, 339 base::Unretained(this), 340 i->first)); 341 342 // Next time we do this, delay it twice as long to avoid being annoying. 343 state->next_reshow_delay *= 2; 344 return; 345 } 346 } 347 } 348 349 void HungPluginTabHelper::KillPlugin(int child_id) { 350 #if defined(OS_WIN) 351 // Dump renderers that are sending or receiving pepper messages, in order to 352 // diagnose inter-process deadlocks. 353 // Only do that on the Canary channel, for 20% of pepper plugin hangs. 354 if (base::RandInt(0, 100) < 20) { 355 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); 356 if (channel == chrome::VersionInfo::CHANNEL_CANARY) { 357 scoped_ptr<OwnedHandleVector> renderer_handles(new OwnedHandleVector); 358 HANDLE current_process = ::GetCurrentProcess(); 359 content::RenderProcessHost::iterator renderer_iter = 360 content::RenderProcessHost::AllHostsIterator(); 361 for (; !renderer_iter.IsAtEnd(); renderer_iter.Advance()) { 362 content::RenderProcessHost* host = renderer_iter.GetCurrentValue(); 363 HANDLE handle = NULL; 364 ::DuplicateHandle(current_process, host->GetHandle(), current_process, 365 &handle, 0, FALSE, DUPLICATE_SAME_ACCESS); 366 renderer_handles->data()->push_back(handle); 367 } 368 // If there are a lot of renderer processes, it is likely that we will 369 // generate too many crash dumps. They might not all be uploaded/recorded 370 // due to our crash dump uploading restrictions. So we just don't generate 371 // renderer crash dumps in that case. 372 if (renderer_handles->data()->size() > 0 && 373 renderer_handles->data()->size() < 4) { 374 content::BrowserThread::PostBlockingPoolSequencedTask( 375 kDumpChildProcessesSequenceName, FROM_HERE, 376 base::Bind(&DumpBrowserInBlockingPool)); 377 content::BrowserThread::PostBlockingPoolSequencedTask( 378 kDumpChildProcessesSequenceName, FROM_HERE, 379 base::Bind(&DumpRenderersInBlockingPool, 380 base::Owned(renderer_handles.release()))); 381 } 382 } 383 } 384 #endif 385 386 PluginStateMap::iterator found = hung_plugins_.find(child_id); 387 DCHECK(found != hung_plugins_.end()); 388 389 content::BrowserThread::PostTask(content::BrowserThread::IO, 390 FROM_HERE, 391 base::Bind(&KillPluginOnIOThread, child_id)); 392 CloseBar(found->second.get()); 393 } 394 395 void HungPluginTabHelper::OnReshowTimer(int child_id) { 396 // The timer should have been cancelled if the record isn't in our map 397 // anymore. 398 PluginStateMap::iterator found = hung_plugins_.find(child_id); 399 DCHECK(found != hung_plugins_.end()); 400 DCHECK(!found->second->infobar); 401 ShowBar(child_id, found->second.get()); 402 } 403 404 void HungPluginTabHelper::ShowBar(int child_id, PluginState* state) { 405 InfoBarService* infobar_service = 406 InfoBarService::FromWebContents(web_contents()); 407 if (!infobar_service) 408 return; 409 410 DCHECK(!state->infobar); 411 state->infobar = HungPluginInfoBarDelegate::Create(infobar_service, this, 412 child_id, state->name); 413 } 414 415 void HungPluginTabHelper::CloseBar(PluginState* state) { 416 InfoBarService* infobar_service = 417 InfoBarService::FromWebContents(web_contents()); 418 if (infobar_service && state->infobar) { 419 infobar_service->RemoveInfoBar(state->infobar); 420 state->infobar = NULL; 421 } 422 } 423