From 2e6d7d8e48835f27fb819e78c362e4b1a1d24e0f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 19:17:37 +0900 Subject: [PATCH] xim: clear callback preedit on owner loss --- fn.h | 1 + strans.c | 4 ++ tests/test.h | 1 + tests/unit_test.c | 1 + tests/xim_adapter_test.c | 106 ++++++++++++++++++++++++++++- xim/xim.c | 139 +++++++++++++++++++++++++++++++++++---- 6 files changed, 237 insertions(+), 15 deletions(-) diff --git a/fn.h b/fn.h index e0f9658..c4c3712 100644 --- a/fn.h +++ b/fn.h @@ -40,6 +40,7 @@ void srvinit(void); void srvthread(void*); void ibusthread(void*); void ximthread(void*); +void ximownernotify(void); void* emalloc(ulong); void* erealloc(void*, ulong); diff --git a/strans.c b/strans.c index fc4c441..576003a 100644 --- a/strans.c +++ b/strans.c @@ -798,7 +798,9 @@ static void imhandlekey(Keyreq *kr) { Keyres res; + void *oldowner; + oldowner = activeowner; sclear(&res.commit); sclear(&res.preedit); res.eaten = 1; @@ -845,6 +847,8 @@ imhandlekey(Keyreq *kr) } break; } + if(activeowner != oldowner) + ximownernotify(); if(kr->owner == activeowner){ if(search.lang) res.preedit = search.text; diff --git a/tests/test.h b/tests/test.h index 8ca40d2..f43fb98 100644 --- a/tests/test.h +++ b/tests/test.h @@ -91,4 +91,5 @@ void xim_adapter_callback_replacement(struct ct*); void xim_adapter_callback_unicode(struct ct*); void xim_adapter_callback_cleanup(struct ct*); void xim_adapter_callback_transfer(struct ct*); +void xim_adapter_callback_owner_loss(struct ct*); void xim_adapter_commit_encoding(struct ct*); diff --git a/tests/unit_test.c b/tests/unit_test.c index 5139f0c..97e35f0 100644 --- a/tests/unit_test.c +++ b/tests/unit_test.c @@ -145,6 +145,7 @@ static const struct ct_test tests[] = { { "xim/callback-unicode", xim_adapter_callback_unicode }, { "xim/callback-cleanup", xim_adapter_callback_cleanup }, { "xim/callback-transfer", xim_adapter_callback_transfer }, + { "xim/callback-owner-loss", xim_adapter_callback_owner_loss }, { "xim/commit-encoding", xim_adapter_commit_encoding }, }; diff --git a/tests/xim_adapter_test.c b/tests/xim_adapter_test.c index 12eb2d1..064b916 100644 --- a/tests/xim_adapter_test.c +++ b/tests/xim_adapter_test.c @@ -659,7 +659,7 @@ xim_adapter_placement(struct ct *t) {XCB_IM_PreeditPosition | XCB_IM_StatusNothing, XCB_XIM_XNSpotLocation_MASK, 1, 1, 1, 1, 1, 103, 204}, {XCB_IM_PreeditPosition | XCB_IM_StatusNothing, - XCB_XIM_XNSpotLocation_MASK, 1, 0, 1, 1, 1, 303, 404}, + XCB_XIM_XNSpotLocation_MASK, 1, 0, 1, 1, 1, 300, 440}, {XCB_IM_PreeditPosition | XCB_IM_StatusNothing, 0, 1, 1, 1, 1, 1, 100, 230}, {XCB_IM_PreeditNothing | XCB_IM_StatusNothing, @@ -1022,6 +1022,110 @@ cleanup: ximend(&f); } +void +xim_adapter_callback_owner_loss(struct ct *t) +{ + Ximfix f; + Ic state; + xcb_im_input_context_t *ic; + xcb_im_packet_header_fr_t hdr; + Keyreq req; + Keyreq trace; + Keyres res; + uchar token; + int foreign, i, n; + + ic = (xcb_im_input_context_t*)(uintptr)51; + memset(&state, 0, sizeof state); + foreign = 0; + if(!ximbegin(t, &f, LangJP)) + goto cleanup; + if(!CT_CHECK(t, wakeinit() == 0)) + goto cleanup; + wakedrain(); + wireclear(); + wirebind(0, ic, &state); + state.xic = ic; + state.style = XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing; + state.cap = Cclientpreedit; + state.encoding = Eutf8; + keypress(&state, 'k', 0, &res); + nexttrace(t, &f, Keypress, &state); + updatepreedit(&state, &res.preedit); + CT_EQ_INT(t, Wstart, wirecalls[0].op); + CT_EQ_INT(t, Wdraw, wirecalls[1].op); + CT_EQ_PTR(t, &state, preowner); + checkownerwake(); + nexttrace(t, &f, Keycap, &state); + CT_EQ_PTR(t, &state, preowner); + + memset(&req, 0, sizeof req); + req.owner = &foreign; + req.op = Keypress; + req.ks = 'n'; + req.reply = replyc; + chansend(keyc, &req); + chanrecv(replyc, &res); + memset(&trace, 0, sizeof trace); + CT_CHECK(t, channbrecv(f.trace, &trace) > 0); + CT_EQ_INT(t, Keypress, trace.op); + CT_EQ_PTR(t, &foreign, trace.owner); + CT_EQ_PTR(t, &foreign, testengineowner()); + /* A full pipe cannot block imthread; all bytes coalesce into one probe. */ + token = 0; + for(i = 0; i < 1<<20; i++){ + n = write(wakefd[1], &token, 1); + if(n < 0 && errno == EINTR){ + i--; + continue; + } + if(n < 0) + break; + } + CT_CHECK(t, i < 1<<20); + CT_CHECK(t, errno == EAGAIN || errno == EWOULDBLOCK); + ximownernotify(); + CT_CHECK(t, wakedrain() > 0); + checkpreowner(); + nexttrace(t, &f, Keycap, &state); + CT_EQ_PTR(t, &foreign, testengineowner()); + CT_EQ_INT(t, 4, nwirecalls); + CT_EQ_INT(t, Wdraw, wirecalls[2].op); + CT_EQ_UINT(t, 1, wirecalls[2].changed); + CT_EQ_INT(t, Wdone, wirecalls[3].op); + CT_CHECK(t, !state.prestarted); + CT_EQ_PTR(t, nil, preowner); + + /* Late XIM lifecycle messages remain idempotent after owner cleanup. */ + release(&state); + nexttrace(t, &f, Keyrelease, &state); + release(&state); + notrace(t, &f); + memset(&hdr, 0, sizeof hdr); + hdr.major_opcode = XCB_XIM_RESET_IC; + callback(nil, nil, ic, &hdr, nil, nil, nil); + notrace(t, &f); + hdr.major_opcode = XCB_XIM_DESTROY_IC; + callback(nil, nil, ic, &hdr, nil, nil, nil); + notrace(t, &f); + CT_EQ_INT(t, 4, nwirecalls); + + memset(&req, 0, sizeof req); + req.owner = &foreign; + req.op = Keyrelease; + req.reply = replyc; + chansend(keyc, &req); + chanrecv(replyc, &res); + memset(&trace, 0, sizeof trace); + CT_CHECK(t, channbrecv(f.trace, &trace) > 0); + CT_EQ_INT(t, Keyrelease, trace.op); + CT_EQ_PTR(t, &foreign, trace.owner); + wakedrain(); +cleanup: + preowner = nil; + ximend(&f); +} + void xim_adapter_commit_encoding(struct ct *t) { diff --git a/xim/xim.c b/xim/xim.c index bc3d052..fe44a1c 100644 --- a/xim/xim.c +++ b/xim/xim.c @@ -1,6 +1,10 @@ #include "dat.h" #include "fn.h" +#include +#include +#include +#include #include #include #include @@ -52,6 +56,7 @@ static Ic *ics; static Ic *preowner; static Clientenc clientenc[Maxclients]; static Channel *replyc; +static int wakefd[2] = {-1, -1}; static char *encs[] = {"COMPOUND_TEXT", "UTF8_STRING"}; static u32int styles[] = { @@ -75,6 +80,71 @@ stylecap(u32int style, int *cap) return 0; } +static int +wakeinit(void) +{ + int fd[2]; + int flags, i; + + if(wakefd[0] >= 0) + return 0; + if(pipe(fd) < 0) + return -1; + for(i = 0; i < nelem(fd); i++){ + flags = fcntl(fd[i], F_GETFL); + if(flags < 0 || fcntl(fd[i], F_SETFL, + flags | O_NONBLOCK) < 0) + goto fail; + flags = fcntl(fd[i], F_GETFD); + if(flags < 0 || fcntl(fd[i], F_SETFD, + flags | FD_CLOEXEC) < 0) + goto fail; + } + wakefd[0] = fd[0]; + wakefd[1] = fd[1]; + return 0; +fail: + close(fd[0]); + close(fd[1]); + return -1; +} + +/* imthread only leaves a wake token; XIM ownership stays on the XIM thread. */ +void +ximownernotify(void) +{ + uchar token; + int n; + + if(wakefd[1] < 0) + return; + token = 0; + do + n = write(wakefd[1], &token, 1); + while(n < 0 && errno == EINTR); + /* EAGAIN means a pending byte will already wake the XIM poll loop. */ +} + +static int +wakedrain(void) +{ + uchar buf[32]; + int n, total; + + total = 0; + for(;;){ + n = read(wakefd[0], buf, sizeof buf); + if(n > 0){ + total += n; + continue; + } + if(n < 0 && errno == EINTR) + continue; + break; + } + return total; +} + static int getencoding(xcb_im_client_t *client) { @@ -272,11 +342,9 @@ place(Ic *state) nwin = 1; if(state->clientwin != state->focuswin) win[nwin++] = state->clientwin; - if(state->hasspot) - for(i = 0; i < nwin; i++) - if(translate(win[i], state->spot.x, state->spot.y, - &state->caret)) - return; + if(state->hasspot && translate(state->focuswin, + state->spot.x, state->spot.y, &state->caret)) + return; for(i = 0; i < nwin; i++) if(placebottom(state, win[i])) return; @@ -395,6 +463,25 @@ sendrequest(Ic *state, int op, u32int key, u32int mod, Keyres *res) chanrecv(replyc, res); } +static void +checkpreowner(void) +{ + Keyres res; + + if(preowner == nil) + return; + sendrequest(preowner, Keycap, 0, 0, &res); + if(!res.eaten) + clearpreedit(preowner); +} + +static void +checkownerwake(void) +{ + if(wakedrain() > 0) + checkpreowner(); +} + static void keypress(Ic *state, u32int key, u32int mod, Keyres *res) { @@ -611,6 +698,10 @@ ximinit(void) ximlog("cannot create reply channel"); return -1; } + if(wakeinit() < 0){ + ximlog("cannot create owner wake pipe"); + return -1; + } st.nStyles = nelem(styles); st.styles = styles; enc.nEncodings = nelem(encs); @@ -653,8 +744,10 @@ ximinit(void) void ximthread(void *arg) { + struct pollfd pfd[2]; xcb_generic_event_t *ev; uint8_t type; + int fd, n; USED(arg); threadsetname("xim"); @@ -662,17 +755,35 @@ ximthread(void *arg) ximclose(); return; } + fd = xcb_get_file_descriptor(conn); + pfd[0].fd = fd; + pfd[0].events = POLLIN; + pfd[1].fd = wakefd[0]; + pfd[1].events = POLLIN; for(;;){ - ev = xcb_wait_for_event(conn); - if(ev == nil) + pfd[0].revents = pfd[1].revents = 0; + n = poll(pfd, nelem(pfd), -1); + if(n < 0){ + if(errno == EINTR) + continue; + break; + } + if(pfd[1].revents & POLLIN) + checkownerwake(); + if(pfd[0].revents) + while((ev = xcb_poll_for_event(conn)) != nil){ + type = ev->response_type & ~0x80; + if(type == XCB_MAPPING_NOTIFY){ + if(kinit() < 0) + ximlog("cannot refresh keyboard mapping"); + }else + xcb_im_filter_event(xim, ev); + free(ev); + /* Ownership changes outrank the next queued X event. */ + checkownerwake(); + } + if(pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL)) break; - type = ev->response_type & ~0x80; - if(type == XCB_MAPPING_NOTIFY){ - if(kinit() < 0) - ximlog("cannot refresh keyboard mapping"); - }else - xcb_im_filter_event(xim, ev); - free(ev); } if(xcb_connection_has_error(conn)) ximlog("X server disconnected");