fix(gtk): restore compose fallback and clean lifecycle

This commit is contained in:
2026-08-14 22:29:14 +09:00
parent 534ddcd9bb
commit 9804c1f55c
2 changed files with 285 additions and 224 deletions

View File

@@ -34,6 +34,13 @@ enum
Ereset,
};
enum
{
Rnormal,
Rbadcommit,
Rbadpreedit,
};
typedef struct Event Event;
typedef struct Server Server;
typedef struct Siglog Siglog;
@@ -58,6 +65,9 @@ struct Server
int wake[2];
int stop;
int old;
int eaten;
int stall;
int response;
int nclose;
char pre[Ipcfieldmax+1];
Event ev[Maxevent];
@@ -128,16 +138,35 @@ closed(Server *s)
}
static int
sendresponse(Server *s, int fd, int eaten, int want)
sendresponse(Server *s, int fd, int eaten, int want, int key)
{
unsigned char buf[Ipcmaxresp];
const char *commit;
char pre[Ipcfieldmax+1];
int n;
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);
n = ipcpackresp(buf, sizeof buf, eaten, "", 0, pre, strlen(pre), want);
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;
}
@@ -161,7 +190,7 @@ request(Server *s, int fd)
pthread_mutex_lock(&s->lock);
old = s->old;
pthread_mutex_unlock(&s->lock);
return sendresponse(s, fd, !old, e.want);
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)
@@ -175,7 +204,11 @@ request(Server *s, int fd)
e.key = key;
e.type = ipcreqreset(buf) ? Ereset : Ekey;
record(s, &e);
return sendresponse(s, fd, e.type == Ekey, want);
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;
}
@@ -230,6 +263,7 @@ startserver(Server *s, char *path)
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)
@@ -317,6 +351,16 @@ setold(Server *s, int 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)
{
@@ -540,16 +584,6 @@ commit(GtkIMContext *ctx, gchar *s, Siglog *l)
logsig(l, 'K');
}
static void
weakgone(void *v, GObject *obj)
{
int *gone;
(void)obj;
gone = v;
*gone = 1;
}
static void
clearlog(Siglog *l)
{
@@ -571,21 +605,35 @@ newcontext(Siglog *l)
}
static int
key(GtkIMContext *ctx, GdkWindow *win, guint keyval)
keyevent(GtkIMContext *ctx, GdkWindow *win, GdkEventType type,
guint keyval, guint state)
{
GdkEvent *ev;
int r;
ev = gdk_event_new(GDK_KEY_PRESS);
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)
{
@@ -607,19 +655,25 @@ main(int argc, char **argv)
Server srv;
Siglog log;
GtkIMContext *ctx, *oldctx, *badctx;
GtkWidget *top, *other;
GdkWindow *win, *win2;
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, gone, ok, rootmade, serverup;
int first, i, ox, oy, scale, closes, ok, rootmade, serverup;
int64_t stalledms, started;
ctx = oldctx = badctx = NULL;
top = other = NULL;
top = NULL;
xvfb = -1;
rootmade = 0;
serverup = 0;
@@ -688,153 +742,60 @@ main(int argc, char **argv)
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);
first = eventcount(&srv);
gtk_im_context_set_use_preedit(ctx, FALSE);
Check(waitcount(&srv, first + 1), "disable capability timed out");
e = getevent(&srv, first);
Check(e.type == Ecap && e.want == 0, "disable capability frame missing");
Check(strcmp(log.event, "CE") == 0 && log.pre[0] == '\0',
"disable signal order was %s (%s)", log.event, log.pre);
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);
gtk_im_context_set_use_preedit(ctx, FALSE);
Check(log.n == 0, "repeated disable emitted %s", log.event);
first = eventcount(&srv);
gtk_im_context_set_use_preedit(ctx, TRUE);
Check(waitcount(&srv, first + 1), "enable capability timed out");
e = getevent(&srv, first);
Check(e.type == Ecap && e.want == 1, "enable capability frame missing");
Check(strcmp(log.event, "SC") == 0 && strcmp(log.pre, "pre") == 0,
"enable signal order was %s (%s)", log.event, log.pre);
/* The retained relative rectangle follows a moved window before a key. */
gtk_window_move(GTK_WINDOW(top), 137, 89);
pump();
first = eventcount(&srv);
setpre(&srv, "pre2");
Check(key(ctx, win, GDK_KEY_b), "key after move was not eaten");
Check(waitcount(&srv, first + 2), "moved caret/key timed out");
e = getevent(&srv, first);
Check(e.type == Ecaret && e.valid, "window move did not refresh caret");
gdk_window_get_origin(win, &ox, &oy);
Check(e.x == (ox + rect.x) * scale && e.y == (oy + rect.y) * scale,
"moved caret root conversion is wrong");
Check(getevent(&srv, first + 1).type == Ekey,
"caret did not precede moved-window key");
other = newwindow(301, 157);
win2 = gtk_widget_get_window(other);
first = eventcount(&srv);
gtk_im_context_set_client_window(ctx, win2);
Check(waitcount(&srv, first + 1), "window replacement invalidation timed out");
e = getevent(&srv, first);
Check(e.type == Ecaret && !e.valid, "window replacement retained old caret");
rect.x = 7;
rect.y = 9;
rect.height = 13;
first = eventcount(&srv);
gtk_im_context_set_cursor_location(ctx, &rect);
Check(waitcount(&srv, first + 1), "replacement caret timed out");
e = getevent(&srv, first);
gdk_window_get_origin(win2, &ox, &oy);
scale = gdk_window_get_scale_factor(win2);
Check(e.type == Ecaret && e.valid && e.x == (ox + rect.x) * scale &&
e.y == (oy + rect.y) * scale && e.h == rect.height * scale,
"replacement window caret is wrong");
first = eventcount(&srv);
gtk_im_context_set_client_window(ctx, NULL);
Check(waitcount(&srv, first + 1), "NULL window invalidation timed out");
e = getevent(&srv, first);
Check(e.type == Ecaret && !e.valid, "NULL window did not invalidate caret");
gtk_im_context_set_client_window(ctx, win2);
rect.x = 1;
rect.y = 1;
rect.height = 1;
first = eventcount(&srv);
gtk_im_context_set_cursor_location(ctx, &rect);
Check(waitcount(&srv, first + 1), "pre-overflow valid caret timed out");
Check(getevent(&srv, first).type == Ecaret &&
getevent(&srv, first).valid, "pre-overflow caret is not valid");
rect.x = G_MAXINT;
rect.y = 0;
rect.height = 1;
first = eventcount(&srv);
gtk_im_context_set_cursor_location(ctx, &rect);
Check(waitcount(&srv, first + 1), "overflow invalidation timed out");
e = getevent(&srv, first);
Check(e.type == Ecaret && !e.valid, "overflow caret was accepted");
rect.x = 4;
rect.y = 5;
rect.height = -1;
first = eventcount(&srv);
gtk_im_context_set_cursor_location(ctx, &rect);
if(eventcount(&srv) > first){
e = getevent(&srv, first);
Check(e.type == Ecaret && !e.valid, "negative caret height was accepted");
}
/* A destroyed retained window invalidates placement on the next key. */
rect.x = 4;
rect.y = 5;
rect.height = 10;
first = eventcount(&srv);
gtk_im_context_set_cursor_location(ctx, &rect);
Check(waitcount(&srv, first + 1) && getevent(&srv, first).valid,
"pre-destruction caret is not valid");
gone = 0;
g_object_weak_ref(G_OBJECT(win2), weakgone, &gone);
gtk_widget_destroy(other);
other = NULL;
Check(!gone, "module did not retain its client window");
first = eventcount(&srv);
setpre(&srv, "destroyed");
Check(key(ctx, win2, GDK_KEY_q), "destroyed-window key was not eaten");
Check(waitcount(&srv, first + 2), "destroyed-window frames timed out");
e = getevent(&srv, first);
Check(e.type == Ecaret && !e.valid &&
getevent(&srv, first + 1).type == Ekey,
"destroyed window did not invalidate before the key");
gtk_im_context_set_client_window(ctx, win);
pump();
if(!gone)
g_object_weak_unref(G_OBJECT(win2), weakgone, &gone);
win2 = win;
gtk_im_context_set_cursor_location(ctx, &rect);
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. */
gtk_im_context_set_client_window(ctx, win2);
rect.height = 12;
gtk_im_context_set_cursor_location(ctx, &rect);
setpre(&srv, "secret");
clearlog(&log);
Check(key(ctx, win2, GDK_KEY_c), "password setup key was not eaten");
Check(strcmp(log.pre, "secret") == 0, "password setup preedit missing");
clearlog(&log);
first = eventcount(&srv);
g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_PASSWORD, NULL);
Check(waitcount(&srv, first + 1), "password reset timed out");
Check(getevent(&srv, first).type == Ereset, "password did not reset owner");
Check(strcmp(log.event, "CE") == 0, "password clear order was %s", log.event);
clearlog(&log);
first = eventcount(&srv);
Check(key(ctx, win2, GDK_KEY_x), "password fallback did not commit");
Check(eventcount(&srv) == first && strcmp(log.commit, "x") == 0,
"password key reached daemon or did not commit");
g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL);
setpre(&srv, "pin");
clearlog(&log);
Check(key(ctx, win2, GDK_KEY_d), "PIN setup key was not eaten");
clearlog(&log);
first = eventcount(&srv);
g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_PIN, NULL);
Check(waitcount(&srv, first + 1), "PIN reset timed out");
Check(getevent(&srv, first).type == Ereset && strcmp(log.event, "CE") == 0,
"PIN transition did not reset and clear");
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, win2, GDK_KEY_e), "reset setup key was not eaten");
Check(key(ctx, win, GDK_KEY_e), "reset setup key was not eaten");
clearlog(&log);
first = eventcount(&srv);
gtk_im_context_reset(ctx);
@@ -843,7 +804,7 @@ main(int argc, char **argv)
"explicit reset did not clear preedit");
setpre(&srv, "focus");
clearlog(&log);
Check(key(ctx, win2, GDK_KEY_f), "focus setup key was not eaten");
Check(key(ctx, win, GDK_KEY_f), "focus setup key was not eaten");
clearlog(&log);
first = eventcount(&srv);
closes = closecount(&srv);
@@ -853,21 +814,58 @@ main(int argc, char **argv)
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);
/* Finalization closes its connection and drops the retained GdkWindow. */
gtk_im_context_set_client_window(ctx, win2);
/* 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, win2, GDK_KEY_g), "finalization setup key was not eaten");
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), "finalization did not close connection");
Check(waitclose(&srv, closes + 1), "dispose did not close connection");
Check(log.n == 0, "dispose emitted %s", log.event);
/* A new module talking to an old daemon keeps legacy inline preedit. */
/* 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, win2);
gtk_im_context_set_client_window(oldctx, win);
rect.x = 2;
rect.y = 3;
rect.height = 10;
@@ -875,29 +873,29 @@ main(int argc, char **argv)
gtk_im_context_set_use_preedit(oldctx, FALSE);
setpre(&srv, "legacy");
first = eventcount(&srv);
Check(key(oldctx, win2, GDK_KEY_h), "old-daemon key was not eaten");
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 == 1, "old fallback did not request preedit");
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(strcmp(log.event, "SC") == 0 && strcmp(log.pre, "legacy") == 0,
"old fallback preedit was %s (%s)", log.event, log.pre);
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 retains the existing plain-key fallback. */
/* A failed first connection uses GtkIMContextSimple. */
stopserver(&srv);
serverup = 0;
unlink(socket);
clearlog(&log);
badctx = newcontext(&log);
Check(key(badctx, win2, GDK_KEY_z), "connection failure did not fall back");
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");
@@ -909,8 +907,6 @@ out:
g_object_unref(oldctx);
if(ctx != NULL)
g_object_unref(ctx);
if(other != NULL)
gtk_widget_destroy(other);
if(top != NULL)
gtk_widget_destroy(top);
pump();
@@ -927,6 +923,7 @@ out:
stopxvfb(&xvfb);
free(module);
if(ok)
printf("gtk_live_test: ok (scale %d)\n", scale);
printf("gtk_live_test: ok (scale %d, stalled peer %lld ms)\n",
scale, (long long)stalledms);
return !ok;
}