From 469278209fc1bd6bcf27938982c3ffc358c559e6 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 01:29:00 +0900 Subject: [PATCH] tests: verify failed startup publishes no endpoints --- .gitignore | 1 + tests/Makefile | 6 +- tests/daemon_failure_test.c | 635 ++++++++++++++++++++++++++++++++++++ 3 files changed, 641 insertions(+), 1 deletion(-) create mode 100644 tests/daemon_failure_test.c diff --git a/.gitignore b/.gitignore index f1763f2..571ee70 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /tests/ibus_live_test /tests/ipc_live_test /tests/daemon_collision_test +/tests/daemon_failure_test /xim/*.o /xim/strans-xim /xim/xim_test diff --git a/tests/Makefile b/tests/Makefile index a799001..4e20a34 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 ipc_live_test daemon_collision_test +LIVE = ibus_live_test ipc_live_test daemon_collision_test daemon_failure_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 @@ -26,6 +26,7 @@ check test: $(PROG) $(LIVE) ./ibus_live_test ../strans ../map ./ipc_live_test ../strans ../map ./daemon_collision_test ../strans ../map + ./daemon_failure_test ../strans $(PROG): $(OBJS) $(LD) -o $@ $(OBJS) $(LIBS) @@ -40,6 +41,9 @@ daemon_collision_test: daemon_collision_test.c ../ipc.c ../ipc.h $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. $(IBUS_CFLAGS) -o $@ \ daemon_collision_test.c ../ipc.c $(IBUS_LIBS) +daemon_failure_test: daemon_failure_test.c + $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -o $@ $< + $(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/daemon_failure_test.c b/tests/daemon_failure_test.c new file mode 100644 index 0000000..f4e02f6 --- /dev/null +++ b/tests/daemon_failure_test.c @@ -0,0 +1,635 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum +{ + Failtimeout = 8000, +}; + +typedef struct Test Test; + +struct Test +{ + pid_t child; + int errfd; + int notifyfd; + int runtimewd; + int buswd; + int endpointclean; + char root[256]; + char runtime[320]; + char config[320]; + char ibus[384]; + char bus[448]; + char home[320]; + char socket[384]; + char badmap[320]; + char missingfont[384]; + char expected[768]; + char err[4096]; + size_t nerr; +}; + +static uint32_t endpointmask = IN_CREATE|IN_MOVED_TO|IN_MOVED_FROM|IN_ATTRIB| + IN_DELETE|IN_MODIFY|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF; + +static int +fail(char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "daemon_failure_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 -1; + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int +leftms(int64_t deadline) +{ + int64_t n; + + n = nowms(); + if(n < 0) + return -1; + n = deadline - n; + 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(Test *t) +{ + ssize_t n; + + if(t->errfd < 0) + return; + while(t->nerr + 1 < sizeof t->err){ + n = read(t->errfd, t->err + t->nerr, + sizeof t->err - t->nerr - 1); + if(n > 0){ + t->nerr += n; + continue; + } + if(n < 0 && errno == EINTR) + continue; + break; + } + t->err[t->nerr] = '\0'; +} + +static void +showerrors(Test *t) +{ + readerrors(t); + if(t->nerr != 0) + fprintf(stderr, "daemon_failure_test: daemon stderr:\n%s", t->err); +} + +static int +setup(Test *t) +{ + memset(t, 0, sizeof *t); + t->child = -1; + t->errfd = -1; + t->notifyfd = -1; + t->runtimewd = -1; + t->buswd = -1; + t->endpointclean = 1; + if(nowms() < 0) + return fail("read monotonic clock: %s", strerror(errno)); + snprintf(t->root, sizeof t->root, "/tmp/strans-failure.XXXXXX"); + if(mkdtemp(t->root) == NULL){ + t->root[0] = '\0'; + return fail("mkdtemp: %s", strerror(errno)); + } + if(snprintf(t->runtime, sizeof t->runtime, "%s/runtime", t->root) + >= (int)sizeof t->runtime || + snprintf(t->config, sizeof t->config, "%s/config", t->root) + >= (int)sizeof t->config || + snprintf(t->ibus, sizeof t->ibus, "%s/ibus", t->config) + >= (int)sizeof t->ibus || + snprintf(t->bus, sizeof t->bus, "%s/bus", t->ibus) + >= (int)sizeof t->bus || + snprintf(t->home, sizeof t->home, "%s/home", t->root) + >= (int)sizeof t->home || + snprintf(t->socket, sizeof t->socket, "%s/strans.sock", t->runtime) + >= (int)sizeof t->socket || + snprintf(t->badmap, sizeof t->badmap, "%s/missing-map", t->root) + >= (int)sizeof t->badmap || + snprintf(t->missingfont, sizeof t->missingfont, + "%s/missing-font.ttf", t->root) >= (int)sizeof t->missingfont || + snprintf(t->expected, sizeof t->expected, + "strans: can't open: %s/hira.map\n", t->badmap) + >= (int)sizeof t->expected) + return fail("temporary path is too long"); + if(!makedir(t->runtime) || !makedir(t->config) || !makedir(t->ibus) || + !makedir(t->bus) || !makedir(t->home)) + return 0; + t->notifyfd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK); + if(t->notifyfd < 0) + return fail("inotify_init1: %s", strerror(errno)); + t->runtimewd = inotify_add_watch(t->notifyfd, t->runtime, endpointmask); + if(t->runtimewd < 0) + return fail("watch runtime directory: %s", strerror(errno)); + t->buswd = inotify_add_watch(t->notifyfd, t->bus, endpointmask); + if(t->buswd < 0) + return fail("watch IBus directory: %s", strerror(errno)); + return 1; +} + +static int +startchild(Test *t, char *program) +{ + int errpipe[2], fd; + long maxfd; + pid_t pid; + + if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0) + return fail("pipe2: %s", strerror(errno)); + pid = fork(); + if(pid < 0){ + close(errpipe[0]); + close(errpipe[1]); + return fail("fork: %s", strerror(errno)); + } + if(pid == 0){ + close(errpipe[0]); + if(dup2(errpipe[1], STDERR_FILENO) < 0) + _exit(126); + close(errpipe[1]); + if(close_range(3, UINT_MAX, 0) < 0){ + maxfd = sysconf(_SC_OPEN_MAX); + if(maxfd < 0) + maxfd = 1024; + for(fd = 3; fd < maxfd; fd++) + close(fd); + } + if(setenv("XDG_RUNTIME_DIR", t->runtime, 1) < 0 || + setenv("XDG_CONFIG_HOME", t->config, 1) < 0 || + setenv("HOME", t->home, 1) < 0 || unsetenv("DISPLAY") < 0 || + unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 || + unsetenv("IBUS_ADDRESS") < 0){ + dprintf(STDERR_FILENO, "set daemon environment: %s\n", + strerror(errno)); + _exit(126); + } + execl(program, program, t->badmap, t->missingfont, (char*)0); + dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno)); + _exit(127); + } + close(errpipe[1]); + t->child = pid; + t->errfd = errpipe[0]; + return 1; +} + +static int +drainnotify(Test *t) +{ + char buf[4096]; + char *where; + struct inotify_event *ev; + ssize_t n; + size_t off; + int ok; + + ok = 1; + for(;;){ + n = read(t->notifyfd, buf, sizeof buf); + if(n < 0 && errno == EINTR) + continue; + if(n < 0 && errno == EAGAIN) + return ok; + if(n < 0) + return fail("read endpoint watches: %s", strerror(errno)); + if(n == 0) + return ok; + for(off = 0; off + sizeof *ev <= (size_t)n; + off += sizeof *ev + ev->len){ + ev = (struct inotify_event*)(buf + off); + if(off + sizeof *ev + ev->len > (size_t)n) + return fail("truncated inotify event"); + if(ev->mask & IN_Q_OVERFLOW){ + t->endpointclean = 0; + fail("endpoint watch queue overflowed"); + ok = 0; + continue; + } + if(ev->mask & (IN_IGNORED|IN_UNMOUNT)){ + t->endpointclean = 0; + fail("endpoint watch became invalid: %#x", ev->mask); + ok = 0; + continue; + } + if(ev->wd != t->runtimewd && ev->wd != t->buswd) + continue; + if(!(ev->mask & endpointmask)) + continue; + where = ev->wd == t->runtimewd ? "runtime" : "IBus"; + t->endpointclean = 0; + fail("unexpected %s endpoint event %#x for %s", where, + ev->mask, ev->len != 0 ? ev->name : "directory"); + ok = 0; + } + if(off != (size_t)n) + return fail("truncated inotify event buffer"); + } +} + +static int +killowned(Test *t, int *status) +{ + pid_t pid; + int n, ok; + + if(t->child <= 0) + return 1; + pid = t->child; + ok = 1; + if(kill(pid, SIGKILL) < 0 && errno != ESRCH){ + fail("kill -9 %ld: %s", (long)pid, strerror(errno)); + ok = 0; + } + do + n = waitpid(pid, status, 0); + while(n < 0 && errno == EINTR); + if(n != pid){ + fail("reap %ld after SIGKILL: %s", (long)pid, + n < 0 ? strerror(errno) : "wrong child"); + ok = 0; + }else + t->child = -1; + return ok; +} + +static int +waitfailed(Test *t) +{ + struct pollfd pfd[2]; + pid_t pid; + int n, ok, status, timeout; + int64_t deadline; + + pid = t->child; + ok = 1; + deadline = nowms(); + if(deadline < 0){ + fail("read monotonic clock before waiting for daemon: %s", + strerror(errno)); + killowned(t, &status); + readerrors(t); + drainnotify(t); + return 0; + } + deadline += Failtimeout; + for(;;){ + do + n = waitpid(pid, &status, WNOHANG); + while(n < 0 && errno == EINTR); + if(n == pid){ + t->child = -1; + readerrors(t); + if(!drainnotify(t)) + ok = 0; + break; + } + if(n < 0){ + fail("waitpid failed daemon %ld: %s", (long)pid, + strerror(errno)); + if(errno == ECHILD){ + t->child = -1; + readerrors(t); + drainnotify(t); + }else{ + killowned(t, &status); + readerrors(t); + drainnotify(t); + } + return 0; + } + timeout = leftms(deadline); + if(timeout < 0){ + fail("read monotonic clock while waiting for daemon: %s", + strerror(errno)); + killowned(t, &status); + readerrors(t); + drainnotify(t); + return 0; + } + if(timeout == 0){ + fail("daemon %ld did not exit after map initialization failure", + (long)pid); + killowned(t, &status); + return 0; + } + pfd[0].fd = t->notifyfd; + pfd[0].events = POLLIN; + pfd[0].revents = 0; + pfd[1].fd = t->errfd; + pfd[1].events = POLLIN|POLLHUP; + pfd[1].revents = 0; + n = poll(pfd, 2, timeout); + if(n < 0 && errno == EINTR) + continue; + if(n < 0){ + fail("poll failed daemon: %s", strerror(errno)); + killowned(t, &status); + readerrors(t); + drainnotify(t); + return 0; + } + if(pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL)){ + fail("endpoint watch became unusable: %#x", pfd[0].revents); + killowned(t, &status); + readerrors(t); + drainnotify(t); + return 0; + } + if(pfd[1].revents & (POLLERR|POLLNVAL)){ + fail("daemon stderr pipe became unusable: %#x", + pfd[1].revents); + killowned(t, &status); + readerrors(t); + drainnotify(t); + return 0; + } + if((pfd[0].revents & POLLIN) && !drainnotify(t)) + ok = 0; + if(pfd[1].revents & (POLLIN|POLLHUP)) + readerrors(t); + } + if(!WIFEXITED(status) || WEXITSTATUS(status) != 1) + return fail("daemon map failure had wait status %#x", status); + if(strcmp(t->err, t->expected) != 0) + return fail("daemon did not report the expected map initialization failure"); + return ok && t->endpointclean; +} + +static int +dirempty(char *path, char *which) +{ + DIR *dir; + struct dirent *de; + int count, ok; + + dir = opendir(path); + if(dir == NULL) + return fail("open %s directory %s: %s", which, path, strerror(errno)); + count = 0; + ok = 1; + errno = 0; + while((de = readdir(dir)) != NULL){ + if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + count++; + fail("unexpected %s directory entry %s", which, de->d_name); + ok = 0; + errno = 0; + } + if(errno != 0){ + fail("read %s directory %s: %s", which, path, strerror(errno)); + ok = 0; + } + if(closedir(dir) < 0){ + fail("close %s directory %s: %s", which, path, strerror(errno)); + ok = 0; + } + if(count != 0) + ok = 0; + return ok; +} + +static int +checkendpoints(Test *t) +{ + struct stat st; + int ok; + + ok = 1; + errno = 0; + if(lstat(t->socket, &st) == 0){ + fail("IPC endpoint exists after failed startup: %s", t->socket); + ok = 0; + }else if(errno != ENOENT){ + fail("lstat failed IPC endpoint: %s", strerror(errno)); + ok = 0; + } + if(!dirempty(t->runtime, "runtime")) + ok = 0; + if(!dirempty(t->bus, "IBus")) + ok = 0; + return ok && t->endpointclean; +} + +static int clearfd(int); + +static int +removeentry(int dirfd, char *name) +{ + struct stat st; + int fd, ok; + + if(fstatat(dirfd, name, &st, AT_SYMLINK_NOFOLLOW) < 0) + return fail("stat cleanup entry %s: %s", name, strerror(errno)); + if(!S_ISDIR(st.st_mode)){ + if(unlinkat(dirfd, name, 0) == 0 || errno == ENOENT) + return 1; + return fail("remove cleanup entry %s: %s", name, strerror(errno)); + } + fd = openat(dirfd, name, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if(fd < 0) + return fail("open cleanup directory %s: %s", name, strerror(errno)); + ok = clearfd(fd); + if(close(fd) < 0){ + fail("close cleanup directory %s: %s", name, strerror(errno)); + ok = 0; + } + if(unlinkat(dirfd, name, AT_REMOVEDIR) < 0 && errno != ENOENT){ + fail("remove cleanup directory %s: %s", name, strerror(errno)); + ok = 0; + } + return ok; +} + +static int +clearfd(int fd) +{ + DIR *dir; + struct dirent *de; + int copy, ok; + + copy = dup(fd); + if(copy < 0) + return fail("duplicate cleanup directory: %s", strerror(errno)); + dir = fdopendir(copy); + if(dir == NULL){ + close(copy); + return fail("open cleanup directory stream: %s", strerror(errno)); + } + ok = 1; + errno = 0; + while((de = readdir(dir)) != NULL){ + if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + if(!removeentry(fd, de->d_name)) + ok = 0; + errno = 0; + } + if(errno != 0){ + fail("read cleanup directory: %s", strerror(errno)); + ok = 0; + } + if(closedir(dir) < 0){ + fail("close cleanup directory stream: %s", strerror(errno)); + ok = 0; + } + return ok; +} + +static int +removeroot(Test *t) +{ + int fd, ok; + + if(t->root[0] == '\0') + return 1; + fd = open(t->root, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if(fd < 0) + return errno == ENOENT || fail("open cleanup root %s: %s", t->root, + strerror(errno)); + ok = clearfd(fd); + if(close(fd) < 0){ + fail("close cleanup root %s: %s", t->root, strerror(errno)); + ok = 0; + } + if(rmdir(t->root) < 0 && errno != ENOENT){ + fail("rmdir %s: %s", t->root, strerror(errno)); + ok = 0; + } + return ok; +} + +static int +cleanup(Test *t) +{ + int n, ok, status; + + ok = 1; + if(t->child > 0){ + do + n = waitpid(t->child, &status, WNOHANG); + while(n < 0 && errno == EINTR); + if(n == t->child) + t->child = -1; + else if(n == 0){ + fail("failed-start daemon still running during cleanup"); + ok = 0; + if(!killowned(t, &status)) + ok = 0; + }else{ + fail("check failed-start daemon during cleanup: %s", + strerror(errno)); + ok = 0; + if(errno == ECHILD) + t->child = -1; + else if(!killowned(t, &status)) + ok = 0; + } + } + readerrors(t); + if(t->errfd >= 0){ + if(close(t->errfd) < 0){ + fail("close daemon stderr: %s", strerror(errno)); + ok = 0; + } + t->errfd = -1; + } + if(t->notifyfd >= 0){ + if(t->runtimewd >= 0 && + inotify_rm_watch(t->notifyfd, t->runtimewd) < 0 && errno != EINVAL){ + fail("remove runtime watch: %s", strerror(errno)); + ok = 0; + } + if(t->buswd >= 0 && + inotify_rm_watch(t->notifyfd, t->buswd) < 0 && errno != EINVAL){ + fail("remove IBus watch: %s", strerror(errno)); + ok = 0; + } + if(close(t->notifyfd) < 0){ + fail("close endpoint watches: %s", strerror(errno)); + ok = 0; + } + t->notifyfd = -1; + } + if(!removeroot(t)) + ok = 0; + return ok; +} + +int +main(int argc, char **argv) +{ + Test test; + int ok; + + if(argc != 2){ + fprintf(stderr, "usage: daemon_failure_test strans\n"); + return 2; + } + ok = setup(&test); + if(ok) + ok = startchild(&test, argv[1]); + if(ok){ + ok = waitfailed(&test); + if(test.child <= 0 && !checkendpoints(&test)) + ok = 0; + } + if(!cleanup(&test)) + ok = 0; + if(!ok){ + showerrors(&test); + return 1; + } + printf("failed daemon startup published no endpoints: ok\n"); + return 0; +}