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>
407 lines
10 KiB
C
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;
|
|
}
|