Files
strans/tests/daemon_restart_test.c
Hojun-Cho dde9fb431b test: probe the dead IBus address in-process
rejectold forked and re-execed the test binary with --reject-address
just to call dbus_connection_open_private once against the crashed
daemon's abstract address, then waited on the helper, drained its
stderr pipe and checked its exit status. An abstract socket with no
listener refuses the connection immediately, so the call cannot block
and needs no separate process.

Open the address in-process and require a null connection with the
D-Bus error set, freeing the error afterwards. This drops the helper
process, its stderr capture, the Test.helper fields and the argv
dispatch in main. killdaemon stays; the daemons still need it.
2026-08-16 16:44:05 +09:00

373 lines
9.4 KiB
C

#define _GNU_SOURCE
#include <errno.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;
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");
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;
}
/* The dead daemon's abstract socket has no listener, so this must fail. */
static int
rejectold(Test *t, char *where)
{
DBusConnection *conn;
DBusError err;
int ok;
dbus_error_init(&err);
conn = dbus_connection_open_private(t->firstaddress, &err);
if(conn != NULL){
dbus_connection_set_exit_on_disconnect(conn, FALSE);
closebus(&conn);
ok = fail("old private address accepted a connection %s", where);
}else
ok = dbus_error_is_set(&err) ||
fail("old private address failed without a D-Bus error %s", where);
dbus_error_free(&err);
return ok;
}
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->first, NULL))
ok = 0;
if(!stopdaemon(&t->second))
ok = 0;
if(!closeerrors(&t->first))
ok = 0;
if(!closeerrors(&t->second))
ok = 0;
if(!liveclean(&t->l))
ok = 0;
return ok;
}
static int
runrestart(Test *t, 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, "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){
fprintf(stderr, "usage: daemon_restart_test strans mapdir\n");
return 2;
}
ok = setup(&test);
if(ok)
ok = runrestart(&test, argv[1], argv[2]);
if(!cleanup(&test))
ok = 0;
if(!ok){
showerrors(&test.first);
showerrors(&test.second);
return 1;
}
printf("daemon hard-crash endpoint recovery: ok\n");
return 0;
}