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->GetRenderViewHost()->FirePageBeforeUnload(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 //////////////////////////////////////////////////////////////////////////////// 184 // UnloadController, content::NotificationObserver implementation: 185 186 void UnloadController::Observe(int type, 187 const content::NotificationSource& source, 188 const content::NotificationDetails& details) { 189 switch (type) { 190 case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: 191 if (is_attempting_to_close_browser_) { 192 ClearUnloadState(content::Source<content::WebContents>(source).ptr(), 193 false); // See comment for ClearUnloadState(). 194 } 195 break; 196 default: 197 NOTREACHED() << "Got a notification we didn't register for."; 198 } 199 } 200 201 //////////////////////////////////////////////////////////////////////////////// 202 // UnloadController, TabStripModelObserver implementation: 203 204 void UnloadController::TabInsertedAt(content::WebContents* contents, 205 int index, 206 bool foreground) { 207 TabAttachedImpl(contents); 208 } 209 210 void UnloadController::TabDetachedAt(content::WebContents* contents, 211 int index) { 212 TabDetachedImpl(contents); 213 } 214 215 void UnloadController::TabReplacedAt(TabStripModel* tab_strip_model, 216 content::WebContents* old_contents, 217 content::WebContents* new_contents, 218 int index) { 219 TabDetachedImpl(old_contents); 220 TabAttachedImpl(new_contents); 221 } 222 223 void UnloadController::TabStripEmpty() { 224 // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not 225 // attempt to add tabs to the browser before it closes. 226 is_attempting_to_close_browser_ = true; 227 } 228 229 //////////////////////////////////////////////////////////////////////////////// 230 // UnloadController, private: 231 232 void UnloadController::TabAttachedImpl(content::WebContents* contents) { 233 // If the tab crashes in the beforeunload or unload handler, it won't be 234 // able to ack. But we know we can close it. 235 registrar_.Add( 236 this, 237 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 238 content::Source<content::WebContents>(contents)); 239 } 240 241 void UnloadController::TabDetachedImpl(content::WebContents* contents) { 242 if (is_attempting_to_close_browser_) 243 ClearUnloadState(contents, false); 244 registrar_.Remove(this, 245 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 246 content::Source<content::WebContents>(contents)); 247 } 248 249 void UnloadController::ProcessPendingTabs() { 250 if (!is_attempting_to_close_browser_) { 251 // Because we might invoke this after a delay it's possible for the value of 252 // is_attempting_to_close_browser_ to have changed since we scheduled the 253 // task. 254 return; 255 } 256 257 if (HasCompletedUnloadProcessing()) { 258 // We've finished all the unload events and can proceed to close the 259 // browser. 260 browser_->OnWindowClosing(); 261 return; 262 } 263 264 // Process beforeunload tabs first. When that queue is empty, process 265 // unload tabs. 266 if (!tabs_needing_before_unload_fired_.empty()) { 267 content::WebContents* web_contents = 268 *(tabs_needing_before_unload_fired_.begin()); 269 // Null check render_view_host here as this gets called on a PostTask and 270 // the tab's render_view_host may have been nulled out. 271 if (web_contents->GetRenderViewHost()) { 272 // If there's a devtools window attached to |web_contents|, 273 // we would like devtools to call its own beforeunload handlers first, 274 // and then call beforeunload handlers for |web_contents|. 275 // See DevToolsWindow::InterceptPageBeforeUnload for details. 276 if (!DevToolsWindow::InterceptPageBeforeUnload(web_contents)) 277 web_contents->GetRenderViewHost()->FirePageBeforeUnload(false); 278 } else { 279 ClearUnloadState(web_contents, true); 280 } 281 } else if (is_calling_before_unload_handlers()) { 282 on_close_confirmed_.Run(true); 283 } else if (!tabs_needing_unload_fired_.empty()) { 284 // We've finished firing all beforeunload events and can proceed with unload 285 // events. 286 // TODO(ojan): We should add a call to browser_shutdown::OnShutdownStarting 287 // somewhere around here so that we have accurate measurements of shutdown 288 // time. 289 // TODO(ojan): We can probably fire all the unload events in parallel and 290 // get a perf benefit from that in the cases where the tab hangs in it's 291 // unload handler or takes a long time to page in. 292 content::WebContents* web_contents = *(tabs_needing_unload_fired_.begin()); 293 // Null check render_view_host here as this gets called on a PostTask and 294 // the tab's render_view_host may have been nulled out. 295 if (web_contents->GetRenderViewHost()) { 296 web_contents->GetRenderViewHost()->ClosePage(); 297 } else { 298 ClearUnloadState(web_contents, true); 299 } 300 } else { 301 NOTREACHED(); 302 } 303 } 304 305 bool UnloadController::HasCompletedUnloadProcessing() const { 306 return is_attempting_to_close_browser_ && 307 tabs_needing_before_unload_fired_.empty() && 308 tabs_needing_unload_fired_.empty(); 309 } 310 311 void UnloadController::CancelWindowClose() { 312 // Closing of window can be canceled from a beforeunload handler. 313 DCHECK(is_attempting_to_close_browser_); 314 tabs_needing_before_unload_fired_.clear(); 315 for (UnloadListenerSet::iterator it = tabs_needing_unload_fired_.begin(); 316 it != tabs_needing_unload_fired_.end(); ++it) { 317 DevToolsWindow::OnPageCloseCanceled(*it); 318 } 319 tabs_needing_unload_fired_.clear(); 320 if (is_calling_before_unload_handlers()) { 321 base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_; 322 on_close_confirmed_.Reset(); 323 on_close_confirmed.Run(false); 324 } 325 is_attempting_to_close_browser_ = false; 326 327 content::NotificationService::current()->Notify( 328 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED, 329 content::Source<Browser>(browser_), 330 content::NotificationService::NoDetails()); 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