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