930 lines
22 KiB
C
930 lines
22 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 <stdarg.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"
|
|
|
|
enum
|
|
{
|
|
Maxevent = 256,
|
|
Timeout = 4000,
|
|
};
|
|
|
|
enum
|
|
{
|
|
Ecap,
|
|
Ecaret,
|
|
Ekey,
|
|
Ereset,
|
|
};
|
|
|
|
enum
|
|
{
|
|
Rnormal,
|
|
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 commit[Ipcfieldmax+1];
|
|
};
|
|
|
|
static int
|
|
fail(char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
fprintf(stderr, "gtk_live_test: ");
|
|
va_start(ap, fmt);
|
|
vfprintf(stderr, fmt, ap);
|
|
va_end(ap);
|
|
fputc('\n', stderr);
|
|
return 0;
|
|
}
|
|
|
|
static int64_t
|
|
nowms(void)
|
|
{
|
|
struct timespec ts;
|
|
|
|
if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0)
|
|
return 0;
|
|
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
|
}
|
|
|
|
static int
|
|
leftms(int64_t deadline)
|
|
{
|
|
int64_t n;
|
|
|
|
n = deadline - nowms();
|
|
if(n <= 0)
|
|
return 0;
|
|
if(n > 0x7fffffff)
|
|
return 0x7fffffff;
|
|
return n;
|
|
}
|
|
|
|
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 == 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 Ipclegacy:
|
|
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 int
|
|
startxvfb(pid_t *pidp, char *display, size_t ndisplay)
|
|
{
|
|
struct pollfd pfd;
|
|
char fdarg[32], line[32];
|
|
int p[2], n, status;
|
|
pid_t pid;
|
|
|
|
if(pipe(p) < 0)
|
|
return fail("Xvfb display pipe: %s", strerror(errno));
|
|
pid = fork();
|
|
if(pid < 0){
|
|
close(p[0]);
|
|
close(p[1]);
|
|
return fail("fork Xvfb: %s", strerror(errno));
|
|
}
|
|
if(pid == 0){
|
|
close(p[0]);
|
|
snprintf(fdarg, sizeof fdarg, "%d", p[1]);
|
|
execlp("Xvfb", "Xvfb", "-displayfd", fdarg, "-screen", "0",
|
|
"1024x768x24", "-nolisten", "tcp", "-noreset", (char*)NULL);
|
|
_exit(127);
|
|
}
|
|
close(p[1]);
|
|
pfd.fd = p[0];
|
|
pfd.events = POLLIN;
|
|
if(poll(&pfd, 1, Timeout) <= 0){
|
|
kill(pid, SIGTERM);
|
|
waitpid(pid, NULL, 0);
|
|
close(p[0]);
|
|
return fail("Xvfb did not publish a display");
|
|
}
|
|
n = read(p[0], line, sizeof line - 1);
|
|
close(p[0]);
|
|
if(n <= 0){
|
|
waitpid(pid, &status, 0);
|
|
return fail("read Xvfb display number");
|
|
}
|
|
line[n] = '\0';
|
|
line[strcspn(line, "\r\n")] = '\0';
|
|
if(snprintf(display, ndisplay, ":%s", line) >= (int)ndisplay){
|
|
kill(pid, SIGTERM);
|
|
waitpid(pid, NULL, 0);
|
|
return fail("Xvfb display number is too long");
|
|
}
|
|
*pidp = pid;
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
stopxvfb(pid_t *pidp)
|
|
{
|
|
int status;
|
|
int64_t deadline;
|
|
|
|
if(*pidp <= 0)
|
|
return;
|
|
kill(*pidp, SIGTERM);
|
|
deadline = nowms() + Timeout;
|
|
while(waitpid(*pidp, &status, WNOHANG) == 0 && leftms(deadline) > 0)
|
|
g_usleep(10000);
|
|
if(waitpid(*pidp, &status, WNOHANG) == 0){
|
|
kill(*pidp, SIGKILL);
|
|
waitpid(*pidp, &status, 0);
|
|
}
|
|
*pidp = -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);
|
|
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
|
|
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;
|
|
}
|
|
|
|
#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;
|
|
GdkWindow *win;
|
|
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 display[32];
|
|
char *module;
|
|
pid_t xvfb;
|
|
int first, i, ox, oy, scale, closes, ok, rootmade, serverup;
|
|
int64_t stalledms, started;
|
|
|
|
ctx = oldctx = badctx = NULL;
|
|
top = NULL;
|
|
xvfb = -1;
|
|
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(&xvfb, display, sizeof display), "start private Xvfb");
|
|
Check(setenv("DISPLAY", 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, "pre");
|
|
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(strcmp(log.event, "SC") == 0 && strcmp(log.pre, "pre") == 0,
|
|
"initial preedit signals were %s (%s)", log.event, log.pre);
|
|
|
|
/* 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);
|
|
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);
|
|
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(waitcount(&srv, first + 1) && getevent(&srv, first).type == Ereset,
|
|
"focus-out reset missing");
|
|
Check(waitclose(&srv, closes + 1), "focus-out did not close connection");
|
|
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);
|
|
stopxvfb(&xvfb);
|
|
free(module);
|
|
if(ok)
|
|
printf("gtk_live_test: ok (scale %d, stalled peer %lld ms)\n",
|
|
scale, (long long)stalledms);
|
|
return !ok;
|
|
}
|