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.
5 #include "chrome/browser/ui/fast_unload_controller.h"
7 #include "base/location.h"
8 #include "base/logging.h"
9 #include "base/single_thread_task_runner.h"
10 #include "base/thread_task_runner_handle.h"
11 #include "chrome/browser/chrome_notification_types.h"
12 #include "chrome/browser/devtools/devtools_window.h"
13 #include "chrome/browser/ui/browser.h"
14 #include "chrome/browser/ui/browser_tabstrip.h"
15 #include "chrome/browser/ui/tab_contents/core_tab_helper.h"
16 #include "chrome/browser/ui/tabs/tab_strip_model.h"
17 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
18 #include "content/public/browser/notification_service.h"
19 #include "content/public/browser/notification_source.h"
20 #include "content/public/browser/notification_types.h"
21 #include "content/public/browser/render_view_host.h"
22 #include "content/public/browser/web_contents.h"
23 #include "content/public/browser/web_contents_delegate.h"
28 ////////////////////////////////////////////////////////////////////////////////
29 // DetachedWebContentsDelegate will delete web contents when they close.
30 class FastUnloadController::DetachedWebContentsDelegate
31 : public content::WebContentsDelegate
{
33 DetachedWebContentsDelegate() { }
34 ~DetachedWebContentsDelegate() override
{}
37 // WebContentsDelegate implementation.
38 bool ShouldSuppressDialogs(content::WebContents
* source
) override
{
39 return true; // Return true so dialogs are suppressed.
42 void CloseContents(content::WebContents
* source
) override
{
43 // Finished detached close.
44 // FastUnloadController will observe
45 // |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|.
49 DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate
);
52 ////////////////////////////////////////////////////////////////////////////////
53 // FastUnloadController, public:
55 FastUnloadController::FastUnloadController(Browser
* browser
)
57 tab_needing_before_unload_ack_(NULL
),
58 is_attempting_to_close_browser_(false),
59 detached_delegate_(new DetachedWebContentsDelegate()),
61 browser_
->tab_strip_model()->AddObserver(this);
64 FastUnloadController::~FastUnloadController() {
65 browser_
->tab_strip_model()->RemoveObserver(this);
68 bool FastUnloadController::CanCloseContents(content::WebContents
* contents
) {
69 // Don't try to close the tab when the whole browser is being closed, since
70 // that avoids the fast shutdown path where we just kill all the renderers.
71 return !is_attempting_to_close_browser_
||
72 is_calling_before_unload_handlers();
76 bool FastUnloadController::ShouldRunUnloadEventsHelper(
77 content::WebContents
* contents
) {
78 // If |contents| is being inspected, devtools needs to intercept beforeunload
80 return DevToolsWindow::GetInstanceForInspectedWebContents(contents
) != NULL
;
84 bool FastUnloadController::RunUnloadEventsHelper(
85 content::WebContents
* contents
) {
86 // If there's a devtools window attached to |contents|,
87 // we would like devtools to call its own beforeunload handlers first,
88 // and then call beforeunload handlers for |contents|.
89 // See DevToolsWindow::InterceptPageBeforeUnload for details.
90 if (DevToolsWindow::InterceptPageBeforeUnload(contents
)) {
93 // If the WebContents is not connected yet, then there's no unload
94 // handler we can fire even if the WebContents has an unload listener.
95 // One case where we hit this is in a tab that has an infinite loop
97 if (contents
->NeedToFireBeforeUnload()) {
98 // If the page has unload listeners, then we tell the renderer to fire
99 // them. Once they have fired, we'll get a message back saying whether
100 // to proceed closing the page or not, which sends us back to this method
101 // with the NeedToFireBeforeUnload bit cleared.
102 contents
->DispatchBeforeUnload(false);
108 bool FastUnloadController::BeforeUnloadFired(content::WebContents
* contents
,
111 DevToolsWindow::OnPageCloseCanceled(contents
);
113 if (!is_attempting_to_close_browser_
) {
115 contents
->SetClosedByUserGesture(false);
117 // No more dialogs are possible, so remove the tab and finish
118 // running unload listeners asynchrounously.
119 browser_
->tab_strip_model()->delegate()->CreateHistoricalTab(contents
);
120 DetachWebContents(contents
);
127 contents
->SetClosedByUserGesture(false);
131 if (tab_needing_before_unload_ack_
== contents
) {
132 // Now that beforeunload has fired, queue the tab to fire unload.
133 tab_needing_before_unload_ack_
= NULL
;
134 tabs_needing_unload_
.insert(contents
);
135 ProcessPendingTabs();
136 // We want to handle firing the unload event ourselves since we want to
137 // fire all the beforeunload events before attempting to fire the unload
138 // events should the user cancel closing the browser.
145 bool FastUnloadController::ShouldCloseWindow() {
146 if (HasCompletedUnloadProcessing())
149 // Special case for when we quit an application. The Devtools window can
150 // close if it's beforeunload event has already fired which will happen due
151 // to the interception of it's content's beforeunload.
152 if (browser_
->is_devtools() &&
153 DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_
)) {
157 // The behavior followed here varies based on the current phase of the
158 // operation and whether a batched shutdown is in progress.
160 // If there are tabs with outstanding beforeunload handlers:
161 // 1. If a batched shutdown is in progress: return false.
162 // This is to prevent interference with batched shutdown already in
164 // 2. Otherwise: start sending beforeunload events and return false.
166 // Otherwise, If there are no tabs with outstanding beforeunload handlers:
167 // 3. If a batched shutdown is in progress: start sending unload events and
169 // 4. Otherwise: return true.
170 is_attempting_to_close_browser_
= true;
172 bool need_beforeunload_fired
= TabsNeedBeforeUnloadFired();
173 if (need_beforeunload_fired
== is_calling_before_unload_handlers())
174 return !need_beforeunload_fired
;
177 on_close_confirmed_
.Reset();
178 ProcessPendingTabs();
182 bool FastUnloadController::CallBeforeUnloadHandlers(
183 const base::Callback
<void(bool)>& on_close_confirmed
) {
184 // The devtools browser gets its beforeunload events as the results of
185 // intercepting events from the inspected tab, so don't send them here as well.
186 if (browser_
->is_devtools() || !TabsNeedBeforeUnloadFired())
189 on_close_confirmed_
= on_close_confirmed
;
190 is_attempting_to_close_browser_
= true;
191 ProcessPendingTabs();
195 void FastUnloadController::ResetBeforeUnloadHandlers() {
196 if (!is_calling_before_unload_handlers())
201 bool FastUnloadController::TabsNeedBeforeUnloadFired() {
202 if (!tabs_needing_before_unload_
.empty() ||
203 tab_needing_before_unload_ack_
!= NULL
)
206 if (!is_calling_before_unload_handlers() && !tabs_needing_unload_
.empty())
209 for (int i
= 0; i
< browser_
->tab_strip_model()->count(); ++i
) {
210 content::WebContents
* contents
=
211 browser_
->tab_strip_model()->GetWebContentsAt(i
);
212 bool should_fire_beforeunload
= contents
->NeedToFireBeforeUnload() ||
213 DevToolsWindow::NeedsToInterceptBeforeUnload(contents
);
214 if (!ContainsKey(tabs_needing_unload_
, contents
) &&
215 !ContainsKey(tabs_needing_unload_ack_
, contents
) &&
216 tab_needing_before_unload_ack_
!= contents
&&
217 should_fire_beforeunload
)
218 tabs_needing_before_unload_
.insert(contents
);
220 return !tabs_needing_before_unload_
.empty();
223 bool FastUnloadController::HasCompletedUnloadProcessing() const {
224 return is_attempting_to_close_browser_
&&
225 tabs_needing_before_unload_
.empty() &&
226 tab_needing_before_unload_ack_
== NULL
&&
227 tabs_needing_unload_
.empty() &&
228 tabs_needing_unload_ack_
.empty();
231 void FastUnloadController::CancelWindowClose() {
232 // Closing of window can be canceled from a beforeunload handler.
233 DCHECK(is_attempting_to_close_browser_
);
234 tabs_needing_before_unload_
.clear();
235 if (tab_needing_before_unload_ack_
!= NULL
) {
236 CoreTabHelper
* core_tab_helper
=
237 CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_
);
238 core_tab_helper
->OnCloseCanceled();
239 DevToolsWindow::OnPageCloseCanceled(tab_needing_before_unload_ack_
);
240 tab_needing_before_unload_ack_
= NULL
;
242 for (WebContentsSet::iterator it
= tabs_needing_unload_
.begin();
243 it
!= tabs_needing_unload_
.end(); it
++) {
244 content::WebContents
* contents
= *it
;
246 CoreTabHelper
* core_tab_helper
= CoreTabHelper::FromWebContents(contents
);
247 core_tab_helper
->OnCloseCanceled();
248 DevToolsWindow::OnPageCloseCanceled(contents
);
250 tabs_needing_unload_
.clear();
252 // No need to clear tabs_needing_unload_ack_. Those tabs are already detached.
254 if (is_calling_before_unload_handlers()) {
255 base::Callback
<void(bool)> on_close_confirmed
= on_close_confirmed_
;
256 on_close_confirmed_
.Reset();
257 on_close_confirmed
.Run(false);
260 is_attempting_to_close_browser_
= false;
262 content::NotificationService::current()->Notify(
263 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED
,
264 content::Source
<Browser
>(browser_
),
265 content::NotificationService::NoDetails());
268 ////////////////////////////////////////////////////////////////////////////////
269 // FastUnloadController, content::NotificationObserver implementation:
271 void FastUnloadController::Observe(
273 const content::NotificationSource
& source
,
274 const content::NotificationDetails
& details
) {
276 case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
: {
277 registrar_
.Remove(this,
278 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
280 content::WebContents
* contents
=
281 content::Source
<content::WebContents
>(source
).ptr();
282 ClearUnloadState(contents
);
286 NOTREACHED() << "Got a notification we didn't register for.";
290 ////////////////////////////////////////////////////////////////////////////////
291 // FastUnloadController, TabStripModelObserver implementation:
293 void FastUnloadController::TabInsertedAt(content::WebContents
* contents
,
296 TabAttachedImpl(contents
);
299 void FastUnloadController::TabDetachedAt(content::WebContents
* contents
,
301 TabDetachedImpl(contents
);
304 void FastUnloadController::TabReplacedAt(TabStripModel
* tab_strip_model
,
305 content::WebContents
* old_contents
,
306 content::WebContents
* new_contents
,
308 TabDetachedImpl(old_contents
);
309 TabAttachedImpl(new_contents
);
312 void FastUnloadController::TabStripEmpty() {
313 // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
314 // attempt to add tabs to the browser before it closes.
315 is_attempting_to_close_browser_
= true;
318 ////////////////////////////////////////////////////////////////////////////////
319 // FastUnloadController, private:
321 void FastUnloadController::TabAttachedImpl(content::WebContents
* contents
) {
322 // If the tab crashes in the beforeunload or unload handler, it won't be
323 // able to ack. But we know we can close it.
326 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
327 content::Source
<content::WebContents
>(contents
));
330 void FastUnloadController::TabDetachedImpl(content::WebContents
* contents
) {
331 if (tabs_needing_unload_ack_
.find(contents
) !=
332 tabs_needing_unload_ack_
.end()) {
333 // Tab needs unload to complete.
334 // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done.
338 // If WEB_CONTENTS_DISCONNECTED was received then the notification may have
339 // already been unregistered.
340 const content::NotificationSource
& source
=
341 content::Source
<content::WebContents
>(contents
);
342 if (registrar_
.IsRegistered(this,
343 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
345 registrar_
.Remove(this,
346 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
350 if (is_attempting_to_close_browser_
)
351 ClearUnloadState(contents
);
354 bool FastUnloadController::DetachWebContents(content::WebContents
* contents
) {
355 int index
= browser_
->tab_strip_model()->GetIndexOfWebContents(contents
);
356 if (index
!= TabStripModel::kNoTab
&&
357 contents
->NeedToFireBeforeUnload()) {
358 tabs_needing_unload_ack_
.insert(contents
);
359 browser_
->tab_strip_model()->DetachWebContentsAt(index
);
360 contents
->SetDelegate(detached_delegate_
.get());
361 CoreTabHelper
* core_tab_helper
= CoreTabHelper::FromWebContents(contents
);
362 core_tab_helper
->OnUnloadDetachedStarted();
368 void FastUnloadController::ProcessPendingTabs() {
369 if (!is_attempting_to_close_browser_
) {
370 // Because we might invoke this after a delay it's possible for the value of
371 // is_attempting_to_close_browser_ to have changed since we scheduled the
376 if (tab_needing_before_unload_ack_
!= NULL
) {
377 // Wait for |BeforeUnloadFired| before proceeding.
381 // Process a beforeunload handler.
382 if (!tabs_needing_before_unload_
.empty()) {
383 WebContentsSet::iterator it
= tabs_needing_before_unload_
.begin();
384 content::WebContents
* contents
= *it
;
385 tabs_needing_before_unload_
.erase(it
);
386 // Null check render_view_host here as this gets called on a PostTask and
387 // the tab's render_view_host may have been nulled out.
388 if (contents
->GetRenderViewHost()) {
389 tab_needing_before_unload_ack_
= contents
;
391 CoreTabHelper
* core_tab_helper
= CoreTabHelper::FromWebContents(contents
);
392 core_tab_helper
->OnCloseStarted();
394 // If there's a devtools window attached to |contents|,
395 // we would like devtools to call its own beforeunload handlers first,
396 // and then call beforeunload handlers for |contents|.
397 // See DevToolsWindow::InterceptPageBeforeUnload for details.
398 if (!DevToolsWindow::InterceptPageBeforeUnload(contents
))
399 contents
->DispatchBeforeUnload(false);
401 ProcessPendingTabs();
406 if (is_calling_before_unload_handlers()) {
407 on_close_confirmed_
.Run(true);
410 // Process all the unload handlers. (The beforeunload handlers have finished.)
411 if (!tabs_needing_unload_
.empty()) {
412 browser_
->OnWindowClosing();
414 // Run unload handlers detached since no more interaction is possible.
415 WebContentsSet::iterator it
= tabs_needing_unload_
.begin();
416 while (it
!= tabs_needing_unload_
.end()) {
417 WebContentsSet::iterator current
= it
++;
418 content::WebContents
* contents
= *current
;
419 tabs_needing_unload_
.erase(current
);
420 // Null check render_view_host here as this gets called on a PostTask
421 // and the tab's render_view_host may have been nulled out.
422 if (contents
->GetRenderViewHost()) {
423 CoreTabHelper
* core_tab_helper
=
424 CoreTabHelper::FromWebContents(contents
);
425 core_tab_helper
->OnUnloadStarted();
426 DetachWebContents(contents
);
427 contents
->ClosePage();
431 // Get the browser hidden.
432 if (browser_
->tab_strip_model()->empty()) {
433 browser_
->TabStripEmpty();
435 browser_
->tab_strip_model()->CloseAllTabs(); // tabs not needing unload
440 if (HasCompletedUnloadProcessing()) {
441 browser_
->OnWindowClosing();
443 // Get the browser closed.
444 if (browser_
->tab_strip_model()->empty()) {
445 browser_
->TabStripEmpty();
447 // There may be tabs if the last tab needing beforeunload crashed.
448 browser_
->tab_strip_model()->CloseAllTabs();
454 void FastUnloadController::ClearUnloadState(content::WebContents
* contents
) {
455 if (tabs_needing_unload_ack_
.erase(contents
) > 0) {
456 if (HasCompletedUnloadProcessing())
457 PostTaskForProcessPendingTabs();
461 if (!is_attempting_to_close_browser_
)
464 if (tab_needing_before_unload_ack_
== contents
) {
465 tab_needing_before_unload_ack_
= NULL
;
466 PostTaskForProcessPendingTabs();
470 if (tabs_needing_before_unload_
.erase(contents
) > 0 ||
471 tabs_needing_unload_
.erase(contents
) > 0) {
472 if (tab_needing_before_unload_ack_
== NULL
)
473 PostTaskForProcessPendingTabs();
477 void FastUnloadController::PostTaskForProcessPendingTabs() {
478 base::ThreadTaskRunnerHandle::Get()->PostTask(
479 FROM_HERE
, base::Bind(&FastUnloadController::ProcessPendingTabs
,
480 weak_factory_
.GetWeakPtr()));
483 } // namespace chrome