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 23 namespace chrome { 24 25 26 //////////////////////////////////////////////////////////////////////////////// 27 // DetachedWebContentsDelegate will delete web contents when they close. 28 class 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 53 FastUnloadController::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 62 FastUnloadController::~FastUnloadController() { 63 browser_->tab_strip_model()->RemoveObserver(this); 64 } 65 66 bool 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 74 bool 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 83 bool 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->DispatchBeforeUnload(false); 102 return true; 103 } 104 return false; 105 } 106 107 bool 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 144 bool 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 181 bool 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 194 void FastUnloadController::ResetBeforeUnloadHandlers() { 195 if (!is_calling_before_unload_handlers()) 196 return; 197 CancelWindowClose(); 198 } 199 200 bool 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 bool FastUnloadController::HasCompletedUnloadProcessing() const { 223 return is_attempting_to_close_browser_ && 224 tabs_needing_before_unload_.empty() && 225 tab_needing_before_unload_ack_ == NULL && 226 tabs_needing_unload_.empty() && 227 tabs_needing_unload_ack_.empty(); 228 } 229 230 void FastUnloadController::CancelWindowClose() { 231 // Closing of window can be canceled from a beforeunload handler. 232 DCHECK(is_attempting_to_close_browser_); 233 tabs_needing_before_unload_.clear(); 234 if (tab_needing_before_unload_ack_ != NULL) { 235 CoreTabHelper* core_tab_helper = 236 CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_); 237 core_tab_helper->OnCloseCanceled(); 238 DevToolsWindow::OnPageCloseCanceled(tab_needing_before_unload_ack_); 239 tab_needing_before_unload_ack_ = NULL; 240 } 241 for (WebContentsSet::iterator it = tabs_needing_unload_.begin(); 242 it != tabs_needing_unload_.end(); it++) { 243 content::WebContents* contents = *it; 244 245 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); 246 core_tab_helper->OnCloseCanceled(); 247 DevToolsWindow::OnPageCloseCanceled(contents); 248 } 249 tabs_needing_unload_.clear(); 250 251 // No need to clear tabs_needing_unload_ack_. Those tabs are already detached. 252 253 if (is_calling_before_unload_handlers()) { 254 base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_; 255 on_close_confirmed_.Reset(); 256 on_close_confirmed.Run(false); 257 } 258 259 is_attempting_to_close_browser_ = false; 260 261 content::NotificationService::current()->Notify( 262 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED, 263 content::Source<Browser>(browser_), 264 content::NotificationService::NoDetails()); 265 } 266 267 //////////////////////////////////////////////////////////////////////////////// 268 // FastUnloadController, content::NotificationObserver implementation: 269 270 void FastUnloadController::Observe( 271 int type, 272 const content::NotificationSource& source, 273 const content::NotificationDetails& details) { 274 switch (type) { 275 case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: { 276 registrar_.Remove(this, 277 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 278 source); 279 content::WebContents* contents = 280 content::Source<content::WebContents>(source).ptr(); 281 ClearUnloadState(contents); 282 break; 283 } 284 default: 285 NOTREACHED() << "Got a notification we didn't register for."; 286 } 287 } 288 289 //////////////////////////////////////////////////////////////////////////////// 290 // FastUnloadController, TabStripModelObserver implementation: 291 292 void FastUnloadController::TabInsertedAt(content::WebContents* contents, 293 int index, 294 bool foreground) { 295 TabAttachedImpl(contents); 296 } 297 298 void FastUnloadController::TabDetachedAt(content::WebContents* contents, 299 int index) { 300 TabDetachedImpl(contents); 301 } 302 303 void FastUnloadController::TabReplacedAt(TabStripModel* tab_strip_model, 304 content::WebContents* old_contents, 305 content::WebContents* new_contents, 306 int index) { 307 TabDetachedImpl(old_contents); 308 TabAttachedImpl(new_contents); 309 } 310 311 void FastUnloadController::TabStripEmpty() { 312 // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not 313 // attempt to add tabs to the browser before it closes. 314 is_attempting_to_close_browser_ = true; 315 } 316 317 //////////////////////////////////////////////////////////////////////////////// 318 // FastUnloadController, private: 319 320 void FastUnloadController::TabAttachedImpl(content::WebContents* contents) { 321 // If the tab crashes in the beforeunload or unload handler, it won't be 322 // able to ack. But we know we can close it. 323 registrar_.Add( 324 this, 325 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 326 content::Source<content::WebContents>(contents)); 327 } 328 329 void FastUnloadController::TabDetachedImpl(content::WebContents* contents) { 330 if (tabs_needing_unload_ack_.find(contents) != 331 tabs_needing_unload_ack_.end()) { 332 // Tab needs unload to complete. 333 // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done. 334 return; 335 } 336 337 // If WEB_CONTENTS_DISCONNECTED was received then the notification may have 338 // already been unregistered. 339 const content::NotificationSource& source = 340 content::Source<content::WebContents>(contents); 341 if (registrar_.IsRegistered(this, 342 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 343 source)) { 344 registrar_.Remove(this, 345 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 346 source); 347 } 348 349 if (is_attempting_to_close_browser_) 350 ClearUnloadState(contents); 351 } 352 353 bool FastUnloadController::DetachWebContents(content::WebContents* contents) { 354 int index = browser_->tab_strip_model()->GetIndexOfWebContents(contents); 355 if (index != TabStripModel::kNoTab && 356 contents->NeedToFireBeforeUnload()) { 357 tabs_needing_unload_ack_.insert(contents); 358 browser_->tab_strip_model()->DetachWebContentsAt(index); 359 contents->SetDelegate(detached_delegate_.get()); 360 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); 361 core_tab_helper->OnUnloadDetachedStarted(); 362 return true; 363 } 364 return false; 365 } 366 367 void FastUnloadController::ProcessPendingTabs() { 368 if (!is_attempting_to_close_browser_) { 369 // Because we might invoke this after a delay it's possible for the value of 370 // is_attempting_to_close_browser_ to have changed since we scheduled the 371 // task. 372 return; 373 } 374 375 if (tab_needing_before_unload_ack_ != NULL) { 376 // Wait for |BeforeUnloadFired| before proceeding. 377 return; 378 } 379 380 // Process a beforeunload handler. 381 if (!tabs_needing_before_unload_.empty()) { 382 WebContentsSet::iterator it = tabs_needing_before_unload_.begin(); 383 content::WebContents* contents = *it; 384 tabs_needing_before_unload_.erase(it); 385 // Null check render_view_host here as this gets called on a PostTask and 386 // the tab's render_view_host may have been nulled out. 387 if (contents->GetRenderViewHost()) { 388 tab_needing_before_unload_ack_ = contents; 389 390 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); 391 core_tab_helper->OnCloseStarted(); 392 393 // If there's a devtools window attached to |contents|, 394 // we would like devtools to call its own beforeunload handlers first, 395 // and then call beforeunload handlers for |contents|. 396 // See DevToolsWindow::InterceptPageBeforeUnload for details. 397 if (!DevToolsWindow::InterceptPageBeforeUnload(contents)) 398 contents->DispatchBeforeUnload(false); 399 } else { 400 ProcessPendingTabs(); 401 } 402 return; 403 } 404 405 if (is_calling_before_unload_handlers()) { 406 on_close_confirmed_.Run(true); 407 return; 408 } 409 // Process all the unload handlers. (The beforeunload handlers have finished.) 410 if (!tabs_needing_unload_.empty()) { 411 browser_->OnWindowClosing(); 412 413 // Run unload handlers detached since no more interaction is possible. 414 WebContentsSet::iterator it = tabs_needing_unload_.begin(); 415 while (it != tabs_needing_unload_.end()) { 416 WebContentsSet::iterator current = it++; 417 content::WebContents* contents = *current; 418 tabs_needing_unload_.erase(current); 419 // Null check render_view_host here as this gets called on a PostTask 420 // and the tab's render_view_host may have been nulled out. 421 if (contents->GetRenderViewHost()) { 422 CoreTabHelper* core_tab_helper = 423 CoreTabHelper::FromWebContents(contents); 424 core_tab_helper->OnUnloadStarted(); 425 DetachWebContents(contents); 426 contents->GetRenderViewHost()->ClosePage(); 427 } 428 } 429 430 // Get the browser hidden. 431 if (browser_->tab_strip_model()->empty()) { 432 browser_->TabStripEmpty(); 433 } else { 434 browser_->tab_strip_model()->CloseAllTabs(); // tabs not needing unload 435 } 436 return; 437 } 438 439 if (HasCompletedUnloadProcessing()) { 440 browser_->OnWindowClosing(); 441 442 // Get the browser closed. 443 if (browser_->tab_strip_model()->empty()) { 444 browser_->TabStripEmpty(); 445 } else { 446 // There may be tabs if the last tab needing beforeunload crashed. 447 browser_->tab_strip_model()->CloseAllTabs(); 448 } 449 return; 450 } 451 } 452 453 void 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 476 void 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