Files
strans/tests/daemon_restart_test.c
Hojun-Cho 997e4c8d93 srv, ibus: a note the daemon lives through keeps its endpoints
srvnote and addrnote unlinked on any note at all, and plan9port marks
SIGPIPE Ignore: notify.c:59 lists it, and signotify runs the handler
chain first and only then finds the Ignore flag and returns.  So one
broken pipe took the IPC socket and the IBus address file away from a
daemon that went on running -- measured on a private runtime dir, a
single kill -PIPE left the process in state Ssl with both files gone,
so every client that focused a widget afterwards silently had no input
method and only a restart brought it back.

libxcb writes with writev, so the note is a broken X connection away;
today xim.c's die() masks it by taking the daemon down on the same
event, which is exactly why the two must not depend on each other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:59:13 +09:00

407 lines
10 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;
}
/*
* plan9port ignores a broken pipe: the note handlers run and the daemon
* goes on serving, so the endpoints it announced must still be there.
*/
static int
survivenote(Test *t)
{
struct stat st;
int fd, ok;
if(kill(t->firstpid, SIGPIPE) < 0)
return fail("send a broken pipe to the first daemon: %s",
strerror(errno));
pausems(200);
if(!daemonalive(&t->first))
return fail("first daemon died of a broken pipe");
if(lstat(t->l.socket, &st) < 0)
return fail("IPC socket taken by a note the daemon survived: %s",
strerror(errno));
if(lstat(t->l.addrfile, &st) < 0)
return fail("IBus address taken by a note the daemon survived: %s",
strerror(errno));
fd = connectsocket(t->l.socket, nowms() + Calltimeout);
if(fd < 0)
return fail("connect after a broken pipe: %s", strerror(errno));
ok = ipcprobe(fd, "after a broken pipe");
if(close(fd) < 0)
return fail("close the client opened after a broken pipe: %s",
strerror(errno));
return ok;
}
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(!survivenote(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 endpoints across a note and a hard crash: ok\n");
return 0;
}