blob: ac70e4bf38d57048790b2a606350c76a9918123c [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/hung_renderer_controller.h"
#import <Cocoa/Cocoa.h>
#include "base/mac/bundle_locations.h"
#include "base/macros.h"
#include "base/scoped_observer.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/hang_monitor/hang_crash_dump.h"
#import "chrome/browser/ui/cocoa/multi_key_equivalent_button.h"
#import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h"
#include "chrome/browser/ui/hung_renderer/hung_renderer_core.h"
#include "chrome/browser/ui/tab_contents/core_tab_helper.h"
#include "chrome/browser/ui/tab_contents/tab_contents_iterator.h"
#include "chrome/common/logging_chrome.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_process_host_observer.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_observer.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/result_codes.h"
#include "skia/ext/skia_utils_mac.h"
#include "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image.h"
using content::WebContents;
@interface HungRendererController ()
// Lays out the interface for the specified number of items.
- (void)layoutForItemCount:(int)count;
// Modifies the dialog to show a warning for the given tab contents.
// The dialog will contain a list of all tabs that share a renderer
// process with |contents|. The caller must not delete any tab
// contents without first calling endForWebContents.
- (void)showForWebContents:(content::WebContents*)contents
renderWidgetHost:(content::RenderWidgetHost*)renderWidget
timeoutRestarter:(base::RepeatingClosure)timeoutRestarter;
// Notifies the dialog that |contents| is either responsive or closed.
// If |contents| shares the same render process as the tab contents
// this dialog was created for, this function will close the dialog.
// If |contents| has a different process, this function does nothing.
- (void)endForWebContents:(content::WebContents*)contents
renderWidgetHost:(content::RenderWidgetHost*)renderWidget;
// Called by |hungContentsObserver_| to indicate that |hungContents_|
// has gone away.
- (void)renderProcessGone;
// Called by |hungContentsObserver_| to indicate that |hungContents_|
// has changed; restart hung renderer timer.
- (void)tabUpdated;
@end
namespace {
// We only support showing one of these at a time per app. The
// controller owns itself and is released when its window is closed.
HungRendererController* g_hung_renderer_controller_instance = nil;
} // namespace
class HungRendererObserverBridge : public content::WebContentsObserver,
public content::RenderProcessHostObserver,
public content::RenderWidgetHostObserver {
public:
HungRendererObserverBridge(WebContents* web_contents,
content::RenderWidgetHost* hung_widget,
HungRendererController* controller)
: content::WebContentsObserver(web_contents),
hung_process_(hung_widget->GetProcess()),
process_observer_(this),
widget_observer_(this),
controller_(controller) {
process_observer_.Add(hung_process_);
widget_observer_.Add(hung_widget);
}
~HungRendererObserverBridge() override = default;
protected:
// WebContentsObserver overrides:
void RenderViewHostChanged(content::RenderViewHost* old_host,
content::RenderViewHost* new_host) override {
[controller_ tabUpdated];
}
void WebContentsDestroyed() override { [controller_ renderProcessGone]; }
// RenderProcessHostObserver overrides:
void RenderProcessExited(
content::RenderProcessHost* host,
const content::ChildProcessTerminationInfo& info) override {
[controller_ renderProcessGone];
}
// RenderWidgetHostObserver overrides:
void RenderWidgetHostDestroyed(
content::RenderWidgetHost* widget_host) override {
[controller_ renderProcessGone];
}
private:
content::RenderProcessHost* hung_process_;
ScopedObserver<content::RenderProcessHost, content::RenderProcessHostObserver>
process_observer_;
ScopedObserver<content::RenderWidgetHost, content::RenderWidgetHostObserver>
widget_observer_;
HungRendererController* controller_; // weak
DISALLOW_COPY_AND_ASSIGN(HungRendererObserverBridge);
};
@implementation HungRendererController
- (id)initWithWindowNibName:(NSString*)nibName {
NSString* nibpath = [base::mac::FrameworkBundle() pathForResource:nibName
ofType:@"nib"];
self = [super initWithWindowNibPath:nibpath owner:self];
if (self) {
[tableView_ setDataSource:self];
}
return self;
}
- (void)dealloc {
DCHECK(!g_hung_renderer_controller_instance);
[tableView_ setDataSource:nil];
[tableView_ setDelegate:nil];
[killButton_ setTarget:nil];
[waitButton_ setTarget:nil];
[super dealloc];
}
- (void)awakeFromNib {
// Load in the image.
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
NSImage* backgroundImage =
rb.GetNativeImageNamed(IDR_FROZEN_TAB_ICON).ToNSImage();
[imageView_ setImage:backgroundImage];
// Make the "wait" button respond to additional keys. By setting this to
// @"\e", it will respond to both Esc and Command-. (period).
KeyEquivalentAndModifierMask key;
key.charCode = @"\e";
[waitButton_ addKeyEquivalent:key];
}
- (void)layoutForItemCount:(int)count {
// Set the messages.
[[self window] setTitle:
l10n_util::GetPluralNSStringF(IDS_BROWSER_HANGMONITOR_RENDERER_TITLE,
count)];
[messageView_ setStringValue:
l10n_util::GetPluralNSStringF(IDS_BROWSER_HANGMONITOR_RENDERER,
count)];
[killButton_ setTitle:l10n_util::GetPluralNSStringF(
IDS_BROWSER_HANGMONITOR_RENDERER_END, count)];
// Make the message fit.
CGFloat messageShift =
[GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:messageView_];
// Move the graphic up to be top even with the message.
NSRect graphicFrame = [imageView_ frame];
graphicFrame.origin.y += messageShift;
[imageView_ setFrame:graphicFrame];
// Make the window taller to fit everything.
NSSize windowDelta = NSMakeSize(0, messageShift);
[GTMUILocalizerAndLayoutTweaker
resizeWindowWithoutAutoResizingSubViews:[self window]
delta:windowDelta];
}
+ (void)showForWebContents:(content::WebContents*)contents
renderWidgetHost:(content::RenderWidgetHost*)renderWidget
timeoutRestarter:(base::RepeatingClosure)timeoutRestarter {
if (!logging::DialogsAreSuppressed()) {
if (!g_hung_renderer_controller_instance)
g_hung_renderer_controller_instance = [[HungRendererController alloc]
initWithWindowNibName:@"HungRendererDialog"];
[g_hung_renderer_controller_instance showForWebContents:contents
renderWidgetHost:renderWidget
timeoutRestarter:timeoutRestarter];
}
}
+ (void)endForWebContents:(content::WebContents*)contents
renderWidgetHost:(content::RenderWidgetHost*)renderWidget {
if (!logging::DialogsAreSuppressed() && g_hung_renderer_controller_instance)
[g_hung_renderer_controller_instance endForWebContents:contents
renderWidgetHost:renderWidget];
}
+ (bool)isShowing {
return g_hung_renderer_controller_instance;
}
- (IBAction)kill:(id)sender {
if (hungWidget_) {
auto* rph = hungWidget_->GetProcess();
CrashDumpHungChildProcess(rph->GetProcess().Handle());
rph->Shutdown(content::RESULT_CODE_HUNG);
}
// Cannot call performClose:, because the close button is disabled.
[self close];
}
- (IBAction)wait:(id)sender {
if (!hangMonitorRestarter_.is_null())
hangMonitorRestarter_.Run();
// Cannot call performClose:, because the close button is disabled.
[self close];
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView {
return [hungTitles_ count];
}
- (id)tableView:(NSTableView*)aTableView
objectValueForTableColumn:(NSTableColumn*)column
row:(NSInteger)rowIndex {
return [NSNumber numberWithInt:NSOffState];
}
- (NSCell*)tableView:(NSTableView*)tableView
dataCellForTableColumn:(NSTableColumn*)tableColumn
row:(NSInteger)rowIndex {
NSCell* cell = [tableColumn dataCellForRow:rowIndex];
if ([[tableColumn identifier] isEqualToString:@"title"]) {
DCHECK([cell isKindOfClass:[NSButtonCell class]]);
NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell);
[buttonCell setTitle:[hungTitles_ objectAtIndex:rowIndex]];
[buttonCell setImage:[hungFavicons_ objectAtIndex:rowIndex]];
[buttonCell setRefusesFirstResponder:YES]; // Don't push in like a button.
[buttonCell setHighlightsBy:NSNoCellMask];
}
return cell;
}
- (void)windowWillClose:(NSNotification*)notification {
// We have to reset g_hung_renderer_controller_instance before autoreleasing
// the window, because we want to avoid reusing the same dialog if someone
// calls chrome::ShowHungRendererDialog() between the autorelease call and the
// actual dealloc.
g_hung_renderer_controller_instance = nil;
// Prevent kills from happening after close if the user had the
// button depressed just when new activity was detected.
hungContents_ = nullptr;
hungWidget_ = nullptr;
hangMonitorRestarter_ = base::RepeatingClosure();
// Reset the observer now. It is not necessarily the case that this class
// actually holds a reference to the containing BrowserWindow, and if it does
// not, the BrowserWindow's destructor can run *before* this object is cleaned
// up by the autorelease pool. This can't happen in practice, but it can
// happen in tests.
hungContentsObserver_.reset();
[self autorelease];
}
// TODO(shess): This could observe all of the tabs referenced in the
// loop, updating the dialog and keeping it up so long as any remain.
// Tabs closed by their renderer will close the dialog (that's
// activity!), so it would not add much value. Also, the views
// implementation only monitors the initiating tab.
- (void)showForWebContents:(WebContents*)contents
renderWidgetHost:(content::RenderWidgetHost*)renderWidget
timeoutRestarter:(base::RepeatingClosure)timeoutRestarter {
DCHECK(contents);
DCHECK(!timeoutRestarter.is_null());
hungContents_ = contents;
hungWidget_ = renderWidget;
hangMonitorRestarter_ = timeoutRestarter;
hungContentsObserver_.reset(
new HungRendererObserverBridge(contents, renderWidget, self));
base::scoped_nsobject<NSMutableArray> titles([[NSMutableArray alloc] init]);
base::scoped_nsobject<NSMutableArray> favicons([[NSMutableArray alloc] init]);
for (auto* hungContents :
GetHungWebContentsList(contents, renderWidget->GetProcess())) {
[titles addObject:base::SysUTF16ToNSString(GetHungWebContentsTitle(
hungContents, renderWidget->GetProcess()))];
[favicons addObject:mac::FaviconForWebContents(hungContents)];
}
hungTitles_.reset([titles copy]);
hungFavicons_.reset([favicons copy]);
[tableView_ reloadData];
[self layoutForItemCount:[titles count]];
[[self window] center];
[self showWindow:self];
}
- (void)endForWebContents:(WebContents*)contents
renderWidgetHost:(content::RenderWidgetHost*)renderWidget {
DCHECK(contents);
DCHECK(hungContents_);
DCHECK(renderWidget);
DCHECK(hungWidget_);
DCHECK(!hangMonitorRestarter_.is_null());
if (hungContents_ && hungWidget_ == renderWidget) {
// Cannot call performClose:, because the close button is disabled.
[self close];
}
}
- (void)renderProcessGone {
// Cannot call performClose:, because the close button is disabled.
[self close];
}
- (void)tabUpdated {
// Tab was updated so restart the hang monitor if necessary and dismiss the
// current dialog.
if (!hangMonitorRestarter_.is_null())
hangMonitorRestarter_.Run();
[self close];
}
@end
@implementation HungRendererController (JustForTesting)
- (NSButton*)killButton {
return killButton_;
}
- (MultiKeyEquivalentButton*)waitButton {
return waitButton_;
}
@end