diff --git a/.gitignore b/.gitignore index 571ee70..a35daeb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /tests/ipc_live_test /tests/daemon_collision_test /tests/daemon_failure_test +/tests/daemon_restart_test /xim/*.o /xim/strans-xim /xim/xim_test diff --git a/tests/Makefile b/tests/Makefile index 4e20a34..a061677 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -9,7 +9,8 @@ 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 daemon_failure_test +LIVE = ibus_live_test ipc_live_test daemon_collision_test daemon_failure_test \ + daemon_restart_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 @@ -27,6 +28,7 @@ check test: $(PROG) $(LIVE) ./ipc_live_test ../strans ../map ./daemon_collision_test ../strans ../map ./daemon_failure_test ../strans + ./daemon_restart_test ../strans ../map $(PROG): $(OBJS) $(LD) -o $@ $(OBJS) $(LIBS) @@ -44,6 +46,10 @@ daemon_collision_test: daemon_collision_test.c ../ipc.c ../ipc.h daemon_failure_test: daemon_failure_test.c $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -o $@ $< +daemon_restart_test: daemon_restart_test.c ../ipc.c ../ipc.h + $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. $(IBUS_CFLAGS) -o $@ \ + daemon_restart_test.c ../ipc.c $(IBUS_LIBS) + $(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_restart_test.c b/tests/daemon_restart_test.c new file mode 100644 index 0000000..a4a33f3 --- /dev/null +++ b/tests/daemon_restart_test.c @@ -0,0 +1,1855 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../ipc.h" + +enum +{ + Calltimeout = 4000, + Starttimeout = 8000, + Stoptimeout = 3000, + Ctrlmask = 1<<2, + + Phaseignore, + Phasequiet, + Phaserecover, +}; + +typedef struct Test Test; + +struct Test +{ + pid_t first; + pid_t firstpid; + pid_t second; + pid_t helper; + int firsterrfd; + int seconderrfd; + int helpererrfd; + int notifyfd; + int runtimewd; + int buswd; + int ipcfirst; + int ipcnew; + int busfirstfd; + int phase; + int socketdeleted; + int socketcreated; + int bustempcreated; + int bustempmoved; + int buspublished; + uint32_t buscookie; + DBusConnection *busfirst; + DBusConnection *busnew; + char root[256]; + char runtime[320]; + char config[320]; + char ibus[384]; + char bus[448]; + char home[320]; + char socket[384]; + char missing[384]; + char addrfile[512]; + char addrbase[256]; + char firstaddress[512]; + char secondaddress[512]; + char firstcontents[2048]; + size_t nfirstcontents; + char secondcontents[2048]; + size_t nsecondcontents; + char bustemp[256]; + struct stat firstsocketst; + struct stat firstaddrst; + struct stat secondsocketst; + struct stat secondaddrst; + char firsterr[8192]; + size_t nfirsterr; + char seconderr[8192]; + size_t nseconderr; + char helpererr[2048]; + size_t nhelpererr; +}; + +static uint32_t watchmask = IN_CREATE|IN_DELETE|IN_ATTRIB|IN_MOVED_FROM| + IN_MOVED_TO|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF; + +static int +fail(char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "daemon_restart_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 +readpipe(int fd, char *buf, size_t cap, size_t *used) +{ + ssize_t n; + + if(fd < 0 || cap == 0) + return; + while(*used + 1 < cap){ + n = read(fd, buf + *used, cap - *used - 1); + if(n > 0){ + *used += n; + continue; + } + if(n < 0 && errno == EINTR) + continue; + break; + } + buf[*used] = '\0'; +} + +static void +showerrors(Test *t) +{ + readpipe(t->firsterrfd, t->firsterr, sizeof t->firsterr, + &t->nfirsterr); + readpipe(t->seconderrfd, t->seconderr, sizeof t->seconderr, + &t->nseconderr); + readpipe(t->helpererrfd, t->helpererr, sizeof t->helpererr, + &t->nhelpererr); + if(t->nfirsterr != 0) + fprintf(stderr, "daemon_restart_test: first daemon stderr:\n%s", + t->firsterr); + if(t->nseconderr != 0) + fprintf(stderr, "daemon_restart_test: second daemon stderr:\n%s", + t->seconderr); + if(t->nhelpererr != 0) + fprintf(stderr, "daemon_restart_test: address helper stderr:\n%s", + t->helpererr); +} + +static int +setup(Test *t) +{ + struct sigaction sa; + + memset(t, 0, sizeof *t); + t->first = -1; + t->firstpid = -1; + t->second = -1; + t->helper = -1; + t->firsterrfd = -1; + t->seconderrfd = -1; + t->helpererrfd = -1; + t->notifyfd = -1; + t->runtimewd = -1; + t->buswd = -1; + t->ipcfirst = -1; + t->ipcnew = -1; + t->busfirstfd = -1; + t->phase = Phaseignore; + memset(&sa, 0, sizeof sa); + sa.sa_handler = SIG_DFL; + if(sigemptyset(&sa.sa_mask) < 0 || sigaction(SIGCHLD, &sa, NULL) < 0) + return fail("establish exact child ownership: %s", strerror(errno)); + if(nowms() < 0) + return fail("read monotonic clock: %s", strerror(errno)); + snprintf(t->root, sizeof t->root, "/tmp/strans-restart.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->missing, sizeof t->missing, "%s/missing-font.ttf", t->root) + >= (int)sizeof t->missing) + 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, watchmask); + if(t->runtimewd < 0) + return fail("watch runtime directory: %s", strerror(errno)); + t->buswd = inotify_add_watch(t->notifyfd, t->bus, watchmask); + if(t->buswd < 0) + return fail("watch IBus directory: %s", strerror(errno)); + return 1; +} + +static int +childstderr(int fd) +{ + int flags; + + if(fd != STDERR_FILENO){ + if(dup2(fd, STDERR_FILENO) < 0) + return 0; + close(fd); + return 1; + } + flags = fcntl(fd, F_GETFD); + if(flags < 0 || fcntl(fd, F_SETFD, flags & ~FD_CLOEXEC) < 0) + return 0; + return 1; +} + +static int +startchild(Test *t, char *program, char *mapdir, pid_t *child, int *errfd) +{ + 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(!childstderr(errpipe[1])) + _exit(126); + 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, mapdir, t->missing, (char*)0); + dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno)); + _exit(127); + } + close(errpipe[1]); + *child = pid; + *errfd = errpipe[0]; + return 1; +} + +static int +runtimeevent(Test *t, struct inotify_event *ev) +{ + uint32_t mask; + + if(ev->len == 0 || strcmp(ev->name, "strans.sock") != 0) + return fail("unexpected runtime entry event %#x for %s", ev->mask, + ev->len != 0 ? ev->name : "directory"); + mask = ev->mask & watchmask; + if(mask & IN_DELETE){ + if(t->socketdeleted || t->socketcreated) + return fail("IPC socket deletion occurred out of order"); + t->socketdeleted = 1; + mask &= ~IN_DELETE; + } + if(mask & IN_CREATE){ + if(!t->socketdeleted || t->socketcreated) + return fail("IPC socket creation occurred before stale deletion"); + t->socketcreated = 1; + mask &= ~IN_CREATE; + } + if(mask & IN_ATTRIB){ + if(!t->socketcreated) + return fail("IPC socket protection occurred before recreation"); + mask &= ~IN_ATTRIB; + } + if(mask != 0) + return fail("unexpected IPC recovery event %#x", ev->mask); + return 1; +} + +static int +tempname(Test *t, char *name) +{ + size_t n, nn; + + n = strlen(t->addrbase); + nn = strlen(name); + if(nn <= n + 5) + return 0; + return strncmp(name, t->addrbase, n) == 0 && + strncmp(name + n, ".tmp.", 5) == 0; +} + +static int +busevent(Test *t, struct inotify_event *ev) +{ + uint32_t mask; + + if(ev->len == 0) + return fail("unexpected unnamed IBus directory event %#x", ev->mask); + mask = ev->mask & watchmask; + if(tempname(t, ev->name)){ + if(mask & IN_CREATE){ + if(t->bustempcreated || snprintf(t->bustemp, sizeof t->bustemp, + "%s", ev->name) >= (int)sizeof t->bustemp) + return fail("invalid replacement IBus temporary file"); + t->bustempcreated = 1; + mask &= ~IN_CREATE; + } + if(!t->bustempcreated || strcmp(ev->name, t->bustemp) != 0) + return fail("unowned IBus temporary file event for %s", ev->name); + if(mask & IN_ATTRIB) + mask &= ~IN_ATTRIB; + if(mask & IN_CLOSE_WRITE) + mask &= ~IN_CLOSE_WRITE; + if(mask & IN_MOVED_FROM){ + if(t->bustempmoved || ev->cookie == 0) + return fail("invalid IBus temporary-file rename"); + t->bustempmoved = 1; + t->buscookie = ev->cookie; + mask &= ~IN_MOVED_FROM; + } + if(mask != 0) + return fail("unexpected IBus temporary-file event %#x", ev->mask); + return 1; + } + if(strcmp(ev->name, t->addrbase) != 0) + return fail("unexpected IBus recovery entry %s", ev->name); + if(mask == IN_MOVED_TO && t->bustempmoved && ev->cookie == t->buscookie){ + t->buspublished = 1; + return 1; + } + return fail("IBus address replacement was not one atomic rename: %#x", + ev->mask); +} + +static int +drainnotify(Test *t) +{ + char buf[4096]; + 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){ + fail("endpoint watch queue overflowed"); + ok = 0; + continue; + } + if(ev->mask & (IN_IGNORED|IN_UNMOUNT)){ + fail("endpoint watch became invalid: %#x", ev->mask); + ok = 0; + continue; + } + if(ev->wd != t->runtimewd && ev->wd != t->buswd) + continue; + if(t->phase == Phaseignore) + continue; + if(t->phase == Phasequiet){ + fail("hard crash changed %s endpoint %s with event %#x", + ev->wd == t->runtimewd ? "IPC" : "IBus", + ev->len != 0 ? ev->name : "directory", ev->mask); + ok = 0; + continue; + } + if(ev->wd == t->runtimewd){ + if(!runtimeevent(t, ev)) + ok = 0; + }else if(!busevent(t, ev)) + ok = 0; + } + if(off != (size_t)n) + return fail("truncated inotify event buffer"); + } +} + +static int +findaddress(Test *t) +{ + DIR *dir; + struct dirent *de; + int count, ok; + + dir = opendir(t->bus); + if(dir == NULL){ + fail("open IBus directory %s: %s", t->bus, strerror(errno)); + return -1; + } + count = 0; + ok = 1; + errno = 0; + while((de = readdir(dir)) != NULL){ + if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0 || + strstr(de->d_name, ".tmp.") != NULL) + continue; + count++; + if(snprintf(t->addrfile, sizeof t->addrfile, "%s/%s", t->bus, + de->d_name) >= (int)sizeof t->addrfile){ + fail("IBus address path is too long"); + ok = 0; + break; + } + } + if(errno != 0){ + fail("read IBus directory %s: %s", t->bus, strerror(errno)); + ok = 0; + } + if(closedir(dir) < 0){ + fail("close IBus directory %s: %s", t->bus, strerror(errno)); + ok = 0; + } + if(!ok) + return -1; + if(count > 1){ + fail("found %d IBus address files", count); + return -1; + } + return count; +} + +static int +readfile(char *path, char *buf, size_t cap, size_t *nread) +{ + struct stat st; + ssize_t n; + size_t off; + char extra; + int fd, ok; + + fd = open(path, O_RDONLY|O_CLOEXEC); + if(fd < 0) + return fail("open %s: %s", path, strerror(errno)); + ok = 0; + if(fstat(fd, &st) < 0){ + fail("stat open address file %s: %s", path, strerror(errno)); + goto out; + } + if(st.st_size < 0 || (uintmax_t)st.st_size >= cap){ + fail("address file %s is too large", path); + goto out; + } + off = 0; + while(off < (size_t)st.st_size){ + n = read(fd, buf + off, (size_t)st.st_size - off); + if(n < 0 && errno == EINTR) + continue; + if(n <= 0){ + fail("read %s: %s", path, + n == 0 ? "unexpected EOF" : strerror(errno)); + goto out; + } + off += n; + } + do + n = read(fd, &extra, 1); + while(n < 0 && errno == EINTR); + if(n != 0){ + fail("address file %s changed while being read", path); + goto out; + } + buf[off] = '\0'; + *nread = off; + ok = 1; +out: + if(close(fd) < 0){ + fail("close %s: %s", path, strerror(errno)); + ok = 0; + } + return ok; +} + +static int +parseaddress(char *contents, size_t ncontents, char *address, size_t naddress, + pid_t *declared) +{ + char found[512]; + long pid; + int consumed; + + consumed = -1; + if(strlen(contents) != ncontents || + sscanf(contents, "IBUS_ADDRESS=%511[^\n]\nIBUS_DAEMON_PID=%ld\n%n", + found, &pid, &consumed) != 2 || consumed != (int)ncontents || pid <= 0) + return fail("invalid IBus address file contents"); + if(snprintf(address, naddress, "%s", found) >= (int)naddress) + return fail("private IBus address is too long"); + *declared = (pid_t)pid; + return 1; +} + +static int +privateaddress(char *address, pid_t pid) +{ + char prefix[96]; + size_t n; + + if(snprintf(prefix, sizeof prefix, "unix:abstract=strans-%ld", + (long)pid) >= (int)sizeof prefix) + return fail("private IBus prefix is too long"); + n = strlen(prefix); + return (strncmp(address, prefix, n) == 0 && address[n] == ',') || + fail("IBus address is not the daemon's private abstract address"); +} + +static int +childalive(pid_t *pid, char *which) +{ + int n, status; + + do + n = waitpid(*pid, &status, WNOHANG); + while(n < 0 && errno == EINTR); + if(n == 0) + return 1; + if(n == *pid){ + fail("%s daemon exited unexpectedly with wait status %#x", which, + status); + *pid = -1; + return 0; + } + if(n < 0){ + fail("check %s daemon %ld: %s", which, (long)*pid, + strerror(errno)); + if(errno == ECHILD) + *pid = -1; + return 0; + } + return fail("waitpid returned the wrong %s child", which); +} + +static int +waitready(Test *t, pid_t *child, int errfd, char *which) +{ + struct pollfd pfd[2]; + struct stat st; + char contents[2048], address[512]; + size_t ncontents; + pid_t declared; + int count, n, socketready, timeout; + int64_t deadline; + + deadline = nowms(); + if(deadline < 0) + return fail("read monotonic clock before %s startup: %s", which, + strerror(errno)); + deadline += Starttimeout; + for(;;){ + socketready = lstat(t->socket, &st) == 0 && S_ISSOCK(st.st_mode) && + (st.st_mode & 0777) == 0600; + count = findaddress(t); + if(count < 0) + return 0; + if(socketready && count == 1 && + readfile(t->addrfile, contents, sizeof contents, &ncontents) && + parseaddress(contents, ncontents, address, sizeof address, + &declared) && declared == *child && privateaddress(address, declared)) + return 1; + if(!childalive(child, which)) + return 0; + timeout = leftms(deadline); + if(timeout < 0) + return fail("read monotonic clock during %s startup: %s", which, + strerror(errno)); + if(timeout == 0) + return fail("timed out waiting for both %s daemon endpoints", which); + pfd[0].fd = t->notifyfd; + pfd[0].events = POLLIN; + pfd[0].revents = 0; + pfd[1].fd = errfd; + pfd[1].events = POLLIN|POLLHUP; + pfd[1].revents = 0; + n = poll(pfd, 2, timeout); + if(n < 0 && errno == EINTR) + continue; + if(n < 0) + return fail("poll %s daemon readiness: %s", which, + strerror(errno)); + if(pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL)) + return fail("endpoint watch became unusable: %#x", pfd[0].revents); + if(pfd[1].revents & (POLLERR|POLLNVAL)) + return fail("%s stderr pipe became unusable: %#x", which, + pfd[1].revents); + if(pfd[0].revents & POLLIN) + if(!drainnotify(t)) + return 0; + if(pfd[1].revents & (POLLIN|POLLHUP)){ + if(child == &t->first) + readpipe(errfd, t->firsterr, sizeof t->firsterr, + &t->nfirsterr); + else + readpipe(errfd, t->seconderr, sizeof t->seconderr, + &t->nseconderr); + } + } +} + +static int +snapshotfirst(Test *t) +{ + char *base; + pid_t declared; + + if(lstat(t->socket, &t->firstsocketst) < 0) + return fail("stat first IPC socket: %s", strerror(errno)); + if(!S_ISSOCK(t->firstsocketst.st_mode) || + (t->firstsocketst.st_mode & 0777) != 0600) + return fail("first IPC endpoint is not a protected socket"); + if(lstat(t->addrfile, &t->firstaddrst) < 0) + return fail("stat first IBus address file: %s", strerror(errno)); + if(!S_ISREG(t->firstaddrst.st_mode) || + (t->firstaddrst.st_mode & 0777) != 0600) + return fail("first IBus address is not a protected regular file"); + if(!readfile(t->addrfile, t->firstcontents, sizeof t->firstcontents, + &t->nfirstcontents) || + !parseaddress(t->firstcontents, t->nfirstcontents, t->firstaddress, + sizeof t->firstaddress, &declared)) + return 0; + if(declared != t->firstpid) + return fail("first IBus address PID %ld, expected %ld", (long)declared, + (long)t->firstpid); + if(!privateaddress(t->firstaddress, declared)) + return 0; + base = strrchr(t->addrfile, '/'); + if(base == NULL || snprintf(t->addrbase, sizeof t->addrbase, "%s", base+1) + >= (int)sizeof t->addrbase) + return fail("first IBus address filename is invalid"); + return 1; +} + +static int +connectsocket(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; + } + for(;;){ + n = leftms(deadline); + if(n <= 0){ + close(fd); + errno = n < 0 ? EIO : ETIMEDOUT; + return -1; + } + pfd.fd = fd; + pfd.events = POLLOUT; + pfd.revents = 0; + 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 = timeout < 0 ? EIO : ETIMEDOUT; + return -1; + } + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + r = poll(&pfd, 1, timeout); + if(r < 0 && errno == EINTR) + continue; + if(r <= 0){ + 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 int +ipcrequest(int fd, uint32_t mod, uint32_t key, unsigned char *want, + size_t nwant, char *where) +{ + unsigned char req[Ipcreqsz], got[16]; + int rv; + + if(nwant > sizeof got) + return fail("%s expected IPC response is too large", where); + ipcpackreq(req, 1, mod, key); + if(ipcsend(fd, req, sizeof req) < 0) + return fail("send %s IPC request: %s", where, strerror(errno)); + rv = readuntil(fd, got, nwant, nowms() + Calltimeout); + if(rv != 1) + return fail("read %s IPC response: %s", where, + rv == 0 ? "peer closed" : strerror(errno)); + if(memcmp(got, want, nwant) != 0) + return fail("%s IPC response did not match", where); + return 1; +} + +static int +ipcprobe(int fd, char *where) +{ + unsigned char empty[] = {0, 0, 0, 0, 0}; + + return ipcrequest(fd, 0, Kmodfirst, empty, sizeof empty, where); +} + +static void +closebus(DBusConnection **conn) +{ + if(*conn == NULL) + return; + dbus_connection_close(*conn); + dbus_connection_unref(*conn); + *conn = NULL; +} + +static DBusMessage* +sendcall(DBusConnection *conn, DBusMessage *m, char *where) +{ + DBusMessage *reply; + DBusPendingCall *pending; + int timeout; + int64_t deadline; + + if(m == NULL){ + fail("allocate %s call", where); + return NULL; + } + pending = NULL; + if(!dbus_connection_send_with_reply(conn, m, &pending, Calltimeout) || + pending == NULL){ + dbus_message_unref(m); + fail("queue %s call", where); + return NULL; + } + dbus_message_unref(m); + deadline = nowms() + Calltimeout; + while(!dbus_pending_call_get_completed(pending)){ + timeout = leftms(deadline); + if(timeout <= 0){ + fail("timed out waiting for %s", where); + goto fail; + } + if(!dbus_connection_read_write_dispatch(conn, timeout)){ + fail("connection closed waiting for %s", where); + goto fail; + } + } + reply = dbus_pending_call_steal_reply(pending); + dbus_pending_call_unref(pending); + if(reply == NULL){ + fail("%s completed without a reply", where); + return NULL; + } + if(dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_METHOD_RETURN){ + fail("%s returned D-Bus message type %d", where, + dbus_message_get_type(reply)); + dbus_message_unref(reply); + return NULL; + } + return reply; +fail: + dbus_pending_call_cancel(pending); + dbus_pending_call_unref(pending); + return NULL; +} + +static DBusConnection* +openbus(char *address, char *where) +{ + DBusConnection *conn; + DBusError err; + + dbus_error_init(&err); + conn = dbus_connection_open_private(address, &err); + if(conn == NULL){ + fail("open %s private IBus connection: %s", where, + err.message != NULL ? err.message : "D-Bus error"); + dbus_error_free(&err); + return NULL; + } + dbus_error_free(&err); + dbus_connection_set_exit_on_disconnect(conn, FALSE); + return conn; +} + +static int +hello(DBusConnection *conn, char *name, size_t nname, char *where) +{ + DBusMessage *m, *reply; + DBusError err; + const char *s; + + m = dbus_message_new_method_call("org.freedesktop.DBus", + "/org/freedesktop/DBus", "org.freedesktop.DBus", "Hello"); + reply = sendcall(conn, m, where); + if(reply == NULL) + return 0; + dbus_error_init(&err); + if(!dbus_message_has_signature(reply, "s") || + !dbus_message_get_args(reply, &err, DBUS_TYPE_STRING, &s, + DBUS_TYPE_INVALID)){ + dbus_message_unref(reply); + dbus_error_free(&err); + return fail("%s returned an invalid Hello reply", where); + } + if(snprintf(name, nname, "%s", s) >= (int)nname){ + dbus_error_free(&err); + dbus_message_unref(reply); + return fail("%s Hello name is too long", where); + } + dbus_error_free(&err); + dbus_message_unref(reply); + return name[0] == ':' || fail("%s returned invalid Hello name %s", + where, name); +} + +static int +createcontext(DBusConnection *conn, char *path, size_t npath, char *where) +{ + DBusMessage *m, *reply; + DBusError err; + const char *client, *p; + + client = "daemon-restart-test"; + m = dbus_message_new_method_call("org.freedesktop.IBus", + "/org/freedesktop/IBus", "org.freedesktop.IBus", + "CreateInputContext"); + if(m == NULL || !dbus_message_append_args(m, DBUS_TYPE_STRING, &client, + DBUS_TYPE_INVALID)){ + if(m != NULL) + dbus_message_unref(m); + return fail("build %s CreateInputContext call", where); + } + reply = sendcall(conn, m, where); + if(reply == NULL) + return 0; + dbus_error_init(&err); + if(!dbus_message_has_signature(reply, "o") || + !dbus_message_get_args(reply, &err, DBUS_TYPE_OBJECT_PATH, &p, + DBUS_TYPE_INVALID)){ + dbus_message_unref(reply); + dbus_error_free(&err); + return fail("%s returned an invalid context reply", where); + } + if(snprintf(path, npath, "%s", p) >= (int)npath){ + dbus_error_free(&err); + dbus_message_unref(reply); + return fail("%s context path is too long", where); + } + dbus_error_free(&err); + dbus_message_unref(reply); + return path[0] == '/' || fail("%s returned invalid context path %s", + where, path); +} + +static DBusMessage* +contextcall(char *path, char *member) +{ + return dbus_message_new_method_call("org.freedesktop.IBus", path, + "org.freedesktop.IBus.InputContext", member); +} + +static int +focusin(DBusConnection *conn, char *path) +{ + DBusMessage *reply; + + reply = sendcall(conn, contextcall(path, "FocusIn"), + "replacement FocusIn"); + if(reply == NULL) + return 0; + if(!dbus_message_has_signature(reply, "")){ + dbus_message_unref(reply); + return fail("replacement FocusIn returned a nonempty reply"); + } + dbus_message_unref(reply); + return 1; +} + +static int +keycall(DBusConnection *conn, char *path, dbus_uint32_t sym, + dbus_uint32_t state, int expected, char *where) +{ + DBusMessage *m, *reply; + DBusError err; + dbus_uint32_t code; + dbus_bool_t eaten; + + code = 0; + m = contextcall(path, "ProcessKeyEvent"); + if(m == NULL || !dbus_message_append_args(m, + DBUS_TYPE_UINT32, &sym, DBUS_TYPE_UINT32, &code, + DBUS_TYPE_UINT32, &state, DBUS_TYPE_INVALID)){ + if(m != NULL) + dbus_message_unref(m); + return fail("build %s ProcessKeyEvent call", where); + } + reply = sendcall(conn, m, where); + if(reply == NULL) + return 0; + dbus_error_init(&err); + if(!dbus_message_has_signature(reply, "b") || + !dbus_message_get_args(reply, &err, DBUS_TYPE_BOOLEAN, &eaten, + DBUS_TYPE_INVALID)){ + dbus_message_unref(reply); + dbus_error_free(&err); + return fail("%s returned an invalid ProcessKeyEvent reply", where); + } + dbus_error_free(&err); + dbus_message_unref(reply); + return (eaten != FALSE) == (expected != 0) || + fail("%s eaten=%d, expected %d", where, eaten != FALSE, expected); +} + +static int +openpersistent(Test *t) +{ + char name[32], path[96]; + int64_t deadline; + + deadline = nowms() + Calltimeout; + t->ipcfirst = connectsocket(t->socket, deadline); + if(t->ipcfirst < 0) + return fail("connect persistent IPC client: %s", strerror(errno)); + if(!ipcprobe(t->ipcfirst, "persistent pre-crash")) + return 0; + t->busfirst = openbus(t->firstaddress, "persistent pre-crash"); + if(t->busfirst == NULL || + !hello(t->busfirst, name, sizeof name, "persistent pre-crash Hello") || + !createcontext(t->busfirst, path, sizeof path, + "persistent pre-crash context")) + return 0; + if(!dbus_connection_get_unix_fd(t->busfirst, &t->busfirstfd)) + return fail("persistent IBus connection has no Unix descriptor"); + return 1; +} + +static int +hardcrash(Test *t) +{ + struct pollfd pfd; + pid_t pid; + int e, n, status, timeout; + int64_t deadline; + + if(!childalive(&t->first, "first")) + return 0; + pid = t->first; + if(kill(pid, SIGKILL) < 0){ + e = errno; + do + n = waitpid(pid, &status, WNOHANG); + while(n < 0 && errno == EINTR); + if(n == pid){ + t->first = -1; + return fail("first daemon exited before planned SIGKILL with status %#x", + status); + } + if(n < 0 && errno == ECHILD) + t->first = -1; + return fail("kill -9 first daemon %ld: %s", (long)pid, strerror(e)); + } + deadline = nowms(); + if(deadline < 0) + return fail("read monotonic clock before first daemon crash: %s", + strerror(errno)); + deadline += Stoptimeout; + for(;;){ + do + n = waitpid(pid, &status, WNOHANG); + while(n < 0 && errno == EINTR); + if(n == pid){ + t->first = -1; + break; + } + if(n < 0){ + e = errno; + if(e == ECHILD) + t->first = -1; + return fail("reap first daemon %ld after SIGKILL: %s", (long)pid, + strerror(e)); + } + timeout = leftms(deadline); + if(timeout < 0) + return fail("read monotonic clock during first daemon crash: %s", + strerror(errno)); + if(timeout == 0) + return fail("first daemon %ld was not reaped after SIGKILL", + (long)pid); + pfd.fd = t->firsterrfd; + pfd.events = POLLIN|POLLHUP; + pfd.revents = 0; + n = poll(&pfd, 1, timeout); + if(n < 0 && errno == EINTR) + continue; + if(n < 0) + return fail("poll first daemon crash: %s", strerror(errno)); + if(pfd.revents & (POLLERR|POLLNVAL)) + return fail("first daemon stderr pipe became unusable: %#x", + pfd.revents); + if(pfd.revents & (POLLIN|POLLHUP)) + readpipe(t->firsterrfd, t->firsterr, sizeof t->firsterr, + &t->nfirsterr); + } + if(!WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL) + return fail("first daemon hard crash had wait status %#x", status); + readpipe(t->firsterrfd, t->firsterr, sizeof t->firsterr, &t->nfirsterr); + return 1; +} + +static int +waitipcclosed(Test *t) +{ + struct pollfd pfd; + unsigned char byte; + ssize_t n; + int timeout; + int64_t deadline; + + deadline = nowms() + Calltimeout; + for(;;){ + timeout = leftms(deadline); + if(timeout <= 0) + return fail("persistent IPC connection did not disconnect promptly"); + pfd.fd = t->ipcfirst; + pfd.events = POLLIN|POLLHUP|POLLERR; + pfd.revents = 0; + n = poll(&pfd, 1, timeout); + if(n < 0 && errno == EINTR) + continue; + if(n <= 0) + return fail("poll persistent IPC disconnection: %s", + n == 0 ? "timed out" : strerror(errno)); + n = recv(t->ipcfirst, &byte, 1, MSG_PEEK); + if(n == 0 || (n < 0 && (errno == ECONNRESET || errno == ENOTCONN || + errno == EPIPE))) + return 1; + if(n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) + continue; + if(n < 0) + return fail("read persistent IPC disconnection: %s", + strerror(errno)); + return fail("persistent IPC connection retained unread data after crash"); + } +} + +static int +waitbusclosed(Test *t) +{ + struct pollfd pfd; + int n, timeout; + int64_t deadline; + + deadline = nowms() + Calltimeout; + for(;;){ + if(!dbus_connection_get_is_connected(t->busfirst)) + return 1; + timeout = leftms(deadline); + if(timeout <= 0) + return fail("persistent IBus connection did not disconnect promptly"); + pfd.fd = t->busfirstfd; + pfd.events = POLLIN|POLLHUP|POLLERR; + pfd.revents = 0; + n = poll(&pfd, 1, timeout); + if(n < 0 && errno == EINTR) + continue; + if(n <= 0) + return fail("poll persistent IBus disconnection: %s", + n == 0 ? "timed out" : strerror(errno)); + if(pfd.revents & POLLNVAL) + return fail("persistent IBus descriptor became invalid"); + dbus_connection_read_write_dispatch(t->busfirst, 0); + } +} + +static int +onlyaddress(Test *t, char *where) +{ + DIR *dir; + struct dirent *de; + int count, ok; + + dir = opendir(t->bus); + if(dir == NULL) + return fail("open IBus directory %s %s: %s", t->bus, where, + 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++; + if(strcmp(de->d_name, t->addrbase) != 0){ + fail("unexpected IBus entry %s %s", de->d_name, where); + ok = 0; + } + errno = 0; + } + if(errno != 0){ + fail("read IBus directory %s %s: %s", t->bus, where, + strerror(errno)); + ok = 0; + } + if(closedir(dir) < 0){ + fail("close IBus directory %s %s: %s", t->bus, where, + strerror(errno)); + ok = 0; + } + if(count != 1){ + fail("IBus directory has %d entries %s", count, where); + ok = 0; + } + return ok; +} + +static int +checkstale(Test *t) +{ + struct stat st; + char contents[2048], address[512]; + size_t ncontents; + pid_t declared; + + if(lstat(t->socket, &st) < 0) + return fail("stale IPC socket disappeared: %s", strerror(errno)); + if(!S_ISSOCK(st.st_mode) || st.st_mode != t->firstsocketst.st_mode || + st.st_dev != t->firstsocketst.st_dev || + st.st_ino != t->firstsocketst.st_ino) + return fail("stale IPC socket identity or protection changed"); + if(lstat(t->addrfile, &st) < 0) + return fail("stale IBus address file disappeared: %s", strerror(errno)); + if(st.st_mode != t->firstaddrst.st_mode || + st.st_dev != t->firstaddrst.st_dev || st.st_ino != t->firstaddrst.st_ino) + return fail("stale IBus address identity or protection changed"); + if(!readfile(t->addrfile, contents, sizeof contents, &ncontents) || + !parseaddress(contents, ncontents, address, sizeof address, &declared)) + return 0; + if(ncontents != t->nfirstcontents || + memcmp(contents, t->firstcontents, ncontents) != 0) + return fail("stale IBus address contents changed after hard crash"); + if(declared != t->firstpid || strcmp(address, t->firstaddress) != 0) + return fail("stale IBus address no longer names the dead first daemon"); + return onlyaddress(t, "after hard crash"); +} + +static int +helpermain(char *address) +{ + DBusConnection *conn; + DBusError err; + + dbus_error_init(&err); + conn = dbus_connection_open_private(address, &err); + if(conn != NULL){ + dbus_connection_set_exit_on_disconnect(conn, FALSE); + dbus_connection_close(conn); + dbus_connection_unref(conn); + dbus_error_free(&err); + fprintf(stderr, "old private address unexpectedly accepted a connection\n"); + return 1; + } + if(!dbus_error_is_set(&err)){ + fprintf(stderr, "old private address failed without a D-Bus error\n"); + dbus_error_free(&err); + return 2; + } + dbus_error_free(&err); + return 0; +} + +static int +killreap(pid_t *child, int errfd, char *which) +{ + struct pollfd pfd; + pid_t pid; + int e, n, ok, status, timeout; + int64_t deadline; + + if(*child <= 0) + return 1; + pid = *child; + ok = 1; + if(kill(pid, SIGKILL) < 0 && errno != ESRCH){ + fail("kill -9 %s %ld: %s", which, (long)pid, strerror(errno)); + ok = 0; + } + deadline = nowms(); + if(deadline < 0){ + fail("read monotonic clock before reaping %s %ld: %s", which, + (long)pid, strerror(errno)); + ok = 0; + }else + deadline += Stoptimeout; + while(deadline >= 0){ + do + n = waitpid(pid, &status, WNOHANG); + while(n < 0 && errno == EINTR); + if(n == pid){ + *child = -1; + return ok; + } + if(n < 0){ + e = errno; + if(e == ECHILD) + *child = -1; + fail("reap %s %ld after SIGKILL: %s", which, (long)pid, + strerror(e)); + return 0; + } + timeout = leftms(deadline); + if(timeout < 0){ + fail("read monotonic clock while reaping %s %ld: %s", which, + (long)pid, strerror(errno)); + ok = 0; + break; + } + if(timeout == 0){ + fail("%s %ld was not reaped promptly after SIGKILL", which, + (long)pid); + ok = 0; + break; + } + pfd.fd = errfd; + pfd.events = 0; + pfd.revents = 0; + n = poll(&pfd, 1, timeout); + if(n < 0 && errno == EINTR) + continue; + if(n < 0){ + fail("poll %s %ld after SIGKILL: %s", which, (long)pid, + strerror(errno)); + ok = 0; + break; + } + if(pfd.revents & (POLLERR|POLLNVAL)){ + fail("%s %ld stderr pipe became unusable: %#x", which, + (long)pid, pfd.revents); + ok = 0; + break; + } + } + /* Keep exact child ownership until the mandatory post-SIGKILL reap. */ + do + n = waitpid(pid, &status, 0); + while(n < 0 && errno == EINTR); + if(n == pid) + *child = -1; + else{ + e = errno; + if(n < 0 && e == ECHILD) + *child = -1; + fail("final reap of %s %ld after SIGKILL: %s", which, (long)pid, + n < 0 ? strerror(e) : "wrong child"); + ok = 0; + } + return ok; +} + +static int +rejectold(Test *t, char *self, char *where) +{ + struct pollfd pfd; + int errpipe[2], fd, n, status, timeout; + long maxfd; + pid_t pid; + int64_t deadline; + + if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0) + return fail("pipe old-address helper: %s", strerror(errno)); + pid = fork(); + if(pid < 0){ + close(errpipe[0]); + close(errpipe[1]); + return fail("fork old-address helper: %s", strerror(errno)); + } + if(pid == 0){ + close(errpipe[0]); + if(!childstderr(errpipe[1])) + _exit(126); + 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); + } + execl(self, self, "--reject-address", t->firstaddress, (char*)0); + dprintf(STDERR_FILENO, "exec %s: %s\n", self, strerror(errno)); + _exit(127); + } + close(errpipe[1]); + t->helper = pid; + t->helpererrfd = errpipe[0]; + t->nhelpererr = 0; + t->helpererr[0] = '\0'; + deadline = nowms() + Calltimeout; + for(;;){ + do + n = waitpid(pid, &status, WNOHANG); + while(n < 0 && errno == EINTR); + if(n == pid){ + t->helper = -1; + break; + } + if(n < 0){ + fail("wait old-address helper %ld: %s", (long)pid, + strerror(errno)); + if(errno == ECHILD) + t->helper = -1; + else + killreap(&t->helper, t->helpererrfd, + "old-address helper"); + return 0; + } + timeout = leftms(deadline); + if(timeout <= 0){ + fail("old private address open did not fail promptly %s", where); + killreap(&t->helper, t->helpererrfd, "old-address helper"); + return 0; + } + pfd.fd = t->helpererrfd; + pfd.events = POLLIN|POLLHUP; + pfd.revents = 0; + n = poll(&pfd, 1, timeout); + if(n < 0 && errno == EINTR) + continue; + if(n < 0){ + fail("poll old-address helper: %s", strerror(errno)); + killreap(&t->helper, t->helpererrfd, "old-address helper"); + return 0; + } + if(pfd.revents & (POLLERR|POLLNVAL)){ + fail("old-address helper pipe became unusable: %#x", pfd.revents); + killreap(&t->helper, t->helpererrfd, "old-address helper"); + return 0; + } + if(pfd.revents & (POLLIN|POLLHUP)) + readpipe(t->helpererrfd, t->helpererr, sizeof t->helpererr, + &t->nhelpererr); + } + readpipe(t->helpererrfd, t->helpererr, sizeof t->helpererr, + &t->nhelpererr); + if(close(t->helpererrfd) < 0){ + t->helpererrfd = -1; + return fail("close old-address helper pipe: %s", strerror(errno)); + } + t->helpererrfd = -1; + if(!WIFEXITED(status) || WEXITSTATUS(status) != 0) + return fail("old private address helper failed %s with status %#x", + where, status); + return 1; +} + +static int +recoveryevents(Test *t) +{ + if(!t->socketdeleted || !t->socketcreated) + return fail("replacement did not delete and recreate the stale IPC path"); + if(!t->bustempcreated || !t->bustempmoved || !t->buspublished) + return fail("replacement IBus address was not atomically published"); + return 1; +} + +static int +snapshotsecond(Test *t) +{ + pid_t declared; + + if(lstat(t->socket, &t->secondsocketst) < 0) + return fail("stat replacement IPC socket: %s", strerror(errno)); + if(!S_ISSOCK(t->secondsocketst.st_mode) || + (t->secondsocketst.st_mode & 0777) != 0600) + return fail("replacement IPC endpoint is not a protected socket"); + if(lstat(t->addrfile, &t->secondaddrst) < 0) + return fail("stat replacement IBus address file: %s", strerror(errno)); + if(!S_ISREG(t->secondaddrst.st_mode) || + (t->secondaddrst.st_mode & 0777) != 0600) + return fail("replacement IBus address is not a protected regular file"); + if(t->secondaddrst.st_dev == t->firstaddrst.st_dev && + t->secondaddrst.st_ino == t->firstaddrst.st_ino) + return fail("replacement IBus address retained the stale inode"); + if(!readfile(t->addrfile, t->secondcontents, sizeof t->secondcontents, + &t->nsecondcontents) || + !parseaddress(t->secondcontents, t->nsecondcontents, t->secondaddress, + sizeof t->secondaddress, &declared)) + return 0; + if(declared != t->second) + return fail("replacement IBus address PID %ld, expected %ld", + (long)declared, (long)t->second); + if(!privateaddress(t->secondaddress, declared)) + return 0; + if(t->nsecondcontents == t->nfirstcontents && + memcmp(t->secondcontents, t->firstcontents, t->nfirstcontents) == 0) + return fail("replacement IBus address contents did not change"); + if(strcmp(t->secondaddress, t->firstaddress) == 0) + return fail("replacement reused the dead private IBus address"); + return onlyaddress(t, "after replacement"); +} + +static int +openreplacement(Test *t) +{ + unsigned char selectjp[] = {1, 0, 0, 0, 0}; + unsigned char keyk[] = {1, 0, 0, 1, 0, 'k'}; + char name[32], path[96]; + + t->ipcnew = connectsocket(t->socket, nowms() + Calltimeout); + if(t->ipcnew < 0) + return fail("connect replacement IPC client: %s", strerror(errno)); + if(!ipcrequest(t->ipcnew, Mctrl, 'n', selectjp, sizeof selectjp, + "replacement select Japanese") || + !ipcrequest(t->ipcnew, 0, 'k', keyk, sizeof keyk, + "replacement real key")) + return 0; + t->busnew = openbus(t->secondaddress, "replacement"); + if(t->busnew == NULL || + !hello(t->busnew, name, sizeof name, "replacement Hello") || + !createcontext(t->busnew, path, sizeof path, "replacement context") || + !focusin(t->busnew, path) || + !keycall(t->busnew, path, 'n', Ctrlmask, 1, + "replacement select Japanese") || + !keycall(t->busnew, path, 'k', 0, 1, "replacement real key")) + return 0; + return childalive(&t->second, "second"); +} + +static int +stopsecond(Test *t) +{ + struct pollfd pfd; + pid_t pid; + int n, ok, reaped, status, timeout; + int64_t deadline; + + if(t->second <= 0) + return 1; + if(!childalive(&t->second, "second")) + return 0; + pid = t->second; + ok = 1; + if(kill(pid, SIGTERM) < 0){ + fail("kill second daemon %ld: %s", (long)pid, strerror(errno)); + ok = 0; + } + reaped = 0; + deadline = nowms() + Stoptimeout; + while(t->second > 0){ + do + n = waitpid(pid, &status, WNOHANG); + while(n < 0 && errno == EINTR); + if(n == pid){ + reaped = 1; + t->second = -1; + break; + } + if(n < 0){ + fail("waitpid second daemon %ld: %s", (long)pid, + strerror(errno)); + if(errno == ECHILD) + t->second = -1; + else if(!killreap(&t->second, t->seconderrfd, "second daemon")) + ok = 0; + ok = 0; + break; + } + timeout = leftms(deadline); + if(timeout <= 0){ + fail("second daemon %ld did not stop after SIGTERM", (long)pid); + ok = 0; + if(!killreap(&t->second, t->seconderrfd, "second daemon")) + ok = 0; + break; + } + pfd.fd = t->seconderrfd; + pfd.events = POLLIN|POLLHUP; + pfd.revents = 0; + n = poll(&pfd, 1, timeout); + if(n < 0 && errno == EINTR) + continue; + if(n < 0 || (pfd.revents & (POLLERR|POLLNVAL))){ + fail("poll second daemon termination: %s", + n < 0 ? strerror(errno) : "stderr pipe failure"); + ok = 0; + if(!killreap(&t->second, t->seconderrfd, "second daemon")) + ok = 0; + break; + } + if(pfd.revents & (POLLIN|POLLHUP)) + readpipe(t->seconderrfd, t->seconderr, sizeof t->seconderr, + &t->nseconderr); + } + if(reaped && + !((WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM) || + (WIFEXITED(status) && WEXITSTATUS(status) == 1))){ + fail("second daemon exited with unexpected wait status %#x", status); + ok = 0; + } + return ok; +} + +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) +{ + struct stat st; + 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; + } + errno = 0; + if(lstat(t->root, &st) == 0 || errno != ENOENT){ + fail("temporary root still exists after cleanup"); + ok = 0; + } + return ok; +} + +static int +cleanup(Test *t) +{ + int ok; + + ok = 1; + closebus(&t->busnew); + closebus(&t->busfirst); + if(t->ipcnew >= 0){ + if(close(t->ipcnew) < 0){ + fail("close replacement IPC client: %s", strerror(errno)); + ok = 0; + } + t->ipcnew = -1; + } + if(t->ipcfirst >= 0){ + if(close(t->ipcfirst) < 0){ + fail("close persistent IPC client: %s", strerror(errno)); + ok = 0; + } + t->ipcfirst = -1; + } + if(t->helper > 0 && !killreap(&t->helper, t->helpererrfd, + "old-address helper")) + ok = 0; + if(t->first > 0 && !killreap(&t->first, t->firsterrfd, "first daemon")) + ok = 0; + t->phase = Phaseignore; + if(!stopsecond(t)) + ok = 0; + readpipe(t->firsterrfd, t->firsterr, sizeof t->firsterr, &t->nfirsterr); + readpipe(t->seconderrfd, t->seconderr, sizeof t->seconderr, + &t->nseconderr); + readpipe(t->helpererrfd, t->helpererr, sizeof t->helpererr, + &t->nhelpererr); + if(t->firsterrfd >= 0){ + if(close(t->firsterrfd) < 0){ + fail("close first daemon stderr: %s", strerror(errno)); + ok = 0; + } + t->firsterrfd = -1; + } + if(t->seconderrfd >= 0){ + if(close(t->seconderrfd) < 0){ + fail("close second daemon stderr: %s", strerror(errno)); + ok = 0; + } + t->seconderrfd = -1; + } + if(t->helpererrfd >= 0){ + if(close(t->helpererrfd) < 0){ + fail("close old-address helper stderr: %s", strerror(errno)); + ok = 0; + } + t->helpererrfd = -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; +} + +static int +runrestart(Test *t, char *self, char *program, char *mapdir) +{ + if(!startchild(t, program, mapdir, &t->first, &t->firsterrfd)) + return 0; + t->firstpid = t->first; + if(!waitready(t, &t->first, t->firsterrfd, "first") || + !snapshotfirst(t) || !openpersistent(t) || !drainnotify(t)) + return 0; + t->phase = Phasequiet; + if(!hardcrash(t) || !waitipcclosed(t) || !waitbusclosed(t) || + !drainnotify(t) || !checkstale(t) || + !rejectold(t, self, "after hard crash") || !drainnotify(t)) + return 0; + closebus(&t->busfirst); + if(close(t->ipcfirst) < 0) + return fail("close disconnected persistent IPC client: %s", + strerror(errno)); + t->ipcfirst = -1; + t->phase = Phaserecover; + if(!startchild(t, program, mapdir, &t->second, &t->seconderrfd) || + !waitready(t, &t->second, t->seconderrfd, "second") || + !drainnotify(t) || !recoveryevents(t) || !snapshotsecond(t) || + !rejectold(t, self, "after replacement") || !openreplacement(t)) + return 0; + return childalive(&t->second, "second"); +} + +int +main(int argc, char **argv) +{ + Test test; + int ok; + + if(argc == 3 && strcmp(argv[1], "--reject-address") == 0) + return helpermain(argv[2]); + if(argc != 3){ + fprintf(stderr, "usage: daemon_restart_test strans mapdir\n"); + return 2; + } + ok = setup(&test); + if(ok) + ok = runrestart(&test, argv[0], argv[1], argv[2]); + if(!cleanup(&test)) + ok = 0; + if(!ok){ + showerrors(&test); + return 1; + } + printf("daemon hard-crash endpoint recovery: ok\n"); + return 0; +}