popupabove() took the stacking order from XQueryTree and then the map state of each child from a round trip of its own. The daemon raises and maps between those, in that order and in one flush, so the loop could pair a stale order -- popup still below the client, where it was created -- with a fresh IsViewable, and report the raise that had already happened as a popup stacked below the client. 1 run in 25 under load, which is the worst kind: often enough to teach you to re-run a red instead of reading it. Widening the gap to 300ms shows it with no load at all, and shows the fix is the right one. Same test, same widener, 15 runs each: before pass=11 fail=4 after pass=15 fail=0 XGrabServer is what makes the two one observation, and the widener is then harmless because nothing can raise inside it. Unwidened, 20 runs clean. Nothing about win.c was wrong: the popup does go above, and the test now only says so when it is looking at a single moment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
621 lines
14 KiB
C
621 lines
14 KiB
C
#define _GNU_SOURCE
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xutil.h>
|
|
#include <X11/keysym.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <locale.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "ipc.h"
|
|
#include "live.h"
|
|
|
|
enum
|
|
{
|
|
Eventtimeout = 4000,
|
|
};
|
|
|
|
typedef struct Run Run;
|
|
typedef struct Prelog Prelog;
|
|
|
|
struct Run
|
|
{
|
|
Live l;
|
|
Daemon xvfb;
|
|
Daemon daemon;
|
|
};
|
|
|
|
struct Prelog
|
|
{
|
|
int start;
|
|
int nonempty;
|
|
int empty;
|
|
int done;
|
|
int doneafterempty;
|
|
int badorder;
|
|
};
|
|
|
|
static int xerror;
|
|
|
|
static int
|
|
waitsocket(Run *r)
|
|
{
|
|
struct stat st;
|
|
int64_t deadline;
|
|
|
|
deadline = nowms() + Starttimeout;
|
|
while(leftms(deadline) > 0){
|
|
if(lstat(r->l.socket, &st) == 0 && S_ISSOCK(st.st_mode) &&
|
|
(st.st_mode & 0777) == 0600)
|
|
return 1;
|
|
if(!daemonalive(&r->daemon))
|
|
return fail("daemon exited before creating its IPC socket");
|
|
pausems(20);
|
|
}
|
|
return fail("timed out waiting for daemon IPC socket");
|
|
}
|
|
|
|
static int
|
|
start(Run *r, char *daemon, char *mapdir, char *xvfb)
|
|
{
|
|
memset(r, 0, sizeof *r);
|
|
daemoninit(&r->xvfb, "Xvfb");
|
|
daemoninit(&r->daemon, "daemon");
|
|
if(!livesetup(&r->l, "xim") || !startxvfb(&r->l, &r->xvfb, xvfb))
|
|
return 0;
|
|
if(setenv("DISPLAY", r->l.display, 1) < 0 ||
|
|
setenv("XMODIFIERS", "@im=strans", 1) < 0)
|
|
return fail("set private environment: %s", strerror(errno));
|
|
if(!startdaemon(&r->l, &r->daemon, daemon, mapdir))
|
|
return 0;
|
|
return waitsocket(r);
|
|
}
|
|
|
|
static void
|
|
cleanup(Run *r, int noisy)
|
|
{
|
|
stopdaemon(&r->daemon);
|
|
stopdaemon(&r->xvfb);
|
|
if(noisy){
|
|
showerrors(&r->daemon);
|
|
showerrors(&r->xvfb);
|
|
}
|
|
closeerrors(&r->daemon);
|
|
closeerrors(&r->xvfb);
|
|
liveclean(&r->l);
|
|
}
|
|
|
|
static int
|
|
xerr(Display *dpy, XErrorEvent *e)
|
|
{
|
|
char msg[128];
|
|
|
|
xerror++;
|
|
XGetErrorText(dpy, e->error_code, msg, sizeof msg);
|
|
fail("X error: %s (request %d.%d)", msg, e->request_code, e->minor_code);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
pumpinput(Display *dpy, XIC ic, char *commit, size_t cap, int timeout)
|
|
{
|
|
struct pollfd pfd;
|
|
XEvent ev;
|
|
KeySym sym;
|
|
Status status;
|
|
char buf[Ipcfieldmax+1];
|
|
int n;
|
|
|
|
XFlush(dpy);
|
|
if(XPending(dpy) == 0){
|
|
pfd.fd = ConnectionNumber(dpy);
|
|
pfd.events = POLLIN;
|
|
pfd.revents = 0;
|
|
while(poll(&pfd, 1, timeout) < 0 && errno == EINTR)
|
|
;
|
|
}
|
|
while(XPending(dpy) != 0){
|
|
XNextEvent(dpy, &ev);
|
|
if(XFilterEvent(&ev, None) || ic == NULL || ev.type != KeyPress)
|
|
continue;
|
|
n = Xutf8LookupString(ic, &ev.xkey, buf, sizeof buf - 1,
|
|
&sym, &status);
|
|
if(n < 0 || (status == XLookupChars &&
|
|
((size_t)n >= cap || commit == NULL)))
|
|
return 0;
|
|
if(status == XLookupChars && n != 0){
|
|
memcpy(commit, buf, n);
|
|
commit[n] = '\0';
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
pump(Display *dpy, int timeout)
|
|
{
|
|
pumpinput(dpy, NULL, NULL, 0, timeout);
|
|
}
|
|
|
|
static int
|
|
prestart(XIC ic, XPointer data, XPointer call)
|
|
{
|
|
Prelog *p;
|
|
|
|
(void)ic;
|
|
(void)call;
|
|
p = (Prelog*)data;
|
|
if(p->start != p->done)
|
|
p->badorder++;
|
|
p->start++;
|
|
return Ipcfieldmax;
|
|
}
|
|
|
|
static void
|
|
predraw(XIM im, XPointer data, XPointer call)
|
|
{
|
|
Prelog *p;
|
|
XIMPreeditDrawCallbackStruct *draw;
|
|
|
|
(void)im;
|
|
p = (Prelog*)data;
|
|
draw = (XIMPreeditDrawCallbackStruct*)call;
|
|
if(draw != NULL && draw->text != NULL && draw->text->length != 0){
|
|
if(p->start != p->done + 1)
|
|
p->badorder++;
|
|
p->nonempty++;
|
|
}else{
|
|
if(p->start != p->done + 1)
|
|
p->badorder++;
|
|
p->empty++;
|
|
}
|
|
}
|
|
|
|
static void
|
|
predone(XIM im, XPointer data, XPointer call)
|
|
{
|
|
Prelog *p;
|
|
|
|
(void)im;
|
|
(void)call;
|
|
p = (Prelog*)data;
|
|
if(p->empty > p->done)
|
|
p->doneafterempty++;
|
|
else
|
|
p->badorder++;
|
|
p->done++;
|
|
}
|
|
|
|
static void
|
|
precaret(XIM im, XPointer data, XPointer call)
|
|
{
|
|
(void)im;
|
|
(void)data;
|
|
(void)call;
|
|
}
|
|
|
|
static XIC
|
|
callbackic(XIM im, Window win, Prelog *log)
|
|
{
|
|
union {
|
|
int (*start)(XIC, XPointer, XPointer);
|
|
XIMProc xim;
|
|
} fn;
|
|
XIMCallback start, draw, done, caret;
|
|
XVaNestedList pre;
|
|
XIC ic;
|
|
|
|
start.client_data = (XPointer)log;
|
|
fn.start = prestart;
|
|
start.callback = fn.xim;
|
|
draw.client_data = (XPointer)log;
|
|
draw.callback = predraw;
|
|
done.client_data = (XPointer)log;
|
|
done.callback = predone;
|
|
caret.client_data = (XPointer)log;
|
|
caret.callback = precaret;
|
|
pre = XVaCreateNestedList(0,
|
|
XNPreeditStartCallback, &start,
|
|
XNPreeditDrawCallback, &draw,
|
|
XNPreeditDoneCallback, &done,
|
|
XNPreeditCaretCallback, &caret,
|
|
NULL);
|
|
if(pre == NULL)
|
|
return NULL;
|
|
ic = XCreateIC(im,
|
|
XNInputStyle, XIMPreeditCallbacks|XIMStatusNothing,
|
|
XNClientWindow, win,
|
|
XNFocusWindow, win,
|
|
XNPreeditAttributes, pre,
|
|
NULL);
|
|
XFree(pre);
|
|
return ic;
|
|
}
|
|
|
|
static int
|
|
sendkey(Display *dpy, XIC ic, Window win, KeySym sym, unsigned int state,
|
|
char *commit, size_t cap)
|
|
{
|
|
XKeyPressedEvent key;
|
|
XEvent ev;
|
|
KeySym got;
|
|
Status status;
|
|
char buf[Ipcfieldmax+1];
|
|
int n;
|
|
|
|
if(cap != 0)
|
|
commit[0] = '\0';
|
|
memset(&key, 0, sizeof key);
|
|
key.type = KeyPress;
|
|
key.display = dpy;
|
|
key.window = win;
|
|
key.root = DefaultRootWindow(dpy);
|
|
key.time = CurrentTime;
|
|
key.x = key.y = key.x_root = key.y_root = 1;
|
|
key.same_screen = True;
|
|
key.keycode = XKeysymToKeycode(dpy, sym);
|
|
key.state = state;
|
|
memset(&ev, 0, sizeof ev);
|
|
ev.xkey = key;
|
|
if(XFilterEvent(&ev, None)){
|
|
return pumpinput(dpy, ic, commit, cap, 20);
|
|
}
|
|
n = Xutf8LookupString(ic, &key, buf, sizeof buf - 1, &got, &status);
|
|
if(n < 0)
|
|
return fail("Xutf8LookupString failed for keysym %#lx",
|
|
(unsigned long)sym);
|
|
if(status == XLookupChars && n != 0){
|
|
if((size_t)n >= cap)
|
|
return fail("commit buffer overflow");
|
|
memcpy(commit, buf, n);
|
|
commit[n] = '\0';
|
|
}
|
|
return pumpinput(dpy, ic, commit, cap, 20);
|
|
}
|
|
|
|
static int
|
|
waitcommit(Display *dpy, XIC ic, char *commit, size_t cap)
|
|
{
|
|
int64_t deadline;
|
|
|
|
deadline = nowms() + Eventtimeout;
|
|
while(leftms(deadline) > 0){
|
|
if(!pumpinput(dpy, ic, commit, cap, leftms(deadline)))
|
|
return fail("read committed XIM text");
|
|
if(commit[0] != '\0')
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
waitpreedit(Display *dpy, Prelog *p)
|
|
{
|
|
int64_t deadline;
|
|
|
|
deadline = nowms() + Eventtimeout;
|
|
while(leftms(deadline) > 0){
|
|
pump(dpy, leftms(deadline));
|
|
if(p->empty != 0 || p->done != 0 || p->badorder != 0)
|
|
return -1;
|
|
if(p->start != 0 && p->nonempty != 0)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static XIM
|
|
openim(Display *dpy)
|
|
{
|
|
XIM im;
|
|
int64_t deadline;
|
|
|
|
deadline = nowms() + Starttimeout;
|
|
do{
|
|
im = XOpenIM(dpy, NULL, "strans", "Strans");
|
|
if(im != NULL)
|
|
return im;
|
|
pump(dpy, 20);
|
|
}while(leftms(deadline) > 0);
|
|
return NULL;
|
|
}
|
|
|
|
static int
|
|
testcallbacks(Display *dpy, XIM im, Window win)
|
|
{
|
|
char commit[Ipcfieldmax+1], *reset;
|
|
Prelog log;
|
|
XIC ic;
|
|
|
|
memset(&log, 0, sizeof log);
|
|
ic = callbackic(im, win, &log);
|
|
if(ic == NULL)
|
|
return fail("create PreeditCallbacks input context");
|
|
XSetICFocus(ic);
|
|
if(!sendkey(dpy, ic, win, XK_s, ControlMask, commit, sizeof commit) ||
|
|
!sendkey(dpy, ic, win, XK_r, 0, commit, sizeof commit)){
|
|
XDestroyIC(ic);
|
|
return 0;
|
|
}
|
|
if(waitpreedit(dpy, &log) != 1){
|
|
int start, draw, empty, done;
|
|
|
|
start = log.start;
|
|
draw = log.nonempty;
|
|
empty = log.empty;
|
|
done = log.done;
|
|
XDestroyIC(ic);
|
|
return fail("invalid callback preedit: start=%d draw=%d empty=%d done=%d",
|
|
start, draw, empty, done);
|
|
}
|
|
pump(dpy, 50);
|
|
if(log.start != 1 || log.nonempty < 1 || log.done != 0 ||
|
|
log.empty != 0 || log.badorder != 0){
|
|
int start, draw, empty, done, ordered, bad;
|
|
|
|
start = log.start;
|
|
draw = log.nonempty;
|
|
empty = log.empty;
|
|
done = log.done;
|
|
ordered = log.doneafterempty;
|
|
bad = log.badorder;
|
|
XDestroyIC(ic);
|
|
return fail("bad initial callback sequence: start=%d draw=%d empty=%d done=%d ordered=%d bad=%d",
|
|
start, draw, empty, done, ordered, bad);
|
|
}
|
|
reset = Xutf8ResetIC(ic);
|
|
if(reset == NULL || strcmp(reset, "ㄱ") != 0){
|
|
if(reset != NULL)
|
|
XFree(reset);
|
|
XDestroyIC(ic);
|
|
return fail("ResetIC did not return pending preedit");
|
|
}
|
|
XFree(reset);
|
|
if(!sendkey(dpy, ic, win, XK_r, 0, commit, sizeof commit) ||
|
|
!sendkey(dpy, ic, win, XK_k, 0, commit, sizeof commit) ||
|
|
!sendkey(dpy, ic, win, XK_Return, 0, commit, sizeof commit)){
|
|
XDestroyIC(ic);
|
|
return 0;
|
|
}
|
|
if(commit[0] == '\0' && !waitcommit(dpy, ic, commit, sizeof commit)){
|
|
XDestroyIC(ic);
|
|
return fail("input context did not compose after ResetIC");
|
|
}
|
|
if(strcmp(commit, "가") != 0){
|
|
XDestroyIC(ic);
|
|
return fail("post-reset composition did not contain 가");
|
|
}
|
|
XUnsetICFocus(ic);
|
|
XDestroyIC(ic);
|
|
pump(dpy, 100);
|
|
if(log.badorder != 0)
|
|
return fail("bad callback order after ResetIC");
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* The popup is the one viewable override-redirect window on the private
|
|
* display; created before the client window, it must still stack above it.
|
|
* The order and the map states have to be one observation: the daemon
|
|
* raises and maps between two ungrabbed round trips, and a stale order read
|
|
* against a fresh map state says the popup is below when it is not.
|
|
*/
|
|
static int
|
|
popupabove(Display *dpy, Window client)
|
|
{
|
|
XWindowAttributes wa;
|
|
Window root, parent, *kids;
|
|
unsigned i, n;
|
|
int64_t deadline;
|
|
int above;
|
|
|
|
deadline = nowms() + Eventtimeout;
|
|
for(;;){
|
|
above = 0;
|
|
root = DefaultRootWindow(dpy);
|
|
XGrabServer(dpy);
|
|
if(!XQueryTree(dpy, root, &root, &parent, &kids, &n)){
|
|
XUngrabServer(dpy);
|
|
return fail("query the window tree");
|
|
}
|
|
for(i = 0; i < n; i++){
|
|
if(kids[i] == client)
|
|
above = 1;
|
|
else if(XGetWindowAttributes(dpy, kids[i], &wa) &&
|
|
wa.override_redirect && wa.map_state == IsViewable)
|
|
break;
|
|
}
|
|
XFree(kids);
|
|
XUngrabServer(dpy);
|
|
if(i < n)
|
|
return above || fail("popup is stacked below the client");
|
|
if(leftms(deadline) == 0)
|
|
return fail("popup did not appear");
|
|
pump(dpy, 20);
|
|
}
|
|
}
|
|
|
|
static int
|
|
testnothing(Display *dpy, XIM im, Window win)
|
|
{
|
|
char commit[Ipcfieldmax+1];
|
|
XIC ic;
|
|
|
|
ic = XCreateIC(im,
|
|
XNInputStyle, XIMPreeditNothing|XIMStatusNothing,
|
|
XNClientWindow, win,
|
|
XNFocusWindow, win,
|
|
NULL);
|
|
if(ic == NULL)
|
|
return fail("create PreeditNothing input context");
|
|
XSetICFocus(ic);
|
|
if(!sendkey(dpy, ic, win, XK_s, ControlMask, commit, sizeof commit)){
|
|
XDestroyIC(ic);
|
|
return 0;
|
|
}
|
|
pump(dpy, 100);
|
|
if(!sendkey(dpy, ic, win, XK_r, 0, commit, sizeof commit) ||
|
|
!popupabove(dpy, win) ||
|
|
!sendkey(dpy, ic, win, XK_k, 0, commit, sizeof commit) ||
|
|
!sendkey(dpy, ic, win, XK_Return, 0, commit, sizeof commit)){
|
|
XDestroyIC(ic);
|
|
return 0;
|
|
}
|
|
if(commit[0] == '\0' &&
|
|
!waitcommit(dpy, ic, commit, sizeof commit)){
|
|
XDestroyIC(ic);
|
|
return fail("PreeditNothing composition produced no commit");
|
|
}
|
|
if(strcmp(commit, "\352\260\200") != 0){
|
|
XDestroyIC(ic);
|
|
return fail("PreeditNothing commit did not contain 가");
|
|
}
|
|
XUnsetICFocus(ic);
|
|
XDestroyIC(ic);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
testposition(Display *dpy, XIM im, Window client, Window focus)
|
|
{
|
|
char commit[Ipcfieldmax+1];
|
|
char **missing, *def;
|
|
int nmissing;
|
|
XPoint spot;
|
|
XVaNestedList pre;
|
|
XFontSet fontset;
|
|
XIC ic;
|
|
|
|
missing = NULL;
|
|
nmissing = 0;
|
|
def = NULL;
|
|
fontset = XCreateFontSet(dpy, "fixed", &missing, &nmissing, &def);
|
|
if(missing != NULL)
|
|
XFreeStringList(missing);
|
|
if(fontset == NULL)
|
|
return fail("create XIM position font set");
|
|
spot.x = 7;
|
|
spot.y = 11;
|
|
pre = XVaCreateNestedList(0, XNSpotLocation, &spot,
|
|
XNFontSet, fontset, NULL);
|
|
if(pre == NULL){
|
|
XFreeFontSet(dpy, fontset);
|
|
return fail("create XNSpotLocation list");
|
|
}
|
|
ic = XCreateIC(im,
|
|
XNInputStyle, XIMPreeditPosition|XIMStatusNothing,
|
|
XNClientWindow, client,
|
|
XNFocusWindow, focus,
|
|
XNPreeditAttributes, pre,
|
|
NULL);
|
|
XFree(pre);
|
|
if(ic == NULL){
|
|
XFreeFontSet(dpy, fontset);
|
|
return fail("create nested PreeditPosition input context");
|
|
}
|
|
XSetICFocus(ic);
|
|
if(!sendkey(dpy, ic, focus, XK_s, ControlMask, commit, sizeof commit) ||
|
|
!sendkey(dpy, ic, focus, XK_r, 0, commit, sizeof commit)){
|
|
XDestroyIC(ic);
|
|
XFreeFontSet(dpy, fontset);
|
|
return 0;
|
|
}
|
|
spot.x = 13;
|
|
spot.y = 17;
|
|
pre = XVaCreateNestedList(0, XNSpotLocation, &spot, NULL);
|
|
if(pre == NULL || XSetICValues(ic, XNPreeditAttributes, pre, NULL) != NULL){
|
|
if(pre != NULL)
|
|
XFree(pre);
|
|
XDestroyIC(ic);
|
|
XFreeFontSet(dpy, fontset);
|
|
return fail("update nested XNSpotLocation");
|
|
}
|
|
XFree(pre);
|
|
if(!sendkey(dpy, ic, focus, XK_Escape, 0, commit, sizeof commit)){
|
|
XDestroyIC(ic);
|
|
XFreeFontSet(dpy, fontset);
|
|
return 0;
|
|
}
|
|
XUnsetICFocus(ic);
|
|
XDestroyIC(ic);
|
|
XFreeFontSet(dpy, fontset);
|
|
return 1;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
Display *dpy;
|
|
Window client, focus, root;
|
|
XIM im;
|
|
Run run;
|
|
int ok;
|
|
char *xvfb;
|
|
|
|
testname = "xim_live_test";
|
|
if(argc != 3 && argc != 4){
|
|
fprintf(stderr, "usage: xim_live_test daemon mapdir [Xvfb]\n");
|
|
return 2;
|
|
}
|
|
xvfb = argc == 4 ? argv[3] : "Xvfb";
|
|
ok = start(&run, argv[1], argv[2], xvfb);
|
|
if(!ok){
|
|
cleanup(&run, 1);
|
|
return 1;
|
|
}
|
|
if(setlocale(LC_CTYPE, "C.UTF-8") == NULL || !XSupportsLocale() ||
|
|
XSetLocaleModifiers("@im=strans") == NULL){
|
|
fail("initialize UTF-8 X locale for strans");
|
|
cleanup(&run, 1);
|
|
return 1;
|
|
}
|
|
XSetErrorHandler(xerr);
|
|
dpy = XOpenDisplay(run.l.display);
|
|
if(dpy == NULL){
|
|
fail("open private display %s", run.l.display);
|
|
cleanup(&run, 1);
|
|
return 1;
|
|
}
|
|
im = openim(dpy);
|
|
if(im == NULL){
|
|
fail("open strans XIM on private display %s", run.l.display);
|
|
XCloseDisplay(dpy);
|
|
cleanup(&run, 1);
|
|
return 1;
|
|
}
|
|
root = DefaultRootWindow(dpy);
|
|
client = XCreateSimpleWindow(dpy, root, 41, 53, 240, 100, 0, 0, 0);
|
|
focus = XCreateSimpleWindow(dpy, client, 17, 19, 160, 50, 0, 0, 0);
|
|
XSelectInput(dpy, client, KeyPressMask|StructureNotifyMask);
|
|
XSelectInput(dpy, focus, KeyPressMask|StructureNotifyMask);
|
|
XMapWindow(dpy, client);
|
|
XMapWindow(dpy, focus);
|
|
XSync(dpy, False);
|
|
ok = testcallbacks(dpy, im, client) &&
|
|
testnothing(dpy, im, client) &&
|
|
testposition(dpy, im, client, focus);
|
|
XDestroyWindow(dpy, focus);
|
|
XDestroyWindow(dpy, client);
|
|
XCloseIM(im);
|
|
XSync(dpy, False);
|
|
XCloseDisplay(dpy);
|
|
if(xerror != 0){
|
|
fail("observed %d X protocol errors", xerror);
|
|
ok = 0;
|
|
}
|
|
cleanup(&run, !ok);
|
|
if(!ok)
|
|
return 1;
|
|
printf("xim_live_test: ok\n");
|
|
return 0;
|
|
}
|