1// Copyright 2013 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/fast_unload_controller.h" 6 7#include "base/logging.h" 8#include "base/message_loop/message_loop.h" 9#include "chrome/browser/chrome_notification_types.h" 10#include "chrome/browser/devtools/devtools_window.h" 11#include "chrome/browser/ui/browser.h" 12#include "chrome/browser/ui/browser_tabstrip.h" 13#include "chrome/browser/ui/tab_contents/core_tab_helper.h" 14#include "chrome/browser/ui/tabs/tab_strip_model.h" 15#include "chrome/browser/ui/tabs/tab_strip_model_delegate.h" 16#include "content/public/browser/notification_service.h" 17#include "content/public/browser/notification_source.h" 18#include "content/public/browser/notification_types.h" 19#include "content/public/browser/render_view_host.h" 20#include "content/public/browser/web_contents.h" 21#include "content/public/browser/web_contents_delegate.h" 22 23namespace chrome { 24 25 26//////////////////////////////////////////////////////////////////////////////// 27// DetachedWebContentsDelegate will delete web contents when they close. 28class FastUnloadController::DetachedWebContentsDelegate 29 : public content::WebContentsDelegate { 30 public: 31 DetachedWebContentsDelegate() { } 32 virtual ~DetachedWebContentsDelegate() { } 33 34 private: 35 // WebContentsDelegate implementation. 36 virtual bool ShouldSuppressDialogs() OVERRIDE { 37 return true; // Return true so dialogs are suppressed. 38 } 39 40 virtual void CloseContents(content::WebContents* source) OVERRIDE { 41 // Finished detached close. 42 // FastUnloadController will observe 43 // |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|. 44 delete source; 45 } 46 47 DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate); 48}; 49 50//////////////////////////////////////////////////////////////////////////////// 51// FastUnloadController, public: 52 53FastUnloadController::FastUnloadController(Browser* browser) 54 : browser_(browser), 55 tab_needing_before_unload_ack_(NULL), 56 is_attempting_to_close_browser_(false), 57 detached_delegate_(new DetachedWebContentsDelegate()), 58 weak_factory_(this) { 59 browser_->tab_strip_model()->AddObserver(this); 60} 61 62FastUnloadController::~FastUnloadController() { 63 browser_->tab_strip_model()->RemoveObserver(this); 64} 65 66bool FastUnloadController::CanCloseContents(content::WebContents* contents) { 67 // Don't try to close the tab when the whole browser is being closed, since 68 // that avoids the fast shutdown path where we just kill all the renderers. 69 return !is_attempting_to_close_browser_ || 70 is_calling_before_unload_handlers(); 71} 72 73// static 74bool FastUnloadController::ShouldRunUnloadEventsHelper( 75 content::WebContents* contents) { 76 // If |contents| is being inspected, devtools needs to intercept beforeunload 77 // events. 78 return DevToolsWindow::GetInstanceForInspectedRenderViewHost( 79 contents->GetRenderViewHost()) != NULL; 80} 81 82// static 83bool FastUnloadController::RunUnloadEventsHelper( 84 content::WebContents* contents) { 85 // If there's a devtools window attached to |contents|, 86 // we would like devtools to call its own beforeunload handlers first, 87 // and then call beforeunload handlers for |contents|. 88 // See DevToolsWindow::InterceptPageBeforeUnload for details. 89 if (DevToolsWindow::InterceptPageBeforeUnload(contents)) { 90 return true; 91 } 92 // If the WebContents is not connected yet, then there's no unload 93 // handler we can fire even if the WebContents has an unload listener. 94 // One case where we hit this is in a tab that has an infinite loop 95 // before load. 96 if (contents->NeedToFireBeforeUnload()) { 97 // If the page has unload listeners, then we tell the renderer to fire 98 // them. Once they have fired, we'll get a message back saying whether 99 // to proceed closing the page or not, which sends us back to this method 100 // with the NeedToFireBeforeUnload bit cleared. 101 contents->GetRenderViewHost()->FirePageBeforeUnload(false); 102 return true; 103 } 104 return false; 105} 106 107bool FastUnloadController::BeforeUnloadFired(content::WebContents* contents, 108 bool proceed) { 109 if (!proceed) 110 DevToolsWindow::OnPageCloseCanceled(contents); 111 112 if (!is_attempting_to_close_browser_) { 113 if (!proceed) { 114 contents->SetClosedByUserGesture(false); 115 } else { 116 // No more dialogs are possible, so remove the tab and finish 117 // running unload listeners asynchrounously. 118 browser_->tab_strip_model()->delegate()->CreateHistoricalTab(contents); 119 DetachWebContents(contents); 120 } 121 return proceed; 122 } 123 124 if (!proceed) { 125 CancelWindowClose(); 126 contents->SetClosedByUserGesture(false); 127 return false; 128 } 129 130 if (tab_needing_before_unload_ack_ == contents) { 131 // Now that beforeunload has fired, queue the tab to fire unload. 132 tab_needing_before_unload_ack_ = NULL; 133 tabs_needing_unload_.insert(contents); 134 ProcessPendingTabs(); 135 // We want to handle firing the unload event ourselves since we want to 136 // fire all the beforeunload events before attempting to fire the unload 137 // events should the user cancel closing the browser. 138 return false; 139 } 140 141 return true; 142} 143 144bool FastUnloadController::ShouldCloseWindow() { 145 if (HasCompletedUnloadProcessing()) 146 return true; 147 148 // Special case for when we quit an application. The Devtools window can 149 // close if it's beforeunload event has already fired which will happen due 150 // to the interception of it's content's beforeunload. 151 if (browser_->is_devtools() && 152 DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_)) { 153 return true; 154 } 155 156 // The behavior followed here varies based on the current phase of the 157 // operation and whether a batched shutdown is in progress. 158 // 159 // If there are tabs with outstanding beforeunload handlers: 160 // 1. If a batched shutdown is in progress: return false. 161 // This is to prevent interference with batched shutdown already in 162 // progress. 163 // 2. Otherwise: start sending beforeunload events and return false. 164 // 165 // Otherwise, If there are no tabs with outstanding beforeunload handlers: 166 // 3. If a batched shutdown is in progress: start sending unload events and 167 // return false. 168 // 4. Otherwise: return true. 169 is_attempting_to_close_browser_ = true; 170 // Cases 1 and 4. 171 bool need_beforeunload_fired = TabsNeedBeforeUnloadFired(); 172 if (need_beforeunload_fired == is_calling_before_unload_handlers()) 173 return !need_beforeunload_fired; 174 175 // Cases 2 and 3. 176 on_close_confirmed_.Reset(); 177 ProcessPendingTabs(); 178 return false; 179} 180 181bool FastUnloadController::CallBeforeUnloadHandlers( 182 const base::Callback<void(bool)>& on_close_confirmed) { 183// The devtools browser gets its beforeunload events as the results of 184// intercepting events from the inspected tab, so don't send them here as well. 185 if (browser_->is_devtools() || !TabsNeedBeforeUnloadFired()) 186 return false; 187 188 on_close_confirmed_ = on_close_confirmed; 189 is_attempting_to_close_browser_ = true; 190 ProcessPendingTabs(); 191 return true; 192} 193 194void FastUnloadController::ResetBeforeUnloadHandlers() { 195 if (!is_calling_before_unload_handlers()) 196 return; 197 CancelWindowClose(); 198} 199 200bool FastUnloadController::TabsNeedBeforeUnloadFired() { 201 if (!tabs_needing_before_unload_.empty() || 202 tab_needing_before_unload_ack_ != NULL) 203 return true; 204 205 if (!is_calling_before_unload_handlers() && !tabs_needing_unload_.empty()) 206 return false; 207 208 for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) { 209 content::WebContents* contents = 210 browser_->tab_strip_model()->GetWebContentsAt(i); 211 bool should_fire_beforeunload = contents->NeedToFireBeforeUnload() || 212 DevToolsWindow::NeedsToInterceptBeforeUnload(contents); 213 if (!ContainsKey(tabs_needing_unload_, contents) && 214 !ContainsKey(tabs_needing_unload_ack_, contents) && 215 tab_needing_before_unload_ack_ != contents && 216 should_fire_beforeunload) 217 tabs_needing_before_unload_.insert(contents); 218 } 219 return !tabs_needing_before_unload_.empty(); 220} 221 222//////////////////////////////////////////////////////////////////////////////// 223// FastUnloadController, content::NotificationObserver implementation: 224 225void FastUnloadController::Observe( 226 int type, 227 const content::NotificationSource& source, 228 const content::NotificationDetails& details) { 229 switch (type) { 230 case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: { 231 registrar_.Remove(this, 232 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 233 source); 234 content::WebContents* contents = 235 content::Source<content::WebContents>(source).ptr(); 236 ClearUnloadState(contents); 237 break; 238 } 239 default: 240 NOTREACHED() << "Got a notification we didn't register for."; 241 } 242} 243 244//////////////////////////////////////////////////////////////////////////////// 245// FastUnloadController, TabStripModelObserver implementation: 246 247void FastUnloadController::TabInsertedAt(content::WebContents* contents, 248 int index, 249 bool foreground) { 250 TabAttachedImpl(contents); 251} 252 253void FastUnloadController::TabDetachedAt(content::WebContents* contents, 254 int index) { 255 TabDetachedImpl(contents); 256} 257 258void FastUnloadController::TabReplacedAt(TabStripModel* tab_strip_model, 259 content::WebContents* old_contents, 260 content::WebContents* new_contents, 261 int index) { 262 TabDetachedImpl(old_contents); 263 TabAttachedImpl(new_contents); 264} 265 266void FastUnloadController::TabStripEmpty() { 267 // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not 268 // attempt to add tabs to the browser before it closes. 269 is_attempting_to_close_browser_ = true; 270} 271 272//////////////////////////////////////////////////////////////////////////////// 273// FastUnloadController, private: 274 275void FastUnloadController::TabAttachedImpl(content::WebContents* contents) { 276 // If the tab crashes in the beforeunload or unload handler, it won't be 277 // able to ack. But we know we can close it. 278 registrar_.Add( 279 this, 280 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 281 content::Source<content::WebContents>(contents)); 282} 283 284void FastUnloadController::TabDetachedImpl(content::WebContents* contents) { 285 if (tabs_needing_unload_ack_.find(contents) != 286 tabs_needing_unload_ack_.end()) { 287 // Tab needs unload to complete. 288 // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done. 289 return; 290 } 291 292 // If WEB_CONTENTS_DISCONNECTED was received then the notification may have 293 // already been unregistered. 294 const content::NotificationSource& source = 295 content::Source<content::WebContents>(contents); 296 if (registrar_.IsRegistered(this, 297 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 298 source)) { 299 registrar_.Remove(this, 300 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 301 source); 302 } 303 304 if (is_attempting_to_close_browser_) 305 ClearUnloadState(contents); 306} 307 308bool FastUnloadController::DetachWebContents(content::WebContents* contents) { 309 int index = browser_->tab_strip_model()->GetIndexOfWebContents(contents); 310 if (index != TabStripModel::kNoTab && 311 contents->NeedToFireBeforeUnload()) { 312 tabs_needing_unload_ack_.insert(contents); 313 browser_->tab_strip_model()->DetachWebContentsAt(index); 314 contents->SetDelegate(detached_delegate_.get()); 315 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); 316 core_tab_helper->OnUnloadDetachedStarted(); 317 return true; 318 } 319 return false; 320} 321 322void FastUnloadController::ProcessPendingTabs() { 323 if (!is_attempting_to_close_browser_) { 324 // Because we might invoke this after a delay it's possible for the value of 325 // is_attempting_to_close_browser_ to have changed since we scheduled the 326 // task. 327 return; 328 } 329 330 if (tab_needing_before_unload_ack_ != NULL) { 331 // Wait for |BeforeUnloadFired| before proceeding. 332 return; 333 } 334 335 // Process a beforeunload handler. 336 if (!tabs_needing_before_unload_.empty()) { 337 WebContentsSet::iterator it = tabs_needing_before_unload_.begin(); 338 content::WebContents* contents = *it; 339 tabs_needing_before_unload_.erase(it); 340 // Null check render_view_host here as this gets called on a PostTask and 341 // the tab's render_view_host may have been nulled out. 342 if (contents->GetRenderViewHost()) { 343 tab_needing_before_unload_ack_ = contents; 344 345 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); 346 core_tab_helper->OnCloseStarted(); 347 348 // If there's a devtools window attached to |contents|, 349 // we would like devtools to call its own beforeunload handlers first, 350 // and then call beforeunload handlers for |contents|. 351 // See DevToolsWindow::InterceptPageBeforeUnload for details. 352 if (!DevToolsWindow::InterceptPageBeforeUnload(contents)) 353 contents->GetRenderViewHost()->FirePageBeforeUnload(false); 354 } else { 355 ProcessPendingTabs(); 356 } 357 return; 358 } 359 360 if (is_calling_before_unload_handlers()) { 361 on_close_confirmed_.Run(true); 362 return; 363 } 364 // Process all the unload handlers. (The beforeunload handlers have finished.) 365 if (!tabs_needing_unload_.empty()) { 366 browser_->OnWindowClosing(); 367 368 // Run unload handlers detached since no more interaction is possible. 369 WebContentsSet::iterator it = tabs_needing_unload_.begin(); 370 while (it != tabs_needing_unload_.end()) { 371 WebContentsSet::iterator current = it++; 372 content::WebContents* contents = *current; 373 tabs_needing_unload_.erase(current); 374 // Null check render_view_host here as this gets called on a PostTask 375 // and the tab's render_view_host may have been nulled out. 376 if (contents->GetRenderViewHost()) { 377 CoreTabHelper* core_tab_helper = 378 CoreTabHelper::FromWebContents(contents); 379 core_tab_helper->OnUnloadStarted(); 380 DetachWebContents(contents); 381 contents->GetRenderViewHost()->ClosePage(); 382 } 383 } 384 385 // Get the browser hidden. 386 if (browser_->tab_strip_model()->empty()) { 387 browser_->TabStripEmpty(); 388 } else { 389 browser_->tab_strip_model()->CloseAllTabs(); // tabs not needing unload 390 } 391 return; 392 } 393 394 if (HasCompletedUnloadProcessing()) { 395 browser_->OnWindowClosing(); 396 397 // Get the browser closed. 398 if (browser_->tab_strip_model()->empty()) { 399 browser_->TabStripEmpty(); 400 } else { 401 // There may be tabs if the last tab needing beforeunload crashed. 402 browser_->tab_strip_model()->CloseAllTabs(); 403 } 404 return; 405 } 406} 407 408bool FastUnloadController::HasCompletedUnloadProcessing() const { 409 return is_attempting_to_close_browser_ && 410 tabs_needing_before_unload_.empty() && 411 tab_needing_before_unload_ack_ == NULL && 412 tabs_needing_unload_.empty() && 413 tabs_needing_unload_ack_.empty(); 414} 415 416void FastUnloadController::CancelWindowClose() { 417 // Closing of window can be canceled from a beforeunload handler. 418 DCHECK(is_attempting_to_close_browser_); 419 tabs_needing_before_unload_.clear(); 420 if (tab_needing_before_unload_ack_ != NULL) { 421 CoreTabHelper* core_tab_helper = 422 CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_); 423 core_tab_helper->OnCloseCanceled(); 424 DevToolsWindow::OnPageCloseCanceled(tab_needing_before_unload_ack_); 425 tab_needing_before_unload_ack_ = NULL; 426 } 427 for (WebContentsSet::iterator it = tabs_needing_unload_.begin(); 428 it != tabs_needing_unload_.end(); it++) { 429 content::WebContents* contents = *it; 430 431 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); 432 core_tab_helper->OnCloseCanceled(); 433 DevToolsWindow::OnPageCloseCanceled(contents); 434 } 435 tabs_needing_unload_.clear(); 436 437 // No need to clear tabs_needing_unload_ack_. Those tabs are already detached. 438 439 if (is_calling_before_unload_handlers()) { 440 base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_; 441 on_close_confirmed_.Reset(); 442 on_close_confirmed.Run(false); 443 } 444 445 is_attempting_to_close_browser_ = false; 446 447 content::NotificationService::current()->Notify( 448 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED, 449 content::Source<Browser>(browser_), 450 content::NotificationService::NoDetails()); 451} 452 453void FastUnloadController::ClearUnloadState(content::WebContents* contents) { 454 if (tabs_needing_unload_ack_.erase(contents) > 0) { 455 if (HasCompletedUnloadProcessing()) 456 PostTaskForProcessPendingTabs(); 457 return; 458 } 459 460 if (!is_attempting_to_close_browser_) 461 return; 462 463 if (tab_needing_before_unload_ack_ == contents) { 464 tab_needing_before_unload_ack_ = NULL; 465 PostTaskForProcessPendingTabs(); 466 return; 467 } 468 469 if (tabs_needing_before_unload_.erase(contents) > 0 || 470 tabs_needing_unload_.erase(contents) > 0) { 471 if (tab_needing_before_unload_ack_ == NULL) 472 PostTaskForProcessPendingTabs(); 473 } 474} 475 476void FastUnloadController::PostTaskForProcessPendingTabs() { 477 base::MessageLoop::current()->PostTask( 478 FROM_HERE, 479 base::Bind(&FastUnloadController::ProcessPendingTabs, 480 weak_factory_.GetWeakPtr())); 481} 482 483} // namespace chrome 484