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