blob: 73ee5dc366a361dad76e84ee5bbedee0b174d9fc [file] [log] [blame]
[email protected]88c92012013-07-02 11:56:341// 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
skyostil380bb2222015-06-12 12:07:057#include "base/location.h"
[email protected]88c92012013-07-02 11:56:348#include "base/logging.h"
avi655876a2015-12-25 07:18:159#include "base/macros.h"
skyostil380bb2222015-06-12 12:07:0510#include "base/single_thread_task_runner.h"
gabb15e19072016-05-11 20:45:4111#include "base/threading/thread_task_runner_handle.h"
[email protected]dcc8fbc2013-07-12 00:54:0912#include "chrome/browser/chrome_notification_types.h"
[email protected]6dfe941a2013-10-12 19:50:5513#include "chrome/browser/devtools/devtools_window.h"
[email protected]88c92012013-07-02 11:56:3414#include "chrome/browser/ui/browser.h"
15#include "chrome/browser/ui/browser_tabstrip.h"
16#include "chrome/browser/ui/tab_contents/core_tab_helper.h"
17#include "chrome/browser/ui/tabs/tab_strip_model.h"
18#include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
[email protected]88c92012013-07-02 11:56:3419#include "content/public/browser/notification_service.h"
20#include "content/public/browser/notification_source.h"
21#include "content/public/browser/notification_types.h"
22#include "content/public/browser/render_view_host.h"
23#include "content/public/browser/web_contents.h"
24#include "content/public/browser/web_contents_delegate.h"
brettw00899e62016-11-12 02:10:1725#include "extensions/features/features.h"
[email protected]88c92012013-07-02 11:56:3426
brettw00899e62016-11-12 02:10:1727#if BUILDFLAG(ENABLE_EXTENSIONS)
rdevlin.cronin235441342016-09-26 22:39:0128#include "extensions/browser/extension_registry.h"
29#include "extensions/common/constants.h"
30#endif // (ENABLE_EXTENSIONS)
[email protected]88c92012013-07-02 11:56:3431
rdevlin.cronin235441342016-09-26 22:39:0132namespace chrome {
[email protected]88c92012013-07-02 11:56:3433
34////////////////////////////////////////////////////////////////////////////////
35// DetachedWebContentsDelegate will delete web contents when they close.
36class FastUnloadController::DetachedWebContentsDelegate
37 : public content::WebContentsDelegate {
38 public:
39 DetachedWebContentsDelegate() { }
dcheng5dd5ff62014-10-21 12:42:3840 ~DetachedWebContentsDelegate() override {}
[email protected]88c92012013-07-02 11:56:3441
42 private:
43 // WebContentsDelegate implementation.
mathiash72a5e462014-11-19 08:18:5044 bool ShouldSuppressDialogs(content::WebContents* source) override {
[email protected]88c92012013-07-02 11:56:3445 return true; // Return true so dialogs are suppressed.
46 }
47
dcheng5dd5ff62014-10-21 12:42:3848 void CloseContents(content::WebContents* source) override {
[email protected]88c92012013-07-02 11:56:3449 // Finished detached close.
50 // FastUnloadController will observe
51 // |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|.
52 delete source;
53 }
54
55 DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate);
56};
57
58////////////////////////////////////////////////////////////////////////////////
59// FastUnloadController, public:
60
61FastUnloadController::FastUnloadController(Browser* browser)
62 : browser_(browser),
63 tab_needing_before_unload_ack_(NULL),
64 is_attempting_to_close_browser_(false),
65 detached_delegate_(new DetachedWebContentsDelegate()),
66 weak_factory_(this) {
67 browser_->tab_strip_model()->AddObserver(this);
68}
69
70FastUnloadController::~FastUnloadController() {
71 browser_->tab_strip_model()->RemoveObserver(this);
72}
73
74bool FastUnloadController::CanCloseContents(content::WebContents* contents) {
75 // Don't try to close the tab when the whole browser is being closed, since
76 // that avoids the fast shutdown path where we just kill all the renderers.
[email protected]2e9d79f2013-08-16 05:45:5677 return !is_attempting_to_close_browser_ ||
78 is_calling_before_unload_handlers();
[email protected]88c92012013-07-02 11:56:3479}
80
[email protected]90354712013-11-16 00:06:3081bool FastUnloadController::ShouldRunUnloadEventsHelper(
82 content::WebContents* contents) {
83 // If |contents| is being inspected, devtools needs to intercept beforeunload
84 // events.
[email protected]db90e8a2014-07-07 17:57:3585 return DevToolsWindow::GetInstanceForInspectedWebContents(contents) != NULL;
[email protected]90354712013-11-16 00:06:3086}
87
[email protected]6dfe941a2013-10-12 19:50:5588bool FastUnloadController::RunUnloadEventsHelper(
89 content::WebContents* contents) {
brettw00899e62016-11-12 02:10:1790#if BUILDFLAG(ENABLE_EXTENSIONS)
rdevlin.cronin235441342016-09-26 22:39:0191 // Don't run for extensions that are disabled or uninstalled; the tabs will
92 // be killed if they make any network requests, and the extension shouldn't
93 // be doing any work if it's removed.
94 GURL url = contents->GetLastCommittedURL();
95 if (url.SchemeIs(extensions::kExtensionScheme) &&
96 !extensions::ExtensionRegistry::Get(browser_->profile())
97 ->enabled_extensions()
98 .GetExtensionOrAppByURL(url)) {
99 return false;
100 }
101#endif // (ENABLE_EXTENSIONS)
102
changwan6ed4d432016-05-19 22:03:54103 // Special case for when we quit an application. The Devtools window can
104 // close if it's beforeunload event has already fired which will happen due
105 // to the interception of it's content's beforeunload.
106 if (browser_->is_devtools() &&
107 DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_))
108 return false;
109
[email protected]90354712013-11-16 00:06:30110 // If there's a devtools window attached to |contents|,
111 // we would like devtools to call its own beforeunload handlers first,
112 // and then call beforeunload handlers for |contents|.
113 // See DevToolsWindow::InterceptPageBeforeUnload for details.
114 if (DevToolsWindow::InterceptPageBeforeUnload(contents)) {
115 return true;
116 }
[email protected]6dfe941a2013-10-12 19:50:55117 // If the WebContents is not connected yet, then there's no unload
118 // handler we can fire even if the WebContents has an unload listener.
119 // One case where we hit this is in a tab that has an infinite loop
120 // before load.
121 if (contents->NeedToFireBeforeUnload()) {
122 // If the page has unload listeners, then we tell the renderer to fire
123 // them. Once they have fired, we'll get a message back saying whether
124 // to proceed closing the page or not, which sends us back to this method
125 // with the NeedToFireBeforeUnload bit cleared.
nasko148bb0b92016-05-10 21:03:19126 contents->DispatchBeforeUnload();
[email protected]6dfe941a2013-10-12 19:50:55127 return true;
128 }
129 return false;
130}
131
[email protected]88c92012013-07-02 11:56:34132bool FastUnloadController::BeforeUnloadFired(content::WebContents* contents,
133 bool proceed) {
[email protected]90354712013-11-16 00:06:30134 if (!proceed)
135 DevToolsWindow::OnPageCloseCanceled(contents);
136
[email protected]88c92012013-07-02 11:56:34137 if (!is_attempting_to_close_browser_) {
138 if (!proceed) {
139 contents->SetClosedByUserGesture(false);
140 } else {
141 // No more dialogs are possible, so remove the tab and finish
142 // running unload listeners asynchrounously.
143 browser_->tab_strip_model()->delegate()->CreateHistoricalTab(contents);
144 DetachWebContents(contents);
145 }
146 return proceed;
147 }
148
149 if (!proceed) {
150 CancelWindowClose();
151 contents->SetClosedByUserGesture(false);
152 return false;
153 }
154
155 if (tab_needing_before_unload_ack_ == contents) {
156 // Now that beforeunload has fired, queue the tab to fire unload.
157 tab_needing_before_unload_ack_ = NULL;
158 tabs_needing_unload_.insert(contents);
159 ProcessPendingTabs();
160 // We want to handle firing the unload event ourselves since we want to
161 // fire all the beforeunload events before attempting to fire the unload
162 // events should the user cancel closing the browser.
163 return false;
164 }
165
166 return true;
167}
168
169bool FastUnloadController::ShouldCloseWindow() {
170 if (HasCompletedUnloadProcessing())
171 return true;
172
[email protected]90354712013-11-16 00:06:30173 // Special case for when we quit an application. The Devtools window can
174 // close if it's beforeunload event has already fired which will happen due
175 // to the interception of it's content's beforeunload.
176 if (browser_->is_devtools() &&
177 DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_)) {
178 return true;
179 }
180
[email protected]2e9d79f2013-08-16 05:45:56181 // The behavior followed here varies based on the current phase of the
182 // operation and whether a batched shutdown is in progress.
183 //
184 // If there are tabs with outstanding beforeunload handlers:
185 // 1. If a batched shutdown is in progress: return false.
186 // This is to prevent interference with batched shutdown already in
187 // progress.
188 // 2. Otherwise: start sending beforeunload events and return false.
189 //
190 // Otherwise, If there are no tabs with outstanding beforeunload handlers:
191 // 3. If a batched shutdown is in progress: start sending unload events and
192 // return false.
193 // 4. Otherwise: return true.
[email protected]88c92012013-07-02 11:56:34194 is_attempting_to_close_browser_ = true;
[email protected]2e9d79f2013-08-16 05:45:56195 // Cases 1 and 4.
196 bool need_beforeunload_fired = TabsNeedBeforeUnloadFired();
197 if (need_beforeunload_fired == is_calling_before_unload_handlers())
198 return !need_beforeunload_fired;
[email protected]88c92012013-07-02 11:56:34199
[email protected]2e9d79f2013-08-16 05:45:56200 // Cases 2 and 3.
201 on_close_confirmed_.Reset();
[email protected]88c92012013-07-02 11:56:34202 ProcessPendingTabs();
203 return false;
204}
205
[email protected]2e9d79f2013-08-16 05:45:56206bool FastUnloadController::CallBeforeUnloadHandlers(
207 const base::Callback<void(bool)>& on_close_confirmed) {
[email protected]90354712013-11-16 00:06:30208// The devtools browser gets its beforeunload events as the results of
209// intercepting events from the inspected tab, so don't send them here as well.
210 if (browser_->is_devtools() || !TabsNeedBeforeUnloadFired())
[email protected]2e9d79f2013-08-16 05:45:56211 return false;
212
213 on_close_confirmed_ = on_close_confirmed;
214 is_attempting_to_close_browser_ = true;
215 ProcessPendingTabs();
216 return true;
217}
218
219void FastUnloadController::ResetBeforeUnloadHandlers() {
220 if (!is_calling_before_unload_handlers())
221 return;
222 CancelWindowClose();
223}
224
[email protected]88c92012013-07-02 11:56:34225bool FastUnloadController::TabsNeedBeforeUnloadFired() {
226 if (!tabs_needing_before_unload_.empty() ||
227 tab_needing_before_unload_ack_ != NULL)
228 return true;
229
[email protected]2e9d79f2013-08-16 05:45:56230 if (!is_calling_before_unload_handlers() && !tabs_needing_unload_.empty())
[email protected]88c92012013-07-02 11:56:34231 return false;
232
233 for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) {
234 content::WebContents* contents =
235 browser_->tab_strip_model()->GetWebContentsAt(i);
[email protected]90354712013-11-16 00:06:30236 bool should_fire_beforeunload = contents->NeedToFireBeforeUnload() ||
237 DevToolsWindow::NeedsToInterceptBeforeUnload(contents);
[email protected]2e9d79f2013-08-16 05:45:56238 if (!ContainsKey(tabs_needing_unload_, contents) &&
239 !ContainsKey(tabs_needing_unload_ack_, contents) &&
240 tab_needing_before_unload_ack_ != contents &&
[email protected]90354712013-11-16 00:06:30241 should_fire_beforeunload)
[email protected]88c92012013-07-02 11:56:34242 tabs_needing_before_unload_.insert(contents);
243 }
244 return !tabs_needing_before_unload_.empty();
245}
246
[email protected]674b65fb2014-02-19 02:43:42247bool FastUnloadController::HasCompletedUnloadProcessing() const {
248 return is_attempting_to_close_browser_ &&
249 tabs_needing_before_unload_.empty() &&
250 tab_needing_before_unload_ack_ == NULL &&
251 tabs_needing_unload_.empty() &&
252 tabs_needing_unload_ack_.empty();
253}
254
255void FastUnloadController::CancelWindowClose() {
256 // Closing of window can be canceled from a beforeunload handler.
257 DCHECK(is_attempting_to_close_browser_);
258 tabs_needing_before_unload_.clear();
259 if (tab_needing_before_unload_ack_ != NULL) {
260 CoreTabHelper* core_tab_helper =
261 CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_);
262 core_tab_helper->OnCloseCanceled();
263 DevToolsWindow::OnPageCloseCanceled(tab_needing_before_unload_ack_);
264 tab_needing_before_unload_ack_ = NULL;
265 }
266 for (WebContentsSet::iterator it = tabs_needing_unload_.begin();
267 it != tabs_needing_unload_.end(); it++) {
268 content::WebContents* contents = *it;
269
270 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
271 core_tab_helper->OnCloseCanceled();
272 DevToolsWindow::OnPageCloseCanceled(contents);
273 }
274 tabs_needing_unload_.clear();
275
276 // No need to clear tabs_needing_unload_ack_. Those tabs are already detached.
277
278 if (is_calling_before_unload_handlers()) {
279 base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_;
280 on_close_confirmed_.Reset();
281 on_close_confirmed.Run(false);
282 }
283
284 is_attempting_to_close_browser_ = false;
285
286 content::NotificationService::current()->Notify(
287 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED,
288 content::Source<Browser>(browser_),
289 content::NotificationService::NoDetails());
290}
291
[email protected]88c92012013-07-02 11:56:34292////////////////////////////////////////////////////////////////////////////////
293// FastUnloadController, content::NotificationObserver implementation:
294
295void FastUnloadController::Observe(
296 int type,
297 const content::NotificationSource& source,
298 const content::NotificationDetails& details) {
thestig1d619242016-06-13 22:24:52299 DCHECK_EQ(content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, type);
300
301 registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
302 source);
303 ClearUnloadState(content::Source<content::WebContents>(source).ptr());
[email protected]88c92012013-07-02 11:56:34304}
305
306////////////////////////////////////////////////////////////////////////////////
307// FastUnloadController, TabStripModelObserver implementation:
308
pmonette9119e492016-09-20 22:14:55309void FastUnloadController::TabInsertedAt(TabStripModel* tab_strip_model,
310 content::WebContents* contents,
[email protected]88c92012013-07-02 11:56:34311 int index,
312 bool foreground) {
313 TabAttachedImpl(contents);
314}
315
316void FastUnloadController::TabDetachedAt(content::WebContents* contents,
317 int index) {
318 TabDetachedImpl(contents);
319}
320
321void FastUnloadController::TabReplacedAt(TabStripModel* tab_strip_model,
322 content::WebContents* old_contents,
323 content::WebContents* new_contents,
324 int index) {
325 TabDetachedImpl(old_contents);
326 TabAttachedImpl(new_contents);
327}
328
329void FastUnloadController::TabStripEmpty() {
330 // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
331 // attempt to add tabs to the browser before it closes.
332 is_attempting_to_close_browser_ = true;
333}
334
335////////////////////////////////////////////////////////////////////////////////
336// FastUnloadController, private:
337
338void FastUnloadController::TabAttachedImpl(content::WebContents* contents) {
339 // If the tab crashes in the beforeunload or unload handler, it won't be
340 // able to ack. But we know we can close it.
341 registrar_.Add(
342 this,
343 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
344 content::Source<content::WebContents>(contents));
345}
346
347void FastUnloadController::TabDetachedImpl(content::WebContents* contents) {
348 if (tabs_needing_unload_ack_.find(contents) !=
349 tabs_needing_unload_ack_.end()) {
350 // Tab needs unload to complete.
351 // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done.
352 return;
353 }
354
355 // If WEB_CONTENTS_DISCONNECTED was received then the notification may have
356 // already been unregistered.
357 const content::NotificationSource& source =
358 content::Source<content::WebContents>(contents);
359 if (registrar_.IsRegistered(this,
360 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
361 source)) {
362 registrar_.Remove(this,
363 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
364 source);
365 }
366
367 if (is_attempting_to_close_browser_)
368 ClearUnloadState(contents);
369}
370
371bool FastUnloadController::DetachWebContents(content::WebContents* contents) {
372 int index = browser_->tab_strip_model()->GetIndexOfWebContents(contents);
373 if (index != TabStripModel::kNoTab &&
374 contents->NeedToFireBeforeUnload()) {
375 tabs_needing_unload_ack_.insert(contents);
376 browser_->tab_strip_model()->DetachWebContentsAt(index);
377 contents->SetDelegate(detached_delegate_.get());
378 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
379 core_tab_helper->OnUnloadDetachedStarted();
380 return true;
381 }
382 return false;
383}
384
385void FastUnloadController::ProcessPendingTabs() {
386 if (!is_attempting_to_close_browser_) {
387 // Because we might invoke this after a delay it's possible for the value of
388 // is_attempting_to_close_browser_ to have changed since we scheduled the
389 // task.
390 return;
391 }
392
393 if (tab_needing_before_unload_ack_ != NULL) {
394 // Wait for |BeforeUnloadFired| before proceeding.
395 return;
396 }
397
398 // Process a beforeunload handler.
399 if (!tabs_needing_before_unload_.empty()) {
400 WebContentsSet::iterator it = tabs_needing_before_unload_.begin();
401 content::WebContents* contents = *it;
402 tabs_needing_before_unload_.erase(it);
403 // Null check render_view_host here as this gets called on a PostTask and
404 // the tab's render_view_host may have been nulled out.
405 if (contents->GetRenderViewHost()) {
406 tab_needing_before_unload_ack_ = contents;
407
408 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
409 core_tab_helper->OnCloseStarted();
410
[email protected]90354712013-11-16 00:06:30411 // If there's a devtools window attached to |contents|,
412 // we would like devtools to call its own beforeunload handlers first,
413 // and then call beforeunload handlers for |contents|.
414 // See DevToolsWindow::InterceptPageBeforeUnload for details.
415 if (!DevToolsWindow::InterceptPageBeforeUnload(contents))
nasko148bb0b92016-05-10 21:03:19416 contents->DispatchBeforeUnload();
[email protected]88c92012013-07-02 11:56:34417 } else {
418 ProcessPendingTabs();
419 }
420 return;
421 }
422
[email protected]2e9d79f2013-08-16 05:45:56423 if (is_calling_before_unload_handlers()) {
424 on_close_confirmed_.Run(true);
425 return;
426 }
[email protected]88c92012013-07-02 11:56:34427 // Process all the unload handlers. (The beforeunload handlers have finished.)
428 if (!tabs_needing_unload_.empty()) {
429 browser_->OnWindowClosing();
430
431 // Run unload handlers detached since no more interaction is possible.
432 WebContentsSet::iterator it = tabs_needing_unload_.begin();
433 while (it != tabs_needing_unload_.end()) {
434 WebContentsSet::iterator current = it++;
435 content::WebContents* contents = *current;
436 tabs_needing_unload_.erase(current);
437 // Null check render_view_host here as this gets called on a PostTask
438 // and the tab's render_view_host may have been nulled out.
439 if (contents->GetRenderViewHost()) {
440 CoreTabHelper* core_tab_helper =
441 CoreTabHelper::FromWebContents(contents);
442 core_tab_helper->OnUnloadStarted();
443 DetachWebContents(contents);
naskoc0fceff2015-04-30 15:53:52444 contents->ClosePage();
[email protected]88c92012013-07-02 11:56:34445 }
446 }
447
448 // Get the browser hidden.
449 if (browser_->tab_strip_model()->empty()) {
450 browser_->TabStripEmpty();
451 } else {
452 browser_->tab_strip_model()->CloseAllTabs(); // tabs not needing unload
453 }
454 return;
455 }
456
457 if (HasCompletedUnloadProcessing()) {
458 browser_->OnWindowClosing();
459
460 // Get the browser closed.
461 if (browser_->tab_strip_model()->empty()) {
462 browser_->TabStripEmpty();
463 } else {
464 // There may be tabs if the last tab needing beforeunload crashed.
465 browser_->tab_strip_model()->CloseAllTabs();
466 }
467 return;
468 }
469}
470
[email protected]88c92012013-07-02 11:56:34471void FastUnloadController::ClearUnloadState(content::WebContents* contents) {
472 if (tabs_needing_unload_ack_.erase(contents) > 0) {
473 if (HasCompletedUnloadProcessing())
474 PostTaskForProcessPendingTabs();
475 return;
476 }
477
478 if (!is_attempting_to_close_browser_)
479 return;
480
481 if (tab_needing_before_unload_ack_ == contents) {
482 tab_needing_before_unload_ack_ = NULL;
483 PostTaskForProcessPendingTabs();
484 return;
485 }
486
487 if (tabs_needing_before_unload_.erase(contents) > 0 ||
488 tabs_needing_unload_.erase(contents) > 0) {
489 if (tab_needing_before_unload_ack_ == NULL)
490 PostTaskForProcessPendingTabs();
491 }
492}
493
494void FastUnloadController::PostTaskForProcessPendingTabs() {
skyostil380bb2222015-06-12 12:07:05495 base::ThreadTaskRunnerHandle::Get()->PostTask(
496 FROM_HERE, base::Bind(&FastUnloadController::ProcessPendingTabs,
497 weak_factory_.GetWeakPtr()));
[email protected]88c92012013-07-02 11:56:34498}
499
500} // namespace chrome