Six live tests carried private copies of the same daemon harness: the
private XDG_RUNTIME_DIR, the fork/exec with captured stderr, the
readiness waits, the SIGTERM and SIGKILL paths, the IBus address file
reader, the socket connect, and the IPC probe. Three of them also
carried the same libdbus helpers.
Extract one implementation into tests/live.c and, so that the binaries
that do not link dbus-1 keep not linking it, the D-Bus half into
tests/livebus.c. Both are compiled into each binary the way ../ipc.c
already is. gtk_live_test keeps its own fake-server harness and only
takes fail, nowms and leftms; xim_live_test keeps its own Xvfb and
process-group daemon spawn, which must inherit DISPLAY, and takes the
temporary directory, timing and cleanup halves.
The copies had drifted; the harness keeps the stricter behaviour.
- nowms reports a broken clock (-1) instead of pretending it read
zero, and leftms turns that into an expired deadline, so a loop
ends in a timeout failure rather than spinning. livesetup checks
the clock once up front, as daemon_restart_test did.
- Timeouts compare with <= 0, not == 0.
- readuntil keeps the three-way result (complete, peer closed, error)
from ipc_live_test and daemon_restart_test rather than folding
peer closure into ECONNRESET.
- The daemon's stdout and stderr are both captured, and a failing
setenv is reported, for every daemon; daemon_failure_test captured
stderr only and said nothing about setenv.
- The child keeps daemon_restart_test's careful redirect that also
works when the pipe lands on fd 1 or 2.
- parseaddress requires a positive declared PID.
- killdaemon reports ECHILD after SIGKILL as a failure; one copy
accepted it. It now reaps with a blocking waitpid, which SIGKILL
guarantees will return, instead of daemon_restart_test's polled
wait with its own timeout diagnostic.
- stopdaemon tolerates ESRCH on SIGTERM, a benign race two copies
reported as an error.
- liveclean removes the socket and address files and then rmdirs
each directory, reporting leftovers, rather than deleting the
temporary root recursively.
- ibus_live_test now waits for the IPC socket and the address file
by polling, dropping its inotify variant; it asserted only the
address file before.
473 lines
12 KiB
C
473 lines
12 KiB
C
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "ipc.h"
|
|
#include "live.h"
|
|
#include "livebus.h"
|
|
|
|
enum
|
|
{
|
|
Ctrlmask = 1<<2,
|
|
};
|
|
|
|
typedef struct Test Test;
|
|
|
|
struct Test
|
|
{
|
|
Live l;
|
|
Daemon first;
|
|
Daemon second;
|
|
Daemon helper;
|
|
pid_t firstpid;
|
|
int ipcfirst;
|
|
int ipcnew;
|
|
int busfirstfd;
|
|
DBusConnection *busfirst;
|
|
DBusConnection *busnew;
|
|
char firstaddress[512];
|
|
char secondaddress[512];
|
|
};
|
|
|
|
static int
|
|
setup(Test *t)
|
|
{
|
|
struct sigaction sa;
|
|
|
|
memset(t, 0, sizeof *t);
|
|
daemoninit(&t->first, "first daemon");
|
|
daemoninit(&t->second, "second daemon");
|
|
daemoninit(&t->helper, "old-address helper");
|
|
t->firstpid = -1;
|
|
t->ipcfirst = -1;
|
|
t->ipcnew = -1;
|
|
t->busfirstfd = -1;
|
|
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));
|
|
return livesetup(&t->l, "restart");
|
|
}
|
|
|
|
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
|
|
focusin(DBusConnection *conn, char *path)
|
|
{
|
|
DBusMessage *reply;
|
|
|
|
reply = callret(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 = callret(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)
|
|
{
|
|
t->ipcfirst = connectsocket(t->l.socket, nowms() + Calltimeout);
|
|
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, NULL, 0, "persistent pre-crash Hello") ||
|
|
!createcontext(t->busfirst, NULL, 0, "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)
|
|
{
|
|
int status;
|
|
|
|
status = 0;
|
|
if(!daemonalive(&t->first) || !killdaemon(&t->first, &status))
|
|
return 0;
|
|
readerrors(&t->first);
|
|
if(!WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL)
|
|
return fail("first daemon hard crash had wait status %#x", status);
|
|
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
|
|
checkstale(Test *t)
|
|
{
|
|
struct stat st;
|
|
char contents[2048], address[512];
|
|
size_t ncontents;
|
|
pid_t declared;
|
|
|
|
if(lstat(t->l.socket, &st) < 0)
|
|
return fail("stale IPC socket disappeared: %s", strerror(errno));
|
|
if(!S_ISSOCK(st.st_mode))
|
|
return fail("stale IPC endpoint is not a socket");
|
|
if(lstat(t->l.addrfile, &st) < 0)
|
|
return fail("stale IBus address file disappeared: %s", strerror(errno));
|
|
if(!readfile(t->l.addrfile, contents, sizeof contents, &ncontents) ||
|
|
!parseaddress(contents, ncontents, address, sizeof address, &declared))
|
|
return 0;
|
|
if(declared != t->firstpid || strcmp(address, t->firstaddress) != 0)
|
|
return fail("stale IBus address no longer names the dead first daemon");
|
|
return 1;
|
|
}
|
|
|
|
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);
|
|
closebus(&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
|
|
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(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);
|
|
}
|
|
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 = pid;
|
|
t->helper.errfd = errpipe[0];
|
|
deadline = nowms() + Calltimeout;
|
|
for(;;){
|
|
do
|
|
n = waitpid(pid, &status, WNOHANG);
|
|
while(n < 0 && errno == EINTR);
|
|
if(n == pid){
|
|
t->helper.pid = -1;
|
|
break;
|
|
}
|
|
if(n < 0){
|
|
fail("wait old-address helper %ld: %s", (long)pid, strerror(errno));
|
|
if(errno == ECHILD)
|
|
t->helper.pid = -1;
|
|
else
|
|
killdaemon(&t->helper, NULL);
|
|
return 0;
|
|
}
|
|
timeout = leftms(deadline);
|
|
if(timeout == 0){
|
|
fail("old private address open did not fail promptly %s", where);
|
|
killdaemon(&t->helper, NULL);
|
|
return 0;
|
|
}
|
|
pfd.fd = t->helper.errfd;
|
|
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));
|
|
killdaemon(&t->helper, NULL);
|
|
return 0;
|
|
}
|
|
if(pfd.revents & (POLLERR|POLLNVAL)){
|
|
fail("old-address helper pipe became unusable: %#x", pfd.revents);
|
|
killdaemon(&t->helper, NULL);
|
|
return 0;
|
|
}
|
|
if(pfd.revents & (POLLIN|POLLHUP))
|
|
readerrors(&t->helper);
|
|
}
|
|
if(!closeerrors(&t->helper))
|
|
return 0;
|
|
if(!WIFEXITED(status) || WEXITSTATUS(status) != 0)
|
|
return fail("old private address helper failed %s with status %#x",
|
|
where, status);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
openreplacement(Test *t)
|
|
{
|
|
unsigned char selectjp[] = {1, 0, 0, 0, 0};
|
|
unsigned char keyk[] = {1, 0, 0, 1, 0, 'k'};
|
|
char path[96];
|
|
|
|
t->ipcnew = connectsocket(t->l.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, NULL, 0, "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 daemonalive(&t->second);
|
|
}
|
|
|
|
static int
|
|
cleanup(Test *t)
|
|
{
|
|
int ok;
|
|
|
|
ok = 1;
|
|
closebus(&t->busnew);
|
|
closebus(&t->busfirst);
|
|
if(t->ipcnew >= 0){
|
|
if(close(t->ipcnew) < 0)
|
|
ok = fail("close replacement IPC client: %s", strerror(errno));
|
|
t->ipcnew = -1;
|
|
}
|
|
if(t->ipcfirst >= 0){
|
|
if(close(t->ipcfirst) < 0)
|
|
ok = fail("close persistent IPC client: %s", strerror(errno));
|
|
t->ipcfirst = -1;
|
|
}
|
|
if(!killdaemon(&t->helper, NULL))
|
|
ok = 0;
|
|
if(!killdaemon(&t->first, NULL))
|
|
ok = 0;
|
|
if(!stopdaemon(&t->second))
|
|
ok = 0;
|
|
if(!closeerrors(&t->first))
|
|
ok = 0;
|
|
if(!closeerrors(&t->second))
|
|
ok = 0;
|
|
if(!closeerrors(&t->helper))
|
|
ok = 0;
|
|
if(!liveclean(&t->l))
|
|
ok = 0;
|
|
return ok;
|
|
}
|
|
|
|
static int
|
|
runrestart(Test *t, char *self, char *program, char *mapdir)
|
|
{
|
|
if(!startdaemon(&t->l, &t->first, program, mapdir))
|
|
return 0;
|
|
t->firstpid = t->first.pid;
|
|
if(!waitready(&t->l, &t->first, t->firstaddress, sizeof t->firstaddress) ||
|
|
!privateaddress(t->firstaddress, t->firstpid) || !openpersistent(t))
|
|
return 0;
|
|
if(!hardcrash(t) || !waitipcclosed(t) || !waitbusclosed(t) ||
|
|
!checkstale(t) || !rejectold(t, self, "after hard crash"))
|
|
return 0;
|
|
closebus(&t->busfirst);
|
|
if(close(t->ipcfirst) < 0)
|
|
return fail("close disconnected persistent IPC client: %s",
|
|
strerror(errno));
|
|
t->ipcfirst = -1;
|
|
if(!startdaemon(&t->l, &t->second, program, mapdir) ||
|
|
!waitready(&t->l, &t->second, t->secondaddress,
|
|
sizeof t->secondaddress) ||
|
|
!privateaddress(t->secondaddress, t->second.pid) ||
|
|
!openreplacement(t))
|
|
return 0;
|
|
return daemonalive(&t->second);
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
Test test;
|
|
int ok;
|
|
|
|
testname = "daemon_restart_test";
|
|
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.first);
|
|
showerrors(&test.second);
|
|
showerrors(&test.helper);
|
|
return 1;
|
|
}
|
|
printf("daemon hard-crash endpoint recovery: ok\n");
|
|
return 0;
|
|
}
|