1// Copyright (c) 2012 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/unload_controller.h" 6 7#include "base/message_loop/message_loop.h" 8#include "chrome/browser/chrome_notification_types.h" 9#include "chrome/browser/devtools/devtools_window.h" 10#include "chrome/browser/ui/browser.h" 11#include "chrome/browser/ui/browser_tabstrip.h" 12#include "chrome/browser/ui/tabs/tab_strip_model.h" 13#include "content/public/browser/notification_service.h" 14#include "content/public/browser/notification_source.h" 15#include "content/public/browser/notification_types.h" 16#include "content/public/browser/render_view_host.h" 17#include "content/public/browser/web_contents.h" 18 19namespace chrome { 20 21//////////////////////////////////////////////////////////////////////////////// 22// UnloadController, public: 23 24UnloadController::UnloadController(Browser* browser) 25 : browser_(browser), 26 is_attempting_to_close_browser_(false), 27 weak_factory_(this) { 28 browser_->tab_strip_model()->AddObserver(this); 29} 30 31UnloadController::~UnloadController() { 32 browser_->tab_strip_model()->RemoveObserver(this); 33} 34 35bool UnloadController::CanCloseContents(content::WebContents* contents) { 36 // Don't try to close the tab when the whole browser is being closed, since 37 // that avoids the fast shutdown path where we just kill all the renderers. 38 if (is_attempting_to_close_browser_) 39 ClearUnloadState(contents, true); 40 return !is_attempting_to_close_browser_ || 41 is_calling_before_unload_handlers(); 42} 43 44// static 45bool UnloadController::ShouldRunUnloadEventsHelper( 46 content::WebContents* contents) { 47 // If |contents| is being inspected, devtools needs to intercept beforeunload 48 // events. 49 return DevToolsWindow::GetInstanceForInspectedWebContents(contents) != NULL; 50} 51 52// static 53bool UnloadController::RunUnloadEventsHelper(content::WebContents* contents) { 54 // If there's a devtools window attached to |contents|, 55 // we would like devtools to call its own beforeunload handlers first, 56 // and then call beforeunload handlers for |contents|. 57 // See DevToolsWindow::InterceptPageBeforeUnload for details. 58 if (DevToolsWindow::InterceptPageBeforeUnload(contents)) { 59 return true; 60 } 61 // If the WebContents is not connected yet, then there's no unload 62 // handler we can fire even if the WebContents has an unload listener. 63 // One case where we hit this is in a tab that has an infinite loop 64 // before load. 65 if (contents->NeedToFireBeforeUnload()) { 66 // If the page has unload listeners, then we tell the renderer to fire 67 // them. Once they have fired, we'll get a message back saying whether 68 // to proceed closing the page or not, which sends us back to this method 69 // with the NeedToFireBeforeUnload bit cleared. 70 contents->DispatchBeforeUnload(false); 71 return true; 72 } 73 return false; 74} 75 76bool UnloadController::BeforeUnloadFired(content::WebContents* contents, 77 bool proceed) { 78 if (!proceed) 79 DevToolsWindow::OnPageCloseCanceled(contents); 80 81 if (!is_attempting_to_close_browser_) { 82 if (!proceed) 83 contents->SetClosedByUserGesture(false); 84 return proceed; 85 } 86 87 if (!proceed) { 88 CancelWindowClose(); 89 contents->SetClosedByUserGesture(false); 90 return false; 91 } 92 93 if (RemoveFromSet(&tabs_needing_before_unload_fired_, contents)) { 94 // Now that beforeunload has fired, put the tab on the queue to fire 95 // unload. 96 tabs_needing_unload_fired_.insert(contents); 97 ProcessPendingTabs(); 98 // We want to handle firing the unload event ourselves since we want to 99 // fire all the beforeunload events before attempting to fire the unload 100 // events should the user cancel closing the browser. 101 return false; 102 } 103 104 return true; 105} 106 107bool UnloadController::ShouldCloseWindow() { 108 if (HasCompletedUnloadProcessing()) 109 return true; 110 111 // Special case for when we quit an application. The devtools window can 112 // close if it's beforeunload event has already fired which will happen due 113 // to the interception of it's content's beforeunload. 114 if (browser_->is_devtools() && 115 DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_)) { 116 return true; 117 } 118 119 // The behavior followed here varies based on the current phase of the 120 // operation and whether a batched shutdown is in progress. 121 // 122 // If there are tabs with outstanding beforeunload handlers: 123 // 1. If a batched shutdown is in progress: return false. 124 // This is to prevent interference with batched shutdown already in 125 // progress. 126 // 2. Otherwise: start sending beforeunload events and return false. 127 // 128 // Otherwise, If there are no tabs with outstanding beforeunload handlers: 129 // 3. If a batched shutdown is in progress: start sending unload events and 130 // return false. 131 // 4. Otherwise: return true. 132 is_attempting_to_close_browser_ = true; 133 // Cases 1 and 4. 134 bool need_beforeunload_fired = TabsNeedBeforeUnloadFired(); 135 if (need_beforeunload_fired == is_calling_before_unload_handlers()) 136 return !need_beforeunload_fired; 137 138 // Cases 2 and 3. 139 on_close_confirmed_.Reset(); 140 ProcessPendingTabs(); 141 return false; 142} 143 144bool UnloadController::CallBeforeUnloadHandlers( 145 const base::Callback<void(bool)>& on_close_confirmed) { 146 // The devtools browser gets its beforeunload events as the results of 147 // intercepting events from the inspected tab, so don't send them here as 148 // well. 149 if (browser_->is_devtools() || HasCompletedUnloadProcessing() || 150 !TabsNeedBeforeUnloadFired()) 151 return false; 152 153 is_attempting_to_close_browser_ = true; 154 on_close_confirmed_ = on_close_confirmed; 155 156 ProcessPendingTabs(); 157 return true; 158} 159 160void UnloadController::ResetBeforeUnloadHandlers() { 161 if (!is_calling_before_unload_handlers()) 162 return; 163 CancelWindowClose(); 164} 165 166bool UnloadController::TabsNeedBeforeUnloadFired() { 167 if (tabs_needing_before_unload_fired_.empty()) { 168 for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) { 169 content::WebContents* contents = 170 browser_->tab_strip_model()->GetWebContentsAt(i); 171 bool should_fire_beforeunload = contents->NeedToFireBeforeUnload() || 172 DevToolsWindow::NeedsToInterceptBeforeUnload(contents); 173 if (!ContainsKey(tabs_needing_unload_fired_, contents) && 174 should_fire_beforeunload) { 175 tabs_needing_before_unload_fired_.insert(contents); 176 } 177 } 178 } 179 return !tabs_needing_before_unload_fired_.empty(); 180} 181 182void UnloadController::CancelWindowClose() { 183 // Closing of window can be canceled from a beforeunload handler. 184 DCHECK(is_attempting_to_close_browser_); 185 tabs_needing_before_unload_fired_.clear(); 186 for (UnloadListenerSet::iterator it = tabs_needing_unload_fired_.begin(); 187 it != tabs_needing_unload_fired_.end(); ++it) { 188 DevToolsWindow::OnPageCloseCanceled(*it); 189 } 190 tabs_needing_unload_fired_.clear(); 191 if (is_calling_before_unload_handlers()) { 192 base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_; 193 on_close_confirmed_.Reset(); 194 on_close_confirmed.Run(false); 195 } 196 is_attempting_to_close_browser_ = false; 197 198 content::NotificationService::current()->Notify( 199 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED, 200 content::Source<Browser>(browser_), 201 content::NotificationService::NoDetails()); 202} 203 204//////////////////////////////////////////////////////////////////////////////// 205// UnloadController, content::NotificationObserver implementation: 206 207void UnloadController::Observe(int type, 208 const content::NotificationSource& source, 209 const content::NotificationDetails& details) { 210 switch (type) { 211 case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: 212 if (is_attempting_to_close_browser_) { 213 ClearUnloadState(content::Source<content::WebContents>(source).ptr(), 214 false); // See comment for ClearUnloadState(). 215 } 216 break; 217 default: 218 NOTREACHED() << "Got a notification we didn't register for."; 219 } 220} 221 222//////////////////////////////////////////////////////////////////////////////// 223// UnloadController, TabStripModelObserver implementation: 224 225void UnloadController::TabInsertedAt(content::WebContents* contents, 226 int index, 227 bool foreground) { 228 TabAttachedImpl(contents); 229} 230 231void UnloadController::TabDetachedAt(content::WebContents* contents, 232 int index) { 233 TabDetachedImpl(contents); 234} 235 236void UnloadController::TabReplacedAt(TabStripModel* tab_strip_model, 237 content::WebContents* old_contents, 238 content::WebContents* new_contents, 239 int index) { 240 TabDetachedImpl(old_contents); 241 TabAttachedImpl(new_contents); 242} 243 244void UnloadController::TabStripEmpty() { 245 // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not 246 // attempt to add tabs to the browser before it closes. 247 is_attempting_to_close_browser_ = true; 248} 249 250//////////////////////////////////////////////////////////////////////////////// 251// UnloadController, private: 252 253void UnloadController::TabAttachedImpl(content::WebContents* contents) { 254 // If the tab crashes in the beforeunload or unload handler, it won't be 255 // able to ack. But we know we can close it. 256 registrar_.Add( 257 this, 258 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 259 content::Source<content::WebContents>(contents)); 260} 261 262void UnloadController::TabDetachedImpl(content::WebContents* contents) { 263 if (is_attempting_to_close_browser_) 264 ClearUnloadState(contents, false); 265 registrar_.Remove(this, 266 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 267 content::Source<content::WebContents>(contents)); 268} 269 270void UnloadController::ProcessPendingTabs() { 271 if (!is_attempting_to_close_browser_) { 272 // Because we might invoke this after a delay it's possible for the value of 273 // is_attempting_to_close_browser_ to have changed since we scheduled the 274 // task. 275 return; 276 } 277 278 if (HasCompletedUnloadProcessing()) { 279 // We've finished all the unload events and can proceed to close the 280 // browser. 281 browser_->OnWindowClosing(); 282 return; 283 } 284 285 // Process beforeunload tabs first. When that queue is empty, process 286 // unload tabs. 287 if (!tabs_needing_before_unload_fired_.empty()) { 288 content::WebContents* web_contents = 289 *(tabs_needing_before_unload_fired_.begin()); 290 // Null check render_view_host here as this gets called on a PostTask and 291 // the tab's render_view_host may have been nulled out. 292 if (web_contents->GetRenderViewHost()) { 293 // If there's a devtools window attached to |web_contents|, 294 // we would like devtools to call its own beforeunload handlers first, 295 // and then call beforeunload handlers for |web_contents|. 296 // See DevToolsWindow::InterceptPageBeforeUnload for details. 297 if (!DevToolsWindow::InterceptPageBeforeUnload(web_contents)) 298 web_contents->DispatchBeforeUnload(false); 299 } else { 300 ClearUnloadState(web_contents, true); 301 } 302 } else if (is_calling_before_unload_handlers()) { 303 base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_; 304 // Reset |on_close_confirmed_| in case the callback tests 305 // |is_calling_before_unload_handlers()|, we want to return that calling 306 // is complete. 307 if (tabs_needing_unload_fired_.empty()) 308 on_close_confirmed_.Reset(); 309 on_close_confirmed.Run(true); 310 } else if (!tabs_needing_unload_fired_.empty()) { 311 // We've finished firing all beforeunload events and can proceed with unload 312 // events. 313 // TODO(ojan): We should add a call to browser_shutdown::OnShutdownStarting 314 // somewhere around here so that we have accurate measurements of shutdown 315 // time. 316 // TODO(ojan): We can probably fire all the unload events in parallel and 317 // get a perf benefit from that in the cases where the tab hangs in it's 318 // unload handler or takes a long time to page in. 319 content::WebContents* web_contents = *(tabs_needing_unload_fired_.begin()); 320 // Null check render_view_host here as this gets called on a PostTask and 321 // the tab's render_view_host may have been nulled out. 322 if (web_contents->GetRenderViewHost()) { 323 web_contents->GetRenderViewHost()->ClosePage(); 324 } else { 325 ClearUnloadState(web_contents, true); 326 } 327 } else { 328 NOTREACHED(); 329 } 330} 331 332bool UnloadController::HasCompletedUnloadProcessing() const { 333 return is_attempting_to_close_browser_ && 334 tabs_needing_before_unload_fired_.empty() && 335 tabs_needing_unload_fired_.empty(); 336} 337 338bool UnloadController::RemoveFromSet(UnloadListenerSet* set, 339 content::WebContents* web_contents) { 340 DCHECK(is_attempting_to_close_browser_); 341 342 UnloadListenerSet::iterator iter = 343 std::find(set->begin(), set->end(), web_contents); 344 if (iter != set->end()) { 345 set->erase(iter); 346 return true; 347 } 348 return false; 349} 350 351void UnloadController::ClearUnloadState(content::WebContents* web_contents, 352 bool process_now) { 353 if (is_attempting_to_close_browser_) { 354 RemoveFromSet(&tabs_needing_before_unload_fired_, web_contents); 355 RemoveFromSet(&tabs_needing_unload_fired_, web_contents); 356 if (process_now) { 357 ProcessPendingTabs(); 358 } else { 359 base::MessageLoop::current()->PostTask( 360 FROM_HERE, 361 base::Bind(&UnloadController::ProcessPendingTabs, 362 weak_factory_.GetWeakPtr())); 363 } 364 } 365} 366 367} // namespace chrome 368