From f8ebfcdc866e6ba57b5ea0de2a5c5006c134fba6 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 00:02:23 +0900 Subject: [PATCH] tests: cover ipc listener capacity and recovery --- .gitignore | 1 + tests/Makefile | 6 +- tests/ipc_live_test.c | 759 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 765 insertions(+), 1 deletion(-) create mode 100644 tests/ipc_live_test.c diff --git a/.gitignore b/.gitignore index 016337e..dd10699 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /tests/*.o /tests/unit_test /tests/ibus_live_test +/tests/ipc_live_test /xim/*.o /xim/strans-xim /xim/xim_test diff --git a/tests/Makefile b/tests/Makefile index f742d83..67ae5c7 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -9,7 +9,7 @@ IBUS_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) LIBS = -lthread -lbio $(FT_LIBS) $(IBUS_LIBS) PROG = unit_test -LIVE = ibus_live_test +LIVE = ibus_live_test ipc_live_test TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \ ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \ popup_test.c font_test.c ibus_test.c server_test.c @@ -24,6 +24,7 @@ all: $(PROG) $(LIVE) check test: $(PROG) $(LIVE) ./$(PROG) $(TESTARGS) ./ibus_live_test ../strans ../map + ./ipc_live_test ../strans ../map $(PROG): $(OBJS) $(LD) -o $@ $(OBJS) $(LIBS) @@ -31,6 +32,9 @@ $(PROG): $(OBJS) ibus_live_test: ibus_live_test.c $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g $(IBUS_CFLAGS) -o $@ $< $(IBUS_LIBS) +ipc_live_test: ipc_live_test.c ../ipc.c ../ipc.h + $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. -o $@ ipc_live_test.c ../ipc.c + $(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h ../cutest/cutest.h engine_test.o: ../strans.c ibus_test.o: CFLAGS += $(IBUS_CFLAGS) diff --git a/tests/ipc_live_test.c b/tests/ipc_live_test.c new file mode 100644 index 0000000..aba2af4 --- /dev/null +++ b/tests/ipc_live_test.c @@ -0,0 +1,759 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../ipc.h" + +enum +{ + Maxclients = 64, + Calltimeout = 4000, + Starttimeout = 8000, + Stoptimeout = 3000, +}; + +typedef struct Daemon Daemon; +typedef struct Response Response; + +struct Daemon +{ + pid_t pid; + int errfd; + char root[256]; + char runtime[320]; + char config[320]; + char ibus[384]; + char bus[448]; + char home[320]; + char socket[384]; + char err[4096]; + size_t nerr; +}; + +struct Response +{ + int eaten; + char commit[Ipcfieldmax+1]; + char preedit[Ipcfieldmax+1]; +}; + +static int +fail(char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "ipc_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 int +makedir(char *path) +{ + if(mkdir(path, 0700) == 0) + return 1; + return fail("mkdir %s: %s", path, strerror(errno)); +} + +static void +readerrors(Daemon *d) +{ + ssize_t n; + + if(d->errfd < 0) + return; + while(d->nerr + 1 < sizeof d->err){ + n = read(d->errfd, d->err + d->nerr, + sizeof d->err - d->nerr - 1); + if(n > 0){ + d->nerr += n; + continue; + } + if(n < 0 && errno == EINTR) + continue; + break; + } + d->err[d->nerr] = '\0'; +} + +static void +showerrors(Daemon *d) +{ + readerrors(d); + if(d->nerr != 0) + fprintf(stderr, "ipc_live_test: daemon stderr:\n%s", d->err); +} + +static int +waitsocket(Daemon *d, int notifyfd) +{ + struct pollfd pfd[2]; + struct stat st; + char buf[4096]; + int n, status, timeout; + int64_t deadline; + + deadline = nowms() + Starttimeout; + for(;;){ + if(lstat(d->socket, &st) == 0 && S_ISSOCK(st.st_mode) && + (st.st_mode & 0777) == 0600) + return 1; + if(waitpid(d->pid, &status, WNOHANG) == d->pid){ + d->pid = -1; + readerrors(d); + return fail("daemon exited before creating IPC socket"); + } + timeout = leftms(deadline); + if(timeout == 0) + return fail("timed out waiting for protected IPC socket %s", + d->socket); + pfd[0].fd = notifyfd; + pfd[0].events = POLLIN; + pfd[0].revents = 0; + pfd[1].fd = d->errfd; + pfd[1].events = POLLIN; + pfd[1].revents = 0; + n = poll(pfd, 2, timeout); + if(n < 0 && errno == EINTR) + continue; + if(n < 0) + return fail("poll for IPC socket: %s", strerror(errno)); + if(n == 0) + continue; + if(pfd[1].revents != 0) + readerrors(d); + if(pfd[0].revents & POLLIN) + while(read(notifyfd, buf, sizeof buf) < 0 && errno == EINTR) + ; + } +} + +static int +startdaemon(Daemon *d, char *program, char *mapdir) +{ + char missing[384]; + int notifyfd, watch, errpipe[2]; + pid_t pid; + + memset(d, 0, sizeof *d); + d->pid = -1; + d->errfd = -1; + snprintf(d->root, sizeof d->root, "/tmp/strans-ipc.XXXXXX"); + if(mkdtemp(d->root) == NULL) + return fail("mkdtemp: %s", strerror(errno)); + if(snprintf(d->runtime, sizeof d->runtime, "%s/runtime", d->root) + >= (int)sizeof d->runtime || + snprintf(d->config, sizeof d->config, "%s/config", d->root) + >= (int)sizeof d->config || + snprintf(d->ibus, sizeof d->ibus, "%s/ibus", d->config) + >= (int)sizeof d->ibus || + snprintf(d->bus, sizeof d->bus, "%s/bus", d->ibus) + >= (int)sizeof d->bus || + snprintf(d->home, sizeof d->home, "%s/home", d->root) + >= (int)sizeof d->home || + snprintf(d->socket, sizeof d->socket, "%s/strans.sock", d->runtime) + >= (int)sizeof d->socket || + snprintf(missing, sizeof missing, "%s/no-such-font.ttf", d->root) + >= (int)sizeof missing) + return fail("temporary path is too long"); + if(!makedir(d->runtime) || !makedir(d->config) || !makedir(d->ibus) || + !makedir(d->bus) || !makedir(d->home)) + return 0; + notifyfd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK); + if(notifyfd < 0) + return fail("inotify_init1: %s", strerror(errno)); + watch = inotify_add_watch(notifyfd, d->runtime, + IN_CREATE|IN_MOVED_TO|IN_ATTRIB); + if(watch < 0){ + close(notifyfd); + return fail("inotify_add_watch: %s", strerror(errno)); + } + if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0){ + close(notifyfd); + return fail("pipe2: %s", strerror(errno)); + } + pid = fork(); + if(pid < 0){ + close(errpipe[0]); + close(errpipe[1]); + close(notifyfd); + return fail("fork: %s", strerror(errno)); + } + if(pid == 0){ + close(errpipe[0]); + close(notifyfd); + if(dup2(errpipe[1], STDOUT_FILENO) < 0 || + dup2(errpipe[1], STDERR_FILENO) < 0) + _exit(126); + close(errpipe[1]); + setenv("XDG_RUNTIME_DIR", d->runtime, 1); + setenv("XDG_CONFIG_HOME", d->config, 1); + setenv("HOME", d->home, 1); + unsetenv("DISPLAY"); + unsetenv("WAYLAND_DISPLAY"); + unsetenv("DBUS_SESSION_BUS_ADDRESS"); + unsetenv("IBUS_ADDRESS"); + execl(program, program, mapdir, missing, (char*)0); + dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno)); + _exit(127); + } + close(errpipe[1]); + d->pid = pid; + d->errfd = errpipe[0]; + if(!waitsocket(d, notifyfd)){ + inotify_rm_watch(notifyfd, watch); + close(notifyfd); + return 0; + } + inotify_rm_watch(notifyfd, watch); + close(notifyfd); + return 1; +} + +static int +clearbus(Daemon *d) +{ + DIR *dir; + struct dirent *de; + char path[576]; + int ok; + + dir = opendir(d->bus); + if(dir == NULL) + return errno == ENOENT || fail("open cleanup directory %s: %s", + d->bus, strerror(errno)); + ok = 1; + errno = 0; + while((de = readdir(dir)) != NULL){ + if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + if(snprintf(path, sizeof path, "%s/%s", d->bus, de->d_name) + >= (int)sizeof path){ + fail("cleanup path is too long: %s", de->d_name); + ok = 0; + continue; + } + if(unlink(path) < 0 && errno != ENOENT){ + fail("remove IBus address file %s: %s", de->d_name, + strerror(errno)); + ok = 0; + } + errno = 0; + } + if(errno != 0){ + fail("read cleanup directory %s: %s", d->bus, strerror(errno)); + ok = 0; + } + closedir(dir); + return ok; +} + +static int +rmdirknown(char *path) +{ + if(path[0] == '\0' || rmdir(path) == 0 || errno == ENOENT) + return 1; + return fail("rmdir %s: %s", path, strerror(errno)); +} + +static int +stopdaemon(Daemon *d) +{ + struct pollfd pfd; + int ok, status, n; + int64_t deadline; + + ok = 1; + if(d->pid > 0){ + if(kill(d->pid, SIGTERM) < 0 && errno != ESRCH){ + fail("kill %ld: %s", (long)d->pid, strerror(errno)); + ok = 0; + } + deadline = nowms() + Stoptimeout; + for(;;){ + n = waitpid(d->pid, &status, WNOHANG); + if(n == d->pid) + break; + if(n < 0 && errno != EINTR){ + fail("waitpid %ld: %s", (long)d->pid, strerror(errno)); + ok = 0; + break; + } + n = leftms(deadline); + if(n == 0){ + fail("daemon %ld did not stop after SIGTERM", (long)d->pid); + ok = 0; + kill(d->pid, SIGKILL); + while(waitpid(d->pid, &status, 0) < 0 && errno == EINTR) + ; + break; + } + pfd.fd = d->errfd; + pfd.events = POLLIN|POLLHUP; + pfd.revents = 0; + if(poll(&pfd, 1, n) < 0 && errno != EINTR){ + fail("poll daemon %ld: %s", (long)d->pid, strerror(errno)); + ok = 0; + kill(d->pid, SIGKILL); + while(waitpid(d->pid, &status, 0) < 0 && errno == EINTR) + ; + break; + } + readerrors(d); + } + d->pid = -1; + } + readerrors(d); + if(d->errfd >= 0){ + close(d->errfd); + d->errfd = -1; + } + if(d->socket[0] != '\0' && unlink(d->socket) < 0 && errno != ENOENT){ + fail("remove IPC socket %s: %s", d->socket, strerror(errno)); + ok = 0; + } + if(!clearbus(d)) ok = 0; + if(!rmdirknown(d->bus)) ok = 0; + if(!rmdirknown(d->ibus)) ok = 0; + if(!rmdirknown(d->config)) ok = 0; + if(!rmdirknown(d->runtime)) ok = 0; + if(!rmdirknown(d->home)) ok = 0; + if(!rmdirknown(d->root)) ok = 0; + return ok; +} + +static int +connectuntil(char *path, int64_t deadline) +{ + struct sockaddr_un addr; + struct pollfd pfd; + socklen_t nerr; + int err, fd, flags, n; + + memset(&addr, 0, sizeof addr); + addr.sun_family = AF_UNIX; + if(snprintf(addr.sun_path, sizeof addr.sun_path, "%s", path) + >= (int)sizeof addr.sun_path){ + errno = ENAMETOOLONG; + return -1; + } + fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if(fd < 0) + return -1; + if(connect(fd, (struct sockaddr*)&addr, sizeof addr) < 0 && + errno != EINPROGRESS){ + err = errno; + close(fd); + errno = err; + return -1; + } + pfd.fd = fd; + pfd.events = POLLOUT; + pfd.revents = 0; + for(;;){ + n = leftms(deadline); + if(n == 0){ + close(fd); + errno = ETIMEDOUT; + return -1; + } + n = poll(&pfd, 1, n); + if(n < 0 && errno == EINTR) + continue; + if(n <= 0){ + err = n == 0 ? ETIMEDOUT : errno; + close(fd); + errno = err; + return -1; + } + break; + } + err = 0; + nerr = sizeof err; + if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &nerr) < 0 || err != 0){ + if(err == 0) + err = errno; + close(fd); + errno = err; + return -1; + } + flags = fcntl(fd, F_GETFL); + if(flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0){ + err = errno; + close(fd); + errno = err; + return -1; + } + return fd; +} + +/* 1 is complete, 0 is peer closure, and -1 is a timeout or I/O error. */ +static int +readuntil(int fd, void *buf, size_t n, int64_t deadline) +{ + struct pollfd pfd; + unsigned char *p; + ssize_t r; + int timeout; + + p = buf; + while(n > 0){ + timeout = leftms(deadline); + if(timeout == 0){ + errno = ETIMEDOUT; + return -1; + } + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + r = poll(&pfd, 1, timeout); + if(r < 0){ + if(errno == EINTR) + continue; + return -1; + } + if(r == 0){ + errno = ETIMEDOUT; + return -1; + } + r = recv(fd, p, n, 0); + if(r < 0 && errno == EINTR) + continue; + if(r < 0) + return -1; + if(r == 0) + return 0; + p += r; + n -= r; + } + return 1; +} + +static size_t +getlen(unsigned char p[Ipclensz]) +{ + return p[0] | (p[1] << 8); +} + +static int +readresponseuntil(int fd, int want, Response *res, int64_t deadline, + int quiet) +{ + unsigned char hdr[Ipcresphdrsz], len[Ipclensz]; + size_t n; + int rv; + + memset(res, 0, sizeof *res); + rv = readuntil(fd, hdr, sizeof hdr, deadline); + if(rv <= 0){ + if(!quiet) + fail("read response header: %s", + rv == 0 ? "peer closed" : strerror(errno)); + return rv; + } + res->eaten = hdr[0] != 0; + n = getlen(hdr + 1); + if(n > Ipcfieldmax){ + fail("invalid commit length %zu", n); + return -1; + } + if(n != 0){ + rv = readuntil(fd, res->commit, n, deadline); + if(rv <= 0){ + if(!quiet) + fail("read commit: %s", + rv == 0 ? "peer closed" : strerror(errno)); + return rv; + } + } + res->commit[n] = '\0'; + if(!want) + return 1; + rv = readuntil(fd, len, sizeof len, deadline); + if(rv <= 0){ + if(!quiet) + fail("read preedit length: %s", + rv == 0 ? "peer closed" : strerror(errno)); + return rv; + } + n = getlen(len); + if(n > Ipcfieldmax){ + fail("invalid preedit length %zu", n); + return -1; + } + if(n != 0){ + rv = readuntil(fd, res->preedit, n, deadline); + if(rv <= 0){ + if(!quiet) + fail("read preedit: %s", + rv == 0 ? "peer closed" : strerror(errno)); + return rv; + } + } + res->preedit[n] = '\0'; + return 1; +} + +static int +sendkey(int fd, int want, uint32_t mod, uint32_t key) +{ + unsigned char req[Ipcreqsz]; + + ipcpackreq(req, want, mod, key); + return ipcsend(fd, req, sizeof req) == 0; +} + +static int +sendreset(int fd, int want) +{ + unsigned char req[Ipcreqsz]; + + ipcpackreset(req, want); + return ipcsend(fd, req, sizeof req) == 0; +} + +static int +expectresponse(int fd, int want, int eaten, char *commit, char *preedit, + char *where) +{ + Response res; + + if(readresponseuntil(fd, want, &res, nowms() + Calltimeout, 0) != 1) + return 0; + if(res.eaten != eaten || strcmp(res.commit, commit) != 0 || + (want && strcmp(res.preedit, preedit) != 0)) + return fail("%s: eaten=%d commit=%s preedit=%s", + where, res.eaten, res.commit, res.preedit); + return 1; +} + +static int +requestreset(int fd, int want, int eaten, char *preedit, char *where) +{ + if(!sendreset(fd, want)) + return fail("send %s: %s", where, strerror(errno)); + return expectresponse(fd, want, eaten, "", preedit, where); +} + +static int +requestkey(int fd, int want, uint32_t mod, uint32_t key, int eaten, + char *commit, char *preedit, char *where) +{ + if(!sendkey(fd, want, mod, key)) + return fail("send %s: %s", where, strerror(errno)); + return expectresponse(fd, want, eaten, commit, preedit, where); +} + +static int +tryclient(char *path, int64_t deadline, int *client) +{ + Response res; + int fd, rv; + + fd = connectuntil(path, deadline); + if(fd < 0) + return errno == ETIMEDOUT ? -1 : 0; + if(!sendkey(fd, 1, 0, Kmodfirst)){ + close(fd); + return 0; + } + rv = readresponseuntil(fd, 1, &res, deadline, 1); + if(rv != 1){ + close(fd); + return rv < 0 && errno == ETIMEDOUT ? -1 : 0; + } + if(res.eaten || res.commit[0] != '\0' || res.preedit[0] != '\0'){ + close(fd); + fail("replacement client returned invalid modifier response"); + return -1; + } + *client = fd; + return 1; +} + +static int +waitslot(char *path, int *client, char *where) +{ + int rv; + int64_t deadline; + + deadline = nowms() + Calltimeout; + for(;;){ + rv = tryclient(path, deadline, client); + if(rv > 0) + return 1; + if(rv < 0 || leftms(deadline) == 0) + return fail("%s: timed out waiting for a worker slot", where); + } +} + +static int +overflowrejected(char *path) +{ + struct pollfd pfd; + unsigned char byte; + ssize_t n; + int fd, timeout; + int64_t deadline; + + deadline = nowms() + Calltimeout; + fd = connectuntil(path, deadline); + if(fd < 0) + return fail("overflow connect: %s", strerror(errno)); + if(!sendkey(fd, 1, 0, Kmodfirst)){ + close(fd); + return 1; + } + for(;;){ + timeout = leftms(deadline); + if(timeout == 0){ + close(fd); + return fail("overflow client was not rejected"); + } + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + n = poll(&pfd, 1, timeout); + if(n < 0 && errno == EINTR) + continue; + if(n <= 0){ + close(fd); + return fail("poll overflow client: %s", + n == 0 ? "timed out" : strerror(errno)); + } + n = recv(fd, &byte, 1, 0); + if(n < 0 && errno == EINTR) + continue; + if(n == 0 || (n < 0 && (errno == ECONNRESET || errno == EPIPE))){ + close(fd); + return 1; + } + if(n < 0){ + close(fd); + return fail("read overflow rejection: %s", strerror(errno)); + } + close(fd); + return fail("overflow client received a response"); + } +} + +static int +runlistener(Daemon *d) +{ + int client[Maxclients]; + int i, ok; + + for(i = 0; i < Maxclients; i++) + client[i] = -1; + ok = 0; + client[0] = connectuntil(d->socket, nowms() + Calltimeout); + if(client[0] < 0){ + fail("connect first client: %s", strerror(errno)); + goto out; + } + if(!requestkey(client[0], 1, Mctrl, 'n', 1, "", "", "select Japanese") || + !requestkey(client[0], 1, 0, 'k', 1, "", "k", "GTK preedit") || + !requestreset(client[0], 1, 1, "", "active reset")) + goto out; + if(!sendkey(client[0], 0, 0, 'k') || + !sendkey(client[0], 1, 0, Kmodfirst)){ + fail("send pipelined XIM/GTK requests: %s", strerror(errno)); + goto out; + } + if(!expectresponse(client[0], 0, 1, "", "", "XIM framing") || + !expectresponse(client[0], 1, 0, "", "k", "GTK after XIM framing")) + goto out; + for(i = 1; i < Maxclients; i++){ + client[i] = connectuntil(d->socket, nowms() + Calltimeout); + if(client[i] < 0){ + fail("connect capacity client %d: %s", i, strerror(errno)); + goto out; + } + if(!requestkey(client[i], 1, 0, Kmodfirst, 0, "", "", + "capacity modifier")) + goto out; + } + if(!overflowrejected(d->socket)) + goto out; + close(client[Maxclients-1]); + client[Maxclients-1] = -1; + if(!waitslot(d->socket, &client[Maxclients-1], "inactive slot recovery")) + goto out; + close(client[0]); + client[0] = -1; + if(!waitslot(d->socket, &client[0], "active slot recovery") || + !requestkey(client[0], 1, 0, 'a', 1, "", "あ", + "post-disconnect composition")) + goto out; + ok = 1; +out: + for(i = 0; i < Maxclients; i++) + if(client[i] >= 0) + close(client[i]); + return ok; +} + +int +main(int argc, char **argv) +{ + Daemon daemon; + int ok; + + if(argc != 3){ + fprintf(stderr, "usage: ipc_live_test strans mapdir\n"); + return 2; + } + ok = startdaemon(&daemon, argv[1], argv[2]); + if(ok) + ok = runlistener(&daemon); + if(!ok) + showerrors(&daemon); + if(!stopdaemon(&daemon)) + ok = 0; + if(!ok) + return 1; + printf("ipc live listener: ok\n"); + return 0; +}