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