#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ipc.h" #include "live.h" enum { Eventtimeout = 4000, }; typedef struct Child Child; typedef struct Run Run; typedef struct Prelog Prelog; struct Child { pid_t pid; pid_t pgid; char log[320]; }; struct Run { Live l; Child xvfb; Child daemon; char display[32]; }; struct Prelog { int start; int nonempty; int empty; int done; int doneafterempty; int badorder; }; static int xerror; static int childlog(char *path) { int fd; fd = open(path, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0600); return fd; } static int spawnxvfb(Run *r, char *program) { char fdstr[16], line[32]; int displayfd[2], errfd, flags, n, status; int64_t deadline; pid_t pid; struct pollfd pfd; ssize_t nr; if(pipe2(displayfd, O_CLOEXEC) < 0) return fail("create Xvfb display pipe: %s", strerror(errno)); errfd = childlog(r->xvfb.log); if(errfd < 0){ close(displayfd[0]); close(displayfd[1]); return fail("open Xvfb log: %s", strerror(errno)); } pid = fork(); if(pid < 0){ close(displayfd[0]); close(displayfd[1]); close(errfd); return fail("fork Xvfb: %s", strerror(errno)); } if(pid == 0){ setpgid(0, 0); close(displayfd[0]); flags = fcntl(displayfd[1], F_GETFD); if(flags < 0 || fcntl(displayfd[1], F_SETFD, flags & ~FD_CLOEXEC) < 0) _exit(126); if(dup2(errfd, STDOUT_FILENO) < 0 || dup2(errfd, STDERR_FILENO) < 0) _exit(126); close(errfd); snprintf(fdstr, sizeof fdstr, "%d", displayfd[1]); execlp(program, program, "-displayfd", fdstr, "-screen", "0", "800x600x24", "-nolisten", "tcp", "-noreset", (char*)0); _exit(127); } close(displayfd[1]); close(errfd); if((setpgid(pid, pid) < 0 && errno != EACCES) || getpgid(pid) != pid){ kill(pid, SIGKILL); while(waitpid(pid, &status, 0) < 0 && errno == EINTR) ; close(displayfd[0]); return fail("isolate Xvfb process group: %s", strerror(errno)); } r->xvfb.pid = pid; r->xvfb.pgid = pid; pfd.fd = displayfd[0]; pfd.events = POLLIN|POLLHUP; deadline = nowms() + Starttimeout; n = 0; while(n + 1 < (int)sizeof line){ pfd.revents = 0; status = poll(&pfd, 1, leftms(deadline)); if(status < 0 && errno == EINTR) continue; if(status <= 0) break; nr = read(displayfd[0], line + n, sizeof line - n - 1); if(nr > 0){ n += nr; line[n] = '\0'; if(strchr(line, '\n') != NULL) break; continue; } if(nr < 0 && errno == EINTR) continue; break; } close(displayfd[0]); if(n == 0 || strchr(line, '\n') == NULL) return fail("Xvfb did not report a private display"); line[strcspn(line, "\r\n")] = '\0'; for(n = 0; line[n] != '\0'; n++) if(line[n] < '0' || line[n] > '9') return fail("invalid Xvfb display number %s", line); if(snprintf(r->display, sizeof r->display, ":%s", line) >= (int)sizeof r->display) return fail("Xvfb display number is too long"); return 1; } static int spawndaemon(Run *r, char *program, char *mapdir) { int errfd; pid_t pid; errfd = childlog(r->daemon.log); if(errfd < 0) return fail("open daemon log: %s", strerror(errno)); pid = fork(); if(pid < 0){ close(errfd); return fail("fork daemon: %s", strerror(errno)); } if(pid == 0){ setpgid(0, 0); if(dup2(errfd, STDOUT_FILENO) < 0 || dup2(errfd, STDERR_FILENO) < 0) _exit(126); close(errfd); execl(program, program, mapdir, (char*)0); _exit(127); } close(errfd); if((setpgid(pid, pid) < 0 && errno != EACCES) || getpgid(pid) != pid){ kill(pid, SIGKILL); while(waitpid(pid, NULL, 0) < 0 && errno == EINTR) ; return fail("isolate daemon process group: %s", strerror(errno)); } r->daemon.pid = pid; r->daemon.pgid = pid; return 1; } static int waitsocket(Run *r) { struct stat st; int status; 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(waitpid(r->daemon.pid, &status, WNOHANG) == r->daemon.pid){ r->daemon.pid = -1; 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); r->xvfb.pid = r->daemon.pid = -1; r->xvfb.pgid = r->daemon.pgid = -1; if(!livesetup(&r->l, "xim")) return 0; if(snprintf(r->xvfb.log, sizeof r->xvfb.log, "%s/xvfb.log", r->l.root) >= (int)sizeof r->xvfb.log || snprintf(r->daemon.log, sizeof r->daemon.log, "%s/daemon.log", r->l.root) >= (int)sizeof r->daemon.log) return fail("temporary path is too long"); if(!spawnxvfb(r, xvfb)) return 0; if(setenv("DISPLAY", r->display, 1) < 0 || setenv("XMODIFIERS", "@im=strans", 1) < 0 || setenv("XDG_RUNTIME_DIR", r->l.runtime, 1) < 0 || setenv("XDG_CONFIG_HOME", r->l.config, 1) < 0 || setenv("HOME", r->l.home, 1) < 0 || unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 || unsetenv("IBUS_ADDRESS") < 0) return fail("set private environment: %s", strerror(errno)); if(!spawndaemon(r, daemon, mapdir)) return 0; return waitsocket(r); } static void showlog(char *name) { char buf[4096]; ssize_t n; int fd; fd = open(name, O_RDONLY|O_CLOEXEC); if(fd < 0) return; while((n = read(fd, buf, sizeof buf)) > 0) fwrite(buf, 1, n, stderr); close(fd); } static void stopchild(Child *c) { int i, status; pid_t n; if(c->pgid > 0 && kill(-c->pgid, SIGTERM) < 0 && errno != ESRCH) fail("terminate process group %ld: %s", (long)c->pgid, strerror(errno)); if(c->pid > 0) for(i = 0; i < Stoptimeout / 20; i++){ n = waitpid(c->pid, &status, WNOHANG); if(n == c->pid || (n < 0 && errno == ECHILD)){ c->pid = -1; break; } if(n < 0 && errno != EINTR) break; pausems(20); } if(c->pgid > 0) kill(-c->pgid, SIGKILL); if(c->pid > 0){ while(waitpid(c->pid, &status, 0) < 0 && errno == EINTR) ; c->pid = -1; } c->pgid = -1; } static void cleanup(Run *r, int noisy) { stopchild(&r->daemon); stopchild(&r->xvfb); if(noisy){ fprintf(stderr, "%s: daemon log:\n", testname); showlog(r->daemon.log); fprintf(stderr, "%s: Xvfb log:\n", testname); showlog(r->xvfb.log); } if(r->daemon.log[0] != '\0') unlink(r->daemon.log); if(r->xvfb.log[0] != '\0') unlink(r->xvfb.log); 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 || status == XLookupBoth) && ((size_t)n >= cap || commit == NULL))) return 0; if((status == XLookupChars || status == XLookupBoth) && 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 || status == XLookupBoth) && 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; } 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) || !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; } memset(&run, 0, sizeof run); run.xvfb.pid = run.daemon.pid = -1; run.xvfb.pgid = run.daemon.pgid = -1; 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.display); if(dpy == NULL){ fail("open private display %s", run.display); cleanup(&run, 1); return 1; } im = openim(dpy); if(im == NULL){ fail("open strans XIM on private display %s", run.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; }