isprivate() reads the GTK input purpose and nothing else, but the purpose is not the only way an application marks a password field, and it is not the older one. GTK 3.24.52, measured: gtk_entry_set_visibility(entry, FALSE) purpose stays FREE_FORM gtk_entry_set_input_purpose(PASSWORD) purpose PASSWORD so an entry hidden the first way looks like ordinary text to the module. This machine runs one. /usr/libexec/xfce-polkit, the XFCE authentication dialog, is up right now under GTK_IM_MODULE=strans, and its binary calls gtk_entry_set_visibility and never gtk_entry_set_input_purpose, so every password typed into it goes through the engine. Against the real daemon, in Korean mode: typed hunter2 stored ㅗㅕ숟ㄱ2 typed correcthorse stored 책ㄱㄷㅊ쇅ㄴㄷ typed P4ssw0rd stored ㅔ4ㄴㄵ0ㄱㅇ The field draws bullets, so nothing on screen says why the authentication failed -- except the pending syllable, which is drawn as itself: three keys into such an entry the old module leaves 한 on screen where the field should read ●●●. With this it reads ●●●, and the entry holds gks. set_visibility sends the input context no signal, so the question cannot be answered where the purpose is, in init and notify::input-purpose. It has to be asked at the key, of the client window, which is the entry's own: gdk_window_get_user_data on it returns the GtkEntry. fcitx5-gtk asks it the same way in all three of its GTK versions -- gtk3/fcitximcontext .cpp:1155-1162, under the comment "seems visibility != PASSWORD hint". It costs one field read and one type check per key, on a path that then does a socket round trip anyway. It does not cover XIM: that protocol has no attribute for this, and no fcitx5 frontend answers it either -- only its GTK and Qt client modules do -- so an X client reached over XIM still composes in its password field. Nor does it notice a "show password" box switched off in the middle of a composition: there is no signal for visibility, so the pending text stays in the daemon until the next focus change. Noticing that needs a signal connection on a widget the module does not own, which is a bigger thing than the hole is. The test is a live one and it earns its line: with the fix removed it says gtk_live_test: hidden-entry key reached daemon or did not commit Both guards were checked by removing them. Without GTK_IS_ENTRY the run takes a Gtk-CRITICAL and fails; without the im->win test it exits 139, on the stalled-peer context, which never gets a client window. An ishidden that always answers yes fails earlier still, at the initial protocol frames. Breaking the test's own helper so it hands back the toplevel's window instead of the entry's fails too, which is what says the assertion watches the right window rather than merely counting no events. That count is the one thing that had to be made deterministic. A new client window invalidates the caret, and the fake daemon records that frame on its own thread, so sampling the event count straight after set_client_window raced it: 2 failures in 8 runs. The block now waits for that frame and names it, and there were 0 in 20 after. 90 unit, check-live, check-stress and valgrind clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
943 lines
24 KiB
C
943 lines
24 KiB
C
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <gtk/gtk.h>
|
|
#include <poll.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/un.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include "ipc.h"
|
|
#include "live.h"
|
|
|
|
enum
|
|
{
|
|
Maxevent = 256,
|
|
Timeout = 4000,
|
|
};
|
|
|
|
enum
|
|
{
|
|
Ecap,
|
|
Ecaret,
|
|
Ekey,
|
|
Ereset,
|
|
};
|
|
|
|
enum
|
|
{
|
|
Rnormal,
|
|
Rcommitpre,
|
|
Rbadcommit,
|
|
Rbadpreedit,
|
|
};
|
|
|
|
typedef struct Event Event;
|
|
typedef struct Server Server;
|
|
typedef struct Siglog Siglog;
|
|
|
|
struct Event
|
|
{
|
|
int type;
|
|
int want;
|
|
int valid;
|
|
uint32_t key;
|
|
int32_t x;
|
|
int32_t y;
|
|
int32_t h;
|
|
};
|
|
|
|
struct Server
|
|
{
|
|
pthread_t thread;
|
|
pthread_mutex_t lock;
|
|
pthread_cond_t changed;
|
|
int listenfd;
|
|
int wake[2];
|
|
int stop;
|
|
int old;
|
|
int eaten;
|
|
int stall;
|
|
int response;
|
|
int nclose;
|
|
char pre[Ipcfieldmax+1];
|
|
Event ev[Maxevent];
|
|
int nev;
|
|
};
|
|
|
|
struct Siglog
|
|
{
|
|
char event[128];
|
|
int n;
|
|
char pre[Ipcfieldmax+1];
|
|
char shown[Ipcfieldmax+1];
|
|
char commit[Ipcfieldmax+1];
|
|
int prechanged;
|
|
};
|
|
|
|
static void
|
|
record(Server *s, Event *e)
|
|
{
|
|
pthread_mutex_lock(&s->lock);
|
|
if(s->nev < Maxevent)
|
|
s->ev[s->nev++] = *e;
|
|
pthread_cond_broadcast(&s->changed);
|
|
pthread_mutex_unlock(&s->lock);
|
|
}
|
|
|
|
static void
|
|
closed(Server *s)
|
|
{
|
|
pthread_mutex_lock(&s->lock);
|
|
s->nclose++;
|
|
pthread_cond_broadcast(&s->changed);
|
|
pthread_mutex_unlock(&s->lock);
|
|
}
|
|
|
|
static int
|
|
sendresponse(Server *s, int fd, int eaten, int want, int key)
|
|
{
|
|
unsigned char buf[Ipcmaxresp];
|
|
const char *commit;
|
|
char pre[Ipcfieldmax+1];
|
|
int n, response, stall;
|
|
size_t ncommit, npreedit;
|
|
|
|
pthread_mutex_lock(&s->lock);
|
|
memcpy(pre, s->pre, sizeof pre);
|
|
response = s->response;
|
|
stall = s->stall;
|
|
pthread_mutex_unlock(&s->lock);
|
|
if(stall)
|
|
return 1;
|
|
commit = "";
|
|
ncommit = 0;
|
|
npreedit = strlen(pre);
|
|
if(key && response == Rcommitpre){
|
|
commit = pre;
|
|
ncommit = npreedit;
|
|
}else if(key && response == Rbadcommit){
|
|
commit = "\xc3(";
|
|
ncommit = 2;
|
|
}else if(key && response == Rbadpreedit){
|
|
pre[0] = 'x';
|
|
pre[1] = 0;
|
|
pre[2] = 'y';
|
|
npreedit = 3;
|
|
}
|
|
n = ipcpackresp(buf, sizeof buf, eaten, commit, ncommit,
|
|
pre, npreedit, want);
|
|
return n >= 0 && ipcsend(fd, buf, n) == 0;
|
|
}
|
|
|
|
static int
|
|
request(Server *s, int fd)
|
|
{
|
|
unsigned char buf[Ipccaretsz];
|
|
uint32_t mod, key;
|
|
Event e;
|
|
int old, type, want;
|
|
|
|
if(ipcreadn(fd, buf, Ipcreqsz) < 0)
|
|
return 0;
|
|
type = ipcreqtype(buf);
|
|
memset(&e, 0, sizeof e);
|
|
switch(type){
|
|
case Ipccap:
|
|
e.type = Ecap;
|
|
ipcunpackreq(buf, &e.want, &mod, &key);
|
|
record(s, &e);
|
|
pthread_mutex_lock(&s->lock);
|
|
old = s->old;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return sendresponse(s, fd, !old, e.want, 0);
|
|
case Ipccaret:
|
|
if(ipcreadn(fd, buf + Ipcreqsz, Ipccaretsz - Ipcreqsz) < 0 ||
|
|
ipcunpackcaret(buf, &e.valid, &e.x, &e.y, &e.h) < 0)
|
|
return 0;
|
|
e.type = Ecaret;
|
|
record(s, &e);
|
|
return 1;
|
|
case Ipckey:
|
|
ipcunpackreq(buf, &want, &mod, &key);
|
|
e.want = want;
|
|
e.key = key;
|
|
e.type = ipcreqreset(buf) ? Ereset : Ekey;
|
|
record(s, &e);
|
|
pthread_mutex_lock(&s->lock);
|
|
old = s->eaten;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return sendresponse(s, fd, e.type == Ekey && old, want,
|
|
e.type == Ekey);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void*
|
|
serverthread(void *v)
|
|
{
|
|
Server *s;
|
|
struct pollfd pfd[3];
|
|
int fd, n;
|
|
char c;
|
|
|
|
s = v;
|
|
fd = -1;
|
|
for(;;){
|
|
pfd[0].fd = s->wake[0];
|
|
pfd[0].events = POLLIN;
|
|
pfd[1].fd = s->listenfd;
|
|
pfd[1].events = fd < 0 ? POLLIN : 0;
|
|
pfd[2].fd = fd;
|
|
pfd[2].events = fd >= 0 ? POLLIN : 0;
|
|
n = poll(pfd, 3, -1);
|
|
if(n < 0 && errno == EINTR)
|
|
continue;
|
|
if(n < 0 || (pfd[0].revents & POLLIN))
|
|
break;
|
|
if(pfd[1].revents & POLLIN){
|
|
fd = accept(s->listenfd, NULL, NULL);
|
|
continue;
|
|
}
|
|
if(fd >= 0 && pfd[2].revents != 0){
|
|
if((pfd[2].revents & POLLIN) && request(s, fd))
|
|
continue;
|
|
close(fd);
|
|
fd = -1;
|
|
closed(s);
|
|
}
|
|
}
|
|
if(fd >= 0){
|
|
close(fd);
|
|
closed(s);
|
|
}
|
|
while(read(s->wake[0], &c, 1) < 0 && errno == EINTR)
|
|
;
|
|
return NULL;
|
|
}
|
|
|
|
static int
|
|
startserver(Server *s, char *path)
|
|
{
|
|
struct sockaddr_un addr;
|
|
int cond;
|
|
|
|
memset(s, 0, sizeof *s);
|
|
s->eaten = 1;
|
|
s->listenfd = -1;
|
|
s->wake[0] = s->wake[1] = -1;
|
|
if(pthread_mutex_init(&s->lock, NULL) != 0)
|
|
return fail("initialize server lock");
|
|
cond = pthread_cond_init(&s->changed, NULL) == 0;
|
|
if(!cond){
|
|
fail("initialize server condition");
|
|
goto bad;
|
|
}
|
|
if(pipe(s->wake) < 0){
|
|
fail("pipe: %s", strerror(errno));
|
|
goto bad;
|
|
}
|
|
s->listenfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if(s->listenfd < 0){
|
|
fail("socket: %s", strerror(errno));
|
|
goto bad;
|
|
}
|
|
memset(&addr, 0, sizeof addr);
|
|
addr.sun_family = AF_UNIX;
|
|
if(strlen(path) >= sizeof addr.sun_path){
|
|
fail("socket path is too long");
|
|
goto bad;
|
|
}
|
|
strcpy(addr.sun_path, path);
|
|
if(bind(s->listenfd, (struct sockaddr*)&addr, sizeof addr) < 0 ||
|
|
listen(s->listenfd, 4) < 0){
|
|
fail("listen %s: %s", path, strerror(errno));
|
|
goto bad;
|
|
}
|
|
if(pthread_create(&s->thread, NULL, serverthread, s) != 0){
|
|
fail("create server thread");
|
|
goto bad;
|
|
}
|
|
return 1;
|
|
bad:
|
|
if(s->listenfd >= 0)
|
|
close(s->listenfd);
|
|
if(s->wake[0] >= 0)
|
|
close(s->wake[0]);
|
|
if(s->wake[1] >= 0)
|
|
close(s->wake[1]);
|
|
if(cond)
|
|
pthread_cond_destroy(&s->changed);
|
|
pthread_mutex_destroy(&s->lock);
|
|
s->listenfd = s->wake[0] = s->wake[1] = -1;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
stopserver(Server *s)
|
|
{
|
|
char c;
|
|
|
|
if(s->wake[1] >= 0){
|
|
c = 0;
|
|
while(write(s->wake[1], &c, 1) < 0 && errno == EINTR)
|
|
;
|
|
pthread_join(s->thread, NULL);
|
|
}
|
|
if(s->listenfd >= 0)
|
|
close(s->listenfd);
|
|
if(s->wake[0] >= 0)
|
|
close(s->wake[0]);
|
|
if(s->wake[1] >= 0)
|
|
close(s->wake[1]);
|
|
pthread_cond_destroy(&s->changed);
|
|
pthread_mutex_destroy(&s->lock);
|
|
s->listenfd = s->wake[0] = s->wake[1] = -1;
|
|
}
|
|
|
|
static void
|
|
setpre(Server *s, char *pre)
|
|
{
|
|
pthread_mutex_lock(&s->lock);
|
|
snprintf(s->pre, sizeof s->pre, "%s", pre);
|
|
pthread_mutex_unlock(&s->lock);
|
|
}
|
|
|
|
static void
|
|
setold(Server *s, int old)
|
|
{
|
|
pthread_mutex_lock(&s->lock);
|
|
s->old = old;
|
|
pthread_mutex_unlock(&s->lock);
|
|
}
|
|
|
|
static void
|
|
setreply(Server *s, int eaten, int response, int stall)
|
|
{
|
|
pthread_mutex_lock(&s->lock);
|
|
s->eaten = eaten;
|
|
s->response = response;
|
|
s->stall = stall;
|
|
pthread_mutex_unlock(&s->lock);
|
|
}
|
|
|
|
static int
|
|
eventcount(Server *s)
|
|
{
|
|
int n;
|
|
|
|
pthread_mutex_lock(&s->lock);
|
|
n = s->nev;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return n;
|
|
}
|
|
|
|
static int
|
|
closecount(Server *s)
|
|
{
|
|
int n;
|
|
|
|
pthread_mutex_lock(&s->lock);
|
|
n = s->nclose;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return n;
|
|
}
|
|
|
|
static int
|
|
waitcount(Server *s, int n)
|
|
{
|
|
struct timespec ts;
|
|
int r;
|
|
|
|
clock_gettime(CLOCK_REALTIME, &ts);
|
|
ts.tv_sec += Timeout / 1000;
|
|
ts.tv_nsec += (Timeout % 1000) * 1000000;
|
|
if(ts.tv_nsec >= 1000000000){
|
|
ts.tv_sec++;
|
|
ts.tv_nsec -= 1000000000;
|
|
}
|
|
pthread_mutex_lock(&s->lock);
|
|
while(s->nev < n){
|
|
r = pthread_cond_timedwait(&s->changed, &s->lock, &ts);
|
|
if(r == ETIMEDOUT)
|
|
break;
|
|
}
|
|
r = s->nev >= n;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return r;
|
|
}
|
|
|
|
static int
|
|
waitclose(Server *s, int n)
|
|
{
|
|
struct timespec ts;
|
|
int r;
|
|
|
|
clock_gettime(CLOCK_REALTIME, &ts);
|
|
ts.tv_sec += Timeout / 1000;
|
|
pthread_mutex_lock(&s->lock);
|
|
while(s->nclose < n){
|
|
r = pthread_cond_timedwait(&s->changed, &s->lock, &ts);
|
|
if(r == ETIMEDOUT)
|
|
break;
|
|
}
|
|
r = s->nclose >= n;
|
|
pthread_mutex_unlock(&s->lock);
|
|
return r;
|
|
}
|
|
|
|
static Event
|
|
getevent(Server *s, int n)
|
|
{
|
|
Event e;
|
|
|
|
memset(&e, 0, sizeof e);
|
|
pthread_mutex_lock(&s->lock);
|
|
if(n >= 0 && n < s->nev)
|
|
e = s->ev[n];
|
|
pthread_mutex_unlock(&s->lock);
|
|
return e;
|
|
}
|
|
|
|
static int
|
|
runquery(char *module, char *cache)
|
|
{
|
|
int fd, status;
|
|
pid_t pid;
|
|
|
|
fd = open(cache, O_WRONLY|O_CREAT|O_TRUNC, 0600);
|
|
if(fd < 0)
|
|
return fail("open module cache: %s", strerror(errno));
|
|
pid = fork();
|
|
if(pid < 0){
|
|
close(fd);
|
|
return fail("fork gtk-query-immodules: %s", strerror(errno));
|
|
}
|
|
if(pid == 0){
|
|
dup2(fd, STDOUT_FILENO);
|
|
close(fd);
|
|
execlp("gtk-query-immodules-3.0", "gtk-query-immodules-3.0",
|
|
module, (char*)NULL);
|
|
_exit(127);
|
|
}
|
|
close(fd);
|
|
if(waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ||
|
|
WEXITSTATUS(status) != 0)
|
|
return fail("gtk-query-immodules-3.0 failed");
|
|
return 1;
|
|
}
|
|
|
|
|
|
static void
|
|
pump(void)
|
|
{
|
|
while(gtk_events_pending())
|
|
gtk_main_iteration();
|
|
}
|
|
|
|
static void
|
|
logsig(Siglog *l, char c)
|
|
{
|
|
if(l->n + 1 < (int)sizeof l->event){
|
|
l->event[l->n++] = c;
|
|
l->event[l->n] = '\0';
|
|
}
|
|
}
|
|
|
|
static void
|
|
prestart(GtkIMContext *ctx, Siglog *l)
|
|
{
|
|
(void)ctx;
|
|
logsig(l, 'S');
|
|
}
|
|
|
|
static void
|
|
prechange(GtkIMContext *ctx, Siglog *l)
|
|
{
|
|
gchar *s;
|
|
|
|
gtk_im_context_get_preedit_string(ctx, &s, NULL, NULL);
|
|
if(strcmp(l->pre, s) != 0)
|
|
l->prechanged = 1;
|
|
snprintf(l->pre, sizeof l->pre, "%s", s);
|
|
g_free(s);
|
|
logsig(l, 'C');
|
|
}
|
|
|
|
static void
|
|
preend(GtkIMContext *ctx, Siglog *l)
|
|
{
|
|
(void)ctx;
|
|
logsig(l, 'E');
|
|
}
|
|
|
|
static void
|
|
commit(GtkIMContext *ctx, gchar *s, Siglog *l)
|
|
{
|
|
(void)ctx;
|
|
snprintf(l->commit, sizeof l->commit, "%s", s);
|
|
logsig(l, 'K');
|
|
}
|
|
|
|
static void
|
|
clearevent(Siglog *l)
|
|
{
|
|
l->event[0] = '\0';
|
|
l->n = 0;
|
|
l->commit[0] = '\0';
|
|
l->prechanged = 0;
|
|
}
|
|
|
|
static void
|
|
applyclient(Siglog *l)
|
|
{
|
|
if(l->commit[0] != '\0')
|
|
l->shown[0] = '\0';
|
|
if(l->prechanged)
|
|
snprintf(l->shown, sizeof l->shown, "%s", l->pre);
|
|
}
|
|
|
|
static void
|
|
clearlog(Siglog *l)
|
|
{
|
|
memset(l, 0, sizeof *l);
|
|
}
|
|
|
|
static GtkIMContext*
|
|
newcontext(Siglog *l)
|
|
{
|
|
GtkIMContext *ctx;
|
|
|
|
ctx = gtk_im_multicontext_new();
|
|
gtk_im_multicontext_set_context_id(GTK_IM_MULTICONTEXT(ctx), "strans");
|
|
g_signal_connect(ctx, "preedit-start", G_CALLBACK(prestart), l);
|
|
g_signal_connect(ctx, "preedit-changed", G_CALLBACK(prechange), l);
|
|
g_signal_connect(ctx, "preedit-end", G_CALLBACK(preend), l);
|
|
g_signal_connect(ctx, "commit", G_CALLBACK(commit), l);
|
|
return ctx;
|
|
}
|
|
|
|
static int
|
|
keyevent(GtkIMContext *ctx, GdkWindow *win, GdkEventType type,
|
|
guint keyval, guint state)
|
|
{
|
|
GdkEvent *ev;
|
|
int r;
|
|
|
|
ev = gdk_event_new(type);
|
|
ev->key.window = g_object_ref(win);
|
|
ev->key.send_event = TRUE;
|
|
ev->key.time = GDK_CURRENT_TIME;
|
|
ev->key.keyval = keyval;
|
|
ev->key.state = state;
|
|
r = gtk_im_context_filter_keypress(ctx, &ev->key);
|
|
gdk_event_free(ev);
|
|
return r;
|
|
}
|
|
|
|
static int
|
|
keystate(GtkIMContext *ctx, GdkWindow *win, guint keyval, guint state)
|
|
{
|
|
return keyevent(ctx, win, GDK_KEY_PRESS, keyval, state);
|
|
}
|
|
|
|
static int
|
|
key(GtkIMContext *ctx, GdkWindow *win, guint keyval)
|
|
{
|
|
return keystate(ctx, win, keyval, 0);
|
|
}
|
|
|
|
static GtkWidget*
|
|
newwindow(int x, int y)
|
|
{
|
|
GtkWidget *w;
|
|
|
|
w = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
|
gtk_window_set_default_size(GTK_WINDOW(w), 240, 120);
|
|
gtk_window_move(GTK_WINDOW(w), x, y);
|
|
gtk_widget_show(w);
|
|
pump();
|
|
return w;
|
|
}
|
|
|
|
/* A GtkEntry gives its context a window of its own, under the toplevel's. */
|
|
static GdkWindow*
|
|
entrywindow(GtkWidget *entry)
|
|
{
|
|
GdkWindow *w;
|
|
GList *kids, *l;
|
|
gpointer u;
|
|
|
|
w = NULL;
|
|
kids = gdk_window_get_children(gtk_widget_get_window(entry));
|
|
for(l = kids; l != NULL; l = l->next){
|
|
gdk_window_get_user_data(l->data, &u);
|
|
if(u == entry)
|
|
w = l->data;
|
|
}
|
|
g_list_free(kids);
|
|
return w;
|
|
}
|
|
|
|
#define Check(c, ...) do{ if(!(c)){ fail(__VA_ARGS__); goto out; } }while(0)
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
Server srv;
|
|
Siglog log;
|
|
GtkIMContext *ctx, *oldctx, *badctx;
|
|
GtkWidget *top, *entry;
|
|
GdkWindow *win, *entrywin;
|
|
GdkRectangle rect;
|
|
Event e;
|
|
static const GtkInputPurpose purposes[] = {
|
|
GTK_INPUT_PURPOSE_PASSWORD,
|
|
GTK_INPUT_PURPOSE_PIN,
|
|
};
|
|
static const int badresponses[] = { Rbadcommit, Rbadpreedit };
|
|
char root[] = "/tmp/strans-gtk.XXXXXX";
|
|
char runtime[320] = "", socket[384] = "", cache[384] = "";
|
|
char *module;
|
|
Daemon xvfb;
|
|
Live l;
|
|
int first, i, ox, oy, scale, closes, ok, rootmade, serverup;
|
|
int64_t stalledms, started;
|
|
|
|
testname = "gtk_live_test";
|
|
ctx = oldctx = badctx = NULL;
|
|
top = NULL;
|
|
memset(&l, 0, sizeof l);
|
|
daemoninit(&xvfb, "Xvfb");
|
|
rootmade = 0;
|
|
serverup = 0;
|
|
ok = 0;
|
|
if(argc != 2){
|
|
fail("usage: gtk_live_test /path/to/im-strans.so");
|
|
return 1;
|
|
}
|
|
module = realpath(argv[1], NULL);
|
|
if(module == NULL){
|
|
fail("realpath %s: %s", argv[1], strerror(errno));
|
|
return 1;
|
|
}
|
|
Check(mkdtemp(root) != NULL, "mkdtemp: %s", strerror(errno));
|
|
rootmade = 1;
|
|
Check(snprintf(runtime, sizeof runtime, "%s/runtime", root) <
|
|
(int)sizeof runtime, "runtime path is too long");
|
|
Check(snprintf(socket, sizeof socket, "%s/strans.sock", runtime) <
|
|
(int)sizeof socket, "socket path is too long");
|
|
Check(snprintf(cache, sizeof cache, "%s/immodules.cache", root) <
|
|
(int)sizeof cache, "cache path is too long");
|
|
Check(mkdir(runtime, 0700) == 0, "mkdir runtime: %s", strerror(errno));
|
|
Check(startxvfb(&l, &xvfb, "Xvfb"), "start private Xvfb");
|
|
Check(setenv("DISPLAY", l.display, 1) == 0 &&
|
|
setenv("GDK_BACKEND", "x11", 1) == 0 &&
|
|
setenv("GDK_SCALE", "2", 1) == 0 &&
|
|
setenv("XDG_RUNTIME_DIR", runtime, 1) == 0,
|
|
"set private environment: %s", strerror(errno));
|
|
Check(runquery(module, cache), "generate GTK module cache");
|
|
Check(setenv("GTK_IM_MODULE_FILE", cache, 1) == 0 &&
|
|
setenv("GTK_IM_MODULE", "strans", 1) == 0,
|
|
"set GTK module environment: %s", strerror(errno));
|
|
Check(startserver(&srv, socket), "start fake daemon");
|
|
serverup = 1;
|
|
gtk_init(&argc, &argv);
|
|
|
|
clearlog(&log);
|
|
ctx = newcontext(&log);
|
|
top = newwindow(73, 41);
|
|
win = gtk_widget_get_window(top);
|
|
gtk_im_context_set_client_window(ctx, win);
|
|
rect.x = 11;
|
|
rect.y = 17;
|
|
rect.width = 3;
|
|
rect.height = 19;
|
|
gtk_im_context_set_cursor_location(ctx, &rect);
|
|
Check(eventcount(&srv) == 0, "cursor reporting opened the socket");
|
|
setpre(&srv, "");
|
|
Check(key(ctx, win, GDK_KEY_a), "initial key was not eaten");
|
|
Check(waitcount(&srv, 3), "initial protocol frames timed out");
|
|
e = getevent(&srv, 0);
|
|
Check(e.type == Ecap && e.want == 1, "default preedit capability missing");
|
|
e = getevent(&srv, 1);
|
|
Check(e.type == Ecaret && e.valid, "cached caret was not negotiated");
|
|
gdk_window_get_origin(win, &ox, &oy);
|
|
Check(ox != 0 && oy != 0, "test window did not get a nonzero root origin");
|
|
scale = gdk_window_get_scale_factor(win);
|
|
Check(scale == 2, "private Xvfb did not honor GDK_SCALE=2");
|
|
Check(e.x == (ox + rect.x) * scale &&
|
|
e.y == (oy + rect.y) * scale && e.h == rect.height * scale,
|
|
"caret conversion got %d,%d,%d want %d,%d,%d",
|
|
e.x, e.y, e.h, (ox + rect.x) * scale,
|
|
(oy + rect.y) * scale, rect.height * scale);
|
|
e = getevent(&srv, 2);
|
|
Check(e.type == Ekey && e.want == 1, "initial key framing changed");
|
|
Check(log.n == 0, "initial empty preedit emitted %s", log.event);
|
|
setpre(&srv, "ㅋ");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_z), "first preedit key was not eaten");
|
|
applyclient(&log);
|
|
Check(strcmp(log.event, "SC") == 0 && strcmp(log.pre, "ㅋ") == 0 &&
|
|
strcmp(log.shown, "ㅋ") == 0,
|
|
"first preedit signals were %s (preedit %s, shown %s)",
|
|
log.event, log.pre, log.shown);
|
|
clearevent(&log);
|
|
setreply(&srv, 1, Rcommitpre, 0);
|
|
Check(key(ctx, win, GDK_KEY_z), "repeated preedit key was not eaten");
|
|
applyclient(&log);
|
|
Check(strcmp(log.event, "CEKSC") == 0 &&
|
|
strcmp(log.commit, "ㅋ") == 0 && strcmp(log.pre, "ㅋ") == 0 &&
|
|
strcmp(log.shown, "ㅋ") == 0,
|
|
"repeated preedit signals were %s (commit %s, preedit %s, shown %s)",
|
|
log.event, log.commit, log.pre, log.shown);
|
|
Check(g_utf8_strlen(log.commit, -1) + g_utf8_strlen(log.shown, -1) == 2,
|
|
"two physical keys produced commit %s and shown preedit %s",
|
|
log.commit, log.shown);
|
|
|
|
/* Unconsumed input uses GtkIMContextSimple, including its stateful forms. */
|
|
setpre(&srv, "");
|
|
setreply(&srv, 0, Rnormal, 0);
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_Multi_key) &&
|
|
key(ctx, win, GDK_KEY_apostrophe) && key(ctx, win, GDK_KEY_e),
|
|
"Compose sequence was not consumed");
|
|
Check(strcmp(log.commit, "é") == 0, "Compose committed %s", log.commit);
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e),
|
|
"dead-key sequence was not consumed");
|
|
Check(strcmp(log.commit, "é") == 0, "dead key committed %s", log.commit);
|
|
|
|
/* A dead key over a preedit hands the pending text back first. */
|
|
setreply(&srv, 1, Rnormal, 0);
|
|
setpre(&srv, "하");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_h), "dead-key setup key was not eaten");
|
|
clearevent(&log);
|
|
first = eventcount(&srv);
|
|
Check(!key(ctx, win, GDK_KEY_Shift_L), "a modifier press was eaten");
|
|
Check(eventcount(&srv) == first && strcmp(log.pre, "하") == 0,
|
|
"a modifier press disturbed the preedit");
|
|
setreply(&srv, 0, Rnormal, 0);
|
|
setpre(&srv, "");
|
|
Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e),
|
|
"dead key over a preedit was not consumed");
|
|
Check(getevent(&srv, first).type == Ereset,
|
|
"dead key did not hand the pending text back");
|
|
Check(strcmp(log.commit, "é") == 0 && log.pre[0] == '\0',
|
|
"dead key over a preedit committed %s (preedit %s)",
|
|
log.commit, log.pre);
|
|
clearlog(&log);
|
|
Check(keystate(ctx, win, GDK_KEY_U, GDK_CONTROL_MASK|GDK_SHIFT_MASK),
|
|
"Unicode prefix was not consumed");
|
|
(void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_U,
|
|
GDK_CONTROL_MASK|GDK_SHIFT_MASK);
|
|
(void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_Shift_L,
|
|
GDK_CONTROL_MASK);
|
|
(void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_Control_L, 0);
|
|
Check(key(ctx, win, GDK_KEY_0), "Unicode digit 1 was not consumed");
|
|
Check(key(ctx, win, GDK_KEY_0), "Unicode digit 2 was not consumed");
|
|
Check(key(ctx, win, GDK_KEY_e), "Unicode digit 3 was not consumed");
|
|
Check(key(ctx, win, GDK_KEY_9), "Unicode digit 4 was not consumed");
|
|
(void)key(ctx, win, GDK_KEY_Return);
|
|
Check(strcmp(log.commit, "é") == 0, "Unicode entry committed %s", log.commit);
|
|
setreply(&srv, 1, Rnormal, 0);
|
|
|
|
/* PASSWORD and PIN transitions reset an existing composition. */
|
|
rect.height = 12;
|
|
gtk_im_context_set_cursor_location(ctx, &rect);
|
|
for(i = 0; i < (int)(sizeof purposes / sizeof purposes[0]); i++){
|
|
g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL);
|
|
setpre(&srv, i == 0 ? "password" : "pin");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_c + i), "private setup key was not eaten");
|
|
clearlog(&log);
|
|
first = eventcount(&srv);
|
|
g_object_set(ctx, "input-purpose", purposes[i], NULL);
|
|
Check(waitcount(&srv, first + 1), "private reset timed out");
|
|
Check(getevent(&srv, first).type == Ereset &&
|
|
strcmp(log.event, "CE") == 0,
|
|
"private transition did not reset and clear");
|
|
clearlog(&log);
|
|
first = eventcount(&srv);
|
|
Check(key(ctx, win, GDK_KEY_x), "private fallback did not commit");
|
|
Check(eventcount(&srv) == first && strcmp(log.commit, "x") == 0,
|
|
"private key reached daemon or did not commit");
|
|
}
|
|
|
|
g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL);
|
|
|
|
/* An entry hidden the older way is private too, purpose or no purpose. */
|
|
entry = gtk_entry_new();
|
|
gtk_container_add(GTK_CONTAINER(top), entry);
|
|
gtk_widget_show(entry);
|
|
pump();
|
|
entrywin = entrywindow(entry);
|
|
Check(entrywin != NULL, "the test GtkEntry has no window of its own");
|
|
first = eventcount(&srv);
|
|
gtk_im_context_set_client_window(ctx, entrywin);
|
|
/* The caret the new window invalidates must land before keys are counted. */
|
|
Check(waitcount(&srv, first + 1) && getevent(&srv, first).type == Ecaret,
|
|
"the entry window did not renegotiate the caret");
|
|
setpre(&srv, "");
|
|
gtk_entry_set_visibility(GTK_ENTRY(entry), FALSE);
|
|
clearlog(&log);
|
|
first = eventcount(&srv);
|
|
Check(key(ctx, entrywin, GDK_KEY_y), "hidden-entry fallback did not commit");
|
|
Check(eventcount(&srv) == first && strcmp(log.commit, "y") == 0,
|
|
"hidden-entry key reached daemon or did not commit");
|
|
gtk_entry_set_visibility(GTK_ENTRY(entry), TRUE);
|
|
clearlog(&log);
|
|
first = eventcount(&srv);
|
|
Check(key(ctx, entrywin, GDK_KEY_y), "visible-entry key was not eaten");
|
|
Check(waitcount(&srv, first + 1) && log.commit[0] == '\0',
|
|
"visible-entry key did not reach the daemon");
|
|
gtk_im_context_set_client_window(ctx, win);
|
|
|
|
setpre(&srv, "reset");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_e), "reset setup key was not eaten");
|
|
clearlog(&log);
|
|
first = eventcount(&srv);
|
|
gtk_im_context_reset(ctx);
|
|
Check(waitcount(&srv, first + 1), "explicit reset timed out");
|
|
Check(getevent(&srv, first).type == Ereset && strcmp(log.event, "CE") == 0,
|
|
"explicit reset did not clear preedit");
|
|
setpre(&srv, "focus");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_f), "focus setup key was not eaten");
|
|
clearlog(&log);
|
|
first = eventcount(&srv);
|
|
closes = closecount(&srv);
|
|
gtk_im_context_focus_out(ctx);
|
|
Check(waitclose(&srv, closes + 1), "focus-out did not close connection");
|
|
Check(eventcount(&srv) == first + 1 &&
|
|
getevent(&srv, first).type == Ereset,
|
|
"focus-out did not hand the pending text back before closing");
|
|
Check(strcmp(log.event, "CE") == 0, "focus-out clear order was %s", log.event);
|
|
|
|
/* Dispose is repeatable, closes the connection, and emits no preedit. */
|
|
gtk_im_context_set_client_window(ctx, win);
|
|
setpre(&srv, "final");
|
|
clearlog(&log);
|
|
Check(key(ctx, win, GDK_KEY_g), "dispose setup key was not eaten");
|
|
clearlog(&log);
|
|
closes = closecount(&srv);
|
|
g_object_run_dispose(G_OBJECT(ctx));
|
|
g_object_run_dispose(G_OBJECT(ctx));
|
|
g_object_unref(ctx);
|
|
ctx = NULL;
|
|
Check(waitclose(&srv, closes + 1), "dispose did not close connection");
|
|
Check(log.n == 0, "dispose emitted %s", log.event);
|
|
|
|
/* Malformed commit and preedit fields close the protocol connection. */
|
|
for(i = 0; i < (int)(sizeof badresponses / sizeof badresponses[0]); i++){
|
|
setreply(&srv, 1, badresponses[i], 0);
|
|
clearlog(&log);
|
|
badctx = newcontext(&log);
|
|
gtk_im_context_set_client_window(badctx, win);
|
|
closes = closecount(&srv);
|
|
Check(key(badctx, win, GDK_KEY_m + i),
|
|
"malformed response did not fall back");
|
|
Check(waitclose(&srv, closes + 1),
|
|
"malformed response did not close connection");
|
|
Check(log.commit[0] == 'm' + i && log.commit[1] == '\0',
|
|
"malformed response fallback committed %s", log.commit);
|
|
g_object_unref(badctx);
|
|
badctx = NULL;
|
|
}
|
|
|
|
/* A peer which accepts a connection but never replies cannot freeze GTK. */
|
|
setreply(&srv, 1, Rnormal, 1);
|
|
clearlog(&log);
|
|
badctx = newcontext(&log);
|
|
closes = closecount(&srv);
|
|
started = nowms();
|
|
Check(key(badctx, win, GDK_KEY_t), "stalled peer did not fall back");
|
|
stalledms = nowms() - started;
|
|
Check(stalledms < 4*Ipcwaitms, "stalled peer blocked GTK");
|
|
Check(waitclose(&srv, closes + 1), "stalled connection was not closed");
|
|
Check(strcmp(log.commit, "t") == 0, "stalled fallback committed %s",
|
|
log.commit);
|
|
g_object_unref(badctx);
|
|
badctx = NULL;
|
|
setreply(&srv, 1, Rnormal, 0);
|
|
|
|
/* An old daemon still honors set_use_preedit(FALSE). */
|
|
setold(&srv, 1);
|
|
clearlog(&log);
|
|
oldctx = newcontext(&log);
|
|
gtk_im_context_set_client_window(oldctx, win);
|
|
rect.x = 2;
|
|
rect.y = 3;
|
|
rect.height = 10;
|
|
gtk_im_context_set_cursor_location(oldctx, &rect);
|
|
gtk_im_context_set_use_preedit(oldctx, FALSE);
|
|
setpre(&srv, "legacy");
|
|
first = eventcount(&srv);
|
|
Check(key(oldctx, win, GDK_KEY_h), "old-daemon key was not eaten");
|
|
Check(waitcount(&srv, first + 2), "old-daemon exchange timed out");
|
|
e = getevent(&srv, first);
|
|
Check(e.type == Ecap && e.want == 0, "old-daemon probe framing is wrong");
|
|
e = getevent(&srv, first + 1);
|
|
Check(e.type == Ekey && e.want == 0, "old daemon forced inline preedit");
|
|
for(i = first; i < eventcount(&srv); i++)
|
|
Check(getevent(&srv, i).type != Ecaret,
|
|
"caret extension sent to old daemon");
|
|
Check(log.n == 0 && log.pre[0] == '\0',
|
|
"old daemon emitted inline preedit %s (%s)", log.event, log.pre);
|
|
closes = closecount(&srv);
|
|
g_object_unref(oldctx);
|
|
oldctx = NULL;
|
|
Check(waitclose(&srv, closes + 1), "old-daemon context did not close");
|
|
|
|
/* A failed first connection uses GtkIMContextSimple. */
|
|
stopserver(&srv);
|
|
serverup = 0;
|
|
unlink(socket);
|
|
clearlog(&log);
|
|
badctx = newcontext(&log);
|
|
Check(key(badctx, win, GDK_KEY_z), "connection failure did not fall back");
|
|
Check(strcmp(log.commit, "z") == 0 && log.pre[0] == '\0',
|
|
"connection failure left stale preedit");
|
|
|
|
ok = 1;
|
|
out:
|
|
if(badctx != NULL)
|
|
g_object_unref(badctx);
|
|
if(oldctx != NULL)
|
|
g_object_unref(oldctx);
|
|
if(ctx != NULL)
|
|
g_object_unref(ctx);
|
|
if(top != NULL)
|
|
gtk_widget_destroy(top);
|
|
pump();
|
|
if(serverup)
|
|
stopserver(&srv);
|
|
if(socket[0] != '\0')
|
|
unlink(socket);
|
|
if(cache[0] != '\0')
|
|
unlink(cache);
|
|
if(runtime[0] != '\0')
|
|
rmdir(runtime);
|
|
if(rootmade)
|
|
rmdir(root);
|
|
stopdaemon(&xvfb);
|
|
closeerrors(&xvfb);
|
|
free(module);
|
|
if(ok)
|
|
printf("gtk_live_test: ok (scale %d, stalled peer %lld ms)\n",
|
|
scale, (long long)stalledms);
|
|
return !ok;
|
|
}
|