1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-10 01:48:00 +00:00

gtkask: rework the mechanism for keyboard grabs.

I've found Unix Pageant's GTK password prompt to be a bit flaky on
Ubuntu 18.04. Part of the reason for that seems to be (I _think_) that
GTK has changed its internal order of setting things up, so that you
can no longer call gtk_widget_show_now() and expect that when it
returns everything is ready to do a gdk_seat_grab. Another part is
that - completely mysteriously as far as I can see - a _failed_
gdk_seat_grab(GDK_SEAT_CAPABILITY_KEYBOARD) has the side effect of
calling gdk_window_hide on the window you gave it!

So I've done a considerable restructuring that means we no longer
attempt to do the keyboard grab synchronously in gtk_askpass_setup.
Instead, we make keyboard grab attempts during the run of gtk_main,
scheduling each one on a timer if the previous attempt fails.

This means I need a visual indication of 'not ready for you to type
anything yet', which I've arranged by filling in the three drawing
areas to mid-grey. At the point when the keyboard grab completes and
the window becomes receptive to input, they turn into the usual one
black and two white.
This commit is contained in:
Simon Tatham 2018-05-13 22:56:52 +01:00
parent 6afa955a2e
commit a3503fd234

View File

@ -27,7 +27,8 @@ struct drawing_area_ctx {
#ifndef DRAW_DEFAULT_CAIRO
GdkColor *cols;
#endif
int width, height, current;
int width, height;
enum { NOT_CURRENT, CURRENT, GREYED_OUT } state;
};
struct askpass_ctx {
@ -39,15 +40,18 @@ struct askpass_ctx {
#endif
#ifndef DRAW_DEFAULT_CAIRO
GdkColormap *colmap;
GdkColor cols[2];
GdkColor cols[3];
#endif
char *passphrase;
char *error_message; /* if we finish without a passphrase */
char *passphrase; /* if we finish with one */
int passlen, passsize;
#if GTK_CHECK_VERSION(3,20,0)
GdkSeat *seat; /* for gdk_seat_grab */
#elif GTK_CHECK_VERSION(3,0,0)
GdkDevice *keyboard; /* for gdk_device_grab */
#endif
int nattempts;
};
static void visually_acknowledge_keypress(struct askpass_ctx *ctx)
@ -56,9 +60,9 @@ static void visually_acknowledge_keypress(struct askpass_ctx *ctx)
new_active = rand() % (N_DRAWING_AREAS - 1);
if (new_active >= ctx->active_area)
new_active++;
ctx->drawingareas[ctx->active_area].current = 0;
ctx->drawingareas[ctx->active_area].state = NOT_CURRENT;
gtk_widget_queue_draw(ctx->drawingareas[ctx->active_area].area);
ctx->drawingareas[new_active].current = 1;
ctx->drawingareas[new_active].state = CURRENT;
gtk_widget_queue_draw(ctx->drawingareas[new_active].area);
ctx->active_area = new_active;
}
@ -113,6 +117,7 @@ static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
event->type == GDK_KEY_PRESS) {
smemclr(ctx->passphrase, ctx->passsize);
ctx->passphrase = NULL;
ctx->error_message = dupstr("passphrase input cancelled");
gtk_main_quit();
} else {
#if GTK_CHECK_VERSION(2,0,0)
@ -178,14 +183,16 @@ static gint configure_area(GtkWidget *widget, GdkEventConfigure *event,
#ifdef DRAW_DEFAULT_CAIRO
static void askpass_redraw_cairo(cairo_t *cr, struct drawing_area_ctx *ctx)
{
cairo_set_source_rgb(cr, 1-ctx->current, 1-ctx->current, 1-ctx->current);
double rgbval = (ctx->state == CURRENT ? 0 :
ctx->state == NOT_CURRENT ? 1 : 0.5);
cairo_set_source_rgb(cr, rgbval, rgbval, rgbval);
cairo_paint(cr);
}
#else
static void askpass_redraw_gdk(GdkWindow *win, struct drawing_area_ctx *ctx)
{
GdkGC *gc = gdk_gc_new(win);
gdk_gc_set_foreground(gc, &ctx->cols[ctx->current]);
gdk_gc_set_foreground(gc, &ctx->cols[ctx->state]);
gdk_draw_rectangle(win, gc, TRUE, 0, 0, ctx->width, ctx->height);
gdk_gc_unref(gc);
}
@ -216,9 +223,10 @@ static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
}
#endif
static int try_grab_keyboard(struct askpass_ctx *ctx)
static gboolean try_grab_keyboard(gpointer vctx)
{
int ret;
struct askpass_ctx *ctx = (struct askpass_ctx *)vctx;
int i, ret;
#if GTK_CHECK_VERSION(3,20,0)
/*
@ -226,16 +234,28 @@ static int try_grab_keyboard(struct askpass_ctx *ctx)
* GdkSeat.
*/
GdkSeat *seat;
GdkWindow *gdkw = gtk_widget_get_window(ctx->dialog);
if (!GDK_IS_WINDOW(gdkw) || !gdk_window_is_visible(gdkw))
goto fail;
seat = gdk_display_get_default_seat
(gtk_widget_get_display(ctx->dialog));
if (!seat)
return FALSE;
goto fail;
ctx->seat = seat;
ret = gdk_seat_grab(seat, gtk_widget_get_window(ctx->dialog),
GDK_SEAT_CAPABILITY_KEYBOARD,
ret = gdk_seat_grab(seat, gdkw, GDK_SEAT_CAPABILITY_KEYBOARD,
TRUE, NULL, NULL, NULL, NULL);
/*
* For some reason GDK 3.22 hides the GDK window as a side effect
* of a failed grab. I've no idea why. But if we're going to retry
* the grab, then we need to unhide it again or else we'll just
* get GDK_GRAB_NOT_VIEWABLE on every subsequent attempt.
*/
if (ret != GDK_GRAB_SUCCESS)
gdk_window_show(gdkw);
#elif GTK_CHECK_VERSION(3,0,0)
/*
* And it has to be done differently again prior to GTK 3.20.
@ -246,16 +266,16 @@ static int try_grab_keyboard(struct askpass_ctx *ctx)
dm = gdk_display_get_device_manager
(gtk_widget_get_display(ctx->dialog));
if (!dm)
return FALSE;
goto fail;
pointer = gdk_device_manager_get_client_pointer(dm);
if (!pointer)
return FALSE;
goto fail;
keyboard = gdk_device_get_associated_device(pointer);
if (!keyboard)
return FALSE;
goto fail;
if (gdk_device_get_source(keyboard) != GDK_SOURCE_KEYBOARD)
return FALSE;
goto fail;
ctx->keyboard = keyboard;
ret = gdk_device_grab(ctx->keyboard,
@ -272,37 +292,83 @@ static int try_grab_keyboard(struct askpass_ctx *ctx)
ret = gdk_keyboard_grab(gtk_widget_get_window(ctx->dialog),
FALSE, GDK_CURRENT_TIME);
#endif
if (ret != GDK_GRAB_SUCCESS)
goto fail;
return ret == GDK_GRAB_SUCCESS;
}
typedef int (try_grab_fn_t)(struct askpass_ctx *ctx);
static int repeatedly_try_grab(struct askpass_ctx *ctx, try_grab_fn_t fn)
{
/*
* Repeatedly try to grab some aspect of the X server. We have to
* do this rather than just trying once, because there is at least
* one important situation in which the grab may fail the first
* time: any user who is launching an add-key operation off some
* kind of window manager hotkey will almost by definition be
* running this script with a keyboard grab already active, namely
* the one-key grab that the WM (or whatever) uses to detect
* presses of the hotkey. So at the very least we have to give the
* user time to release that key.
* Now that we've got the keyboard grab, connect up our keyboard
* handlers.
*/
const useconds_t ms_limit = 5*1000000; /* try for 5 seconds */
const useconds_t ms_step = 1000000/8; /* at 1/8 second intervals */
useconds_t ms;
#if GTK_CHECK_VERSION(2,0,0)
g_signal_connect(G_OBJECT(ctx->imc), "commit",
G_CALLBACK(input_method_commit_event), ctx);
#endif
g_signal_connect(G_OBJECT(ctx->dialog), "key_press_event",
G_CALLBACK(key_event), ctx);
g_signal_connect(G_OBJECT(ctx->dialog), "key_release_event",
G_CALLBACK(key_event), ctx);
#if GTK_CHECK_VERSION(2,0,0)
gtk_im_context_set_client_window(ctx->imc,
gtk_widget_get_window(ctx->dialog));
#endif
for (ms = 0; ms < ms_limit; ms += ms_step) {
if (fn(ctx))
return TRUE;
usleep(ms_step);
/*
* And repaint the key-acknowledgment drawing areas as not greyed
* out.
*/
ctx->active_area = rand() % N_DRAWING_AREAS;
for (i = 0; i < N_DRAWING_AREAS; i++) {
ctx->drawingareas[i].state =
(i == ctx->active_area ? CURRENT : NOT_CURRENT);
gtk_widget_queue_draw(ctx->drawingareas[i].area);
}
return FALSE;
fail:
/*
* If we didn't get the grab, reschedule ourself on a timer to try
* again later.
*
* We have to do this rather than just trying once, because there
* is at least one important situation in which the grab may fail
* the first time: any user who is launching an add-key operation
* off some kind of window manager hotkey will almost by
* definition be running this script with a keyboard grab already
* active, namely the one-key grab that the WM (or whatever) uses
* to detect presses of the hotkey. So at the very least we have
* to give the user time to release that key.
*/
if (++ctx->nattempts >= 4) {
smemclr(ctx->passphrase, ctx->passsize);
ctx->passphrase = NULL;
ctx->error_message = dupstr("unable to grab keyboard after 5 seconds");
gtk_main_quit();
} else {
g_timeout_add(1000/8, try_grab_keyboard, ctx);
}
return FALSE;
}
void realize(GtkWidget *widget, gpointer vctx)
{
struct askpass_ctx *ctx = (struct askpass_ctx *)vctx;
gtk_grab_add(ctx->dialog);
/*
* Schedule the first attempt at the keyboard grab.
*/
ctx->nattempts = 0;
#if GTK_CHECK_VERSION(3,20,0)
ctx->seat = NULL;
#elif GTK_CHECK_VERSION(3,0,0)
ctx->keyboard = NULL;
#endif
g_idle_add(try_grab_keyboard, ctx);
}
static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
const char *window_title,
const char *prompt_text)
@ -338,6 +404,7 @@ static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
ctx->colmap = gdk_colormap_get_system();
ctx->cols[0].red = ctx->cols[0].green = ctx->cols[0].blue = 0xFFFF;
ctx->cols[1].red = ctx->cols[1].green = ctx->cols[1].blue = 0;
ctx->cols[2].red = ctx->cols[2].green = ctx->cols[2].blue = 0x8000;
gdk_colormap_alloc_colors(ctx->colmap, ctx->cols, 2,
FALSE, TRUE, success);
if (!success[0] | !success[1])
@ -352,7 +419,7 @@ static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
#ifndef DRAW_DEFAULT_CAIRO
ctx->drawingareas[i].cols = ctx->cols;
#endif
ctx->drawingareas[i].current = 0;
ctx->drawingareas[i].state = GREYED_OUT;
ctx->drawingareas[i].width = ctx->drawingareas[i].height = 0;
/* It would be nice to choose this size in some more
* context-sensitive way, like measuring the size of some
@ -383,8 +450,7 @@ static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
gtk_widget_show(ctx->drawingareas[i].area);
}
ctx->active_area = rand() % N_DRAWING_AREAS;
ctx->drawingareas[ctx->active_area].current = 1;
ctx->active_area = -1;
/*
* Arrange to receive key events. We don't really need to worry
@ -393,40 +459,23 @@ static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
* the prompt label at random, and we'll use gtk_grab_add to
* ensure key events go to it.
*/
gtk_widget_set_sensitive(ctx->promptlabel, TRUE);
gtk_widget_set_sensitive(ctx->dialog, TRUE);
#if GTK_CHECK_VERSION(2,0,0)
gtk_window_set_keep_above(GTK_WINDOW(ctx->dialog), TRUE);
#endif
/*
* Actually show the window, and wait for it to be shown.
* Wait for the key-receiving widget to actually be created, in
* order to call gtk_grab_add on it.
*/
gtk_widget_show_now(ctx->dialog);
g_signal_connect(G_OBJECT(ctx->dialog), "realize",
G_CALLBACK(realize), ctx);
/*
* Now that the window is displayed, make it grab the input focus.
* Show the window.
*/
gtk_grab_add(ctx->promptlabel);
if (!repeatedly_try_grab(ctx, try_grab_keyboard))
return "unable to grab keyboard";
/*
* And now that we've got the keyboard grab, connect up our
* keyboard handlers.
*/
#if GTK_CHECK_VERSION(2,0,0)
g_signal_connect(G_OBJECT(ctx->imc), "commit",
G_CALLBACK(input_method_commit_event), ctx);
#endif
g_signal_connect(G_OBJECT(ctx->promptlabel), "key_press_event",
G_CALLBACK(key_event), ctx);
g_signal_connect(G_OBJECT(ctx->promptlabel), "key_release_event",
G_CALLBACK(key_event), ctx);
#if GTK_CHECK_VERSION(2,0,0)
gtk_im_context_set_client_window(ctx->imc,
gtk_widget_get_window(ctx->dialog));
#endif
gtk_widget_show(ctx->dialog);
return NULL;
}
@ -434,9 +483,11 @@ static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
static void gtk_askpass_cleanup(struct askpass_ctx *ctx)
{
#if GTK_CHECK_VERSION(3,20,0)
gdk_seat_ungrab(ctx->seat);
if (ctx->seat)
gdk_seat_ungrab(ctx->seat);
#elif GTK_CHECK_VERSION(3,0,0)
gdk_device_ungrab(ctx->keyboard, GDK_CURRENT_TIME);
if (ctx->keyboard)
gdk_device_ungrab(ctx->keyboard, GDK_CURRENT_TIME);
#else
gdk_keyboard_ungrab(GDK_CURRENT_TIME);
#endif
@ -481,6 +532,9 @@ char *gtk_askpass_main(const char *display, const char *wintitle,
struct askpass_ctx actx, *ctx = &actx;
const char *err;
ctx->passphrase = NULL;
ctx->error_message = NULL;
/* In case gtk_init hasn't been called yet by the program */
if (!setup_gtk(display)) {
*success = FALSE;
@ -499,7 +553,7 @@ char *gtk_askpass_main(const char *display, const char *wintitle,
return ctx->passphrase;
} else {
*success = FALSE;
return dupstr("passphrase input cancelled");
return ctx->error_message;
}
}