Index: chrome/browser/chromeos/login/webui_screen_locker.cc |
diff --git a/chrome/browser/chromeos/login/webui_screen_locker.cc b/chrome/browser/chromeos/login/webui_screen_locker.cc |
index 6b64dc5afe73556e9648540b0a87d4a39fcb1e61..8a253499208a4369e8eda6e7114d795683904ab8 100644 |
--- a/chrome/browser/chromeos/login/webui_screen_locker.cc |
+++ b/chrome/browser/chromeos/login/webui_screen_locker.cc |
@@ -23,9 +23,11 @@ |
#include "chrome/browser/chromeos/login/screen_locker.h" |
#include "chrome/browser/chromeos/login/user_manager.h" |
#include "chrome/browser/chromeos/login/webui_login_display.h" |
+#include "chrome/browser/ui/views/dom_view.h" |
#include "chrome/browser/ui/webui/chromeos/login/oobe_ui.h" |
#include "chrome/common/chrome_notification_types.h" |
#include "chrome/common/url_constants.h" |
+#include "content/browser/renderer_host/render_widget_host_view.h" |
#include "content/public/browser/notification_service.h" |
#include "content/public/browser/notification_types.h" |
#include "ui/base/l10n/l10n_util.h" |
@@ -38,27 +40,29 @@ namespace { |
// URL which corresponds to the login WebUI. |
const char kLoginURL[] = "chrome://oobe/login"; |
+// The maximum duration for which locker should try to grab the keyboard and |
+// mouse and its interval for regrabbing on failure. |
+const int kMaxGrabFailureSec = 30; |
+const int64 kRetryGrabIntervalMs = 500; |
+ |
+// Maximum number of times we'll try to grab the keyboard and mouse before |
+// giving up. If we hit the limit, Chrome exits and the session is terminated. |
+const int kMaxGrabFailures = kMaxGrabFailureSec * 1000 / kRetryGrabIntervalMs; |
+ |
// A ScreenLock window that covers entire screen to keep the keyboard |
// focus/events inside the grab widget. |
class LockWindow : public views::NativeWidgetGtk { |
public: |
- LockWindow() |
+ explicit LockWindow(chromeos::WebUIScreenLocker* webui_screen_locker) |
: views::NativeWidgetGtk(new views::Widget), |
- toplevel_focus_widget_(NULL) { |
+ toplevel_focus_widget_(NULL), |
+ webui_screen_locker_(webui_screen_locker) { |
EnableDoubleBuffer(true); |
} |
- // GTK propagates key events from parents to children. |
- // Make sure LockWindow will never handle key events. |
- virtual gboolean OnEventKey(GtkWidget* widget, GdkEventKey* event) OVERRIDE { |
- // Don't handle key event in the lock window. |
- return false; |
- } |
- |
virtual gboolean OnButtonPress(GtkWidget* widget, |
GdkEventButton* event) OVERRIDE { |
- // Don't handle mouse event in the lock wnidow and |
- // nor propagate to child. |
+ // Never propagate mouse events to parent. |
return true; |
} |
@@ -72,6 +76,10 @@ class LockWindow : public views::NativeWidgetGtk { |
gtk_widget_grab_focus(toplevel_focus_widget_); |
} |
+ virtual void HandleGtkGrabBroke() OVERRIDE { |
+ webui_screen_locker_->HandleGtkGrabBroke(); |
+ } |
+ |
// Sets the widget to move the focus to when clearning the native |
// widget's focus. |
void set_toplevel_focus_widget(GtkWidget* widget) { |
@@ -81,15 +89,35 @@ class LockWindow : public views::NativeWidgetGtk { |
private: |
// The widget we set focus to when clearning the focus on native |
- // widget. In screen locker, gdk input is grabbed in GrabWidget, |
- // and resetting the focus by using gtk_window_set_focus seems to |
- // confuse gtk and doesn't let focus move to native widget under |
- // GrabWidget. |
+ // widget. Gdk input is grabbed in WebUIScreenLocker, and resetting the focus |
+ // by using gtk_window_set_focus seems to confuse gtk and doesn't let focus |
+ // move to native widget under this. |
GtkWidget* toplevel_focus_widget_; |
+ // The WebUI screen locker. |
+ chromeos::WebUIScreenLocker* webui_screen_locker_; |
+ |
DISALLOW_COPY_AND_ASSIGN(LockWindow); |
}; |
+// Define separate methods for each error code so that stack trace |
+// will tell which error the grab failed with. |
+void FailedWithGrabAlreadyGrabbed() { |
+ LOG(FATAL) << "Grab already grabbed"; |
+} |
+void FailedWithGrabInvalidTime() { |
+ LOG(FATAL) << "Grab invalid time"; |
+} |
+void FailedWithGrabNotViewable() { |
+ LOG(FATAL) << "Grab not viewable"; |
+} |
+void FailedWithGrabFrozen() { |
+ LOG(FATAL) << "Grab frozen"; |
+} |
+void FailedWithUnknownError() { |
+ LOG(FATAL) << "Grab uknown"; |
+} |
+ |
} // namespace |
namespace chromeos { |
@@ -98,7 +126,13 @@ namespace chromeos { |
// WebUIScreenLocker implementation. |
WebUIScreenLocker::WebUIScreenLocker(ScreenLocker* screen_locker) |
- : ScreenLockerDelegate(screen_locker) { |
+ : ScreenLockerDelegate(screen_locker), |
+ drawn_(false), |
+ input_grabbed_(false), |
+ ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)), |
+ grab_failure_count_(0), |
+ kbd_grab_status_(GDK_GRAB_INVALID_TIME), |
+ mouse_grab_status_(GDK_GRAB_INVALID_TIME) { |
} |
void WebUIScreenLocker::LockScreen(bool unlock_on_input) { |
@@ -106,7 +140,7 @@ void WebUIScreenLocker::LockScreen(bool unlock_on_input) { |
gfx::Rect bounds(gfx::Screen::GetMonitorAreaNearestWindow(NULL)); |
- LockWindow* lock_window = new LockWindow(); |
+ LockWindow* lock_window = new LockWindow(this); |
lock_window_ = lock_window->GetWidget(); |
views::Widget::InitParams params( |
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
@@ -146,6 +180,12 @@ void WebUIScreenLocker::LockScreen(bool unlock_on_input) { |
chrome::NOTIFICATION_LOGIN_USER_IMAGE_CHANGED, |
content::NotificationService::AllSources()); |
+ ClearGtkGrab(); |
+ |
+ // Call this after lock_window_->Show(); otherwise the 1st invocation |
+ // of gdk_xxx_grab() will always fail. |
+ TryGrabAllInputs(); |
+ |
// Add the window to its own group so that its grab won't be stolen if |
// gtk_grab_add() gets called on behalf on a non-screen-locker widget (e.g. |
// a modal dialog) -- see http://crosbug.com/8999. We intentionally do this |
@@ -159,11 +199,161 @@ void WebUIScreenLocker::LockScreen(bool unlock_on_input) { |
lock_window->set_toplevel_focus_widget(lock_window->window_contents()); |
} |
+void WebUIScreenLocker::OnGrabInputs() { |
+ DVLOG(1) << "OnGrabInputs"; |
+ input_grabbed_ = true; |
+ if (drawn_) |
+ ScreenLockReady(); |
+} |
+ |
+void WebUIScreenLocker::OnWindowManagerReady() { |
+ DVLOG(1) << "OnClientEvent: drawn for lock"; |
+ drawn_ = true; |
+ if (input_grabbed_) |
+ ScreenLockReady(); |
+} |
+ |
void WebUIScreenLocker::ScreenLockReady() { |
ScreenLockerDelegate::ScreenLockReady(); |
SetInputEnabled(true); |
} |
+void WebUIScreenLocker::ClearGtkGrab() { |
+ GtkWidget* current_grab_window; |
+ // Grab gtk input first so that the menu holding gtk grab will |
+ // close itself. |
+ gtk_grab_add(webui_login_->native_view()); |
+ |
+ // Make sure there is no gtk grab widget so that gtk simply propagates |
+ // an event. GTK maintains grab widgets in a linked-list, so we need to |
+ // remove until it's empty. |
+ while ((current_grab_window = gtk_grab_get_current()) != NULL) |
+ gtk_grab_remove(current_grab_window); |
+} |
+ |
+void WebUIScreenLocker::TryGrabAllInputs() { |
+ // Grab on the RenderWidgetHostView hosting the WebUI login screen. |
+ GdkWindow* grab_widget = webui_login_->dom_contents()->tab_contents()-> |
+ GetRenderWidgetHostView()->GetNativeView()->window; |
+ // Grab x server so that we can atomically grab and take |
+ // action when grab fails. |
+ gdk_x11_grab_server(); |
+ gtk_grab_add(webui_login_->native_view()); |
+ if (kbd_grab_status_ != GDK_GRAB_SUCCESS) |
+ kbd_grab_status_ = gdk_keyboard_grab(grab_widget, FALSE, GDK_CURRENT_TIME); |
+ if (mouse_grab_status_ != GDK_GRAB_SUCCESS) { |
+ mouse_grab_status_ = |
+ gdk_pointer_grab(grab_widget, |
+ FALSE, |
+ static_cast<GdkEventMask>( |
+ GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | |
+ GDK_POINTER_MOTION_MASK), |
+ NULL, |
+ NULL, |
+ GDK_CURRENT_TIME); |
+ } |
+ if ((kbd_grab_status_ != GDK_GRAB_SUCCESS || |
+ mouse_grab_status_ != GDK_GRAB_SUCCESS) && |
+ grab_failure_count_++ < kMaxGrabFailures) { |
+ LOG(WARNING) << "Failed to grab inputs. Trying again in " |
+ << kRetryGrabIntervalMs << " ms: kbd=" |
+ << kbd_grab_status_ << ", mouse=" << mouse_grab_status_; |
+ TryUngrabOtherClients(); |
+ gdk_x11_ungrab_server(); |
+ MessageLoop::current()->PostDelayedTask( |
+ FROM_HERE, |
+ base::Bind(&WebUIScreenLocker::TryGrabAllInputs, |
+ weak_factory_.GetWeakPtr()), |
+ kRetryGrabIntervalMs); |
+ } else { |
+ gdk_x11_ungrab_server(); |
+ GdkGrabStatus status = kbd_grab_status_; |
+ if (status == GDK_GRAB_SUCCESS) { |
+ status = mouse_grab_status_; |
+ } |
+ switch (status) { |
+ case GDK_GRAB_SUCCESS: |
+ break; |
+ case GDK_GRAB_ALREADY_GRABBED: |
+ FailedWithGrabAlreadyGrabbed(); |
+ break; |
+ case GDK_GRAB_INVALID_TIME: |
+ FailedWithGrabInvalidTime(); |
+ break; |
+ case GDK_GRAB_NOT_VIEWABLE: |
+ FailedWithGrabNotViewable(); |
+ break; |
+ case GDK_GRAB_FROZEN: |
+ FailedWithGrabFrozen(); |
+ break; |
+ default: |
+ FailedWithUnknownError(); |
+ break; |
+ } |
+ DVLOG(1) << "Grab Success"; |
+ OnGrabInputs(); |
+ } |
+} |
+ |
+void WebUIScreenLocker::TryUngrabOtherClients() { |
+#if !defined(NDEBUG) |
+ { |
+ int event_base, error_base; |
+ int major, minor; |
+ // Make sure we have XTest extension. |
+ DCHECK(XTestQueryExtension(ui::GetXDisplay(), |
+ &event_base, &error_base, |
+ &major, &minor)); |
+ } |
+#endif |
+ |
+ // The following code is an attempt to grab inputs by closing |
+ // supposedly opened menu. This happens when a plugin has a menu |
+ // opened. |
+ if (mouse_grab_status_ == GDK_GRAB_ALREADY_GRABBED || |
+ mouse_grab_status_ == GDK_GRAB_FROZEN) { |
+ // Successfully grabbed the keyboard, but pointer is still |
+ // grabbed by other client. Another attempt to close supposedly |
+ // opened menu by emulating keypress at the left top corner. |
+ Display* display = ui::GetXDisplay(); |
+ Window root, child; |
+ int root_x, root_y, win_x, winy; |
+ unsigned int mask; |
+ XQueryPointer(display, |
+ ui::GetX11WindowFromGtkWidget( |
+ static_cast<LockWindow*>(lock_window_->native_widget())-> |
+ window_contents()), |
+ &root, &child, &root_x, &root_y, |
+ &win_x, &winy, &mask); |
+ XTestFakeMotionEvent(display, -1, -10000, -10000, CurrentTime); |
+ XTestFakeButtonEvent(display, 1, True, CurrentTime); |
+ XTestFakeButtonEvent(display, 1, False, CurrentTime); |
+ // Move the pointer back. |
+ XTestFakeMotionEvent(display, -1, root_x, root_y, CurrentTime); |
+ XFlush(display); |
+ } else if (kbd_grab_status_ == GDK_GRAB_ALREADY_GRABBED || |
+ kbd_grab_status_ == GDK_GRAB_FROZEN) { |
+ // Successfully grabbed the pointer, but keyboard is still grabbed |
+ // by other client. Another attempt to close supposedly opened |
+ // menu by emulating escape key. Such situation must be very |
+ // rare, but handling this just in case |
+ Display* display = ui::GetXDisplay(); |
+ KeyCode escape = XKeysymToKeycode(display, XK_Escape); |
+ XTestFakeKeyEvent(display, escape, True, CurrentTime); |
+ XTestFakeKeyEvent(display, escape, False, CurrentTime); |
+ XFlush(display); |
+ } |
+} |
+ |
+void WebUIScreenLocker::HandleGtkGrabBroke() { |
+ // Input should never be stolen from ScreenLocker once it's |
+ // grabbed. If this happens, it's a bug and has to be fixed. We |
+ // let chrome crash to get a crash report and dump, and |
+ // SessionManager will terminate the session to logout. |
+ CHECK_NE(GDK_GRAB_SUCCESS, kbd_grab_status_); |
+ CHECK_NE(GDK_GRAB_SUCCESS, mouse_grab_status_); |
+} |
+ |
void WebUIScreenLocker::OnAuthenticate() { |
} |
@@ -210,7 +400,7 @@ void WebUIScreenLocker::OnClientEvent(GtkWidget* widge, GdkEventClient* event) { |
WmIpc::Message msg; |
WmIpc::instance()->DecodeMessage(*event, &msg); |
if (msg.type() == WM_IPC_MESSAGE_CHROME_NOTIFY_SCREEN_REDRAWN_FOR_LOCK) |
- ScreenLockReady(); |
+ OnWindowManagerReady(); |
} |
//////////////////////////////////////////////////////////////////////////////// |