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 #import "chrome/browser/ui/cocoa/hung_renderer_controller.h" 6 7 #import <Cocoa/Cocoa.h> 8 9 #include "base/mac/bundle_locations.h" 10 #include "base/mac/mac_util.h" 11 #include "base/strings/sys_string_conversions.h" 12 #include "chrome/browser/favicon/favicon_tab_helper.h" 13 #include "chrome/browser/ui/browser_dialogs.h" 14 #import "chrome/browser/ui/cocoa/multi_key_equivalent_button.h" 15 #import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h" 16 #include "chrome/browser/ui/tab_contents/core_tab_helper.h" 17 #include "chrome/browser/ui/tab_contents/tab_contents_iterator.h" 18 #include "chrome/common/logging_chrome.h" 19 #include "content/public/browser/render_process_host.h" 20 #include "content/public/browser/render_view_host.h" 21 #include "content/public/browser/web_contents.h" 22 #include "content/public/common/result_codes.h" 23 #include "grit/chromium_strings.h" 24 #include "grit/generated_resources.h" 25 #include "grit/theme_resources.h" 26 #include "grit/ui_resources.h" 27 #include "skia/ext/skia_utils_mac.h" 28 #include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" 29 #include "ui/base/l10n/l10n_util_mac.h" 30 #include "ui/base/resource/resource_bundle.h" 31 #include "ui/gfx/image/image.h" 32 33 using content::WebContents; 34 35 namespace { 36 // We only support showing one of these at a time per app. The 37 // controller owns itself and is released when its window is closed. 38 HungRendererController* g_instance = NULL; 39 } // namespace 40 41 class WebContentsObserverBridge : public content::WebContentsObserver { 42 public: 43 WebContentsObserverBridge(WebContents* web_contents, 44 HungRendererController* controller) 45 : content::WebContentsObserver(web_contents), 46 controller_(controller) { 47 } 48 49 protected: 50 // WebContentsObserver overrides: 51 virtual void RenderProcessGone(base::TerminationStatus status) OVERRIDE { 52 [controller_ renderProcessGone]; 53 } 54 virtual void WebContentsDestroyed(WebContents* tab) OVERRIDE { 55 [controller_ renderProcessGone]; 56 } 57 58 private: 59 HungRendererController* controller_; // weak 60 61 DISALLOW_COPY_AND_ASSIGN(WebContentsObserverBridge); 62 }; 63 64 @implementation HungRendererController 65 66 - (id)initWithWindowNibName:(NSString*)nibName { 67 NSString* nibpath = [base::mac::FrameworkBundle() pathForResource:nibName 68 ofType:@"nib"]; 69 self = [super initWithWindowNibPath:nibpath owner:self]; 70 if (self) { 71 [tableView_ setDataSource:self]; 72 } 73 return self; 74 } 75 76 - (void)dealloc { 77 DCHECK(!g_instance); 78 [tableView_ setDataSource:nil]; 79 [tableView_ setDelegate:nil]; 80 [killButton_ setTarget:nil]; 81 [waitButton_ setTarget:nil]; 82 [super dealloc]; 83 } 84 85 - (void)awakeFromNib { 86 // Load in the image 87 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 88 NSImage* backgroundImage = 89 rb.GetNativeImageNamed(IDR_FROZEN_TAB_ICON).ToNSImage(); 90 [imageView_ setImage:backgroundImage]; 91 92 // Make the message fit. 93 CGFloat messageShift = 94 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:messageView_]; 95 96 // Move the graphic up to be top even with the message. 97 NSRect graphicFrame = [imageView_ frame]; 98 graphicFrame.origin.y += messageShift; 99 [imageView_ setFrame:graphicFrame]; 100 101 // Make the window taller to fit everything. 102 NSSize windowDelta = NSMakeSize(0, messageShift); 103 [GTMUILocalizerAndLayoutTweaker 104 resizeWindowWithoutAutoResizingSubViews:[self window] 105 delta:windowDelta]; 106 107 // Make the "wait" button respond to additional keys. By setting this to 108 // @"\e", it will respond to both Esc and Command-. (period). 109 KeyEquivalentAndModifierMask key; 110 key.charCode = @"\e"; 111 [waitButton_ addKeyEquivalent:key]; 112 } 113 114 - (IBAction)kill:(id)sender { 115 if (hungContents_) 116 base::KillProcess(hungContents_->GetRenderProcessHost()->GetHandle(), 117 content::RESULT_CODE_HUNG, false); 118 // Cannot call performClose:, because the close button is disabled. 119 [self close]; 120 } 121 122 - (IBAction)wait:(id)sender { 123 if (hungContents_ && hungContents_->GetRenderViewHost()) 124 hungContents_->GetRenderViewHost()->RestartHangMonitorTimeout(); 125 // Cannot call performClose:, because the close button is disabled. 126 [self close]; 127 } 128 129 - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView { 130 return [hungTitles_ count]; 131 } 132 133 - (id)tableView:(NSTableView*)aTableView 134 objectValueForTableColumn:(NSTableColumn*)column 135 row:(NSInteger)rowIndex { 136 return [NSNumber numberWithInt:NSOffState]; 137 } 138 139 - (NSCell*)tableView:(NSTableView*)tableView 140 dataCellForTableColumn:(NSTableColumn*)tableColumn 141 row:(NSInteger)rowIndex { 142 NSCell* cell = [tableColumn dataCellForRow:rowIndex]; 143 144 if ([[tableColumn identifier] isEqualToString:@"title"]) { 145 DCHECK([cell isKindOfClass:[NSButtonCell class]]); 146 NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell); 147 [buttonCell setTitle:[hungTitles_ objectAtIndex:rowIndex]]; 148 [buttonCell setImage:[hungFavicons_ objectAtIndex:rowIndex]]; 149 [buttonCell setRefusesFirstResponder:YES]; // Don't push in like a button. 150 [buttonCell setHighlightsBy:NSNoCellMask]; 151 } 152 return cell; 153 } 154 155 - (void)windowWillClose:(NSNotification*)notification { 156 // We have to reset g_instance before autoreleasing the window, 157 // because we want to avoid reusing the same dialog if someone calls 158 // chrome::ShowHungRendererDialog() between the autorelease call and the 159 // actual dealloc. 160 g_instance = nil; 161 162 // Prevent kills from happening after close if the user had the 163 // button depressed just when new activity was detected. 164 hungContents_ = NULL; 165 166 [self autorelease]; 167 } 168 169 // TODO(shess): This could observe all of the tabs referenced in the 170 // loop, updating the dialog and keeping it up so long as any remain. 171 // Tabs closed by their renderer will close the dialog (that's 172 // activity!), so it would not add much value. Also, the views 173 // implementation only monitors the initiating tab. 174 - (void)showForWebContents:(WebContents*)contents { 175 DCHECK(contents); 176 hungContents_ = contents; 177 hungContentsObserver_.reset(new WebContentsObserverBridge(contents, self)); 178 base::scoped_nsobject<NSMutableArray> titles([[NSMutableArray alloc] init]); 179 base::scoped_nsobject<NSMutableArray> favicons([[NSMutableArray alloc] init]); 180 for (TabContentsIterator it; !it.done(); it.Next()) { 181 if (it->GetRenderProcessHost() == hungContents_->GetRenderProcessHost()) { 182 base::string16 title = it->GetTitle(); 183 if (title.empty()) 184 title = CoreTabHelper::GetDefaultTitle(); 185 [titles addObject:base::SysUTF16ToNSString(title)]; 186 [favicons addObject:mac::FaviconForWebContents(*it)]; 187 } 188 } 189 hungTitles_.reset([titles copy]); 190 hungFavicons_.reset([favicons copy]); 191 [tableView_ reloadData]; 192 193 [[self window] center]; 194 [self showWindow:self]; 195 } 196 197 - (void)endForWebContents:(WebContents*)contents { 198 DCHECK(contents); 199 DCHECK(hungContents_); 200 if (hungContents_ && hungContents_->GetRenderProcessHost() == 201 contents->GetRenderProcessHost()) { 202 // Cannot call performClose:, because the close button is disabled. 203 [self close]; 204 } 205 } 206 207 - (void)renderProcessGone { 208 // Cannot call performClose:, because the close button is disabled. 209 [self close]; 210 } 211 212 @end 213 214 @implementation HungRendererController (JustForTesting) 215 - (NSButton*)killButton { 216 return killButton_; 217 } 218 219 - (MultiKeyEquivalentButton*)waitButton { 220 return waitButton_; 221 } 222 @end 223 224 namespace chrome { 225 226 void ShowHungRendererDialog(WebContents* contents) { 227 if (!logging::DialogsAreSuppressed()) { 228 if (!g_instance) 229 g_instance = [[HungRendererController alloc] 230 initWithWindowNibName:@"HungRendererDialog"]; 231 [g_instance showForWebContents:contents]; 232 } 233 } 234 235 void HideHungRendererDialog(WebContents* contents) { 236 if (!logging::DialogsAreSuppressed() && g_instance) 237 [g_instance endForWebContents:contents]; 238 } 239 240 } // namespace chrome 241