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/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"
26 ////////////////////////////////////////////////////////////////////////////////
27 // DetachedWebContentsDelegate will delete web contents when they close.
28 class FastUnloadController::DetachedWebContentsDelegate
29 : public content::WebContentsDelegate
{
31 DetachedWebContentsDelegate() { }
32 ~DetachedWebContentsDelegate() override
{}
35 // WebContentsDelegate implementation.
36 bool ShouldSuppressDialogs(content::WebContents
* source
) override
{
37 return true; // Return true so dialogs are suppressed.
40 void CloseContents(content::WebContents
* source
) override
{
41 // Finished detached close.
42 // FastUnloadController will observe
43 // |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|.
47 DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate
);
50 ////////////////////////////////////////////////////////////////////////////////
51 // FastUnloadController, public:
53 FastUnloadController::FastUnloadController(Browser
* browser
)
55 tab_needing_before_unload_ack_(NULL
),
56 is_attempting_to_close_browser_(false),
57 detached_delegate_(new DetachedWebContentsDelegate()),
59 browser_
->tab_strip_model()->AddObserver(this);
62 FastUnloadController::~FastUnloadController() {
63 browser_
->tab_strip_model()->RemoveObserver(this);
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();
74 bool FastUnloadController::ShouldRunUnloadEventsHelper(
75 content::WebContents
* contents
) {
76 // If |contents| is being inspected, devtools needs to intercept beforeunload
78 return DevToolsWindow::GetInstanceForInspectedWebContents(contents
) != NULL
;
82 bool 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
)) {
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
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);
106 bool FastUnloadController::BeforeUnloadFired(content::WebContents
* contents
,
109 DevToolsWindow::OnPageCloseCanceled(contents
);
111 if (!is_attempting_to_close_browser_
) {
113 contents
->SetClosedByUserGesture(false);
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
);
125 contents
->SetClosedByUserGesture(false);
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.
143 bool FastUnloadController::ShouldCloseWindow() {
144 if (HasCompletedUnloadProcessing())
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_
)) {
155 // The behavior followed here varies based on the current phase of the
156 // operation and whether a batched shutdown is in progress.
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
162 // 2. Otherwise: start sending beforeunload events and return false.
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
167 // 4. Otherwise: return true.
168 is_attempting_to_close_browser_
= true;
170 bool need_beforeunload_fired
= TabsNeedBeforeUnloadFired();
171 if (need_beforeunload_fired
== is_calling_before_unload_handlers())
172 return !need_beforeunload_fired
;
175 on_close_confirmed_
.Reset();
176 ProcessPendingTabs();
180 bool 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())
187 on_close_confirmed_
= on_close_confirmed
;
188 is_attempting_to_close_browser_
= true;
189 ProcessPendingTabs();
193 void FastUnloadController::ResetBeforeUnloadHandlers() {
194 if (!is_calling_before_unload_handlers())
199 bool FastUnloadController::TabsNeedBeforeUnloadFired() {
200 if (!tabs_needing_before_unload_
.empty() ||
201 tab_needing_before_unload_ack_
!= NULL
)
204 if (!is_calling_before_unload_handlers() && !tabs_needing_unload_
.empty())
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
);
218 return !tabs_needing_before_unload_
.empty();
221 bool 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();
229 void 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
;
240 for (WebContentsSet::iterator it
= tabs_needing_unload_
.begin();
241 it
!= tabs_needing_unload_
.end(); it
++) {
242 content::WebContents
* contents
= *it
;
244 CoreTabHelper
* core_tab_helper
= CoreTabHelper::FromWebContents(contents
);
245 core_tab_helper
->OnCloseCanceled();
246 DevToolsWindow::OnPageCloseCanceled(contents
);
248 tabs_needing_unload_
.clear();
250 // No need to clear tabs_needing_unload_ack_. Those tabs are already detached.
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);
258 is_attempting_to_close_browser_
= false;
260 content::NotificationService::current()->Notify(
261 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED
,
262 content::Source
<Browser
>(browser_
),
263 content::NotificationService::NoDetails());
266 ////////////////////////////////////////////////////////////////////////////////
267 // FastUnloadController, content::NotificationObserver implementation:
269 void FastUnloadController::Observe(
271 const content::NotificationSource
& source
,
272 const content::NotificationDetails
& details
) {
274 case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
: {
275 registrar_
.Remove(this,
276 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
278 content::WebContents
* contents
=
279 content::Source
<content::WebContents
>(source
).ptr();
280 ClearUnloadState(contents
);
284 NOTREACHED() << "Got a notification we didn't register for.";
288 ////////////////////////////////////////////////////////////////////////////////
289 // FastUnloadController, TabStripModelObserver implementation:
291 void FastUnloadController::TabInsertedAt(content::WebContents
* contents
,
294 TabAttachedImpl(contents
);
297 void FastUnloadController::TabDetachedAt(content::WebContents
* contents
,
299 TabDetachedImpl(contents
);
302 void FastUnloadController::TabReplacedAt(TabStripModel
* tab_strip_model
,
303 content::WebContents
* old_contents
,
304 content::WebContents
* new_contents
,
306 TabDetachedImpl(old_contents
);
307 TabAttachedImpl(new_contents
);
310 void 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;
316 ////////////////////////////////////////////////////////////////////////////////
317 // FastUnloadController, private:
319 void 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.
324 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
325 content::Source
<content::WebContents
>(contents
));
328 void 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.
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
,
343 registrar_
.Remove(this,
344 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
348 if (is_attempting_to_close_browser_
)
349 ClearUnloadState(contents
);
352 bool 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();
366 void 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
374 if (tab_needing_before_unload_ack_
!= NULL
) {
375 // Wait for |BeforeUnloadFired| before proceeding.
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
;
389 CoreTabHelper
* core_tab_helper
= CoreTabHelper::FromWebContents(contents
);
390 core_tab_helper
->OnCloseStarted();
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);
399 ProcessPendingTabs();
404 if (is_calling_before_unload_handlers()) {
405 on_close_confirmed_
.Run(true);
408 // Process all the unload handlers. (The beforeunload handlers have finished.)
409 if (!tabs_needing_unload_
.empty()) {
410 browser_
->OnWindowClosing();
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();
429 // Get the browser hidden.
430 if (browser_
->tab_strip_model()->empty()) {
431 browser_
->TabStripEmpty();
433 browser_
->tab_strip_model()->CloseAllTabs(); // tabs not needing unload
438 if (HasCompletedUnloadProcessing()) {
439 browser_
->OnWindowClosing();
441 // Get the browser closed.
442 if (browser_
->tab_strip_model()->empty()) {
443 browser_
->TabStripEmpty();
445 // There may be tabs if the last tab needing beforeunload crashed.
446 browser_
->tab_strip_model()->CloseAllTabs();
452 void FastUnloadController::ClearUnloadState(content::WebContents
* contents
) {
453 if (tabs_needing_unload_ack_
.erase(contents
) > 0) {
454 if (HasCompletedUnloadProcessing())
455 PostTaskForProcessPendingTabs();
459 if (!is_attempting_to_close_browser_
)
462 if (tab_needing_before_unload_ack_
== contents
) {
463 tab_needing_before_unload_ack_
= NULL
;
464 PostTaskForProcessPendingTabs();
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();
475 void FastUnloadController::PostTaskForProcessPendingTabs() {
476 base::MessageLoop::current()->PostTask(
478 base::Bind(&FastUnloadController::ProcessPendingTabs
,
479 weak_factory_
.GetWeakPtr()));
482 } // namespace chrome