Files
strans/tests/daemon_collision_test.c
Hojun-Cho 7220286ab5 test: share one daemon and D-Bus harness across live tests
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.
2026-08-16 16:44:05 +09:00

215 lines
5.1 KiB
C

#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "live.h"
#include "livebus.h"
typedef struct Test Test;
struct Test
{
Live l;
Daemon first;
Daemon second;
int ipcfirst;
int ipcnew;
DBusConnection *busfirst;
DBusConnection *busnew;
char address[512];
};
static int
setup(Test *t)
{
memset(t, 0, sizeof *t);
daemoninit(&t->first, "first daemon");
daemoninit(&t->second, "second daemon");
t->ipcfirst = -1;
t->ipcnew = -1;
return livesetup(&t->l, "collision");
}
static int
addressunchanged(Test *t)
{
char contents[2048], address[512];
size_t ncontents;
pid_t declared;
if(!readfile(t->l.addrfile, contents, sizeof contents, &ncontents) ||
!parseaddress(contents, ncontents, address, sizeof address, &declared))
return 0;
if(declared != t->first.pid)
return fail("IBus address PID %ld, expected %ld", (long)declared,
(long)t->first.pid);
return strcmp(address, t->address) == 0 ||
fail("first private IBus address changed after collision");
}
static int
waitsecond(Test *t)
{
struct pollfd pfd;
int n, status, timeout;
int64_t deadline;
deadline = nowms() + Starttimeout;
for(;;){
do
n = waitpid(t->second.pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == t->second.pid){
t->second.pid = -1;
readerrors(&t->second);
if(!WIFEXITED(status) || WEXITSTATUS(status) == 0)
return fail("second daemon did not exit unsuccessfully: %#x",
status);
if(strstr(t->second.err, "IPC endpoint is already in use") == NULL)
return fail("second daemon did not report the IPC collision");
return 1;
}
if(n < 0){
fail("waitpid second daemon %ld: %s", (long)t->second.pid,
strerror(errno));
if(errno == ECHILD)
t->second.pid = -1;
return 0;
}
if(!daemonalive(&t->first))
return 0;
timeout = leftms(deadline);
if(timeout == 0){
fail("second daemon did not exit after the endpoint collision");
killdaemon(&t->second, NULL);
return 0;
}
pfd.fd = t->second.errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
n = poll(&pfd, 1, timeout > 20 ? 20 : timeout);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("poll second daemon: %s", strerror(errno));
if(pfd.revents != 0)
readerrors(&t->second);
}
}
static int
runcollision(Test *t, char *program, char *mapdir)
{
if(!startdaemon(&t->l, &t->first, program, mapdir) ||
!waitready(&t->l, &t->first, t->address, sizeof t->address))
return 0;
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-collision"))
return 0;
t->busfirst = openbus(t->address, "persistent pre-collision");
if(t->busfirst == NULL ||
!hello(t->busfirst, NULL, 0, "persistent pre-collision Hello"))
return 0;
if(!startdaemon(&t->l, &t->second, program, mapdir) ||
!waitsecond(t) || !daemonalive(&t->first) || !addressunchanged(t))
return 0;
if(!ipcprobe(t->ipcfirst, "persistent post-collision"))
return 0;
if(!createcontext(t->busfirst, NULL, 0,
"persistent post-collision context"))
return 0;
t->ipcnew = connectsocket(t->l.socket, nowms() + Calltimeout);
if(t->ipcnew < 0)
return fail("connect new IPC client after collision: %s",
strerror(errno));
if(!ipcprobe(t->ipcnew, "new post-collision"))
return 0;
t->busnew = openbus(t->address, "new post-collision");
if(t->busnew == NULL ||
!hello(t->busnew, NULL, 0, "new post-collision Hello") ||
!createcontext(t->busnew, NULL, 0, "new post-collision context"))
return 0;
return daemonalive(&t->first);
}
static int
cleanup(Test *t)
{
int n, ok, status;
ok = 1;
closebus(&t->busnew);
closebus(&t->busfirst);
if(t->ipcnew >= 0){
if(close(t->ipcnew) < 0)
ok = fail("close new 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(t->second.pid > 0){
do
n = waitpid(t->second.pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == t->second.pid)
t->second.pid = -1;
else if(n == 0){
ok = fail("second daemon still running during cleanup");
if(!killdaemon(&t->second, NULL))
ok = 0;
}else{
fail("check second daemon during cleanup: %s", strerror(errno));
ok = 0;
if(errno == ECHILD)
t->second.pid = -1;
else if(!killdaemon(&t->second, NULL))
ok = 0;
}
}
if(!stopdaemon(&t->first))
ok = 0;
if(!closeerrors(&t->first))
ok = 0;
if(!closeerrors(&t->second))
ok = 0;
if(!liveclean(&t->l))
ok = 0;
return ok;
}
int
main(int argc, char **argv)
{
Test test;
int ok;
testname = "daemon_collision_test";
if(argc != 3){
fprintf(stderr, "usage: daemon_collision_test strans mapdir\n");
return 2;
}
ok = setup(&test);
if(ok)
ok = runcollision(&test, argv[1], argv[2]);
if(!cleanup(&test))
ok = 0;
if(!ok){
showerrors(&test.first);
showerrors(&test.second);
return 1;
}
printf("daemon endpoint collision ownership: ok\n");
return 0;
}