aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKimplul <kimi.h.kuparinen@gmail.com>2025-08-17 23:36:54 +0300
committerKimplul <kimi.h.kuparinen@gmail.com>2025-08-17 23:36:54 +0300
commit7a811406dd16e057204bed1aa15cfe33d81ccb6b (patch)
tree67437eea5c2b922cf3847f4b24b4988b0a566d36
downloadcovsrv-7a811406dd16e057204bed1aa15cfe33d81ccb6b.tar.gz
covsrv-7a811406dd16e057204bed1aa15cfe33d81ccb6b.zip
initial commit
-rw-r--r--.gitignore4
-rw-r--r--Makefile32
-rw-r--r--include/covsrv/covsrv.h23
-rwxr-xr-xscripts/run-test26
-rw-r--r--src/client.c97
-rw-r--r--src/server.c389
-rw-r--r--tests/fail.c32
-rw-r--r--tests/pass.c39
-rw-r--r--tests/test.h10
9 files changed, 652 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a31998e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+build
+reports
+coverage
+vgcore.*
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f4b03ae
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,32 @@
+CFLAGS = -Wall -Wextra -O0 -g -Iinclude
+
+.PHONY: all
+all:
+ mkdir -p build
+ $(CC) $(CFLAGS) src/server.c -o build/covsrv
+
+.PHONY: clean
+clean:
+ rm -rf build reports coverage vgcore.*
+
+build/pass: include/covsrv/covsrv.h src/client.c tests/pass.c
+ $(CC) $(CFLAGS) --coverage -DCOVERAGE=1 src/client.c tests/pass.c -o build/pass
+
+.PHONY: xpass
+.IGNORE: xpass
+xpass: build/pass
+ ./scripts/run-test build/pass
+
+build/fail: include/covsrv/covsrv.h src/client.c tests/fail.c
+ $(CC) $(CFLAGS) --coverage -DCOVERAGE=1 src/client.c tests/fail.c -o build/fail
+
+.PHONY: xfail
+.IGNORE: xfail
+xfail: build/fail
+ ./scripts/run-test build/fail
+
+.PHONY: check
+coverage: all xpass xfail
+ mkdir -p coverage
+ lcov --capture --directory . --out coverage/covsrv.info
+ genhtml coverage/covsrv.info --out coverage
diff --git a/include/covsrv/covsrv.h b/include/covsrv/covsrv.h
new file mode 100644
index 0000000..30dd348
--- /dev/null
+++ b/include/covsrv/covsrv.h
@@ -0,0 +1,23 @@
+#ifndef COVSRV_H
+#define COVSRV_H
+
+#define COVSRV_NUM2STR2(x) #x
+#define COVSRV_NUM2STR(x) COVSRV_NUM2STR2(x)
+#define COVSRV_ID (__FILE__ ":" COVSRV_NUM2STR(__LINE__))
+
+#if defined(COVERAGE)
+
+#define covsrv_die() covsrv_die2(COVSRV_ID)
+int covsrv_die2(const char *id);
+int covsrv_init();
+void covsrv_destroy();
+
+#else
+
+#define covsrv_die() 0
+static inline int covsrv_init() {return 0;}
+static inline void covsrv_destroy() {}
+
+#endif
+
+#endif /* COVSRV_H */
diff --git a/scripts/run-test b/scripts/run-test
new file mode 100755
index 0000000..c971120
--- /dev/null
+++ b/scripts/run-test
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+TEST="$1"
+NAME=$(basename "$TEST")
+
+mkdir -p "reports/$NAME"
+
+I=0
+while :; do
+ # if no error happened, consider it a pass
+ if valgrind -q --leak-check=full --error-exitcode=1 \
+ "./$TEST" > "reports/$NAME/run-$I" 2>&1
+ then
+ echo "$NAME: PASSED"
+ exit 0
+ fi
+
+ # an error occured, was it handled properly?
+ if grep 'COVSRV: EXIT' "reports/$NAME/run-$I" >/dev/null; then
+ I=$((I+1))
+ continue
+ fi
+
+ echo "$NAME: FAILED"
+ exit 1
+done
diff --git a/src/client.c b/src/client.c
new file mode 100644
index 0000000..e95bb0f
--- /dev/null
+++ b/src/client.c
@@ -0,0 +1,97 @@
+/* LCOV_EXCL_START */
+#if defined(COVERAGE)
+
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <assert.h>
+
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include <covsrv/covsrv.h>
+
+int sock = -1;
+
+int covsrv_init()
+{
+ char *name = getenv("COVSRV_SOCKET");
+ if (!name) {
+ fprintf(stderr, "\tCOVSRV: SOCK -- COVSRV_SOCKET not defined\n");
+ return -1;
+ }
+
+ sock = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+ if (sock == -1) {
+ fprintf(stderr, "\tCOVSRV: SOCK -- failed opening %s: %s\n",
+ name, strerror(errno));
+ return -1;
+ }
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, name, sizeof(addr.sun_path) - 1);
+
+ if (connect(sock, (const struct sockaddr *)&addr, sizeof(addr)) == -1) {
+ fprintf(stderr, "\tCOVSRV: SOCK -- failed opening connection to %s: %s\n",
+ name, strerror(errno));
+ close(sock);
+ sock = -1;
+ return -1;
+ }
+
+ return 0;
+}
+
+int covsrv_die2(const char *id)
+{
+ assert(sock != -1);
+ ssize_t len = (ssize_t)strlen(id);
+ ssize_t w = write(sock, id, len);
+ if (w == -1) {
+ fprintf(stderr, "\tCOVSRV: WRITE -- write failed: %s\n",
+ strerror(errno));
+
+ abort();
+ return 0;
+ }
+
+ /* could try to send the rest instead of printing a warning? */
+ if (w != len)
+ fprintf(stderr, "\tCOVSRV: WRITE -- partial write\n");
+
+ int seen = 0;
+ ssize_t r = read(sock, &seen, sizeof(seen));
+ if (r == -1) {
+ fprintf(stderr, "\tCOVSRV: READ -- read failed: %s\n",
+ strerror(errno));
+ abort();
+ return 0;
+ }
+
+ if (r != sizeof(seen)) {
+ fprintf(stderr, "\tCOVSRV: READ -- partial read\n");
+ abort();
+ return 0;
+ }
+
+ if (seen)
+ return 0;
+
+ printf("\tCOVSRV: DIE @ %s\n", id);
+ return 1;
+}
+
+void covsrv_destroy()
+{
+ assert(sock != -1);
+ close(sock);
+ printf("\tCOVSRV: EXIT\n");
+}
+
+#endif /* COVERAGE */
+/* LCOV_EXCL_STOP */
diff --git a/src/server.c b/src/server.c
new file mode 100644
index 0000000..3a41a87
--- /dev/null
+++ b/src/server.c
@@ -0,0 +1,389 @@
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <assert.h>
+
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <poll.h>
+
+enum action {
+ QUIT,
+ ABORT,
+ NEXT
+};
+
+/* vector of pollfds */
+struct fds {
+ size_t n;
+ struct pollfd *buf;
+};
+
+/* simple hashmap */
+struct ids {
+ size_t n, s;
+ char **buf;
+};
+
+static struct fds fds_create()
+{
+ return (struct fds) {
+ .n = 0,
+ .buf = NULL
+ };
+}
+
+static int fds_add(struct fds *fds, int fd)
+{
+ for (size_t i = 0; i < fds->n; ++i) {
+ /* reuse existing slot */
+ if (fds->buf[i].fd == -1) {
+ fds->buf[i].fd = fd;
+ return 0;
+ }
+ }
+
+ /* resize vector */
+ size_t size = (fds->n + 1) * sizeof(struct pollfd);
+ struct pollfd *new_buf = realloc(fds->buf, size);
+ if (!new_buf)
+ return -1;
+
+ fds->buf = new_buf;
+ fds->buf[fds->n].fd = fd;
+ fds->buf[fds->n].events = POLLIN;
+ fds->buf[fds->n].revents = 0;
+ fds->n++;
+ return 0;
+}
+
+static void fds_remove(struct fds *fds, size_t i)
+{
+ assert(i < fds->n);
+ if (fds->buf[i].fd != -1) {
+ close(fds->buf[i].fd);
+ fds->buf[i].fd = -1;
+ }
+}
+
+static void fds_destroy(struct fds *fds)
+{
+ for (size_t i = 0; i < fds->n; ++i)
+ fds_remove(fds, i);
+
+ free(fds->buf);
+}
+
+static struct ids ids_create()
+{
+ return (struct ids){
+ .n = 0,
+ .s = 0,
+ .buf = NULL
+ };
+}
+
+static void ids_destroy(struct ids *ids)
+{
+ for (size_t i = 0; i < ids->s; ++i)
+ free(ids->buf[i]);
+}
+
+static inline size_t djb2(const char *s)
+{
+ size_t l = strlen(s);
+ size_t h = 5381;
+ for (size_t i = 0; i < l; ++i)
+ h = ((h << 5) + h) + s[i];
+
+ return h;
+}
+
+static char **ids_insert(struct ids *ids, char *id)
+{
+ /* when over 50% full, rebuild into new pow2 buffer */
+ if (2 * ids->n >= ids->s) {
+ size_t old_s = ids->s;
+ char **old_buf = ids->buf;
+
+ size_t new_s = ids->s ? 2 * ids->s : 1;
+ char **new_buf = calloc(new_s, sizeof(char *));
+ if (!new_buf)
+ return NULL;
+
+ ids->n = 0;
+ ids->s = new_s;
+ ids->buf = new_buf;
+
+ /* place all items in old buffer into new buffer, this should
+ * always succeed */
+ for (size_t i = 0; i < old_s; ++i) {
+ if (!old_buf[i])
+ continue;
+
+ char **ok = ids_insert(ids, old_buf[i]);
+ assert(ok);
+
+ /* maybe unused when compiled with NDEBUG, avoid warning */
+ (void)ok;
+ }
+
+ free(old_buf);
+ }
+
+ size_t h = djb2(id);
+ for (size_t i = 0; i < ids->s; ++i) {
+ /* fast pow2 modulo */
+ size_t idx = (h + i) & (ids->s - 1);
+
+ char **slot = &ids->buf[idx];
+ /* insert into empty slot */
+ if (!*slot) {
+ ids->n++;
+ *slot = id;
+ return slot;
+ }
+
+ /* does this item already exist? */
+ if (strcmp(*slot, id) == 0)
+ return slot;
+ }
+
+ /* shouldn't happen */
+ return NULL;
+}
+
+static enum action handle_req(struct ids *ids, int fd)
+{
+ const size_t len = 4096;
+ char *buf = malloc(len);
+ if (!buf) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed allocating req buf\n");
+ free(buf);
+ return ABORT;
+ }
+
+ ssize_t r = read(fd, buf, len - 1);
+ if (r == 0) {
+ /* EOF for socket */
+ free(buf);
+ return NEXT;
+ }
+
+ if (r == -1) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed reading req: %s\n",
+ strerror(errno));
+ free(buf);
+ return ABORT;
+ }
+
+ /* ensure null termination */
+ buf[r] = '\0';
+
+ /* does the client want us to exit? */
+ if (strcmp(buf, "quit\n") == 0) {
+ free(buf);
+ return QUIT;
+ }
+
+ /* check if we've already seen this location */
+ char **item = ids_insert(ids, buf);
+ if (!item) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed inserting location\n");
+ free(buf);
+ return ABORT;
+ }
+
+ int exists = *item != buf;
+ if (exists)
+ free(buf);
+
+ ssize_t w = write(fd, &exists, sizeof(exists));
+ if (w == -1) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed writing response: %s\n",
+ strerror(errno));
+ return ABORT;
+ }
+
+ if (w != sizeof(exists)) {
+ fprintf(stderr, "\tCOVSRV: SRV -- partial write\n");
+ return ABORT;
+ }
+
+ return NEXT;
+}
+
+static int handle_new(struct fds *fds, int sock)
+{
+ int client = accept(sock, NULL, NULL);
+ if (client == -1) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed accepting connection: %s\n",
+ strerror(errno));
+ return -1;
+ }
+
+ if (fds_add(fds, client)) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed inserting new connection\n");
+ close(client);
+ return -1;
+ }
+
+ return 0;
+}
+
+static enum action handle_in(struct fds *fds, struct ids *ids, size_t i, int sock)
+{
+ if (fds->buf[i].fd == sock) {
+ if (handle_new(fds, sock))
+ return ABORT;
+
+ return NEXT;
+ } else {
+ enum action a = handle_req(ids, fds->buf[i].fd);
+ if (a != NEXT)
+ return a;
+ }
+
+ return NEXT;
+}
+
+static enum action handle_event(struct fds *fds, struct ids *ids, int sock)
+{
+ for (size_t i = 0; i < fds->n; ++i) {
+ if (fds->buf[i].revents & POLLIN) {
+ enum action a = handle_in(fds, ids, i, sock);
+ if (a != NEXT)
+ return a;
+ }
+ else if (fds->buf[i].revents & (POLLHUP | POLLERR)) {
+ /* shouldn't happen...? */
+ if (fds->buf[i].fd == sock)
+ return ABORT;
+
+ fds_remove(fds, i);
+ }
+ else if (fds->buf[i].revents) {
+ /* should never happen */
+ abort();
+ }
+
+ /* because we directly add to the queue, we might end up going
+ * over an fd with zero, but there's no need to react to it in
+ * any way */
+ }
+
+ return NEXT;
+}
+
+static enum action loop(struct ids *ids, int sock)
+{
+ struct fds fds = fds_create();
+ if (fds_add(&fds, sock)) {
+ close(sock);
+ return ABORT;
+ }
+
+ int ret = 0;
+ enum action a = ABORT;
+ while ((ret = poll(fds.buf, fds.n, -1)) > 0) {
+ a = handle_event(&fds, ids, sock);
+ if (a == NEXT)
+ continue;
+
+ break;
+ }
+
+ fds_destroy(&fds);
+ return ret <= 0 ? ABORT : a;
+}
+
+static void usage(FILE *f, char *name)
+{
+ fprintf(f, "usage: COVSRV_SOCKET=<socket> %s <quit>\n", name);
+}
+
+static int quit(char *argv[], const char *name, int sock, struct sockaddr_un addr)
+{
+ if (strcmp(argv[1], "quit") != 0) {
+ fprintf(stderr, "\tCOVSRV: SRV -- unknown command '%s'\n", argv[1]);
+ usage(stderr, argv[0]);
+ close(sock);
+ return -1;
+ }
+
+ if (connect(sock, (const struct sockaddr *)&addr, sizeof(addr)) == -1) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed opening connection to %s: %s\n",
+ name, strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ const char *msg = "quit\n";
+ ssize_t w = write(sock, msg, strlen(msg));
+ if (w != (ssize_t)strlen(msg)) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed sending quit command: %s\n",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ close(sock);
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ if (argc > 2) {
+ usage(stderr, argv[0]);
+ return -1;
+ }
+
+ char *name = getenv("COVSRV_SOCKET");
+ if (!name) {
+ fprintf(stderr, "\tCOVSRV: SRV -- COVSRV_SOCKET not defined\n");
+ return -1;
+ }
+
+ int sock = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+ if (sock == -1) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed opening %s: %s\n",
+ name, strerror(errno));
+ return -1;
+ }
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, name, sizeof(addr.sun_path) - 1);
+
+ if (argc == 2)
+ return quit(argv, name, sock, addr);
+
+ if (bind(sock, (const struct sockaddr *)&addr, sizeof(addr)) == -1) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed binding %s: %s\n",
+ name, strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ /* should probably be something like k * $(nproc), but 128 is probably
+ * fine for now */
+ if (listen(sock, 128) == -1) {
+ fprintf(stderr, "\tCOVSRV: SRV -- failed listening to %s: %s\n",
+ name, strerror(errno));
+ unlink(name);
+ close(sock);
+ return -1;
+ }
+
+ struct ids ids = ids_create();
+ enum action a = loop(&ids, sock);
+ ids_destroy(&ids);
+
+ /* sock should've been closed by loop but we still need to unbind it */
+ unlink(name);
+ return a != QUIT;
+}
diff --git a/tests/fail.c b/tests/fail.c
new file mode 100644
index 0000000..b1cd416
--- /dev/null
+++ b/tests/fail.c
@@ -0,0 +1,32 @@
+#include <stdio.h>
+#include <assert.h>
+
+#include "test.h"
+
+/* this is a well behaved program that prints out a smiley face */
+
+int main()
+{
+#if defined(COVERAGE)
+ assert(!covsrv_init());
+ atexit(covsrv_destroy);
+#endif
+
+ /* alloc memory for the smiley */
+ char *p = mallocc(2 * sizeof(char));
+ if (!p) {
+ fprintf(stderr, "oops, malloc failed :(\n");
+ return -1;
+ }
+
+ p[0] = ':';
+ p[1] = ')';
+
+ /* oops, forgot the terminating null, let's realloc
+ * (but in a dangerous way) */
+ p = reallocc(p, 3 * sizeof(char));
+ p[2] = '\0';
+ printf("%s\n", p);
+
+ free(p);
+}
diff --git a/tests/pass.c b/tests/pass.c
new file mode 100644
index 0000000..bd5bfc3
--- /dev/null
+++ b/tests/pass.c
@@ -0,0 +1,39 @@
+#include <stdio.h>
+#include <assert.h>
+
+#include "test.h"
+
+/* this is a well behaved program that prints out a smiley face */
+
+int main()
+{
+#if defined(COVERAGE)
+ assert(!covsrv_init());
+ atexit(covsrv_destroy);
+#endif
+
+ /* alloc memory for the smiley */
+ char *p = mallocc(2 * sizeof(char));
+ if (!p) {
+ fprintf(stderr, "oops, malloc failed :(\n");
+ return -1;
+ }
+
+ p[0] = ':';
+ p[1] = ')';
+
+ /* oops, forgot the terminating null, let's realloc */
+ char *new_p = reallocc(p, 3 * sizeof(char));
+ if (!new_p) {
+ fprintf(stderr, "oops, realloc failed :(\n");
+ free(p);
+ return -1;
+ }
+
+ p = new_p;
+
+ p[2] = '\0';
+ printf("%s\n", p);
+
+ free(p);
+}
diff --git a/tests/test.h b/tests/test.h
new file mode 100644
index 0000000..d7832d1
--- /dev/null
+++ b/tests/test.h
@@ -0,0 +1,10 @@
+#ifndef TEST_H
+#define TEST_H
+
+#include <stdlib.h>
+#include <covsrv/covsrv.h>
+
+#define mallocc(n) ({covsrv_die() ? NULL : malloc(n);})
+#define reallocc(p, n) ({covsrv_die() ? NULL : realloc(p, n);})
+
+#endif /* TEST_ALLOC_H */