// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/fast_unload_controller.h" #include "base/location.h" #include "base/logging.h" #include "base/single_thread_task_runner.h" #include "base/thread_task_runner_handle.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/devtools/devtools_window.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_tabstrip.h" #include "chrome/browser/ui/tab_contents/core_tab_helper.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/notification_types.h" #include "content/public/browser/render_view_host.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_delegate.h" namespace chrome { //////////////////////////////////////////////////////////////////////////////// // DetachedWebContentsDelegate will delete web contents when they close. class FastUnloadController::DetachedWebContentsDelegate : public content::WebContentsDelegate { public: DetachedWebContentsDelegate() { } ~DetachedWebContentsDelegate() override {} private: // WebContentsDelegate implementation. bool ShouldSuppressDialogs(content::WebContents* source) override { return true; // Return true so dialogs are suppressed. } void CloseContents(content::WebContents* source) override { // Finished detached close. // FastUnloadController will observe // |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|. delete source; } DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate); }; //////////////////////////////////////////////////////////////////////////////// // FastUnloadController, public: FastUnloadController::FastUnloadController(Browser* browser) : browser_(browser), tab_needing_before_unload_ack_(NULL), is_attempting_to_close_browser_(false), detached_delegate_(new DetachedWebContentsDelegate()), weak_factory_(this) { browser_->tab_strip_model()->AddObserver(this); } FastUnloadController::~FastUnloadController() { browser_->tab_strip_model()->RemoveObserver(this); } bool FastUnloadController::CanCloseContents(content::WebContents* contents) { // Don't try to close the tab when the whole browser is being closed, since // that avoids the fast shutdown path where we just kill all the renderers. return !is_attempting_to_close_browser_ || is_calling_before_unload_handlers(); } // static bool FastUnloadController::ShouldRunUnloadEventsHelper( content::WebContents* contents) { // If |contents| is being inspected, devtools needs to intercept beforeunload // events. return DevToolsWindow::GetInstanceForInspectedWebContents(contents) != NULL; } // static bool FastUnloadController::RunUnloadEventsHelper( content::WebContents* contents) { // If there's a devtools window attached to |contents|, // we would like devtools to call its own beforeunload handlers first, // and then call beforeunload handlers for |contents|. // See DevToolsWindow::InterceptPageBeforeUnload for details. if (DevToolsWindow::InterceptPageBeforeUnload(contents)) { return true; } // If the WebContents is not connected yet, then there's no unload // handler we can fire even if the WebContents has an unload listener. // One case where we hit this is in a tab that has an infinite loop // before load. if (contents->NeedToFireBeforeUnload()) { // If the page has unload listeners, then we tell the renderer to fire // them. Once they have fired, we'll get a message back saying whether // to proceed closing the page or not, which sends us back to this method // with the NeedToFireBeforeUnload bit cleared. contents->DispatchBeforeUnload(false); return true; } return false; } bool FastUnloadController::BeforeUnloadFired(content::WebContents* contents, bool proceed) { if (!proceed) DevToolsWindow::OnPageCloseCanceled(contents); if (!is_attempting_to_close_browser_) { if (!proceed) { contents->SetClosedByUserGesture(false); } else { // No more dialogs are possible, so remove the tab and finish // running unload listeners asynchrounously. browser_->tab_strip_model()->delegate()->CreateHistoricalTab(contents); DetachWebContents(contents); } return proceed; } if (!proceed) { CancelWindowClose(); contents->SetClosedByUserGesture(false); return false; } if (tab_needing_before_unload_ack_ == contents) { // Now that beforeunload has fired, queue the tab to fire unload. tab_needing_before_unload_ack_ = NULL; tabs_needing_unload_.insert(contents); ProcessPendingTabs(); // We want to handle firing the unload event ourselves since we want to // fire all the beforeunload events before attempting to fire the unload // events should the user cancel closing the browser. return false; } return true; } bool FastUnloadController::ShouldCloseWindow() { if (HasCompletedUnloadProcessing()) return true; // Special case for when we quit an application. The Devtools window can // close if it's beforeunload event has already fired which will happen due // to the interception of it's content's beforeunload. if (browser_->is_devtools() && DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_)) { return true; } // The behavior followed here varies based on the current phase of the // operation and whether a batched shutdown is in progress. // // If there are tabs with outstanding beforeunload handlers: // 1. If a batched shutdown is in progress: return false. // This is to prevent interference with batched shutdown already in // progress. // 2. Otherwise: start sending beforeunload events and return false. // // Otherwise, If there are no tabs with outstanding beforeunload handlers: // 3. If a batched shutdown is in progress: start sending unload events and // return false. // 4. Otherwise: return true. is_attempting_to_close_browser_ = true; // Cases 1 and 4. bool need_beforeunload_fired = TabsNeedBeforeUnloadFired(); if (need_beforeunload_fired == is_calling_before_unload_handlers()) return !need_beforeunload_fired; // Cases 2 and 3. on_close_confirmed_.Reset(); ProcessPendingTabs(); return false; } bool FastUnloadController::CallBeforeUnloadHandlers( const base::Callback& on_close_confirmed) { // The devtools browser gets its beforeunload events as the results of // intercepting events from the inspected tab, so don't send them here as well. if (browser_->is_devtools() || !TabsNeedBeforeUnloadFired()) return false; on_close_confirmed_ = on_close_confirmed; is_attempting_to_close_browser_ = true; ProcessPendingTabs(); return true; } void FastUnloadController::ResetBeforeUnloadHandlers() { if (!is_calling_before_unload_handlers()) return; CancelWindowClose(); } bool FastUnloadController::TabsNeedBeforeUnloadFired() { if (!tabs_needing_before_unload_.empty() || tab_needing_before_unload_ack_ != NULL) return true; if (!is_calling_before_unload_handlers() && !tabs_needing_unload_.empty()) return false; for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) { content::WebContents* contents = browser_->tab_strip_model()->GetWebContentsAt(i); bool should_fire_beforeunload = contents->NeedToFireBeforeUnload() || DevToolsWindow::NeedsToInterceptBeforeUnload(contents); if (!ContainsKey(tabs_needing_unload_, contents) && !ContainsKey(tabs_needing_unload_ack_, contents) && tab_needing_before_unload_ack_ != contents && should_fire_beforeunload) tabs_needing_before_unload_.insert(contents); } return !tabs_needing_before_unload_.empty(); } bool FastUnloadController::HasCompletedUnloadProcessing() const { return is_attempting_to_close_browser_ && tabs_needing_before_unload_.empty() && tab_needing_before_unload_ack_ == NULL && tabs_needing_unload_.empty() && tabs_needing_unload_ack_.empty(); } void FastUnloadController::CancelWindowClose() { // Closing of window can be canceled from a beforeunload handler. DCHECK(is_attempting_to_close_browser_); tabs_needing_before_unload_.clear(); if (tab_needing_before_unload_ack_ != NULL) { CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_); core_tab_helper->OnCloseCanceled(); DevToolsWindow::OnPageCloseCanceled(tab_needing_before_unload_ack_); tab_needing_before_unload_ack_ = NULL; } for (WebContentsSet::iterator it = tabs_needing_unload_.begin(); it != tabs_needing_unload_.end(); it++) { content::WebContents* contents = *it; CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); core_tab_helper->OnCloseCanceled(); DevToolsWindow::OnPageCloseCanceled(contents); } tabs_needing_unload_.clear(); // No need to clear tabs_needing_unload_ack_. Those tabs are already detached. if (is_calling_before_unload_handlers()) { base::Callback on_close_confirmed = on_close_confirmed_; on_close_confirmed_.Reset(); on_close_confirmed.Run(false); } is_attempting_to_close_browser_ = false; content::NotificationService::current()->Notify( chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED, content::Source(browser_), content::NotificationService::NoDetails()); } //////////////////////////////////////////////////////////////////////////////// // FastUnloadController, content::NotificationObserver implementation: void FastUnloadController::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { switch (type) { case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: { registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, source); content::WebContents* contents = content::Source(source).ptr(); ClearUnloadState(contents); break; } default: NOTREACHED() << "Got a notification we didn't register for."; } } //////////////////////////////////////////////////////////////////////////////// // FastUnloadController, TabStripModelObserver implementation: void FastUnloadController::TabInsertedAt(content::WebContents* contents, int index, bool foreground) { TabAttachedImpl(contents); } void FastUnloadController::TabDetachedAt(content::WebContents* contents, int index) { TabDetachedImpl(contents); } void FastUnloadController::TabReplacedAt(TabStripModel* tab_strip_model, content::WebContents* old_contents, content::WebContents* new_contents, int index) { TabDetachedImpl(old_contents); TabAttachedImpl(new_contents); } void FastUnloadController::TabStripEmpty() { // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not // attempt to add tabs to the browser before it closes. is_attempting_to_close_browser_ = true; } //////////////////////////////////////////////////////////////////////////////// // FastUnloadController, private: void FastUnloadController::TabAttachedImpl(content::WebContents* contents) { // If the tab crashes in the beforeunload or unload handler, it won't be // able to ack. But we know we can close it. registrar_.Add( this, content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, content::Source(contents)); } void FastUnloadController::TabDetachedImpl(content::WebContents* contents) { if (tabs_needing_unload_ack_.find(contents) != tabs_needing_unload_ack_.end()) { // Tab needs unload to complete. // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done. return; } // If WEB_CONTENTS_DISCONNECTED was received then the notification may have // already been unregistered. const content::NotificationSource& source = content::Source(contents); if (registrar_.IsRegistered(this, content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, source)) { registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, source); } if (is_attempting_to_close_browser_) ClearUnloadState(contents); } bool FastUnloadController::DetachWebContents(content::WebContents* contents) { int index = browser_->tab_strip_model()->GetIndexOfWebContents(contents); if (index != TabStripModel::kNoTab && contents->NeedToFireBeforeUnload()) { tabs_needing_unload_ack_.insert(contents); browser_->tab_strip_model()->DetachWebContentsAt(index); contents->SetDelegate(detached_delegate_.get()); CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); core_tab_helper->OnUnloadDetachedStarted(); return true; } return false; } void FastUnloadController::ProcessPendingTabs() { if (!is_attempting_to_close_browser_) { // Because we might invoke this after a delay it's possible for the value of // is_attempting_to_close_browser_ to have changed since we scheduled the // task. return; } if (tab_needing_before_unload_ack_ != NULL) { // Wait for |BeforeUnloadFired| before proceeding. return; } // Process a beforeunload handler. if (!tabs_needing_before_unload_.empty()) { WebContentsSet::iterator it = tabs_needing_before_unload_.begin(); content::WebContents* contents = *it; tabs_needing_before_unload_.erase(it); // Null check render_view_host here as this gets called on a PostTask and // the tab's render_view_host may have been nulled out. if (contents->GetRenderViewHost()) { tab_needing_before_unload_ack_ = contents; CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); core_tab_helper->OnCloseStarted(); // If there's a devtools window attached to |contents|, // we would like devtools to call its own beforeunload handlers first, // and then call beforeunload handlers for |contents|. // See DevToolsWindow::InterceptPageBeforeUnload for details. if (!DevToolsWindow::InterceptPageBeforeUnload(contents)) contents->DispatchBeforeUnload(false); } else { ProcessPendingTabs(); } return; } if (is_calling_before_unload_handlers()) { on_close_confirmed_.Run(true); return; } // Process all the unload handlers. (The beforeunload handlers have finished.) if (!tabs_needing_unload_.empty()) { browser_->OnWindowClosing(); // Run unload handlers detached since no more interaction is possible. WebContentsSet::iterator it = tabs_needing_unload_.begin(); while (it != tabs_needing_unload_.end()) { WebContentsSet::iterator current = it++; content::WebContents* contents = *current; tabs_needing_unload_.erase(current); // Null check render_view_host here as this gets called on a PostTask // and the tab's render_view_host may have been nulled out. if (contents->GetRenderViewHost()) { CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); core_tab_helper->OnUnloadStarted(); DetachWebContents(contents); contents->ClosePage(); } } // Get the browser hidden. if (browser_->tab_strip_model()->empty()) { browser_->TabStripEmpty(); } else { browser_->tab_strip_model()->CloseAllTabs(); // tabs not needing unload } return; } if (HasCompletedUnloadProcessing()) { browser_->OnWindowClosing(); // Get the browser closed. if (browser_->tab_strip_model()->empty()) { browser_->TabStripEmpty(); } else { // There may be tabs if the last tab needing beforeunload crashed. browser_->tab_strip_model()->CloseAllTabs(); } return; } } void FastUnloadController::ClearUnloadState(content::WebContents* contents) { if (tabs_needing_unload_ack_.erase(contents) > 0) { if (HasCompletedUnloadProcessing()) PostTaskForProcessPendingTabs(); return; } if (!is_attempting_to_close_browser_) return; if (tab_needing_before_unload_ack_ == contents) { tab_needing_before_unload_ack_ = NULL; PostTaskForProcessPendingTabs(); return; } if (tabs_needing_before_unload_.erase(contents) > 0 || tabs_needing_unload_.erase(contents) > 0) { if (tab_needing_before_unload_ack_ == NULL) PostTaskForProcessPendingTabs(); } } void FastUnloadController::PostTaskForProcessPendingTabs() { base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::Bind(&FastUnloadController::ProcessPendingTabs, weak_factory_.GetWeakPtr())); } } // namespace chrome