changeset 18:600204c31bf0

misc: refactor
author David Demelier <markand@malikania.fr>
date Tue, 12 Jul 2022 20:20:51 +0200
parents 40fe70256fb0
children de4bf839b565
files .hgignore Makefile config.mk db.c db.h doc/api.md http.c http.h lib/log.c lib/log.h lib/types.c lib/types.h lib/util.c lib/util.h log.c log.h page-api-jobs.c page-api-jobs.h page-api-projects.c page-api-projects.h page-api-workers.c page-api-workers.h page-index.c page.c page.h req.c req.h scictl.c scid.c scid/db.c scid/db.h scid/http.c scid/http.h scid/main.c scid/page-api-jobs.c scid/page-api-jobs.h scid/page-api-projects.c scid/page-api-projects.h scid/page-api-workers.c scid/page-api-workers.h scid/page-index.c scid/page.c scid/page.h sciwebd.c sciworkerd.c sciworkerd/main.c sciworkerd/task.c sciworkerd/task.h types.c types.h util.c util.h
diffstat 52 files changed, 3251 insertions(+), 3626 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Oct 06 16:22:23 2021 +0200
+++ b/.hgignore	Tue Jul 12 20:20:51 2022 +0200
@@ -20,10 +20,6 @@
 
 # executables.
 ^bcc$
-^scictl$
-^scid$
-^sciwebd$
-^sciworkerd$
 
 # tests.
 ^tests/test-db$
--- a/Makefile	Wed Oct 06 16:22:23 2021 +0200
+++ b/Makefile	Tue Jul 12 20:20:51 2022 +0200
@@ -20,53 +20,43 @@
 
 include config.mk
 
-LIBSCI_SRCS=            db.c \
-                        log.c \
-                        req.c \
-                        types.c \
-                        util.c \
-                        extern/libsqlite/sqlite3.c
-LIBSCI_DATA=            sql/init.h \
-                        sql/job-add.h \
-                        sql/job-todo.h \
-                        sql/jobresult-add.h \
-                        sql/project-add.h \
-                        sql/project-find-id.h \
-                        sql/project-find.h \
-                        sql/project-list.h \
-                        sql/project-update.h \
-                        sql/worker-add.h \
-                        sql/worker-find.h \
-                        sql/worker-find-id.h \
-                        sql/worker-list.h
+LIBSCI_SRCS=            lib/log.c                       \
+                        lib/types.c                     \
+                        lib/util.c
 LIBSCI_OBJS=            ${LIBSCI_SRCS:.c=.o}
 LIBSCI_DEPS=            ${LIBSCI_SRCS:.c=.d}
 
-SCID_SRCS=              scid.c
+SCID_SQL=               sql/init.h                      \
+                        sql/job-add.h                   \
+                        sql/job-todo.h                  \
+                        sql/jobresult-add.h             \
+                        sql/project-add.h               \
+                        sql/project-find-id.h           \
+                        sql/project-find.h              \
+                        sql/project-list.h              \
+                        sql/project-update.h            \
+                        sql/worker-add.h                \
+                        sql/worker-find.h               \
+                        sql/worker-find-id.h            \
+                        sql/worker-list.h
+
+SCID_SRCS=              extern/libsqlite/sqlite3.c      \
+                        scid/db.c                       \
+                        scid/http.c                     \
+                        scid/main.c                     \
+                        scid/page-api-jobs.c            \
+                        scid/page-api-projects.c        \
+                        scid/page-api-workers.c         \
+                        scid/page.c
 SCID_OBJS=              ${SCID_SRCS:.c=.o}
 SCID_DEPS=              ${SCID_SRCS:.c=.d}
 
-SCIWORKERD_SRCS=        sciworkerd.c
+SCIWORKERD_SRCS=        sciworkerd/main.c sciworkerd/task.c
 SCIWORKERD_OBJS=        ${SCIWORKERD_SRCS:.c=.o}
 SCIWORKERD_DEPS=        ${SCIWORKERD_SRCS:.c=.d}
 
-SCICTL_SRCS=            scictl.c
-SCICTL_OBJS=            ${SCICTL_SRCS:.c=.o}
-SCICTL_DEPS=            ${SCICTL_SRCS:.c=.d}
-
-SCIWEBD_SRCS=           http.c \
-                        page-api-jobs.c \
-                        page-api-projects.c \
-                        page-api-workers.c \
-                        page.c \
-                        sciwebd.c
-SCIWEBD_OBJS=           ${SCIWEBD_SRCS:.c=.o}
-SCIWEBD_DEPS=           ${SCIWEBD_SRCS:.c=.d}
-
 MAN7=                   man/sci.7
-MAN8=                   man/scictl.8 \
-                        man/scid.8 \
-                        man/sciwebd.8 \
+MAN8=                   man/scid.8 \
                         man/sciworkerd.8
 
 TESTS=                  tests/test-db.c
@@ -87,6 +77,7 @@
 
 INCS=                   -Iextern/libsqlite \
                         -Iextern/libgreatest \
+                        -Ilib \
                         -I.
 DEFS=                   -DVARDIR=\"${VARDIR}\" \
                         -DTMPDIR=\"${TMPDIR}\" \
@@ -98,7 +89,7 @@
 .SUFFIXES:
 .SUFFIXES: .c .o .sql .h .in
 
-all: scid scictl sciwebd sciworkerd ${MAN7} ${MAN8}
+all: scid/scid sciworkerd/sciworkerd ${MAN7} ${MAN8}
 
 # for unit tests.
 .c:
@@ -116,7 +107,7 @@
 .sql.h:
 	./bcc -sc0 $< $< > $@
 
--include ${LIBSCI_DEPS} ${SCID_DEPS} ${SCIWORKERD_DEPS} ${SCICTL_DEPS} ${SCIWEBD_DEPS} ${TESTS_DEPS}
+-include ${LIBSCI_DEPS} ${SCID_DEPS} ${SCIWORKERD_DEPS} ${TESTS_DEPS}
 
 config.h:
 	@echo "using default configuration"
@@ -125,39 +116,25 @@
 bcc: extern/bcc/bcc.c
 	${CC} ${CFLAGS} -o $@ $< ${LDFLAGS}
 
-${LIBSCI_DATA}: bcc
-${LIBSCI_OBJS}: config.h ${LIBSCI_DATA}
+${LIBSCI_OBJS}: config.h
 
-libsci.a: ${LIBSCI_OBJS}
+lib/libsci.a: ${LIBSCI_OBJS}
 	${AR} -rc $@ ${LIBSCI_OBJS}
 
-${SCID_OBJS}: libsci.a
-
-scid: ${SCID_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCID_OBJS} libsci.a ${LIBBSD_LIBS} \
-		${JANSSON_LIBS} ${LDFLAGS}
-
-${SCIWORKERD_OBJS}: libsci.a
-
-sciworkerd: ${SCIWORKERD_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCIWORKERD_OBJS} libsci.a ${LIBBSD_LIBS} \
-		${LIBCURL_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
+${SCID_SQL}: bcc
+${SCID_OBJS}: ${SCID_SQL}
 
-${SCICTL_OBJS}: libsci.a
-
-scictl: ${SCICTL_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCICTL_OBJS} libsci.a ${LIBBSD_LIBS} \
-		${JANSSON_LIBS} ${LDFLAGS}
+scid/scid: lib/libsci.a ${SCID_OBJS}
+	${CC} ${CFLAGS} -o $@ ${SCID_OBJS} lib/libsci.a ${LIBBSD_LIBS} \
+		${JANSSON_LIBS} ${KCGI_LIBS} ${LDFLAGS}
 
-${SCIWEBD_OBJS}: libsci.a
-
-sciwebd: ${SCIWEBD_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCIWEBD_OBJS} libsci.a ${LIBBSD_LIBS} \
-		${KCGI_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
+sciworkerd/sciworkerd: lib/libsci.a ${SCIWORKERD_OBJS}
+	${CC} ${CFLAGS} -o $@ ${SCIWORKERD_OBJS} lib/libsci.a ${LIBBSD_LIBS} \
+		${LIBCURL_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
 
 install:
 	mkdir -p ${DESTDIR}${BINDIR}
-	cp scid scictl sciwebd sciworkerd ${DESTDIR}${BINDIR}
+	cp scid/scid sciworkerd/sciworkerd ${DESTDIR}${BINDIR}
 	mkdir -p ${DESTDIR}${MANDIR}/man7
 	cp ${MAN7} ${DESTDIR}${MANDIR}/man7
 	mkdir -p ${DESTDIR}${MANDIR}/man8
@@ -165,16 +142,12 @@
 
 clean:
 	rm -f bcc config.h tags cscope.out ${MAN7} ${MAN8}
-	rm -f libsci.a ${LIBSCI_OBJS} ${LIBSCI_DATA} ${LIBSCI_DEPS}
-	rm -f scid ${SCID_OBJS} ${SCID_DEPS}
-	rm -f scictl ${SCICTL_OBJS} ${SCICTL_DEPS}
-	rm -f sciworkerd ${SCIWORKERD_OBJS} ${SCIWORKERD_DEPS}
-	rm -f sciwebd ${SCIWEBD_OBJS} ${SCIWEBD_DEPS}
+	rm -f libsci.a ${LIBSCI_OBJS} ${LIBSCI_DEPS}
+	rm -f scid/scid ${SCID_OBJS} ${SCID_DEPS}
+	rm -f sciworkerd/sciworkerd ${SCIWORKERD_OBJS} ${SCIWORKERD_DEPS}
 	rm -f ${TESTS_OBJS} ${TESTS_DEPS}
 
-${TESTS_OBJS}: libsci.a
-
-tests: ${TESTS_OBJS}
+tests: lib/libsci.a ${TESTS_OBJS}
 	for t in ${TESTS_OBJS}; do $$t -v; done
 
 .PHONY: all clean install tests
--- a/config.mk	Wed Oct 06 16:22:23 2021 +0200
+++ b/config.mk	Tue Jul 12 20:20:51 2022 +0200
@@ -1,5 +1,5 @@
 CC=             cc
-CFLAGS=         -Wall -Wextra -g -O0 -Wno-cpp
+CFLAGS=         -g -O0 -Wno-cpp
 #CFLAGS=         -Wall -Wextra -fsanitize=address,undefined -g -O0
 #LDFLAGS=        -fsanitize=address,undefined
 
@@ -7,5 +7,4 @@
 BINDIR=         ${PREFIX}/bin
 MANDIR=         ${PREFIX}/share/man
 SHAREDIR=       ${PREFIX}/share
-TMPDIR=         ${PREFIX}/tmp
 VARDIR=         ${PREFIX}/var
--- a/db.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,419 +0,0 @@
-/*
- * db.c -- scid database access
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/queue.h>
-#include <assert.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include <sqlite3.h>
-
-#include "db.h"
-#include "log.h"
-#include "types.h"
-#include "util.h"
-
-#include "sql/init.h"
-#include "sql/job-add.h"
-#include "sql/job-todo.h"
-#include "sql/jobresult-add.h"
-#include "sql/project-add.h"
-#include "sql/project-update.h"
-#include "sql/project-find.h"
-#include "sql/project-find-id.h"
-#include "sql/project-list.h"
-#include "sql/worker-add.h"
-#include "sql/worker-find.h"
-#include "sql/worker-find-id.h"
-#include "sql/worker-list.h"
-
-#define CHAR(v) (const char *)(v)
-
-static sqlite3 *db;
-
-typedef void (*unpacker)(sqlite3_stmt *, struct db_ctx *, void *);
-
-struct str {
-	char *str;
-	SLIST_ENTRY(str) link;
-};
-
-struct list {
-	unpacker unpack;
-	void *data;
-	size_t datasz;
-	size_t elemwidth;
-	struct db_ctx *ctx;
-};
-
-SLIST_HEAD(strlist, str);
-
-static struct strlist *
-strlist_new(void)
-{
-	struct strlist *l;
-
-	l = util_calloc(1, sizeof (*l));
-	SLIST_INIT(l);
-
-	return l;
-}
-
-static const char *
-strlist_add(struct strlist *l, const char *text)
-{
-	struct str *s;
-
-	s = util_calloc(1, sizeof (*s));
-	s->str = util_strdup(text);
-
-	SLIST_INSERT_HEAD(l, s, link);
-
-	return s->str;
-}
-
-static void
-strlist_free(struct strlist *l)
-{
-	struct str *s, *tmp;
-
-	SLIST_FOREACH_SAFE(s, l, link, tmp) {
-		free(s->str);
-		free(s);
-	}
-
-	free(l);
-}
-
-static void
-project_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct project *project)
-{
-	project->id = sqlite3_column_int(stmt, 0);
-	project->name = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 1)));
-	project->desc = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 2)));
-	project->url = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 3)));
-	project->script = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 4)));
-}
-
-static void
-worker_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct worker *w)
-{
-	w->id = sqlite3_column_int(stmt, 0);
-	w->name = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 1)));
-	w->desc = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 2)));
-}
-
-static void
-job_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct job *job)
-{
-	job->id = sqlite3_column_int(stmt, 0);
-	job->tag = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 1)));
-	job->project_id = sqlite3_column_int(stmt, 2);
-}
-
-static void
-vbind(sqlite3_stmt *stmt, const char *fmt, va_list ap)
-{
-	for (int index = 1; *fmt; ++fmt) {
-		switch (*fmt) {
-		case 'i':
-			sqlite3_bind_int(stmt, index++, va_arg(ap, int));
-			break;
-		case 's':
-			sqlite3_bind_text(stmt, index++, va_arg(ap, const char *), -1, SQLITE_STATIC);
-			break;
-		case 'z':
-			sqlite3_bind_int64(stmt, index++, va_arg(ap, size_t));
-			break;
-		default:
-			break;
-		}
-	}
-}
-
-static int
-insert(const char *sql, const char *fmt, ...)
-{
-	assert(sql);
-	assert(fmt);
-
-	sqlite3_stmt *stmt = NULL;
-	va_list ap;
-	int ret = -1;
-
-	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
-		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
-
-	va_start(ap, fmt);
-	vbind(stmt, fmt, ap);
-	va_end(ap);
-
-	if (sqlite3_step(stmt) != SQLITE_DONE)
-		log_warn("db: %s", sqlite3_errmsg(db));
-	else
-		ret = sqlite3_last_insert_rowid(db);
-
-	sqlite3_finalize(stmt);
-
-	return ret;
-}
-
-static int
-update(const char *sql, const char *fmt, ...)
-{
-	assert(sql);
-	assert(fmt);
-
-	sqlite3_stmt *stmt = NULL;
-	va_list ap;
-	int ret = 1;
-
-	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
-		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
-
-	va_start(ap, fmt);
-	vbind(stmt, fmt, ap);
-	va_end(ap);
-
-	if (sqlite3_step(stmt) != SQLITE_DONE)
-		log_warn("db: %s", sqlite3_errmsg(db));
-	else
-		ret = 0;
-
-	sqlite3_finalize(stmt);
-
-	return ret;
-}
-
-static ssize_t
-list(struct list *sel, const char *sql, const char *args, ...)
-{
-	sqlite3_stmt *stmt = NULL;
-
-	va_list ap;
-	int step;
-	ssize_t ret = -1;
-	size_t tot = 0;
-
-	sel->ctx->handle = NULL;
-
-	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
-		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
-
-	va_start(ap, args);
-	vbind(stmt, args, ap);
-	va_end(ap);
-
-	sel->ctx->handle = strlist_new();
-
-	while (tot < sel->datasz && (step = sqlite3_step(stmt)) == SQLITE_ROW)
-		sel->unpack(stmt, sel->ctx, (unsigned char *)sel->data + (tot++ * sel->elemwidth));
-
-	if (step == SQLITE_OK || step == SQLITE_DONE || step == SQLITE_ROW)
-		ret = tot;
-	else {
-		memset(sel->data, 0, sel->datasz * sel->elemwidth);
-		strlist_free(sel->ctx->handle);
-		sel->ctx->handle = NULL;
-	}
-
-	sqlite3_finalize(stmt);
-
-	return ret;
-}
-
-int
-db_open(const char *path)
-{
-	assert(path);
-
-	if (sqlite3_open(path, &db) != SQLITE_OK)
-		return log_warn("db: open error: %s", sqlite3_errmsg(db)), -1;
-
-	/* Wait for 30 seconds to lock the database. */
-	sqlite3_busy_timeout(db, 30000);
-
-	if (sqlite3_exec(db, CHAR(sql_init), NULL, NULL, NULL) != SQLITE_OK)
-		return log_warn("db: initialization error: %s", sqlite3_errmsg(db)), -1;
-
-	return 0;
-}
-
-int
-db_project_add(struct project *p)
-{
-	return (p->id = insert(CHAR(sql_project_add), "ssss", p->name, p->desc,
-	    p->url, p->script)) < 0 ? -1 : 0;
-}
-
-int
-db_project_update(const struct project *p)
-{
-	assert(p);
-
-	return update(CHAR(sql_project_update), "ssssi", p->name, p->desc,
-	    p->url, p->script, p->id);
-}
-
-ssize_t
-db_project_list(struct db_ctx *ctx, struct project *projects, size_t projectsz)
-{
-	struct list sel = {
-		.unpack = (unpacker)project_unpacker,
-		.data = projects,
-		.datasz = projectsz,
-		.elemwidth = sizeof (*projects),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_project_list), "z", projectsz);
-}
-
-int
-db_project_find(struct db_ctx *ctx, struct project *project)
-{
-	struct list sel = {
-		.unpack = (unpacker)project_unpacker,
-		.data = project,
-		.datasz = 1,
-		.elemwidth = sizeof (*project),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_project_find), "s", project->name) == 1 ? 0 : -1;
-}
-
-int
-db_project_find_id(struct db_ctx *ctx, struct project *project)
-{
-	struct list sel = {
-		.unpack = (unpacker)project_unpacker,
-		.data = project,
-		.datasz = 1,
-		.elemwidth = sizeof (*project),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_project_find_id), "i", project->id) == 1 ? 0 : -1;
-}
-
-int
-db_worker_add(struct worker *wk)
-{
-	assert(wk);
-
-	return (wk->id = insert(CHAR(sql_worker_add), "ss", wk->name, wk->desc)) < 0 ? -1 : 0;
-}
-
-ssize_t
-db_worker_list(struct db_ctx *ctx, struct worker *wk, size_t wksz)
-{
-	assert(ctx);
-	assert(wk);
-
-	struct list sel = {
-		.unpack = (unpacker)worker_unpacker,
-		.data = wk,
-		.datasz = wksz,
-		.elemwidth = sizeof (*wk),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_worker_list), "z", wksz);
-}
-
-int
-db_worker_find(struct db_ctx *ctx, struct worker *wk)
-{
-	struct list sel = {
-		.unpack = (unpacker)worker_unpacker,
-		.data = wk,
-		.datasz = 1,
-		.elemwidth = sizeof (*wk),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_worker_find), "s", wk->name) == 1 ? 0 : -1;
-}
-
-int
-db_worker_find_id(struct db_ctx *ctx, struct worker *wk)
-{
-	struct list sel = {
-		.unpack = (unpacker)worker_unpacker,
-		.data = wk,
-		.datasz = 1,
-		.elemwidth = sizeof (*wk),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_worker_find_id), "i", wk->id) == 1 ? 0 : -1;
-}
-
-int
-db_job_add(struct job *job)
-{
-	assert(job);
-
-	return (job->id = insert(CHAR(sql_job_add),
-	    "si", job->tag, job->project_id)) < 0 ? -1 : 0;
-}
-
-ssize_t
-db_job_todo(struct db_ctx *ctx, struct job *jobs, size_t jobsz, int worker_id)
-{
-	assert(ctx);
-	assert(jobs);
-
-	struct list sel = {
-		.unpack = (unpacker)job_unpacker,
-		.data = jobs,
-		.datasz = jobsz,
-		.elemwidth = sizeof (*jobs),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_job_todo), "iiz", worker_id, worker_id, jobsz);
-}
-
-int
-db_jobresult_add(struct jobresult *r)
-{
-	assert(r);
-
-	return (r->id = insert(CHAR(sql_jobresult_add), "iiis", r->job_id,
-	    r->worker_id, r->exitcode, r->log)) < 0 ? -1 : 0;
-}
-
-void
-db_finish(void)
-{
-	if (db) {
-		sqlite3_close(db);
-		db = NULL;
-	}
-}
-
-void
-db_ctx_finish(struct db_ctx *ctx)
-{
-	if (ctx->handle) {
-		strlist_free(ctx->handle);
-		ctx->handle = NULL;
-	}
-}
--- a/db.h	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-/*
- * db.h -- scid database access
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_DB_H
-#define SCI_DB_H
-
-#include <sys/types.h>
-#include <stddef.h>
-
-struct project;
-struct worker;
-struct job;
-struct jobresult;
-
-struct db_ctx {
-	void *handle;
-};
-
-int
-db_open(const char *);
-
-int
-db_job_add(struct job *);
-
-ssize_t
-db_job_todo(struct db_ctx *, struct job *, size_t, int);
-
-int
-db_jobresult_add(struct jobresult *);
-
-int
-db_project_add(struct project *);
-
-int
-db_project_update(const struct project *);
-
-ssize_t
-db_project_list(struct db_ctx *, struct project *, size_t);
-
-int
-db_project_find(struct db_ctx *, struct project *);
-
-int
-db_project_find_id(struct db_ctx *, struct project *);
-
-int
-db_worker_add(struct worker *);
-
-ssize_t
-db_worker_list(struct db_ctx *, struct worker *, size_t);
-
-int
-db_worker_find(struct db_ctx *, struct worker *);
-
-int
-db_worker_find_id(struct db_ctx *, struct worker *);
-
-void
-db_finish(void);
-
-void
-db_ctx_finish(struct db_ctx *);
-
-#endif /* !SCI_DB_H */
--- a/doc/api.md	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-# Network REST API
-
-This document describes the network API for access through the `scid` program
-from `sciworker`.
-
-# Base definitions
-
-The API is versioned and starts with the `/api/v?` where the question mark is
-replaced with the current version.
-
-# API v1
-
-## (GET) /jobs/<w>
-
-Get the jobs pending that the worker `w` is supposed to run.
-
-Request:
-
-No data.
-
-Response:
-
-```
-[
-	{
-		"project": "foobar",
-		"tag": "1234"
-	}
-]
-```
-
-## (POST) /jobs/<w>
-
-Post the result of the a job from the worker `w`.
-
-Request:
-
-```
-{
-	"project": "foobar",
-	"tag": 1234",
-	"output": "stdout/stderr combined"
-}
-```
-
-### (GET) /script/<p>
-
-Get script code for project `p`.
-
-Request:
-
-No data.
-
-Reponse:
-
-```
-{
-	"code": "#!/bin/sh exit 0"
-}
-```
--- a/http.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-/*
- * http.c -- HTTP parsing and rendering
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/types.h>
-#include <assert.h>
-#include <stdarg.h>
-#include <stdlib.h>
-#include <stdio.h>
-#include <string.h>
-
-#include <kcgi.h>
-
-#include "http.h"
-#include "log.h"
-#include "page.h"
-#include "page-api-jobs.h"
-#include "page-api-projects.h"
-#include "page-api-workers.h"
-#include "req.h"
-
-enum page {
-	PAGE_API,
-	PAGE_LAST       /* Not used. */
-};
-
-static void
-dispatch_api(struct kreq *req)
-{
-	static const struct {
-		const char *prefix;
-		void (*handler)(struct kreq *);
-	} apis[] = {
-		{ "v1/jobs",            page_api_v1_jobs        },
-		{ "v1/projects",        page_api_v1_projects    },
-		{ "v1/workers",         page_api_v1_workers     },
-		{ NULL,                 NULL                    }
-	};
-
-	for (size_t i = 0; apis[i].prefix; ++i)
-		if (strncmp(req->path, apis[i].prefix, strlen(apis[i].prefix)) == 0)
-			return apis[i].handler(req);
-
-	page(req, NULL, KHTTP_404, KMIME_TEXT_HTML, "pages/404.html");
-}
-
-static const char *pages[] = {
-	[PAGE_API]              = "api"
-};
-
-static void (*handlers[])(struct kreq *req) = {
-	[PAGE_API]              = dispatch_api
-};
-
-static void
-process(struct kreq *req)
-{
-	assert(req);
-
-	log_debug("http: accessing page '%s'", req->path);
-
-	if (req->page == PAGE_LAST)
-		page(req, NULL, KHTTP_404, KMIME_TEXT_HTML, "pages/404.html");
-	else
-		handlers[req->page](req);
-}
-
-void
-http_fcgi_run(void)
-{
-	struct kreq req;
-	struct kfcgi *fcgi;
-
-	if (khttp_fcgi_init(&fcgi, NULL, 0, pages, PAGE_LAST, 0) != KCGI_OK)
-		return;
-
-	while (khttp_fcgi_parse(fcgi, &req) == KCGI_OK)
-		process(&req);
-
-	khttp_fcgi_free(fcgi);
-}
-
-void
-http_cgi_run(void)
-{
-	struct kreq req;
-
-	if (khttp_parse(&req, NULL, 0, pages, PAGE_LAST, 0) == KCGI_OK)
-		process(&req);
-}
--- a/http.h	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-/*
- * http.h -- HTTP parsing and rendering
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_HTTP_H
-#define SCI_HTTP_H
-
-void
-http_fcgi_run(void);
-
-void
-http_cgi_run(void);
-
-#endif /* !SCI_HTTP_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/log.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,76 @@
+/*
+ * log.c -- logging routines
+ *
+ * Copyright (c) 2020-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <syslog.h>
+
+#include "log.h"
+
+/* TODO: replace. */
+static struct {
+	enum log_level verbosity;
+} config = {
+	.verbosity = LOG_LEVEL_DEBUG
+};
+
+static int syslog_levels[] = {
+	[LOG_LEVEL_DEBUG]       = LOG_DEBUG,
+	[LOG_LEVEL_INFO]        = LOG_INFO,
+	[LOG_LEVEL_WARNING]     = LOG_WARNING
+};
+
+void
+log_open(const char *name)
+{
+	openlog(name, 0, LOG_USER);
+}
+
+void
+log_write(enum log_level level, const char *fmt, ...)
+{
+	assert(level >= LOG_LEVEL_WARNING && level <= LOG_LEVEL_DEBUG);
+	assert(fmt);
+
+	if (config.verbosity >= level) {
+		va_list ap;
+
+		va_start(ap, fmt);
+		log_vwrite(level, fmt, ap);
+		va_end(ap);
+	}
+}
+
+void
+log_vwrite(enum log_level level, const char *fmt, va_list ap)
+{
+	assert(level >= LOG_LEVEL_WARNING && level <= LOG_LEVEL_DEBUG);
+	assert(fmt);
+
+	char line[BUFSIZ];
+
+	vsnprintf(line, sizeof (line), fmt, ap);
+	syslog(syslog_levels[level], "%s", line);
+}
+
+void
+log_finish(void)
+{
+	if (config.verbosity > 0)
+		closelog();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/log.h	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,51 @@
+/*
+ * log.h -- logging routines
+ *
+ * Copyright (c) 2020-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_LOG_H
+#define SCI_LOG_H
+
+#include <stdarg.h>
+#include <stdlib.h>
+
+enum log_level {
+	LOG_LEVEL_WARNING = 1,
+	LOG_LEVEL_INFO,
+	LOG_LEVEL_DEBUG
+};
+
+#define log_debug(...)  log_write(LOG_LEVEL_DEBUG, __VA_ARGS__)
+#define log_warn(...)   log_write(LOG_LEVEL_WARNING, __VA_ARGS__)
+#define log_info(...)   log_write(LOG_LEVEL_INFO, __VA_ARGS__)
+#define log_die(...) do {                                               \
+        log_write(LOG_LEVEL_WARNING, __VA_ARGS__);                      \
+        exit(1);                                                        \
+} while (0)
+
+void
+log_open(const char *);
+
+void
+log_write(enum log_level, const char *, ...);
+
+void
+log_vwrite(enum log_level, const char *, va_list);
+
+void
+log_finish(void);
+
+#endif /* !SCI_LOG_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/types.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,222 @@
+/*
+ * types.c -- type definitions and conversions
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+
+#include "types.h"
+
+typedef json_t * (*packer)(const void *);
+typedef int      (*unpacker)(void *, json_t *);
+
+static inline json_t *
+job_packer(const struct job *job)
+{
+	return json_pack("{si si ss}",
+	    "id",               job->id,
+	    "project_id",       job->project_id,
+	    "tag",              job->tag
+	);
+}
+
+static inline int
+job_unpacker(struct job *job, json_t *doc)
+{
+	return json_unpack(doc, "{si si ss}",
+	    "id",               &job->id,
+	    "project_id",       &job->project_id,
+	    "tag",              &job->tag
+	);
+}
+
+static inline json_t *
+jobresult_packer(const struct jobresult *res)
+{
+	return json_pack("{si si si si ss}",
+	    "id",               res->id,
+	    "job_id",           res->job_id,
+	    "worker_id",        res->worker_id,
+	    "exitcode",         res->exitcode,
+	    "log",              res->log
+	);
+}
+
+static inline int
+jobresult_unpacker(struct jobresult *res, json_t *doc)
+{
+	return json_unpack(doc, "{si si si si ss}",
+	    "id",               &res->id,
+	    "job_id",           &res->job_id,
+	    "worker_id",        &res->worker_id,
+	    "exitcode",         &res->exitcode,
+	    "log",              &res->log
+	);
+}
+
+static inline json_t *
+worker_packer(const struct worker *w)
+{
+	return json_pack("{si ss ss}",
+	    "id",               w->id,
+	    "name",             w->name,
+	    "desc",             w->desc
+	);
+}
+
+static inline int
+worker_unpacker(struct worker *w, json_t *doc)
+{
+	return json_unpack(doc, "{si ss ss}",
+	    "id",               &w->id,
+	    "name",             &w->name,
+	    "desc",             &w->desc
+	);
+}
+
+static inline json_t *
+project_packer(struct project *p)
+{
+	return json_pack("{si ss ss ss ss}",
+	    "id",               p->id,
+	    "name",             p->name,
+	    "desc",             p->desc,
+	    "url",              p->url,
+	    "script",           p->script
+	);
+}
+
+static inline int
+project_unpacker(struct project *p, json_t *doc)
+{
+	return json_unpack(doc, "{si ss ss ss ss}",
+	    "id",               &p->id,
+	    "name",             &p->name,
+	    "desc",             &p->desc,
+	    "url",              &p->url,
+	    "script",           &p->script
+	);
+}
+
+static json_t *
+to(const void *array, size_t arraysz, size_t width, packer fn)
+{
+	json_t *doc;
+
+	if (arraysz == 1)
+		doc = fn(array);
+	else {
+		doc = json_array();
+
+		for (size_t i = 0; i < arraysz; ++i)
+			json_array_append(doc, fn((char *)array + (i * width)));
+	}
+
+	return doc;
+}
+
+static ssize_t
+from(void *array, size_t arraysz, size_t width, json_t *doc, unpacker fn)
+{
+	json_t *val;
+	size_t i, tot = 0;
+
+	if (json_is_array(doc)) {
+		json_array_foreach(doc, i, val) {
+			if (tot >= arraysz)
+				return errno = ERANGE, -1;
+			if (fn((char *)array + (tot++ * width), val) < 0)
+				return errno = EILSEQ, -1;
+		}
+	} else if (json_is_object(doc)) {
+		tot = 1;
+
+		if (fn(array, doc) < 0)
+			return errno = EILSEQ, -1;
+	} else
+		return errno = EINVAL, -1;
+
+	return tot;
+}
+
+json_t *
+job_to(const struct job *jobs, size_t jobsz)
+{
+	assert(jobs);
+
+	return to(jobs, jobsz, sizeof (*jobs), (packer)job_packer);
+}
+
+ssize_t
+job_from(struct job *jobs, size_t jobsz, json_t *doc)
+{
+	assert(jobs);
+	assert(doc);
+
+	return from(jobs, jobsz, sizeof (*jobs), doc, (unpacker)job_unpacker);
+}
+
+json_t *
+jobresult_to(const struct jobresult *res, size_t resz)
+{
+	assert(res);
+
+	return to(res, resz, sizeof (*res), (packer)jobresult_packer);
+}
+
+ssize_t
+jobresult_from(struct jobresult *res, size_t resz, json_t *doc)
+{
+	assert(res);
+	assert(doc);
+
+	return from(res, resz, sizeof (*res), doc, (unpacker)jobresult_unpacker);
+}
+
+json_t *
+worker_to(const struct worker *w, size_t wsz)
+{
+	assert(w);
+
+	return to(w, wsz, sizeof (*w), (packer)worker_packer);
+}
+
+ssize_t
+worker_from(struct worker *w, size_t wsz, json_t *doc)
+{
+	assert(w);
+	assert(doc);
+
+	return from(w, wsz, sizeof (*w), doc, (unpacker)worker_unpacker);
+}
+
+json_t *
+project_to(const struct project *proj, size_t projsz)
+{
+	assert(proj);
+
+	return to(proj, projsz, sizeof (*proj), (packer)project_packer);
+}
+
+ssize_t
+project_from(struct project *proj, size_t projsz, json_t *doc)
+{
+	assert(proj);
+	assert(doc);
+
+	return from(proj, projsz, sizeof (*proj), doc, (unpacker)project_unpacker);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/types.h	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,79 @@
+/*
+ * types.h -- type definitions and conversions
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_TYPES_H
+#define SCI_TYPES_H
+
+#include <sys/types.h>
+#include <stddef.h>
+
+#include <jansson.h>
+
+struct job {
+	int id;
+	int project_id;
+	const char *tag;
+};
+
+struct jobresult {
+	int id;
+	int job_id;
+	int worker_id;
+	int exitcode;
+	const char *log;
+};
+
+struct worker {
+	int id;
+	const char *name;
+	const char *desc;
+};
+
+struct project {
+	int id;
+	const char *name;
+	const char *desc;
+	const char *url;
+	const char *script;
+};
+
+json_t *
+job_to(const struct job *, size_t);
+
+ssize_t
+job_from(struct job *, size_t, json_t *);
+
+json_t *
+jobresult_to(const struct jobresult *, size_t);
+
+ssize_t
+jobresult_from(struct jobresult *, size_t, json_t *);
+
+json_t *
+worker_to(const struct worker *, size_t);
+
+ssize_t
+worker_from(struct worker *, size_t, json_t *);
+
+json_t *
+project_to(const struct project *, size_t);
+
+ssize_t
+project_from(struct project *, size_t, json_t *);
+
+#endif /* !SCI_TYPES_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/util.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,187 @@
+/*
+ * util.c -- miscellaneous utilities
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/stat.h>
+#include <assert.h>
+#include <err.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+void *
+util_malloc(size_t size)
+{
+	void *ret;
+
+	if (!(ret = malloc(size)))
+		err(1, "malloc");
+
+	return ret;
+}
+
+void *
+util_calloc(size_t n, size_t size)
+{
+	void *ret;
+
+	if (!(ret = calloc(n, size)))
+		err(1, "calloc");
+
+	return ret;
+}
+
+void *
+util_realloc(void *ptr, size_t size)
+{
+	void *ret;
+
+	if (!(ret = realloc(ptr, size)) && size)
+		err(1, "realloc");
+
+	return ret;
+}
+
+void *
+util_memdup(const void *ptr, size_t size)
+{
+	void *ret;
+
+	if (!(ret = malloc(size)))
+		err(1, "malloc");
+
+	return memcpy(ret, ptr, size);
+}
+
+char *
+util_strdup(const char *src)
+{
+	char *ret;
+
+	if (!(ret = strdup(src)))
+		err(1, "strdup");
+
+	return ret;
+}
+
+char *
+util_strndup(const char *src, size_t n)
+{
+	assert(src);
+
+	char *ret;
+
+	if (!(ret = strndup(src, n)))
+		err(1, "strndup");
+
+	return ret;
+}
+
+char *
+util_basename(const char *str)
+{
+	static char ret[PATH_MAX];
+	char tmp[PATH_MAX];
+
+	strlcpy(tmp, str, sizeof (tmp));
+	strlcpy(ret, basename(tmp), sizeof (ret));
+
+	return ret;
+}
+
+char *
+util_dirname(const char *str)
+{
+	static char ret[PATH_MAX];
+	char tmp[PATH_MAX];
+
+	strlcpy(tmp, str, sizeof (tmp));
+	strlcpy(ret, dirname(tmp), sizeof (ret));
+
+	return ret;
+}
+
+FILE *
+util_fmemopen(void *buf, size_t size, const char *mode)
+{
+	FILE *fp;
+
+	if (!(fp = fmemopen(buf, size, mode)))
+		err(1, "fmemopen");
+
+	return fp;
+}
+
+char *
+util_printf(char *buf, size_t bufsz, const char *fmt, ...)
+{
+	assert(buf);
+	assert(bufsz);
+	assert(fmt);
+
+	va_list ap;
+
+	va_start(ap, fmt);
+	vsnprintf(buf, bufsz, fmt, ap);
+	va_end(ap);
+
+	return buf;
+}
+
+char *
+util_read(const char *path)
+{
+	int fd;
+	struct stat st;
+	char *ret;
+
+	if ((fd = open(path, O_RDONLY)) < 0)
+		return NULL;
+	if (fstat(fd, &st) < 0)
+		return close(fd), NULL;
+
+	ret = util_calloc(1, st.st_size + 1);
+
+	if (read(fd, ret, st.st_size) != st.st_size) {
+		free(ret);
+		ret = NULL;
+	}
+
+	close(fd);
+
+	return ret;
+}
+
+const char *
+util_path(const char *filename)
+{
+	assert(filename);
+
+	/* Build path to the template file. */
+	static char path[PATH_MAX];
+
+	//snprintf(path, sizeof (path), "%s/%s", config.themedir, filename);
+
+	return path;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/util.h	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,66 @@
+/*
+ * util.h -- miscellaneous utilities
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_UTIL_H
+#define SCI_UTIL_H
+
+#include <stddef.h>
+#include <stdio.h>
+
+#define UTIL_SIZE(x) (sizeof (x) / sizeof (x[0]))
+
+void *
+util_malloc(size_t);
+
+void *
+util_calloc(size_t, size_t);
+
+void *
+util_realloc(void *, size_t);
+
+void *
+util_reallocarray(void *, size_t, size_t);
+
+void *
+util_memdup(const void *, size_t);
+
+char *
+util_strdup(const char *);
+
+char *
+util_strndup(const char *, size_t);
+
+char *
+util_basename(const char *);
+
+char *
+util_dirname(const char *);
+
+FILE *
+util_fmemopen(void *, size_t, const char *);
+
+char *
+util_printf(char *, size_t, const char *, ...);
+
+char *
+util_read(const char *);
+
+const char *
+util_path(const char *);
+
+#endif /* !SCI_UTIL_H */
--- a/log.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-/*
- * log.c -- logging routines
- *
- * Copyright (c) 2020-2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <assert.h>
-#include <stdio.h>
-#include <syslog.h>
-
-#include "log.h"
-
-/* TODO: replace. */
-static struct {
-	enum log_level verbosity;
-} config = {
-	.verbosity = LOG_LEVEL_DEBUG
-};
-
-static int syslog_levels[] = {
-	[LOG_LEVEL_DEBUG]       = LOG_DEBUG,
-	[LOG_LEVEL_INFO]        = LOG_INFO,
-	[LOG_LEVEL_WARNING]     = LOG_WARNING
-};
-
-void
-log_open(const char *name)
-{
-	openlog(name, 0, LOG_USER);
-}
-
-void
-log_write(enum log_level level, const char *fmt, ...)
-{
-	assert(level >= LOG_LEVEL_WARNING && level <= LOG_LEVEL_DEBUG);
-	assert(fmt);
-
-	if (config.verbosity >= level) {
-		va_list ap;
-
-		va_start(ap, fmt);
-		log_vwrite(level, fmt, ap);
-		va_end(ap);
-	}
-}
-
-void
-log_vwrite(enum log_level level, const char *fmt, va_list ap)
-{
-	assert(level >= LOG_LEVEL_WARNING && level <= LOG_LEVEL_DEBUG);
-	assert(fmt);
-
-	char line[BUFSIZ];
-
-	vsnprintf(line, sizeof (line), fmt, ap);
-	syslog(syslog_levels[level], "%s", line);
-}
-
-void
-log_finish(void)
-{
-	if (config.verbosity > 0)
-		closelog();
-}
--- a/log.h	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-/*
- * log.h -- logging routines
- *
- * Copyright (c) 2020-2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_LOG_H
-#define SCI_LOG_H
-
-#include <stdarg.h>
-
-enum log_level {
-	LOG_LEVEL_WARNING = 1,
-	LOG_LEVEL_INFO,
-	LOG_LEVEL_DEBUG
-};
-
-#define log_debug(...)  log_write(LOG_LEVEL_DEBUG, __VA_ARGS__)
-#define log_warn(...)   log_write(LOG_LEVEL_WARNING, __VA_ARGS__)
-#define log_info(...)   log_write(LOG_LEVEL_INFO, __VA_ARGS__)
-
-void
-log_open(const char *);
-
-void
-log_write(enum log_level, const char *, ...);
-
-void
-log_vwrite(enum log_level, const char *, va_list);
-
-void
-log_finish(void);
-
-#endif /* !SCI_LOG_H */
--- a/page-api-jobs.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-/*
- * page-api-jobs.c -- /api/v?/jobs route
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/types.h>
-#include <assert.h>
-#include <stdarg.h>
-#include <stdint.h>
-#include <string.h>
-
-#include <kcgi.h>
-#include <jansson.h>
-
-#include "config.h"
-#include "log.h"
-#include "page-api-jobs.h"
-#include "page.h"
-#include "req.h"
-#include "types.h"
-#include "util.h"
-
-static void
-list(struct kreq *r, const struct job *jobs, size_t jobsz)
-{
-	json_t *doc;
-	char *dump;
-
-	doc = job_to(jobs, jobsz);
-	dump = json_dumps(doc, JSON_COMPACT);
-
-	khttp_puts(r, dump);
-	free(dump);
-	json_decref(doc);
-}
-
-static int
-save(const char *json)
-{
-	struct req req = {0};
-	struct jobresult res = {0};
-	int ret = -1;
-
-	json_t *doc;
-	json_error_t err;
-
-	if (!(doc = json_loads(json, 0, &err)))
-		log_warn("api/post: invalid JSON input: %s", err.text);
-	else if (jobresult_from(&res, 1, doc) < 0)
-		log_warn("api/post: failed to decode parameters");
-	else if ((req = req_jobresult_add(&res)).status)
-		log_warn("api/post: save error: %s", strerror(req.status));
-	else
-		ret = 0;
-
-	json_decref(doc);
-	req_finish(&req);
-
-	return ret;
-}
-
-static void
-get(struct kreq *r)
-{
-	struct req req;
-	struct job jobs[SCI_JOB_LIST_MAX];
-	size_t jobsz = UTIL_SIZE(jobs);
-	const char *worker = util_basename(r->path);
-
-	if ((req = req_job_todo(jobs, &jobsz, worker)).status)
-		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		list(r, jobs, jobsz);
-		req_finish(&req);
-		khttp_free(r);
-	}
-}
-
-static void
-post(struct kreq *r)
-{
-	if (r->fieldsz < 1)
-		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
-	else if (save(r->fields[0].val) < 0)
-		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		khttp_free(r);
-	}
-}
-
-void
-page_api_v1_jobs(struct kreq *r)
-{
-	assert(r);
-
-	switch (r->method) {
-	case KMETHOD_GET:
-		get(r);
-		break;
-	case KMETHOD_POST:
-		post(r);
-		break;
-	default:
-		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
-		break;
-	}
-}
--- a/page-api-jobs.h	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-/*
- * page-api-jobs.h -- /api/v?/jobs route
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_PAGE_API_JOBS_H
-#define SCI_PAGE_API_JOBS_H
-
-struct kreq;
-
-void
-page_api_v1_jobs(struct kreq *);
-
-#endif /* !SCI_PAGE_API_JOBS_H */
--- a/page-api-projects.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,136 +0,0 @@
-/*
- * page-api-projects.c -- /api/v?/projects route
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <assert.h>
-
-#include "config.h"
-#include "page-api-projects.h"
-#include "page.h"
-#include "req.h"
-#include "types.h"
-#include "util.h"
-
-static void
-list(struct kreq *r, const struct project *projects, size_t projectsz)
-{
-	struct json_t *doc;
-	char *dump;
-
-	doc = project_to(projects, projectsz);
-	dump = json_dumps(doc, JSON_COMPACT);
-
-	khttp_puts(r, dump);
-	free(dump);
-	json_decref(doc);
-}
-
-static void
-push(struct kreq *r, const struct project *p)
-{
-	struct json_t *json;
-	char *dump;
-
-	json = project_to(p, 1);
-	dump = json_dumps(json, JSON_COMPACT);
-
-	khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-	khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-	khttp_body(r);
-	khttp_puts(r, dump);
-	khttp_free(r);
-
-	free(dump);
-	json_decref(json);
-}
-
-static void
-get_one(struct kreq *r, const char *name)
-{
-	struct project project;
-	struct req req;
-
-	if ((req = req_project_find(&project, name)).status)
-		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
-	else
-		push(r, &project);
-}
-
-static void
-get_one_id(struct kreq *r, int id)
-{
-	struct project project;
-	struct req req;
-
-	if ((req = req_project_find_id(&project, id)).status)
-		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
-	else
-		push(r, &project);
-}
-
-static void
-get_all(struct kreq *r)
-{
-	struct project projects[SCI_PROJECT_MAX];
-	struct req req;
-	size_t projectsz = UTIL_SIZE(projects);
-
-	if ((req = req_project_list(projects, &projectsz)).status)
-		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		list(r, projects, projectsz);
-		req_finish(&req);
-		khttp_free(r);
-	}
-}
-
-static void
-get(struct kreq *r)
-{
-	char name[128];
-	int id;
-
-	if (sscanf(r->path, "v1/projects/%d", &id) == 1)
-		get_one_id(r, id);
-	else if (sscanf(r->path, "v1/projects/%127s", name) == 1)
-		get_one(r, name);
-	else
-		get_all(r);
-}
-
-void
-page_api_v1_projects(struct kreq *r)
-{
-	assert(r);
-
-	switch (r->method) {
-	case KMETHOD_GET:
-		get(r);
-		break;
-#if 0
-	case KMETHOD_POST:
-		post(r);
-		break;
-#endif
-	default:
-		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
-		break;
-	}
-}
--- a/page-api-projects.h	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-/*
- * page-api-projects.h -- /api/v?/projects route
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_PAGE_API_PROJECTS_H
-#define SCI_PAGE_API_PROJECTS_H
-
-struct kreq;
-
-void
-page_api_v1_projects(struct kreq *);
-
-#endif /* !SCI_PAGE_API_PROJECTS_H */
--- a/page-api-workers.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-#include <assert.h>
-
-#include "config.h"
-#include "page-api-workers.h"
-#include "page.h"
-#include "req.h"
-#include "types.h"
-#include "util.h"
-
-static void
-list(struct kreq *r, const struct worker *workers, size_t workersz)
-{
-	struct json_t *doc;
-	char *dump;
-
-	doc = worker_to(workers, workersz);
-	dump = json_dumps(doc, JSON_COMPACT);
-
-	khttp_puts(r, dump);
-	free(dump);
-	json_decref(doc);
-}
-
-static void
-push(struct kreq *r, const struct worker *p)
-{
-	struct json_t *json;
-	char *dump;
-
-	json = worker_to(p, 1);
-	dump = json_dumps(json, JSON_COMPACT);
-
-	khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-	khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-	khttp_body(r);
-	khttp_puts(r, dump);
-	khttp_free(r);
-
-	free(dump);
-	json_decref(json);
-}
-
-static void
-get_one(struct kreq *r, const char *name)
-{
-	struct worker worker;
-	struct req req;
-
-	if ((req = req_worker_find(&worker, name)).status)
-		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
-	else
-		push(r, &worker);
-}
-
-static void
-get_one_id(struct kreq *r, int id)
-{
-	struct worker worker;
-	struct req req;
-
-	if ((req = req_worker_find_id(&worker, id)).status)
-		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
-	else
-		push(r, &worker);
-}
-
-static void
-get_all(struct kreq *r)
-{
-	struct worker workers[SCI_PROJECT_MAX];
-	struct req req;
-	size_t workersz = UTIL_SIZE(workers);
-
-	if ((req = req_worker_list(workers, &workersz)).status)
-		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		list(r, workers, workersz);
-		req_finish(&req);
-		khttp_free(r);
-	}
-}
-
-static void
-get(struct kreq *r)
-{
-	char name[128];
-	int id;
-
-	if (sscanf(r->path, "v1/workers/%d", &id) == 1)
-		get_one_id(r, id);
-	else if (sscanf(r->path, "v1/workers/%127s", name) == 1)
-		get_one(r, name);
-	else
-		get_all(r);
-}
-
-void
-page_api_v1_workers(struct kreq *r)
-{
-	assert(r);
-
-	switch (r->method) {
-	case KMETHOD_GET:
-		get(r);
-		break;
-	default:
-		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
-		break;
-	}
-}
--- a/page-api-workers.h	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-#ifndef SCI_PAGE_API_WORKERS_H
-#define SCI_PAGE_API_WORKERS_H
-
-struct kreq;
-
-void
-page_api_v1_workers(struct kreq *);
-
-#endif /* !SCI_PAGE_API_WORKERS_H */
--- a/page-index.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/*
- * page-index.c -- page /
- *
- * Copyright (c) 2020-2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/types.h>
-#include <assert.h>
-#include <stdarg.h>
-#include <stdint.h>
-
-#include <kcgi.h>
-
-#include "database.h"
-#include "fragment-paste.h"
-#include "page-index.h"
-#include "page.h"
-#include "paste.h"
-#include "util.h"
-
-struct template {
-	struct kreq *req;
-	const struct paste *pastes;
-	size_t pastesz;
-};
-
-static const char *keywords[] = {
-	"pastes"
-};
-
-static int
-template(size_t keyword, void *arg)
-{
-	struct template *tp = arg;
-
-	switch (keyword) {
-	case 0:
-		for (size_t i = 0; i < tp->pastesz; ++i)
-			fragment_paste(tp->req, &tp->pastes[i]);
-		break;
-	default:
-		break;
-	}
-
-	return 1;
-}
-
-static void
-get(struct kreq *r)
-{
-	struct paste pastes[10] = {0};
-	size_t pastesz = NELEM(pastes);
-
-	if (!database_recents(pastes, &pastesz))
-		page(r, NULL, KHTTP_500, "pages/500.html");
-	else
-		page_index_render(r, pastes, pastesz);
-
-	for (size_t i = 0; i < pastesz; ++i)
-		paste_finish(&pastes[i]);
-}
-
-void
-page_index_render(struct kreq *r, const struct paste *pastes, size_t pastesz)
-{
-	struct template data = {
-		.req = r,
-		.pastes = pastes,
-		.pastesz = pastesz
-	};
-
-	struct ktemplate kt = {
-		.key = keywords,
-		.keysz = NELEM(keywords),
-		.arg = &data,
-		.cb = template
-	};
-
-	page(r, &kt, KHTTP_200, "pages/index.html");
-}
-
-void
-page_index(struct kreq *r)
-{
-	switch (r->method) {
-	case KMETHOD_GET:
-		get(r);
-		break;
-	default:
-		page(r, NULL, KHTTP_400, "400.html");
-		break;
-	}
-}
--- a/page.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-/*
- * page.c -- page renderer
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include "page.h"
-#include "util.h"
-
-void
-page(struct kreq *req,
-     const struct ktemplate *tmpl,
-     enum khttp status,
-     enum kmime mime,
-     const char *file)
-{
-	khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[mime]);
-	khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[status]);
-	khttp_body(req);
-
-	if (file) {
-		khttp_template(req, NULL, util_path("fragments/header.html"));
-		khttp_template(req, tmpl, util_path(file));
-		khttp_template(req, NULL, util_path("fragments/footer.html"));
-	}
-
-	khttp_free(req);
-}
--- a/page.h	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-/*
- * page.h -- page renderer
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_PAGE_H
-#define SCI_PAGE_H
-
-#include <sys/types.h>
-#include <stdarg.h>
-#include <stdint.h>
-#include <kcgi.h>
-
-void
-page(struct kreq *, const struct ktemplate *, enum khttp, enum kmime, const char *);
-
-#endif /* !SCI_PAGE_H */
--- a/req.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,313 +0,0 @@
-/*
- * req.c -- client requests to scid
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/socket.h>
-#include <sys/time.h>
-#include <sys/un.h>
-#include <assert.h>
-#include <err.h>
-#include <errno.h>
-#include <limits.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "config.h"
-#include "req.h"
-#include "types.h"
-#include "util.h"
-
-static char path[PATH_MAX] = VARDIR "/run/sci.sock";
-
-static int
-attach(const char *path)
-{
-	struct sockaddr_un sun;
-	struct timeval tv = { .tv_sec = 3 };
-	int sock;
-
-	if ((sock = socket(PF_LOCAL, SOCK_STREAM, 0)) < 0)
-		return -1;
-
-	sun.sun_family = PF_LOCAL;
-	strlcpy(sun.sun_path, path, sizeof (sun.sun_path));
-
-	if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof (tv)) < 0 ||
-	    setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof (tv)) < 0 ||
-	    connect(sock, (const struct sockaddr *)&sun, sizeof (sun)) < 0) {
-		close(sock);
-		return -1;
-	}
-
-	return sock;
-}
-
-static struct req
-exchange(const char *cmd, json_t *doc)
-{
-	char *json, *response, buf[BUFSIZ];
-	struct req res = {0};
-	int sock;
-	ssize_t nr;
-	FILE *fp;
-
-	if ((sock = attach(path)) < 0)
-		goto connect_err;
-
-	json_object_set(doc, "cmd", json_string(cmd));
-	json = json_dumps(doc, JSON_COMPACT);
-
-	if (send(sock, json, strlen(json), MSG_NOSIGNAL) <= 0 ||
-	    send(sock, "\r\n", 2, MSG_NOSIGNAL) <= 0)
-		goto send_err;
-
-	response = util_malloc(SCI_MSG_MAX);
-	fp = util_fmemopen(response, SCI_MSG_MAX, "w");
-	setbuf(fp, NULL);
-
-	while ((nr = recv(sock, buf, sizeof (buf), MSG_NOSIGNAL)) > 0)
-		if (fwrite(buf, 1, nr, fp) != (size_t)nr)
-			goto io_err;
-
-	if (!(res.msg = json_loads(response, 0, NULL)))
-		errno = EILSEQ;
-	else
-		errno = 0;
-
-	/* TODO: generic error handling */
-
-io_err:
-	fclose(fp);
-	free(response);
-
-send_err:
-	close(sock);
-	free(json);
-
-connect_err:
-	res.status = errno;
-	json_decref(doc);
-
-	return res;
-}
-
-void
-req_set_path(const char *newpath)
-{
-	assert(newpath);
-
-	strlcpy(path, newpath, sizeof (path));
-}
-
-struct req
-req_job_add(const struct job *job)
-{
-	assert(job);
-
-	return exchange("job-add", job_to(job, 1));
-}
-
-struct req
-req_job_todo(struct job *jobs, size_t *jobsz, const char *worker)
-{
-	assert(jobs);
-	assert(jobsz);
-	assert(worker);
-
-	struct req st;
-	struct worker wk;
-
-	/*
-	 * Retrieve worker id first, we don't need JSON document to stay
-	 * because we only refer to the id field.
-	 */
-	if ((st = req_worker_find(&wk, worker)).status)
-		return st;
-
-	req_finish(&st);
-
-	if ((st = exchange("job-todo", json_pack("{si}", "worker_id", wk.id))).status)
-		return st;
-
-	if ((*jobsz = job_from(jobs, *jobsz, st.msg)) == (size_t)-1) {
-		json_decref(st.msg);
-		st.msg = NULL;
-		st.status = errno;
-	}
-
-	return st;
-}
-
-struct req
-req_jobresult_add(const struct jobresult *res)
-{
-	assert(res);
-
-	return exchange("jobresult-add", jobresult_to(res, 1));
-}
-
-struct req
-req_project_add(const struct project *p)
-{
-	assert(p);
-
-	return exchange("project-add", project_to(p, 1));
-}
-
-struct req
-req_project_update(const struct project *p)
-{
-	assert(p);
-
-	return exchange("project-update", project_to(p, 1));
-}
-
-struct req
-req_project_find(struct project *p, const char *name)
-{
-	assert(p);
-	assert(name);
-
-	struct req st;
-
-	if ((st = exchange("project-find", json_pack("{ss}", "name", name))).status)
-		return st;
-
-	if (project_from(p, 1, st.msg) < 0) {
-		json_decref(st.msg);
-		st.msg = NULL;
-		st.status = errno;
-	}
-
-	return st;
-}
-
-struct req
-req_project_find_id(struct project *p, int id)
-{
-	assert(p);
-
-	struct req st;
-
-	if ((st = exchange("project-find-id", json_pack("{si}", "id", id))).status)
-		return st;
-
-	if (project_from(p, 1, st.msg) < 0) {
-		json_decref(st.msg);
-		st.msg = NULL;
-		st.status = errno;
-	}
-
-	return st;
-}
-
-struct req
-req_project_list(struct project *projects, size_t *projectsz)
-{
-	assert(projects);
-	assert(projectsz);
-
-	struct req st;
-
-	if ((st = exchange("project-list", json_object())).status)
-		return st;
-
-	if ((*projectsz = project_from(projects, *projectsz, st.msg)) == (size_t)-1) {
-		json_decref(st.msg);
-		st.msg = NULL;
-		st.status = errno;
-	}
-
-	return st;
-}
-
-struct req
-req_worker_add(const struct worker *w)
-{
-	assert(w);
-
-	return exchange("worker-add", worker_to(w, 1));
-}
-
-struct req
-req_worker_find(struct worker *wk, const char *name)
-{
-	assert(wk);
-	assert(name);
-
-	struct req st;
-
-	if ((st = exchange("worker-find", json_pack("{ss}", "name", name))).status)
-		return st;
-	if (worker_from(wk, 1, st.msg) < 0) {
-		json_decref(st.msg);
-		st.msg = NULL;
-		st.status = errno;
-	}
-
-	return st;
-}
-
-struct req
-req_worker_find_id(struct worker *wk, int id)
-{
-	assert(wk);
-
-	struct req st;
-
-	if ((st = exchange("worker-find-id", json_pack("{si}", "id", id))).status)
-		return st;
-	if (worker_from(wk, 1, st.msg) < 0) {
-		json_decref(st.msg);
-		st.msg = NULL;
-		st.status = errno;
-	}
-
-	return st;
-}
-
-struct req
-req_worker_list(struct worker *wk, size_t *wksz)
-{
-	assert(wk);
-	assert(wksz);
-
-	struct req st;
-
-	if ((st = exchange("worker-list", json_object())).status)
-		return st;
-
-	if ((*wksz = worker_from(wk, *wksz, st.msg)) == (size_t)-1) {
-		json_decref(st.msg);
-		st.msg = NULL;
-		st.status = errno;
-	}
-
-	return st;
-}
-
-void
-req_finish(struct req *st)
-{
-	if (st->msg)
-		json_decref(st->msg);
-
-	memset(st, 0, sizeof (*st));
-}
--- a/req.h	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/*
- * req.h -- client requests to scid
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_REQ_H
-#define SCI_REQ_H
-
-#include <stddef.h>
-
-#include <jansson.h>
-
-struct job;
-struct jobresult;
-struct project;
-struct worker;
-
-struct req {
-	int status;
-	json_t *msg;
-};
-
-void
-req_set_path(const char *path);
-
-struct req
-req_job_add(const struct job *);
-
-struct req
-req_job_todo(struct job *, size_t *, const char *);
-
-struct req
-req_jobresult_add(const struct jobresult *);
-
-struct req
-req_project_add(const struct project *);
-
-struct req
-req_project_update(const struct project *);
-
-struct req
-req_project_find(struct project *, const char *);
-
-struct req
-req_project_find_id(struct project *, int);
-
-struct req
-req_project_list(struct project *, size_t *);
-
-struct req
-req_worker_add(const struct worker *);
-
-struct req
-req_worker_find(struct worker *, const char *);
-
-struct req
-req_worker_find_id(struct worker *, int);
-
-struct req
-req_worker_list(struct worker *, size_t *);
-
-void
-req_finish(struct req *);
-
-#endif /* !SCI_REQ_H */
--- a/scictl.c	Wed Oct 06 16:22:23 2021 +0200
+++ b/scictl.c	Tue Jul 12 20:20:51 2022 +0200
@@ -81,17 +81,69 @@
 	return console;
 }
 
+static size_t
+extract(char *s, size_t w, size_t n, void *data)
+{
+	return fwrite(s, w, n, data);
+}
+
+static json_t *
+parse(const char *data)
+{
+	json_t *doc;
+	json_error_t err;
+
+	if (!(json_loads(doc, 0, &err)))
+		die("abort: unable to parse JSON: %s\n", err.text);
+
+	return doc;
+}
+
+static json_t *
+get(const char *url)
+{
+	CURL *curl;
+	CURLcode code;
+	FILE *fp;
+	char buf[HTTP_BUF_MAX];
+	long ret;
+
+	if (!(fp = fmemopen(buf, sizeof (buf), "w")))
+		die("abort: %s", strerror(errno));
+
+	curl = curl_easy_init();
+	curl_easy_setopt(curl, CURLOPT_URL, url);
+	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, extract);
+	curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
+	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
+	code = curl_easy_perform(curl);
+
+	if (code != CURLE_OK)
+		die("abort: %s", curl_easy_strerror(code));
+
+	curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &ret);
+
+	if (ret != 200)
+		die("abort: HTTP %ld\n", ret);
+
+	curl_easy_cleanup(curl);
+	fclose(fp);
+
+	return parse(buf);
+}
+
 static struct req
 cmd_job_add(int argc, char **argv)
 {
 	struct job job = {0};
 	struct project project = {0};
-	struct req rp, rj;
 
 	if (argc < 2)
 		usage();
 
-	if ((rp = req_project_find(&project, argv[0])).status)
+	project.name = argv[0];
+
+	if (_project_find(&project, argv[0])).status)
 		return rp;
 
 	job.project_id = project.id;
--- a/scid.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,427 +0,0 @@
-/*
- * scid.c -- main scid(8) program file
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/un.h>
-#include <sys/socket.h>
-#include <assert.h>
-#include <ctype.h>
-#include <err.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <limits.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdnoreturn.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "config.h"
-#include "db.h"
-#include "log.h"
-#include "types.h"
-#include "util.h"
-
-static char dbpath[PATH_MAX] = VARDIR "/db/sci/sci.db";
-static char scpath[PATH_MAX] = VARDIR "/run/sci.sock";
-static int sc;
-
-noreturn static inline void
-usage(void)
-{
-	fprintf(stderr, "usage: %s [-d database] [-s sock]\n", getprogname());
-	exit(1);
-}
-
-static void
-init(void)
-{
-	struct sockaddr_un sun;
-	int reuse = 1;
-
-	memset(&sun, 0, sizeof (sun));
-	strlcpy(sun.sun_path, scpath, sizeof (sun.sun_path));
-	sun.sun_family = PF_LOCAL;
-
-	log_open("scid");
-	log_info("opening database %s", dbpath);
-	log_info("opening incoming connection on %s", scpath);
-
-	remove(scpath);
-
-	if ((sc = socket(PF_LOCAL, SOCK_STREAM, 0)) < 0)
-		err(1, "socket");
-	if (setsockopt(sc, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof (reuse)) < 0)
-		err(1, "setsockopt");
-	if (bind(sc, (const struct sockaddr *)&sun, sizeof (sun)) < 0)
-		err(1, "bind");
-	if (listen(sc, 32) < 0)
-		err(1, "listen");
-	if (db_open(dbpath) < 0)
-		err(1, "unable to open database");
-}
-
-static int
-answer(int fd, json_t *json)
-{
-	char *str;
-	int ret = 0;
-
-	str = json_dumps(json, JSON_COMPACT);
-
-	if (send(fd, str, strlen(str), MSG_NOSIGNAL) < 0 || send(fd, "\r\n", 2, 0) < 0)
-		ret = -1;
-
-	free(str);
-	json_decref(json);
-
-	return ret;
-}
-
-static inline int
-ok(int fd)
-{
-	return answer(fd, json_pack("{ss}", "status", "ok"));
-}
-
-static inline int
-error(int fd, const char *fmt, ...)
-{
-	char buf[1024];
-	va_list ap;
-
-	va_start(ap, fmt);
-	vsnprintf(buf, sizeof (buf), fmt, ap);
-	va_end(ap);
-
-	return answer(fd, json_pack("{ss ss}",
-	    "status", "error",
-	    "error", buf
-	));
-}
-
-static int
-cmd_job_add(int fd, json_t *doc)
-{
-	struct job job = {0};
-
-	if (job_from(&job, 1, doc) < 0)
-		return EINVAL;
-	if (db_job_add(&job) < 0)
-		return error(fd, "unable to create job");
-
-	log_info("queued new job (%d), with tag %s", job.id, job.tag);
-
-	return ok(fd);
-}
-
-static int
-cmd_job_todo(int fd, json_t *doc)
-{
-	struct db_ctx ctx;
-	struct job jobs[SCI_JOB_LIST_MAX];
-	ssize_t jobsz;
-	int worker_id, ret;
-
-	if (json_unpack(doc, "{si}", "worker_id", &worker_id) < 0)
-		return EINVAL;
-	if ((jobsz = db_job_todo(&ctx, jobs, UTIL_SIZE(jobs), worker_id)) < 0)
-		return error(fd, "unable to retrieve list");
-
-	ret = answer(fd, job_to(jobs, jobsz));
-	db_ctx_finish(&ctx);
-
-	return ret;
-}
-
-static int
-cmd_jobresult_add(int fd, json_t *doc)
-{
-	struct jobresult res = {0};
-
-	if (jobresult_from(&res, 1, doc) < 0)
-		return EINVAL;
-	if (db_jobresult_add(&res) < 0)
-		return error(fd, "unable to create job result");
-
-	log_info("insert new result (%d), with job_id %d (exitcode=%d)",
-	    res.id, res.job_id, res.exitcode);
-
-	return ok(fd);
-}
-
-static int
-cmd_project_add(int fd, json_t *doc)
-{
-	struct project proj = {0};
-
-	if (project_from(&proj, 1, doc) < 0)
-		return EINVAL;
-	if (db_project_add(&proj) < 0)
-		return error(fd, "unable to create project");
-
-	log_info("created new project (%d) %s", proj.id, proj.name);
-
-	return ok(fd);
-}
-
-static int
-cmd_project_find(int fd, json_t *doc)
-{
-	struct db_ctx ctx;
-	struct project proj = {0};
-	int ret;
-
-	if (json_unpack(doc, "{ss}", "name", &proj.name) < 0)
-		return EINVAL;
-	if (db_project_find(&ctx, &proj) < 0)
-		return error(fd, "unable to retrieve project");
-
-	ret = answer(fd, project_to(&proj, 1));
-	db_ctx_finish(&ctx);
-
-	return ret;
-}
-
-static int
-cmd_project_find_id(int fd, json_t *doc)
-{
-	struct db_ctx ctx;
-	struct project proj = {0};
-	int ret;
-
-	if (json_unpack(doc, "{si}", "id", &proj.id) < 0)
-		return EINVAL;
-	if (db_project_find_id(&ctx, &proj) < 0)
-		return error(fd, "unable to retrieve project");
-
-	ret = answer(fd, project_to(&proj, 1));
-	db_ctx_finish(&ctx);
-
-	return ret;
-}
-
-static int
-cmd_project_list(int fd, json_t *doc)
-{
-	(void)doc;
-
-	struct db_ctx ctx;
-	struct project projects[SCI_PROJECT_MAX];
-	ssize_t projectsz;
-	int ret;
-
-	if ((projectsz = db_project_list(&ctx, projects, UTIL_SIZE(projects))) < 0)
-		return EINVAL;
-
-	ret = answer(fd, project_to(projects, projectsz));
-	db_ctx_finish(&ctx);
-
-	return ret;
-}
-
-static int
-cmd_project_update(int fd, json_t *doc)
-{
-	struct project pc;
-
-	if (project_from(&pc, 1, doc) < 0)
-		return EINVAL;
-	if (db_project_update(&pc) < 0)
-		return error(fd, "unable to update project");
-
-	return ok(fd);
-}
-
-static int
-cmd_worker_add(int fd, json_t *doc)
-{
-	struct worker wk = {0};
-
-	if (worker_from(&wk, 1, doc) < 0)
-		return EINVAL;
-	if (db_worker_add(&wk) <0)
-		return error(fd, "unable to create worker");
-
-	log_info("created new worker (%d) %s", wk.id, wk.name);
-
-	return ok(fd);
-}
-
-static int
-cmd_worker_find(int fd, json_t *doc)
-{
-	struct worker wk = {0};
-	struct db_ctx ctx;
-	int ret;
-
-	if (json_unpack(doc, "{ss}", "name", &wk.name) < 0)
-		return EINVAL;
-	if (db_worker_find(&ctx, &wk) < 0)
-		return error(fd, "unable to retrieve worker");
-
-	ret = answer(fd, worker_to(&wk, 1));
-	db_ctx_finish(&ctx);
-
-	return ret;
-}
-
-static int
-cmd_worker_find_id(int fd, json_t *doc)
-{
-	struct worker wk = {0};
-	struct db_ctx ctx;
-	int ret;
-
-	if (json_unpack(doc, "{si}", "id", &wk.id) < 0)
-		return EINVAL;
-	if (db_worker_find_id(&ctx, &wk) < 0)
-		return error(fd, "unable to retrieve worker");
-
-	ret = answer(fd, worker_to(&wk, 1));
-	db_ctx_finish(&ctx);
-
-	return ret;
-}
-
-static int
-cmd_worker_list(int fd, json_t *doc)
-{
-	(void)doc;
-
-	struct db_ctx ctx;
-	struct worker workers[SCI_WORKER_MAX];
-	ssize_t workersz;
-	int ret;
-
-	if ((workersz = db_worker_list(&ctx, workers, UTIL_SIZE(workers))) < 0)
-		return error(fd, "unable to retrieve worker list");
-
-	ret = answer(fd, worker_to(workers, workersz));
-	db_ctx_finish(&ctx);
-
-	return ret;
-}
-
-static void
-dispatch(int fd, const char *msg)
-{
-	static const struct {
-		const char *name;
-		int (*exec)(int, json_t *);
-	} cmds[] = {
-		{ "job-add",            cmd_job_add             },
-		{ "job-todo",           cmd_job_todo            },
-		{ "jobresult-add",      cmd_jobresult_add       },
-		{ "project-add",        cmd_project_add         },
-		{ "project-find",       cmd_project_find        },
-		{ "project-find-id",    cmd_project_find_id     },
-		{ "project-list",       cmd_project_list        },
-		{ "project-update",     cmd_project_update      },
-		{ "worker-add",         cmd_worker_add          },
-		{ "worker-find",        cmd_worker_find         },
-		{ "worker-find-id",     cmd_worker_find_id      },
-		{ "worker-list",        cmd_worker_list         },
-		{ NULL,                 NULL                    }
-	};
-
-	json_t *doc, *cmd;
-	json_error_t err;
-
-	if (!(doc = json_loads(msg, 0, &err)))
-		return log_warn("json error: %s", err.text);
-	if (!json_is_string((cmd = json_object_get(doc, "cmd")))) {
-		json_decref(doc);
-		return log_warn("json invalid");
-	}
-
-	for (size_t i = 0; cmds[i].name; ++i) {
-		if (strcmp(cmds[i].name, json_string_value(cmd)) == 0) {
-			int ret = 0;
-
-			if ((ret = cmds[i].exec(fd, doc)) != 0)
-				error(fd, strerror(errno));
-
-			json_decref(doc);
-			break;
-		}
-	}
-}
-
-static void
-run(void)
-{
-	int client;
-	FILE *fp;
-	char *msg, *endl, buf[BUFSIZ];
-	ssize_t nr;
-	struct timeval tv = { .tv_sec = 30 };
-
-	if ((client = accept(sc, NULL, 0)) < 0)
-		err(1, "accept");
-	if (setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof (tv)) < 0)
-		err(1, "setsockopt");
-	if (setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof (tv)) < 0)
-		err(1, "setsockopt");
-
-	fp = util_fmemopen((msg = util_calloc(1, SCI_MSG_MAX)), SCI_MSG_MAX, "w");
-	setbuf(fp, NULL);
-
-	while (!strstr(msg, "\r\n") && (nr = recv(client, buf, sizeof (buf), MSG_NOSIGNAL)) > 0)
-		if (fwrite(buf, 1, nr, fp) != (size_t)nr)
-			warn("fwrite");
-
-	if (nr < 0)
-		warn("recv");
-	if ((endl = strstr(msg, "\r\n")))
-		*endl = 0;
-
-	fclose(fp);
-	dispatch(client, msg);
-	close(client);
-	free(msg);
-}
-
-int
-main(int argc, char **argv)
-{
-	int ch;
-
-	setprogname("scid");
-
-	while ((ch = getopt(argc, argv, "d:s:")) != -1) {
-		switch (ch) {
-		case 'd':
-			strlcpy(dbpath, optarg, sizeof (dbpath));
-			break;
-		case 's':
-			strlcpy(scpath, optarg, sizeof (scpath));
-			break;
-		default:
-			usage();
-			break;
-		}
-	}
-
-	argc -= optind;
-	argv += optind;
-
-	init();
-
-	for (;;)
-		run();
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/db.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,419 @@
+/*
+ * db.c -- scid database access
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/queue.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sqlite3.h>
+
+#include "db.h"
+#include "log.h"
+#include "types.h"
+#include "util.h"
+
+#include "sql/init.h"
+#include "sql/job-add.h"
+#include "sql/job-todo.h"
+#include "sql/jobresult-add.h"
+#include "sql/project-add.h"
+#include "sql/project-update.h"
+#include "sql/project-find.h"
+#include "sql/project-find-id.h"
+#include "sql/project-list.h"
+#include "sql/worker-add.h"
+#include "sql/worker-find.h"
+#include "sql/worker-find-id.h"
+#include "sql/worker-list.h"
+
+#define CHAR(v) (const char *)(v)
+
+static sqlite3 *db;
+
+typedef void (*unpacker)(sqlite3_stmt *, struct db_ctx *, void *);
+
+struct str {
+	char *str;
+	SLIST_ENTRY(str) link;
+};
+
+struct list {
+	unpacker unpack;
+	void *data;
+	size_t datasz;
+	size_t elemwidth;
+	struct db_ctx *ctx;
+};
+
+SLIST_HEAD(strlist, str);
+
+static struct strlist *
+strlist_new(void)
+{
+	struct strlist *l;
+
+	l = util_calloc(1, sizeof (*l));
+	SLIST_INIT(l);
+
+	return l;
+}
+
+static const char *
+strlist_add(struct strlist *l, const char *text)
+{
+	struct str *s;
+
+	s = util_calloc(1, sizeof (*s));
+	s->str = util_strdup(text);
+
+	SLIST_INSERT_HEAD(l, s, link);
+
+	return s->str;
+}
+
+static void
+strlist_free(struct strlist *l)
+{
+	struct str *s, *tmp;
+
+	SLIST_FOREACH_SAFE(s, l, link, tmp) {
+		free(s->str);
+		free(s);
+	}
+
+	free(l);
+}
+
+static void
+project_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct project *project)
+{
+	project->id = sqlite3_column_int(stmt, 0);
+	project->name = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 1)));
+	project->desc = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 2)));
+	project->url = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 3)));
+	project->script = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 4)));
+}
+
+static void
+worker_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct worker *w)
+{
+	w->id = sqlite3_column_int(stmt, 0);
+	w->name = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 1)));
+	w->desc = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 2)));
+}
+
+static void
+job_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct job *job)
+{
+	job->id = sqlite3_column_int(stmt, 0);
+	job->tag = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 1)));
+	job->project_id = sqlite3_column_int(stmt, 2);
+}
+
+static void
+vbind(sqlite3_stmt *stmt, const char *fmt, va_list ap)
+{
+	for (int index = 1; *fmt; ++fmt) {
+		switch (*fmt) {
+		case 'i':
+			sqlite3_bind_int(stmt, index++, va_arg(ap, int));
+			break;
+		case 's':
+			sqlite3_bind_text(stmt, index++, va_arg(ap, const char *), -1, SQLITE_STATIC);
+			break;
+		case 'z':
+			sqlite3_bind_int64(stmt, index++, va_arg(ap, size_t));
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+static int
+insert(const char *sql, const char *fmt, ...)
+{
+	assert(sql);
+	assert(fmt);
+
+	sqlite3_stmt *stmt = NULL;
+	va_list ap;
+	int ret = -1;
+
+	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
+
+	va_start(ap, fmt);
+	vbind(stmt, fmt, ap);
+	va_end(ap);
+
+	if (sqlite3_step(stmt) != SQLITE_DONE)
+		log_warn("db: %s", sqlite3_errmsg(db));
+	else
+		ret = sqlite3_last_insert_rowid(db);
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+}
+
+static int
+update(const char *sql, const char *fmt, ...)
+{
+	assert(sql);
+	assert(fmt);
+
+	sqlite3_stmt *stmt = NULL;
+	va_list ap;
+	int ret = 1;
+
+	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
+
+	va_start(ap, fmt);
+	vbind(stmt, fmt, ap);
+	va_end(ap);
+
+	if (sqlite3_step(stmt) != SQLITE_DONE)
+		log_warn("db: %s", sqlite3_errmsg(db));
+	else
+		ret = 0;
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+}
+
+static ssize_t
+list(struct list *sel, const char *sql, const char *args, ...)
+{
+	sqlite3_stmt *stmt = NULL;
+
+	va_list ap;
+	int step;
+	ssize_t ret = -1;
+	size_t tot = 0;
+
+	sel->ctx->handle = NULL;
+
+	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
+
+	va_start(ap, args);
+	vbind(stmt, args, ap);
+	va_end(ap);
+
+	sel->ctx->handle = strlist_new();
+
+	while (tot < sel->datasz && (step = sqlite3_step(stmt)) == SQLITE_ROW)
+		sel->unpack(stmt, sel->ctx, (unsigned char *)sel->data + (tot++ * sel->elemwidth));
+
+	if (step == SQLITE_OK || step == SQLITE_DONE || step == SQLITE_ROW)
+		ret = tot;
+	else {
+		memset(sel->data, 0, sel->datasz * sel->elemwidth);
+		strlist_free(sel->ctx->handle);
+		sel->ctx->handle = NULL;
+	}
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+}
+
+int
+db_open(const char *path)
+{
+	assert(path);
+
+	if (sqlite3_open(path, &db) != SQLITE_OK)
+		return log_warn("db: open error: %s", sqlite3_errmsg(db)), -1;
+
+	/* Wait for 30 seconds to lock the database. */
+	sqlite3_busy_timeout(db, 30000);
+
+	if (sqlite3_exec(db, CHAR(sql_init), NULL, NULL, NULL) != SQLITE_OK)
+		return log_warn("db: initialization error: %s", sqlite3_errmsg(db)), -1;
+
+	return 0;
+}
+
+int
+db_project_add(struct project *p)
+{
+	return (p->id = insert(CHAR(sql_project_add), "ssss", p->name, p->desc,
+	    p->url, p->script)) < 0 ? -1 : 0;
+}
+
+int
+db_project_update(const struct project *p)
+{
+	assert(p);
+
+	return update(CHAR(sql_project_update), "ssssi", p->name, p->desc,
+	    p->url, p->script, p->id);
+}
+
+ssize_t
+db_project_list(struct db_ctx *ctx, struct project *projects, size_t projectsz)
+{
+	struct list sel = {
+		.unpack = (unpacker)project_unpacker,
+		.data = projects,
+		.datasz = projectsz,
+		.elemwidth = sizeof (*projects),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_project_list), "z", projectsz);
+}
+
+int
+db_project_find(struct db_ctx *ctx, struct project *project)
+{
+	struct list sel = {
+		.unpack = (unpacker)project_unpacker,
+		.data = project,
+		.datasz = 1,
+		.elemwidth = sizeof (*project),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_project_find), "s", project->name) == 1 ? 0 : -1;
+}
+
+int
+db_project_find_id(struct db_ctx *ctx, struct project *project)
+{
+	struct list sel = {
+		.unpack = (unpacker)project_unpacker,
+		.data = project,
+		.datasz = 1,
+		.elemwidth = sizeof (*project),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_project_find_id), "i", project->id) == 1 ? 0 : -1;
+}
+
+int
+db_worker_add(struct worker *wk)
+{
+	assert(wk);
+
+	return (wk->id = insert(CHAR(sql_worker_add), "ss", wk->name, wk->desc)) < 0 ? -1 : 0;
+}
+
+ssize_t
+db_worker_list(struct db_ctx *ctx, struct worker *wk, size_t wksz)
+{
+	assert(ctx);
+	assert(wk);
+
+	struct list sel = {
+		.unpack = (unpacker)worker_unpacker,
+		.data = wk,
+		.datasz = wksz,
+		.elemwidth = sizeof (*wk),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_worker_list), "z", wksz);
+}
+
+int
+db_worker_find(struct db_ctx *ctx, struct worker *wk)
+{
+	struct list sel = {
+		.unpack = (unpacker)worker_unpacker,
+		.data = wk,
+		.datasz = 1,
+		.elemwidth = sizeof (*wk),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_worker_find), "s", wk->name) == 1 ? 0 : -1;
+}
+
+int
+db_worker_find_id(struct db_ctx *ctx, struct worker *wk)
+{
+	struct list sel = {
+		.unpack = (unpacker)worker_unpacker,
+		.data = wk,
+		.datasz = 1,
+		.elemwidth = sizeof (*wk),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_worker_find_id), "i", wk->id) == 1 ? 0 : -1;
+}
+
+int
+db_job_add(struct job *job)
+{
+	assert(job);
+
+	return (job->id = insert(CHAR(sql_job_add),
+	    "si", job->tag, job->project_id)) < 0 ? -1 : 0;
+}
+
+ssize_t
+db_job_todo(struct db_ctx *ctx, struct job *jobs, size_t jobsz, int worker_id)
+{
+	assert(ctx);
+	assert(jobs);
+
+	struct list sel = {
+		.unpack = (unpacker)job_unpacker,
+		.data = jobs,
+		.datasz = jobsz,
+		.elemwidth = sizeof (*jobs),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_job_todo), "iiz", worker_id, worker_id, jobsz);
+}
+
+int
+db_jobresult_add(struct jobresult *r)
+{
+	assert(r);
+
+	return (r->id = insert(CHAR(sql_jobresult_add), "iiis", r->job_id,
+	    r->worker_id, r->exitcode, r->log)) < 0 ? -1 : 0;
+}
+
+void
+db_finish(void)
+{
+	if (db) {
+		sqlite3_close(db);
+		db = NULL;
+	}
+}
+
+void
+db_ctx_finish(struct db_ctx *ctx)
+{
+	if (ctx->handle) {
+		strlist_free(ctx->handle);
+		ctx->handle = NULL;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/db.h	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,79 @@
+/*
+ * db.h -- scid database access
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_DB_H
+#define SCI_DB_H
+
+#include <sys/types.h>
+#include <stddef.h>
+
+struct project;
+struct worker;
+struct job;
+struct jobresult;
+
+struct db_ctx {
+	void *handle;
+};
+
+int
+db_open(const char *);
+
+int
+db_job_add(struct job *);
+
+ssize_t
+db_job_todo(struct db_ctx *, struct job *, size_t, int);
+
+int
+db_jobresult_add(struct jobresult *);
+
+int
+db_project_add(struct project *);
+
+int
+db_project_update(const struct project *);
+
+ssize_t
+db_project_list(struct db_ctx *, struct project *, size_t);
+
+int
+db_project_find(struct db_ctx *, struct project *);
+
+int
+db_project_find_id(struct db_ctx *, struct project *);
+
+int
+db_worker_add(struct worker *);
+
+ssize_t
+db_worker_list(struct db_ctx *, struct worker *, size_t);
+
+int
+db_worker_find(struct db_ctx *, struct worker *);
+
+int
+db_worker_find_id(struct db_ctx *, struct worker *);
+
+void
+db_finish(void);
+
+void
+db_ctx_finish(struct db_ctx *);
+
+#endif /* !SCI_DB_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/http.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,103 @@
+/*
+ * http.c -- HTTP parsing and rendering
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <assert.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <kcgi.h>
+
+#include "http.h"
+#include "log.h"
+#include "page.h"
+#include "page-api-jobs.h"
+#include "page-api-projects.h"
+#include "page-api-workers.h"
+
+enum page {
+	PAGE_API,
+	PAGE_LAST       /* Not used. */
+};
+
+static void
+dispatch_api(struct kreq *req)
+{
+	static const struct {
+		const char *prefix;
+		void (*handler)(struct kreq *);
+	} apis[] = {
+		{ "v1/jobs",            page_api_v1_jobs        },
+		{ "v1/projects",        page_api_v1_projects    },
+		{ "v1/workers",         page_api_v1_workers     },
+		{ NULL,                 NULL                    }
+	};
+
+	for (size_t i = 0; apis[i].prefix; ++i)
+		if (strncmp(req->path, apis[i].prefix, strlen(apis[i].prefix)) == 0)
+			return apis[i].handler(req);
+
+	page(req, NULL, KHTTP_404, KMIME_TEXT_HTML, "pages/404.html");
+}
+
+static const char *pages[] = {
+	[PAGE_API]              = "api"
+};
+
+static void (*handlers[])(struct kreq *req) = {
+	[PAGE_API]              = dispatch_api
+};
+
+static void
+process(struct kreq *req)
+{
+	assert(req);
+
+	log_debug("http: accessing page '%s'", req->path);
+
+	if (req->page == PAGE_LAST)
+		page(req, NULL, KHTTP_404, KMIME_TEXT_HTML, "pages/404.html");
+	else
+		handlers[req->page](req);
+}
+
+void
+http_fcgi_run(void)
+{
+	struct kreq req;
+	struct kfcgi *fcgi;
+
+	if (khttp_fcgi_init(&fcgi, NULL, 0, pages, PAGE_LAST, 0) != KCGI_OK)
+		return;
+
+	while (khttp_fcgi_parse(fcgi, &req) == KCGI_OK)
+		process(&req);
+
+	khttp_fcgi_free(fcgi);
+}
+
+void
+http_cgi_run(void)
+{
+	struct kreq req;
+
+	if (khttp_parse(&req, NULL, 0, pages, PAGE_LAST, 0) == KCGI_OK)
+		process(&req);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/http.h	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,28 @@
+/*
+ * http.h -- HTTP parsing and rendering
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_HTTP_H
+#define SCI_HTTP_H
+
+void
+http_fcgi_run(void);
+
+void
+http_cgi_run(void);
+
+#endif /* !SCI_HTTP_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/main.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,84 @@
+/*
+ * scid.c -- main scid(8) program file
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <limits.h>
+#include <stdio.h>
+#include <stdnoreturn.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "db.h"
+#include "log.h"
+#include "http.h"
+
+static char dbpath[PATH_MAX] = VARDIR "/db/sci/sci.db";
+
+noreturn static void
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-d database] [-s sock]\n", getprogname());
+	exit(1);
+}
+
+static void
+init(void)
+{
+	log_open("scid");
+	log_info("opening database %s", dbpath);
+
+	if (db_open(dbpath) < 0)
+		log_die("abort: unable to open database");
+}
+
+static void
+finish(void)
+{
+	db_finish();
+	log_finish();
+}
+
+int
+main(int argc, char **argv)
+{
+	int ch;
+	void (*run)(void) = &(http_cgi_run);
+
+	while ((ch = getopt(argc, argv, "d:f")) != -1) {
+		switch (ch) {
+		case 'd':
+			strlcpy(dbpath, optarg, sizeof (dbpath));
+			break;
+		case 'f':
+			run = &(http_fcgi_run);
+			break;
+		default:
+			usage();
+			break;
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	init();
+
+	for (;;)
+		run();
+
+	finish();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page-api-jobs.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,133 @@
+/*
+ * page-api-jobs.c -- /api/v?/jobs route
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <assert.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <kcgi.h>
+#include <jansson.h>
+
+#include "config.h"
+#include "db.h"
+#include "log.h"
+#include "page-api-jobs.h"
+#include "page.h"
+#include "types.h"
+#include "util.h"
+
+static void
+list(struct kreq *r, const struct job *jobs, size_t jobsz)
+{
+	json_t *doc;
+	char *dump;
+
+	doc = job_to(jobs, jobsz);
+	dump = json_dumps(doc, JSON_COMPACT);
+
+	khttp_puts(r, dump);
+	free(dump);
+	json_decref(doc);
+}
+
+static int
+save(const char *json)
+{
+	struct jobresult res = {0};
+	int ret = -1;
+
+	json_t *doc;
+	json_error_t err;
+
+	if (!(doc = json_loads(json, 0, &err)))
+		log_warn("api/post: invalid JSON input: %s", err.text);
+	else if (jobresult_from(&res, 1, doc) < 0)
+		log_warn("api/post: failed to decode parameters");
+	else if (db_jobresult_add(&res) < 0)
+		log_warn("api/post: database save error");
+	else
+		ret = 0;
+
+	json_decref(doc);
+
+	return ret;
+}
+
+static void
+get(struct kreq *r)
+{
+	struct db_ctx ctx;
+	struct job jobs[SCI_JOB_LIST_MAX];
+	ssize_t jobsz;
+	struct worker wk = {
+		.name = util_basename(r->path)
+	};
+
+	if (db_worker_find(&ctx, &wk) < 0) {
+		page(r, NULL, KHTTP_404, KMIME_APP_JSON, NULL);
+		return;
+	}
+
+	db_ctx_finish(&ctx);
+
+	if ((jobsz = db_job_todo(&ctx, jobs, UTIL_SIZE(jobs), wk.id)) < 0)
+		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
+	else {
+		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
+		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+		khttp_body(r);
+		list(r, jobs, jobsz);
+		db_ctx_finish(&ctx);
+		khttp_free(r);
+	}
+}
+
+static void
+post(struct kreq *r)
+{
+	if (r->fieldsz < 1)
+		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
+	else if (save(r->fields[0].val) < 0)
+		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
+	else {
+		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
+		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+		khttp_body(r);
+		khttp_free(r);
+	}
+}
+
+void
+page_api_v1_jobs(struct kreq *r)
+{
+	assert(r);
+
+	switch (r->method) {
+	case KMETHOD_GET:
+		get(r);
+		break;
+	case KMETHOD_POST:
+		post(r);
+		break;
+	default:
+		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
+		break;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page-api-jobs.h	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,27 @@
+/*
+ * page-api-jobs.h -- /api/v?/jobs route
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_PAGE_API_JOBS_H
+#define SCI_PAGE_API_JOBS_H
+
+struct kreq;
+
+void
+page_api_v1_jobs(struct kreq *);
+
+#endif /* !SCI_PAGE_API_JOBS_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page-api-projects.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,182 @@
+/*
+ * page-api-projects.c -- /api/v?/projects route
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include "config.h"
+#include "db.h"
+#include "log.h"
+#include "page-api-projects.h"
+#include "page.h"
+#include "types.h"
+#include "util.h"
+
+static void
+list(struct kreq *r, const struct project *projects, size_t projectsz)
+{
+	struct json_t *doc;
+	char *dump;
+
+	doc = project_to(projects, projectsz);
+	dump = json_dumps(doc, JSON_COMPACT);
+
+	khttp_puts(r, dump);
+	free(dump);
+	json_decref(doc);
+}
+
+static int
+save(const char *json)
+{
+	struct project res = {0};
+	int ret = -1;
+
+	json_t *doc;
+	json_error_t err;
+
+	if (!(doc = json_loads(json, 0, &err)))
+		log_warn("api/post: invalid JSON input: %s", err.text);
+	else if (project_from(&res, 1, doc) < 0)
+		log_warn("api/post: failed to decode parameters");
+	else if (db_project_add(&res) < 0)
+		log_warn("api/post: database save error");
+	else
+		ret = 0;
+
+	json_decref(doc);
+
+	return ret;
+}
+
+static void
+push(struct kreq *r, const struct project *p)
+{
+	struct json_t *json;
+	char *dump;
+
+	json = project_to(p, 1);
+	dump = json_dumps(json, JSON_COMPACT);
+
+	khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
+	khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+	khttp_body(r);
+	khttp_puts(r, dump);
+	khttp_free(r);
+
+	free(dump);
+	json_decref(json);
+}
+
+static void
+get_one(struct kreq *r, const char *name)
+{
+	struct db_ctx ctx;
+	struct project project = {
+		.name = name
+	};
+
+	if (db_project_find(&ctx, &project) < 0)
+		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
+	else {
+		push(r, &project);
+		db_ctx_finish(&ctx);
+	}
+}
+
+static void
+get_one_id(struct kreq *r, int id)
+{
+	struct db_ctx ctx;
+	struct project project = {
+		.id = id
+	};
+
+	if (db_project_find_id(&ctx, &project) < 0)
+		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
+	else {
+		push(r, &project);
+		db_ctx_finish(&ctx);
+	}
+}
+
+static void
+get_all(struct kreq *r)
+{
+	struct db_ctx ctx;
+	struct project projects[SCI_PROJECT_MAX];
+	ssize_t projectsz;
+
+	if ((projectsz = db_project_list(&ctx, projects, UTIL_SIZE(projects))) < 0)
+		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
+	else {
+		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
+		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+		khttp_body(r);
+		list(r, projects, projectsz);
+		db_ctx_finish(&ctx);
+		khttp_free(r);
+	}
+}
+
+static void
+get(struct kreq *r)
+{
+	char name[128];
+	int id;
+
+	if (sscanf(r->path, "v1/projects/%d", &id) == 1)
+		get_one_id(r, id);
+	else if (sscanf(r->path, "v1/projects/%127s", name) == 1)
+		get_one(r, name);
+	else
+		get_all(r);
+}
+
+static void
+post(struct kreq *r)
+{
+	if (r->fieldsz < 1)
+		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
+	else if (save(r->fields[0].val) < 0)
+		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
+	else {
+		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
+		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+		khttp_body(r);
+		khttp_free(r);
+	}
+}
+
+void
+page_api_v1_projects(struct kreq *r)
+{
+	assert(r);
+
+	switch (r->method) {
+	case KMETHOD_GET:
+		get(r);
+		break;
+	case KMETHOD_POST:
+		post(r);
+		break;
+	default:
+		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
+		break;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page-api-projects.h	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,27 @@
+/*
+ * page-api-projects.h -- /api/v?/projects route
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_PAGE_API_PROJECTS_H
+#define SCI_PAGE_API_PROJECTS_H
+
+struct kreq;
+
+void
+page_api_v1_projects(struct kreq *);
+
+#endif /* !SCI_PAGE_API_PROJECTS_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page-api-workers.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,163 @@
+#include <assert.h>
+
+#include "config.h"
+#include "db.h"
+#include "log.h"
+#include "page-api-workers.h"
+#include "page.h"
+#include "types.h"
+#include "util.h"
+
+static void
+list(struct kreq *r, const struct worker *workers, size_t workersz)
+{
+	struct json_t *doc;
+	char *dump;
+
+	doc = worker_to(workers, workersz);
+	dump = json_dumps(doc, JSON_COMPACT);
+
+	khttp_puts(r, dump);
+	free(dump);
+	json_decref(doc);
+}
+
+static int
+save(const char *json)
+{
+	struct worker res = {0};
+	int ret = -1;
+
+	json_t *doc;
+	json_error_t err;
+
+	if (!(doc = json_loads(json, 0, &err)))
+		log_warn("api/post: invalid JSON input: %s", err.text);
+	else if (worker_from(&res, 1, doc) < 0)
+		log_warn("api/post: failed to decode parameters");
+	else if (db_worker_add(&res) < 0)
+		log_warn("api/post: database save error");
+	else
+		ret = 0;
+
+	json_decref(doc);
+
+	return ret;
+}
+
+static void
+push(struct kreq *r, const struct worker *p)
+{
+	struct json_t *json;
+	char *dump;
+
+	json = worker_to(p, 1);
+	dump = json_dumps(json, JSON_COMPACT);
+
+	khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
+	khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+	khttp_body(r);
+	khttp_puts(r, dump);
+	khttp_free(r);
+
+	free(dump);
+	json_decref(json);
+}
+
+static void
+get_one(struct kreq *r, const char *name)
+{
+	struct db_ctx ctx;
+	struct worker worker = {
+		.name = name
+	};
+
+	if (db_worker_find(&ctx, &worker) < 0)
+		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
+	else {
+		push(r, &worker);
+		db_ctx_finish(&ctx);
+	}
+}
+
+static void
+get_one_id(struct kreq *r, int id)
+{
+	struct db_ctx ctx;
+	struct worker worker = {
+		.id = id
+	};
+
+	if (db_worker_find_id(&ctx, &worker) < 0)
+		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
+	else {
+		push(r, &worker);
+		db_ctx_finish(&ctx);
+	}
+}
+
+static void
+get_all(struct kreq *r)
+{
+	struct db_ctx ctx;
+	struct worker workers[SCI_PROJECT_MAX];
+	ssize_t workersz;
+
+	if ((workersz = db_worker_list(&ctx, workers, UTIL_SIZE(workers))) < 0)
+		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
+	else {
+		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
+		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+		khttp_body(r);
+		list(r, workers, workersz);
+		db_ctx_finish(&ctx);
+		khttp_free(r);
+	}
+}
+
+static void
+get(struct kreq *r)
+{
+	char name[128];
+	int id;
+
+	if (sscanf(r->path, "v1/workers/%d", &id) == 1)
+		get_one_id(r, id);
+	else if (sscanf(r->path, "v1/workers/%127s", name) == 1)
+		get_one(r, name);
+	else
+		get_all(r);
+}
+
+static void
+post(struct kreq *r)
+{
+	if (r->fieldsz < 1)
+		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
+	else if (save(r->fields[0].val) < 0)
+		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
+	else {
+		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
+		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+		khttp_body(r);
+		khttp_free(r);
+	}
+}
+
+void
+page_api_v1_workers(struct kreq *r)
+{
+	assert(r);
+
+	switch (r->method) {
+	case KMETHOD_GET:
+		get(r);
+		break;
+	case KMETHOD_POST:
+		post(r);
+		break;
+	default:
+		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
+		break;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page-api-workers.h	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,9 @@
+#ifndef SCI_PAGE_API_WORKERS_H
+#define SCI_PAGE_API_WORKERS_H
+
+struct kreq;
+
+void
+page_api_v1_workers(struct kreq *);
+
+#endif /* !SCI_PAGE_API_WORKERS_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page-index.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,105 @@
+/*
+ * page-index.c -- page /
+ *
+ * Copyright (c) 2020-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <assert.h>
+#include <stdarg.h>
+#include <stdint.h>
+
+#include <kcgi.h>
+
+#include "database.h"
+#include "fragment-paste.h"
+#include "page-index.h"
+#include "page.h"
+#include "paste.h"
+#include "util.h"
+
+struct template {
+	struct kreq *req;
+	const struct paste *pastes;
+	size_t pastesz;
+};
+
+static const char *keywords[] = {
+	"pastes"
+};
+
+static int
+template(size_t keyword, void *arg)
+{
+	struct template *tp = arg;
+
+	switch (keyword) {
+	case 0:
+		for (size_t i = 0; i < tp->pastesz; ++i)
+			fragment_paste(tp->req, &tp->pastes[i]);
+		break;
+	default:
+		break;
+	}
+
+	return 1;
+}
+
+static void
+get(struct kreq *r)
+{
+	struct paste pastes[10] = {0};
+	size_t pastesz = NELEM(pastes);
+
+	if (!database_recents(pastes, &pastesz))
+		page(r, NULL, KHTTP_500, "pages/500.html");
+	else
+		page_index_render(r, pastes, pastesz);
+
+	for (size_t i = 0; i < pastesz; ++i)
+		paste_finish(&pastes[i]);
+}
+
+void
+page_index_render(struct kreq *r, const struct paste *pastes, size_t pastesz)
+{
+	struct template data = {
+		.req = r,
+		.pastes = pastes,
+		.pastesz = pastesz
+	};
+
+	struct ktemplate kt = {
+		.key = keywords,
+		.keysz = NELEM(keywords),
+		.arg = &data,
+		.cb = template
+	};
+
+	page(r, &kt, KHTTP_200, "pages/index.html");
+}
+
+void
+page_index(struct kreq *r)
+{
+	switch (r->method) {
+	case KMETHOD_GET:
+		get(r);
+		break;
+	default:
+		page(r, NULL, KHTTP_400, "400.html");
+		break;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,40 @@
+/*
+ * page.c -- page renderer
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "page.h"
+#include "util.h"
+
+void
+page(struct kreq *req,
+     const struct ktemplate *tmpl,
+     enum khttp status,
+     enum kmime mime,
+     const char *file)
+{
+	khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[mime]);
+	khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[status]);
+	khttp_body(req);
+
+	if (file) {
+		khttp_template(req, NULL, util_path("fragments/header.html"));
+		khttp_template(req, tmpl, util_path(file));
+		khttp_template(req, NULL, util_path("fragments/footer.html"));
+	}
+
+	khttp_free(req);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page.h	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,30 @@
+/*
+ * page.h -- page renderer
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_PAGE_H
+#define SCI_PAGE_H
+
+#include <sys/types.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <kcgi.h>
+
+void
+page(struct kreq *, const struct ktemplate *, enum khttp, enum kmime, const char *);
+
+#endif /* !SCI_PAGE_H */
--- a/sciwebd.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-/*
- * sciwebd.c -- main sciwebd(8) CGI handler file
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdnoreturn.h>
-#include <unistd.h>
-
-#include "http.h"
-#include "log.h"
-
-const char *sock = VARDIR "/run/sci.sock";
-
-noreturn static void
-usage(void)
-{
-	fprintf(stderr, "usage: %s [-f] [-s sock]\n", getprogname());
-	exit(1);
-}
-
-static void
-init(void)
-{
-	log_open(getprogname());
-}
-
-static void
-finish(void)
-{
-	log_finish();
-}
-
-int
-main(int argc, char **argv)
-{
-	int ch;
-	void (*run)(void) = &(http_cgi_run);
-
-	setprogname("sciwebd");
-
-	while ((ch = getopt(argc, argv, "fs:")) != -1) {
-		switch (ch) {
-		case 'f':
-			run = &(http_fcgi_run);
-			break;
-		case 's':
-			sock = optarg;
-			break;
-		default:
-			usage();
-			break;
-		}
-	}
-
-	init();
-	run();
-	finish();
-}
--- a/sciworkerd.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,676 +0,0 @@
-/*
- * sciworkerd.c -- main sciworkerd(8) program file
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/queue.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <assert.h>
-#include <err.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <limits.h>
-#include <poll.h>
-#include <signal.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdnoreturn.h>
-#include <string.h>
-#include <unistd.h>
-
-#include <curl/curl.h>
-#include <jansson.h>
-
-#include "config.h"
-#include "log.h"
-#include "types.h"
-#include "util.h"
-
-#define TAG_MAX 256
-
-enum taskst {
-	TASKST_PENDING,         /* not started yet. */
-	TASKST_RUNNING,         /* currently running. */
-	TASKST_COMPLETED,       /* completed but not synced yet. */
-	TASKST_SYNCING          /* was unable to send result to host. */
-};
-
-struct task {
-	enum taskst status;
-	pid_t child;
-	int pipe[2];
-	int exitcode;
-	int job_id;
-	int project_id;
-	char job_tag[TAG_MAX];
-	char out[SCI_CONSOLE_MAX];
-	char script[PATH_MAX];
-	int scriptfd;
-	TAILQ_ENTRY(task) link;
-};
-
-TAILQ_HEAD(tasks, task);
-
-struct fds {
-	struct pollfd *list;
-	size_t listsz;
-};
-
-struct fetch {
-	char buf[SCI_MSG_MAX];
-	FILE *bufp;
-};
-
-static struct {
-	char *url;
-	char *worker;
-	int maxbuilds;
-} config = {
-	.url = "http://localhost",
-	.worker = "default",
-	.maxbuilds = 4
-};
-
-static struct tasks tasks = TAILQ_HEAD_INITIALIZER(tasks);
-static struct worker worker;
-static int alive = 1;
-
-noreturn static void
-usage(void)
-{
-	fprintf(stderr, "usage: %s [-m maxbuild] [-u url] [-w worker]\n", getprogname());
-	exit(1);
-}
-
-static inline struct task *
-find_by_fd(int fd)
-{
-	struct task *tk;
-
-	TAILQ_FOREACH(tk, &tasks, link)
-		if (tk->pipe[0] == fd)
-			return tk;
-
-	return NULL;
-}
-
-static inline struct task *
-find_by_pid(pid_t pid)
-{
-	struct task *t;
-
-	TAILQ_FOREACH(t, &tasks, link)
-		if (t->child == pid)
-			return t;
-
-	return NULL;
-}
-
-static void
-destroy(struct task *tk)
-{
-	log_debug("destroying task %d", tk->job_id);
-	unlink(tk->script);
-
-	if (tk->pipe[0])
-		close(tk->pipe[0]);
-	if (tk->pipe[1])
-		close(tk->pipe[1]);
-	if (tk->scriptfd)
-		close(tk->scriptfd);
-
-	TAILQ_REMOVE(&tasks, tk, link);
-	memset(tk, 0, sizeof (*tk));
-	free(tk);
-}
-
-static int
-spawn(struct task *tk)
-{
-	if (pipe(tk->pipe) < 0)
-		goto cleanup;
-
-	switch ((tk->child = fork())) {
-	case -1:
-		log_warn("spawn: %s", strerror(errno));
-		goto cleanup;
-	case 0:
-		/* Child. */
-		dup2(tk->pipe[1], STDOUT_FILENO);
-		dup2(tk->pipe[1], STDERR_FILENO);
-		close(tk->pipe[0]);
-		close(tk->pipe[1]);
-		log_debug("spawn: running process (%lld) %s",
-		    (long long int)tk->child, tk->script);
-
-		if (execl(tk->script, tk->script, tk->job_tag, NULL) < 0) {
-			tk->status = TASKST_PENDING;
-			log_warn("exec %s: %s", tk->script, strerror(errno));
-			exit(0);
-		}
-		break;
-	default:
-		/* Parent */
-		tk->status = TASKST_RUNNING;
-		break;
-	}
-
-	return 0;
-
-cleanup:
-	destroy(tk);
-
-	return -1;
-}
-
-static const char *
-makeurl(const char *fmt, ...)
-{
-	assert(fmt);
-
-	static char url[256];
-	char page[128] = {0};
-	va_list ap;
-
-	va_start(ap, fmt);
-	vsnprintf(page, sizeof (page), fmt, ap);
-	va_end(ap);
-
-	snprintf(url, sizeof (url), "%s/%s", config.url, page);
-
-	return url;
-}
-
-static void
-complete(int signum, siginfo_t *sinfo, void *ctx)
-{
-	(void)ctx;
-	(void)signum;
-
-	struct task *tk;
-
-	if (waitpid(sinfo->si_pid, NULL, 0) < 0)
-		log_warn("waitpid: %s", strerror(errno));
-
-	if ((tk = find_by_pid(sinfo->si_pid))) {
-		log_debug("process %d terminated (exitcode=%d)",
-		    (int)sinfo->si_pid, sinfo->si_status);
-
-		close(tk->pipe[1]);
-		tk->status = TASKST_COMPLETED;
-		tk->exitcode = sinfo->si_status;
-		tk->pipe[1] = 0;
-	}
-}
-
-static void
-stop(int signum)
-{
-	log_warn("exiting on signal %d", signum);
-	alive = 0;
-}
-
-static char *
-uploadenc(const struct task *tk)
-{
-	json_t *doc;
-
-	struct jobresult res = {0};
-	char *dump;
-
-	res.job_id = tk->job_id;
-	res.exitcode = tk->exitcode;
-	res.log = tk->out;
-	res.worker_id = worker.id;
-
-	doc = jobresult_to(&res, 1);
-	dump = json_dumps(doc, JSON_COMPACT);
-
-	json_decref(doc);
-
-	return dump;
-}
-
-static size_t
-getcb(char *in, size_t n, size_t w, FILE *fp)
-{
-	if (fwrite(in, n, w, fp) != w)
-		return log_warn("get: %s", strerror(errno)), 0;
-
-	return w;
-}
-
-static json_t *
-get(const char *topic, const char *url)
-{
-	CURL *curl;
-	CURLcode code;
-
-	json_t *doc;
-	json_error_t error;
-
-	char buf[SCI_MSG_MAX];
-	long status;
-	FILE *fp;
-
-	curl = curl_easy_init();
-
-	if (!(fp = fmemopen(buf, sizeof (buf), "w")))
-		err(1, "fmemopen");
-
-	curl_easy_setopt(curl, CURLOPT_URL, url);
-	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
-	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
-	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, getcb);
-	curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
-
-	if ((code = curl_easy_perform(curl)) != CURLE_OK)
-		log_warn("%s: %s", topic, curl_easy_strerror(code));
-
-	curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
-	curl_easy_cleanup(curl);
-
-	fclose(fp);
-
-	if (code != CURLE_OK)
-		return log_warn("%s: %s", topic, curl_easy_strerror(code)), NULL;
-	if (status != 200)
-		return log_warn("%s: unexpected status code %ld", topic, status), NULL;
-	if (!(doc = json_loads(buf, 0, &error)))
-		return log_warn("%s: %s", topic, error.text), NULL;
-
-	return doc;
-}
-
-static size_t
-silent(char *in, size_t n, size_t w, void *data)
-{
-	(void)in;
-	(void)n;
-	(void)data;
-
-	return w;
-}
-
-static void
-upload(struct task *tk)
-{
-	CURL *curl;
-	CURLcode code;
-	struct curl_slist *headers = NULL;
-	long status;
-	char *dump;
-
-	curl = curl_easy_init();
-	headers = curl_slist_append(headers, "Content-Type: application/json");
-	curl_easy_setopt(curl, CURLOPT_URL, makeurl("api/v1/jobs"));
-	//curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:4000");
-	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
-	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
-	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, silent);
-	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, (dump = uploadenc(tk)));
-	curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(dump));
-	curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
-	code = curl_easy_perform(curl);
-	curl_slist_free_all(headers);
-
-	/*
-	 * If we fail to upload data, we put the result into syncing mode so
-	 * that we retry later without redoing the job over and over
-	 */
-	tk->status = TASKST_SYNCING;
-
-	if (code != CURLE_OK)
-		log_warn("upload: %s", curl_easy_strerror(code));
-	else {
-		curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
-
-		if (status != 200)
-			log_warn("upload: unexpected return code: %ld", status);
-		else
-			destroy(tk);
-	}
-
-	free(dump);
-	curl_easy_cleanup(curl);
-}
-
-static inline int
-pending(int id)
-{
-	struct task *t;
-
-	TAILQ_FOREACH(t, &tasks, link)
-		if (t->job_id == id)
-			return 1;
-
-	return 0;
-}
-
-static void
-queue(int id, int project_id, const char *tag)
-{
-	struct task *tk;
-
-	log_info("queued job build (%d) for tag %s\n", id, tag);
-
-	tk = util_calloc(1, sizeof (*tk));
-	tk->job_id = id;
-	tk->project_id = project_id;
-	strlcpy(tk->job_tag, tag, sizeof (tk->job_tag));
-
-	TAILQ_INSERT_TAIL(&tasks, tk, link);
-}
-
-static void
-merge(json_t *doc)
-{
-	struct job jobs[SCI_JOB_LIST_MAX];
-	ssize_t jobsz;
-
-	if ((jobsz = job_from(jobs, UTIL_SIZE(jobs), doc)) < 0)
-		log_warn("fetchjobs: %s", strerror(errno));
-	else {
-		for (ssize_t i = 0; i < jobsz; ++i) {
-			if (!pending(jobs[i].id))
-				queue(jobs[i].id, jobs[i].project_id, jobs[i].tag);
-		}
-	}
-
-	json_decref(doc);
-}
-
-static void
-fetchjobs(void)
-{
-	json_t *doc;
-
-	if (!(doc = get("fetch", makeurl("api/v1/jobs/%s", config.worker))))
-		log_warn("unable to retrieve jobs");
-	else
-		merge(doc);
-}
-
-/*
- * This function reads stdout/stderr pipe from child and optionally remove them
- * if they have completed.
- */
-static void
-readall(struct fds *fds)
-{
-	struct task *tk;
-	char buf[BUFSIZ];
-	ssize_t nr;
-
-	for (size_t i = 0; i < fds->listsz; ++i) {
-		if (fds->list[i].revents == 0)
-			continue;
-		if (!(tk = find_by_fd(fds->list[i].fd)))
-			continue;
-
-		/* Read stdout/stderr from children pipe. */
-		if ((nr = read(fds->list[i].fd, buf, sizeof (buf) - 1)) <= 0)
-			tk->status = TASKST_SYNCING;
-		else {
-			buf[nr] = 0;
-			strlcat(tk->out, buf, sizeof (tk->out));
-		}
-	}
-}
-
-/*
- * Retrieve status code from spawned process complete or upload again if they
- * failed to sync.
- */
-static void
-flushall(void)
-{
-	struct task *tk, *tmp;
-
-	TAILQ_FOREACH_SAFE(tk, &tasks, link, tmp)
-		if (tk->status == TASKST_SYNCING)
-			upload(tk);
-}
-
-static int
-extract(struct task *tk, json_t *doc)
-{
-	struct project proj;
-	size_t len;
-
-	if (project_from(&proj, 1, doc) < 0) {
-		json_decref(doc);
-		log_warn("fetchproject: %s", strerror(errno));
-		return -1;
-	}
-
-	len = strlen(proj.script);
-
-	if ((size_t)write(tk->scriptfd, proj.script, len) != len) {
-		json_decref(doc);
-		log_warn("fetchproject: %s", strerror(errno));
-		return -1;
-	}
-
-	/* Close so we can finally spawn it. */
-	close(tk->scriptfd);
-	tk->scriptfd = 0;
-
-	return 0;
-}
-
-static int
-fetchproject(struct task *tk)
-{
-	json_t *doc;
-
-	if (!(doc = get("fetchproject", makeurl("api/v1/projects/%d", tk->project_id))))
-		return -1;
-
-	return extract(tk, doc);
-}
-
-/*
- * Create a task to run the script. This will retrieve the project script code
- * at this moment and put it in a temporary file.
- */
-static void
-createtask(struct task *tk)
-{
-	if (tk->status != TASKST_PENDING)
-		return;
-
-	log_debug("creating task (id=%d, tag=%s)", tk->job_id, tk->job_tag);
-	snprintf(tk->script, sizeof (tk->script), "/tmp/sciworkerd-%d-XXXXXX", tk->job_id);
-
-	if ((tk->scriptfd = mkstemp(tk->script)) < 0 ||
-	    fchmod(tk->scriptfd, S_IRUSR | S_IWUSR | S_IXUSR) < 0) {
-		unlink(tk->script);
-		log_warn("%s", strerror(errno));
-		return;
-	}
-
-	if (fetchproject(tk) < 0) {
-		unlink(tk->script);
-		close(tk->scriptfd);
-		tk->scriptfd = 0;
-	} else
-		spawn(tk);
-}
-
-/*
- * Start all pending tasks if the limit of running tasks is not reached.
- */
-static void
-startall(void)
-{
-	size_t nrunning = 0;
-	struct task *tk;
-
-	TAILQ_FOREACH(tk, &tasks, link)
-		if (tk->status == TASKST_RUNNING)
-			++nrunning;
-
-	if (nrunning >= (size_t)config.maxbuilds)
-		log_debug("not spawning new process because limit is reached");
-	else {
-		tk = TAILQ_FIRST(&tasks);
-
-		while (tk && nrunning++ < (size_t)config.maxbuilds) {
-			createtask(tk);
-			tk = TAILQ_NEXT(tk, link);
-		}
-	}
-}
-
-static void
-fetchworker(void)
-{
-	json_t *doc;
-
-	if (!(doc = get("fetchworker", makeurl("api/v1/workers/%s", config.worker))) ||
-	    worker_from(&worker, 1, doc) < 0)
-		errx(1, "unable to retrieve worker id");
-
-	log_info("worker id: %d", worker.id);
-	log_info("worker name: %s", worker.name);
-	log_info("worker description: %s", worker.desc);
-
-	json_decref(doc);
-}
-
-static void
-init(void)
-{
-	struct sigaction sa;
-
-	sa.sa_flags = SA_SIGINFO | SA_RESTART;
-	sa.sa_sigaction = complete;
-	sigemptyset(&sa.sa_mask);
-
-	if (sigaction(SIGCHLD, &sa, NULL) < 0)
-		err(1, "sigaction");
-
-	sa.sa_flags = SA_RESTART;
-	sa.sa_handler = stop;
-	sigemptyset(&sa.sa_mask);
-
-	if (sigaction(SIGTERM, &sa, NULL) < 0 || sigaction(SIGINT, &sa, NULL) < 0)
-		err(1, "sigaction");
-
-	log_open("sciworkerd");
-	fetchworker();
-}
-
-static struct fds
-prepare(void)
-{
-	struct fds fds = {0};
-	struct task *tk;
-	size_t i = 0;
-
-	TAILQ_FOREACH(tk, &tasks, link)
-		if (tk->status == TASKST_RUNNING || tk->status == TASKST_COMPLETED)
-			fds.listsz++;
-
-	fds.list = util_calloc(fds.listsz, sizeof (*fds.list));
-
-	TAILQ_FOREACH(tk, &tasks, link) {
-		if (tk->status == TASKST_RUNNING || tk->status == TASKST_COMPLETED) {
-			fds.list[i].fd = tk->pipe[0];
-			fds.list[i++].events = POLLIN | POLLPRI;
-		}
-	}
-
-	return fds;
-}
-
-static void
-run(void)
-{
-	struct fds fds;
-
-	fds = prepare();
-
-	if (poll(fds.list, fds.listsz, 5000) < 0 && errno != EINTR)
-		err(1, "poll");
-
-	fetchjobs();
-	readall(&fds);
-	startall();
-	flushall();
-}
-
-static void
-finish(void)
-{
-	size_t tot = 0;
-	struct task *tk, *tmp;
-
-	TAILQ_FOREACH(tk, &tasks, link)
-		tot++;
-
-	signal(SIGCHLD, SIG_IGN);
-	log_debug("killing remaining %zu tasks", tot);
-
-	TAILQ_FOREACH_SAFE(tk, &tasks, link, tmp) {
-		if (tk->status == TASKST_RUNNING) {
-			kill(tk->child, SIGTERM);
-			waitpid(tk->child, NULL, 0);
-		}
-
-		destroy(tk);
-	}
-}
-
-int
-main(int argc, char **argv)
-{
-	int ch;
-	const char *errstr;
-
-	setprogname("sciworkerd");
-
-	while ((ch = getopt(argc, argv, "m:u:w:")) != -1) {
-		switch (ch) {
-		case 'm':
-			config.maxbuilds = strtonum(optarg, 0, INT_MAX, &errstr);
-
-			if (errstr)
-				errx(1, "%s: %s", optarg, errstr);
-
-			break;
-		case 'u':
-			config.url = optarg;
-			break;
-		case 'w':
-			config.worker = optarg;
-			break;
-		default:
-			usage();
-			break;
-		}
-	}
-
-	init();
-
-	while (alive)
-		run();
-
-	finish();
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sciworkerd/main.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,734 @@
+/*
+ * sciworkerd.c -- main sciworkerd(8) program file
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#if 0
+
+#include <sys/queue.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdnoreturn.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <curl/curl.h>
+#include <jansson.h>
+
+#include "config.h"
+#include "log.h"
+#include "types.h"
+#include "util.h"
+
+#define TAG_MAX 256
+
+struct task {
+	enum taskst status;
+	pid_t child;
+	int pipe[2];
+	int exitcode;
+	int job_id;
+	int project_id;
+	char job_tag[TAG_MAX];
+	char out[SCI_CONSOLE_MAX];
+	char script[PATH_MAX];
+	int scriptfd;
+	TAILQ_ENTRY(task) link;
+};
+
+TAILQ_HEAD(tasks, task);
+
+struct fds {
+	struct pollfd *list;
+	size_t listsz;
+};
+
+struct fetch {
+	char buf[SCI_MSG_MAX];
+	FILE *bufp;
+};
+
+static struct {
+	char *url;
+	char *worker;
+	int maxbuilds;
+} config = {
+	.url = "http://localhost",
+	.worker = "default",
+	.maxbuilds = 4
+};
+
+static struct tasks tasks = TAILQ_HEAD_INITIALIZER(tasks);
+static struct worker worker;
+static int alive = 1;
+
+/*
+ * Show usage and exit with code 1.
+ */
+noreturn static void
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-m maxbuild] [-u url] [-w worker]\n", getprogname());
+	exit(1);
+}
+
+/*
+ * Find a task by its id.
+ */
+static inline struct task *
+find_by_fd(int fd)
+{
+	struct task *tk;
+
+	TAILQ_FOREACH(tk, &tasks, link)
+		if (tk->pipe[0] == fd)
+			return tk;
+
+	return NULL;
+}
+
+/*
+ * Find a task by its pid number.
+ */
+static inline struct task *
+find_by_pid(pid_t pid)
+{
+	struct task *t;
+
+	TAILQ_FOREACH(t, &tasks, link)
+		if (t->child == pid)
+			return t;
+
+	return NULL;
+}
+
+/*
+ * Destroy a task entirely.
+ */
+static void
+destroy(struct task *tk)
+{
+	log_debug("destroying task %d", tk->job_id);
+	unlink(tk->script);
+
+	if (tk->pipe[0])
+		close(tk->pipe[0]);
+	if (tk->pipe[1])
+		close(tk->pipe[1]);
+	if (tk->scriptfd)
+		close(tk->scriptfd);
+
+	TAILQ_REMOVE(&tasks, tk, link);
+	memset(tk, 0, sizeof (*tk));
+	free(tk);
+}
+
+static const char *
+makeurl(const char *fmt, ...)
+{
+	assert(fmt);
+
+	static char url[256];
+	char page[128] = {0};
+	va_list ap;
+
+	va_start(ap, fmt);
+	vsnprintf(page, sizeof (page), fmt, ap);
+	va_end(ap);
+
+	snprintf(url, sizeof (url), "%s/%s", config.url, page);
+
+	return url;
+}
+
+static void
+complete(int signum, siginfo_t *sinfo, void *ctx)
+{
+	(void)ctx;
+	(void)signum;
+
+	struct task *tk;
+
+	if (waitpid(sinfo->si_pid, NULL, 0) < 0)
+		log_warn("waitpid: %s", strerror(errno));
+
+	if ((tk = find_by_pid(sinfo->si_pid))) {
+		log_debug("process %d terminated (exitcode=%d)",
+		    (int)sinfo->si_pid, sinfo->si_status);
+
+		close(tk->pipe[1]);
+		tk->status = TASKST_COMPLETED;
+		tk->exitcode = sinfo->si_status;
+		tk->pipe[1] = 0;
+	}
+}
+
+static void
+stop(int signum)
+{
+	log_warn("exiting on signal %d", signum);
+	alive = 0;
+}
+
+static char *
+uploadenc(const struct task *tk)
+{
+	json_t *doc;
+
+	struct jobresult res = {0};
+	char *dump;
+
+	res.job_id = tk->job_id;
+	res.exitcode = tk->exitcode;
+	res.log = tk->out;
+	res.worker_id = worker.id;
+
+	doc = jobresult_to(&res, 1);
+	dump = json_dumps(doc, JSON_COMPACT);
+
+	json_decref(doc);
+
+	return dump;
+}
+
+static size_t
+getcb(char *in, size_t n, size_t w, FILE *fp)
+{
+	if (fwrite(in, n, w, fp) != w)
+		return log_warn("get: %s", strerror(errno)), 0;
+
+	return w;
+}
+
+static json_t *
+get(const char *topic, const char *url)
+{
+	CURL *curl;
+	CURLcode code;
+
+	json_t *doc;
+	json_error_t error;
+
+	char buf[SCI_MSG_MAX];
+	long status;
+	FILE *fp;
+
+	curl = curl_easy_init();
+
+	if (!(fp = fmemopen(buf, sizeof (buf), "w")))
+		err(1, "fmemopen");
+
+	curl_easy_setopt(curl, CURLOPT_URL, url);
+	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
+	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, getcb);
+	curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
+
+	if ((code = curl_easy_perform(curl)) != CURLE_OK)
+		log_warn("%s: %s", topic, curl_easy_strerror(code));
+
+	curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
+	curl_easy_cleanup(curl);
+
+	fclose(fp);
+
+	if (code != CURLE_OK)
+		return log_warn("%s: %s", topic, curl_easy_strerror(code)), NULL;
+	if (status != 200)
+		return log_warn("%s: unexpected status code %ld", topic, status), NULL;
+	if (!(doc = json_loads(buf, 0, &error)))
+		return log_warn("%s: %s", topic, error.text), NULL;
+
+	return doc;
+}
+
+static size_t
+silent(char *in, size_t n, size_t w, void *data)
+{
+	(void)in;
+	(void)n;
+	(void)data;
+
+	return w;
+}
+
+static void
+upload(struct task *tk)
+{
+	CURL *curl;
+	CURLcode code;
+	struct curl_slist *headers = NULL;
+	long status;
+	char *dump;
+
+	curl = curl_easy_init();
+	headers = curl_slist_append(headers, "Content-Type: application/json");
+	curl_easy_setopt(curl, CURLOPT_URL, makeurl("api/v1/jobs"));
+	//curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:4000");
+	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
+	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, silent);
+	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, (dump = uploadenc(tk)));
+	curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(dump));
+	curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+	code = curl_easy_perform(curl);
+	curl_slist_free_all(headers);
+
+	/*
+	 * If we fail to upload data, we put the result into syncing mode so
+	 * that we retry later without redoing the job over and over
+	 */
+	tk->status = TASKST_SYNCING;
+
+	if (code != CURLE_OK)
+		log_warn("upload: %s", curl_easy_strerror(code));
+	else {
+		curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
+
+		if (status != 200)
+			log_warn("upload: unexpected return code: %ld", status);
+		else
+			destroy(tk);
+	}
+
+	free(dump);
+	curl_easy_cleanup(curl);
+}
+
+static inline int
+pending(int id)
+{
+	struct task *t;
+
+	TAILQ_FOREACH(t, &tasks, link)
+		if (t->job_id == id)
+			return 1;
+
+	return 0;
+}
+
+static void
+queue(int id, int project_id, const char *tag)
+{
+	struct task *tk;
+
+	log_info("queued job build (%d) for tag %s\n", id, tag);
+
+	tk = util_calloc(1, sizeof (*tk));
+	tk->job_id = id;
+	tk->project_id = project_id;
+	strlcpy(tk->job_tag, tag, sizeof (tk->job_tag));
+
+	TAILQ_INSERT_TAIL(&tasks, tk, link);
+}
+
+static void
+merge(json_t *doc)
+{
+	struct job jobs[SCI_JOB_LIST_MAX];
+	ssize_t jobsz;
+
+	if ((jobsz = job_from(jobs, UTIL_SIZE(jobs), doc)) < 0)
+		log_warn("fetchjobs: %s", strerror(errno));
+	else {
+		for (ssize_t i = 0; i < jobsz; ++i) {
+			if (!pending(jobs[i].id))
+				queue(jobs[i].id, jobs[i].project_id, jobs[i].tag);
+		}
+	}
+
+	json_decref(doc);
+}
+
+static void
+fetchjobs(void)
+{
+	json_t *doc;
+
+	if (!(doc = get("fetch", makeurl("api/v1/jobs/%s", config.worker))))
+		log_warn("unable to retrieve jobs");
+	else
+		merge(doc);
+}
+
+/*
+ * This function reads stdout/stderr pipe from child and optionally remove them
+ * if they have completed.
+ */
+static void
+readall(struct fds *fds)
+{
+	struct task *tk;
+	char buf[BUFSIZ];
+	ssize_t nr;
+
+	for (size_t i = 0; i < fds->listsz; ++i) {
+		if (fds->list[i].revents == 0)
+			continue;
+		if (!(tk = find_by_fd(fds->list[i].fd)))
+			continue;
+
+		/* Read stdout/stderr from children pipe. */
+		if ((nr = read(fds->list[i].fd, buf, sizeof (buf) - 1)) <= 0)
+			tk->status = TASKST_SYNCING;
+		else {
+			buf[nr] = 0;
+			strlcat(tk->out, buf, sizeof (tk->out));
+		}
+	}
+}
+
+/*
+ * Retrieve status code from spawned process complete or upload again if they
+ * failed to sync.
+ */
+static void
+flushall(void)
+{
+	struct task *tk, *tmp;
+
+	TAILQ_FOREACH_SAFE(tk, &tasks, link, tmp)
+		if (tk->status == TASKST_SYNCING)
+			upload(tk);
+}
+
+static int
+extract(struct task *tk, json_t *doc)
+{
+	struct project proj;
+	size_t len;
+
+	if (project_from(&proj, 1, doc) < 0) {
+		json_decref(doc);
+		log_warn("fetchproject: %s", strerror(errno));
+		return -1;
+	}
+
+	len = strlen(proj.script);
+
+	if ((size_t)write(tk->scriptfd, proj.script, len) != len) {
+		json_decref(doc);
+		log_warn("fetchproject: %s", strerror(errno));
+		return -1;
+	}
+
+	/* Close so we can finally spawn it. */
+	close(tk->scriptfd);
+	tk->scriptfd = 0;
+
+	return 0;
+}
+
+static int
+fetchproject(struct task *tk)
+{
+	json_t *doc;
+
+	if (!(doc = get("fetchproject", makeurl("api/v1/projects/%d", tk->project_id))))
+		return -1;
+
+	return extract(tk, doc);
+}
+
+/*
+ * Create a task to run the script. This will retrieve the project script code
+ * at this moment and put it in a temporary file.
+ */
+static void
+createtask(struct task *tk)
+{
+	if (tk->status != TASKST_PENDING)
+		return;
+
+	log_debug("creating task (id=%d, tag=%s)", tk->job_id, tk->job_tag);
+	snprintf(tk->script, sizeof (tk->script), "/tmp/sciworkerd-%d-XXXXXX", tk->job_id);
+
+	if ((tk->scriptfd = mkstemp(tk->script)) < 0 ||
+	    fchmod(tk->scriptfd, S_IRUSR | S_IWUSR | S_IXUSR) < 0) {
+		unlink(tk->script);
+		log_warn("%s", strerror(errno));
+		return;
+	}
+
+	if (fetchproject(tk) < 0) {
+		unlink(tk->script);
+		close(tk->scriptfd);
+		tk->scriptfd = 0;
+	} else
+		spawn(tk);
+}
+
+/*
+ * Start all pending tasks if the limit of running tasks is not reached.
+ */
+static void
+startall(void)
+{
+	size_t nrunning = 0;
+	struct task *tk;
+
+	TAILQ_FOREACH(tk, &tasks, link)
+		if (tk->status == TASKST_RUNNING)
+			++nrunning;
+
+	if (nrunning >= (size_t)config.maxbuilds)
+		log_debug("not spawning new process because limit is reached");
+	else {
+		tk = TAILQ_FIRST(&tasks);
+
+		while (tk && nrunning++ < (size_t)config.maxbuilds) {
+			createtask(tk);
+			tk = TAILQ_NEXT(tk, link);
+		}
+	}
+}
+
+static void
+fetchworker(void)
+{
+	json_t *doc;
+
+	if (!(doc = get("fetchworker", makeurl("api/v1/workers/%s", config.worker))) ||
+	    worker_from(&worker, 1, doc) < 0)
+		errx(1, "unable to retrieve worker id");
+
+	log_info("worker id: %d", worker.id);
+	log_info("worker name: %s", worker.name);
+	log_info("worker description: %s", worker.desc);
+
+	json_decref(doc);
+}
+
+static void
+init(void)
+{
+	struct sigaction sa;
+
+	sa.sa_flags = SA_SIGINFO | SA_RESTART;
+	sa.sa_sigaction = complete;
+	sigemptyset(&sa.sa_mask);
+
+	if (sigaction(SIGCHLD, &sa, NULL) < 0)
+		err(1, "sigaction");
+
+	sa.sa_flags = SA_RESTART;
+	sa.sa_handler = stop;
+	sigemptyset(&sa.sa_mask);
+
+	if (sigaction(SIGTERM, &sa, NULL) < 0 || sigaction(SIGINT, &sa, NULL) < 0)
+		err(1, "sigaction");
+
+	log_open("sciworkerd");
+	fetchworker();
+}
+
+static struct fds
+prepare(void)
+{
+	struct fds fds = {0};
+	struct task *tk;
+	size_t i = 0;
+
+	TAILQ_FOREACH(tk, &tasks, link)
+		if (tk->status == TASKST_RUNNING || tk->status == TASKST_COMPLETED)
+			fds.listsz++;
+
+	fds.list = util_calloc(fds.listsz, sizeof (*fds.list));
+
+	TAILQ_FOREACH(tk, &tasks, link) {
+		if (tk->status == TASKST_RUNNING || tk->status == TASKST_COMPLETED) {
+			fds.list[i].fd = tk->pipe[0];
+			fds.list[i++].events = POLLIN | POLLPRI;
+		}
+	}
+
+	return fds;
+}
+
+static void
+run(void)
+{
+	struct fds fds;
+
+	fds = prepare();
+
+	if (poll(fds.list, fds.listsz, 5000) < 0 && errno != EINTR)
+		err(1, "poll");
+
+	fetchjobs();
+	readall(&fds);
+	startall();
+	flushall();
+}
+
+static void
+finish(void)
+{
+	size_t tot = 0;
+	struct task *tk, *tmp;
+
+	TAILQ_FOREACH(tk, &tasks, link)
+		tot++;
+
+	signal(SIGCHLD, SIG_IGN);
+	log_debug("killing remaining %zu tasks", tot);
+
+	TAILQ_FOREACH_SAFE(tk, &tasks, link, tmp) {
+		if (tk->status == TASKST_RUNNING) {
+			kill(tk->child, SIGTERM);
+			waitpid(tk->child, NULL, 0);
+		}
+
+		destroy(tk);
+	}
+}
+
+int
+main(int argc, char **argv)
+{
+	int ch;
+	const char *errstr;
+
+	setprogname("sciworkerd");
+
+	while ((ch = getopt(argc, argv, "m:u:w:")) != -1) {
+		switch (ch) {
+		case 'm':
+			config.maxbuilds = strtonum(optarg, 0, INT_MAX, &errstr);
+
+			if (errstr)
+				errx(1, "%s: %s", optarg, errstr);
+
+			break;
+		case 'u':
+			config.url = optarg;
+			break;
+		case 'w':
+			config.worker = optarg;
+			break;
+		default:
+			usage();
+			break;
+		}
+	}
+
+	init();
+
+	while (alive)
+		run();
+
+	finish();
+}
+#endif
+
+
+
+
+
+
+
+
+
+
+#include <err.h>
+#include <errno.h>
+#include <poll.h>
+#include <signal.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "types.h"
+#include "task.h"
+
+#define SCRIPT \
+	"#!/bin/sh\n" \
+	"echo yes\n" \
+	"sleep 10\n" \
+	"echo no 1>&2\n" \
+	"sleep 1\n" \
+	"exit 1"
+
+int
+main(void)
+{
+	struct job job = {
+		.project_id = 10,
+		.id = 10,
+		.tag = "1234"
+	};
+	struct sigaction sa = {0};
+	struct pollfd fd;
+	struct task *t;
+	int run = 1;
+
+	t = task_new(&job);
+
+	if (task_setup(t, SCRIPT) < 0)
+		err(1, "task_set_script");
+	if (task_start(t) < 0)
+		err(1, "task_start");
+
+	while (run) {
+		if (difftime(time(NULL), task_uptime(t)) >= 3) {
+			printf("task timeout !\n");
+			task_kill(t);
+			task_wait(t);
+			break;
+		}
+
+		task_prepare(t, &fd);
+
+		if (poll(&fd, 1, 250) < 0 && errno != EINTR)
+			err(1, "poll");
+
+		switch (task_sync(t, &fd)) {
+		case -1:
+			err(1, "task_sync");
+		case 0:
+			run = 0;
+			task_wait(t);
+			break;
+		default:
+			/* Keep going... */
+			break;
+		}
+	}
+
+	switch (task_status(t)) {
+	case TASKSTATUS_EXITED:
+		printf("process exited with code: %d\n", task_code(t).exitcode);
+		break;
+	case TASKSTATUS_KILLED:
+		printf("process killed with signal %d\n", task_code(t).sigcode);
+		break;
+	default:
+		break;
+	}
+
+	printf("== console ==\n%s==\n", task_console(t));
+	task_free(t);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sciworkerd/task.c	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,248 @@
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "log.h"
+#include "task.h"
+#include "types.h"
+#include "util.h"
+
+#define MODE S_IRUSR | S_IWUSR | S_IXUSR
+
+struct task {
+	enum taskstatus status;
+	pid_t child;
+	int pipe[2];
+	int exitcode;
+	int sigcode;
+	int job_id;
+	int project_id;
+	char *job_tag;
+	FILE *fp;
+	char *console;
+	size_t consolesz;
+	int scriptfd;
+	char scriptpath[PATH_MAX];
+	time_t startup;
+};
+
+struct task *
+task_new(const struct job *job)
+{
+	struct task *task;
+
+	task = util_calloc(1, sizeof (*task));
+	task->project_id = job->project_id;
+	task->job_id = job->id;
+	task->job_tag = util_strdup(job->tag);
+	task->pipe[0] = task->pipe[1] = -1;
+	task->child = -1;
+
+	if (!(task->fp = open_memstream(&task->console, &task->consolesz))) {
+		task_free(task);
+		return NULL;
+	}
+
+	return task;
+}
+
+int
+task_setup(struct task *self, const char *script)
+{
+	assert(self);
+	assert(script);
+
+	const size_t len = strlen(script);
+
+	snprintf(self->scriptpath, sizeof (self->scriptpath),
+	    "/tmp/sciworkerd-%d-XXXXXX", self->job_id);
+
+	if ((self->scriptfd = mkstemp(self->scriptpath)) < 0)
+		goto failed;
+	if (fchmod(self->scriptfd, MODE) < 0)
+		goto failed;
+	if (write(self->scriptfd, script, len) != len)
+		goto failed;
+
+	return 0;
+
+failed:
+	log_warn("%s", strerror(errno));
+	unlink(self->scriptpath);
+
+	return -1;
+}
+
+pid_t
+task_start(struct task *self)
+{
+	assert(self);
+	assert(self->status == TASKSTATUS_PENDING);
+
+	if (pipe(self->pipe) < 0)
+		return -1;
+
+	switch ((self->child = fork())) {
+	case -1:
+		return -1;
+	case 0:
+		dup2(self->pipe[1], STDOUT_FILENO);
+		dup2(self->pipe[1], STDERR_FILENO);
+		close(self->pipe[0]);
+		close(self->pipe[1]);
+		close(STDIN_FILENO);
+
+		if (execl(self->scriptpath, self->scriptpath, self->job_tag, NULL) < 0)
+			_exit(1);
+		break;
+	default:
+		close(self->pipe[1]);
+		self->pipe[1] = -1;
+		self->status = TASKSTATUS_RUNNING;
+		self->startup = time(NULL);
+		break;
+	}
+
+	return 0;
+}
+
+int
+task_wait(struct task *self)
+{
+	assert(self);
+	assert(self->status == TASKSTATUS_RUNNING);
+
+	int status;
+	pid_t ret;
+
+	while ((ret = waitpid(self->child, &status, 0)) < 0 && errno == EINTR)
+		continue;
+	if (ret < 0)
+		return ret;
+
+	if (WIFEXITED(status)) {
+		self->exitcode = WEXITSTATUS(status);
+		self->status = TASKSTATUS_EXITED;
+	} else if (WIFSIGNALED(status)) {
+		self->sigcode = WTERMSIG(status);
+		self->status = TASKSTATUS_KILLED;
+	}
+
+	self->child = -1;
+
+	/* Close file output so user can get access to the console. */
+	fclose(self->fp);
+	self->fp = NULL;
+
+	return 0;
+}
+
+int
+task_kill(struct task *self)
+{
+	assert(self);
+	assert(self->status == TASKSTATUS_RUNNING);
+
+	if (kill(self->child, SIGTERM) < 0)
+		return -1;
+
+	return 0;
+}
+
+time_t
+task_uptime(const struct task *self)
+{
+	assert(self);
+
+	return self->startup;
+}
+
+const char *
+task_console(const struct task *self)
+{
+	assert(self);
+
+	return self->console;
+}
+
+void
+task_prepare(struct task *self, struct pollfd *fd)
+{
+	assert(self);
+	assert(self->status == TASKSTATUS_RUNNING);
+	assert(fd);
+
+	fd->fd = self->pipe[0];
+	fd->events = POLLIN | POLLPRI;
+}
+
+int
+task_sync(struct task *self, const struct pollfd *fd)
+{
+	assert(self);
+	assert(self->status == TASKSTATUS_RUNNING);
+	assert(fd->fd == self->pipe[0]);
+
+	char buf[BUFSIZ];
+	ssize_t nr;
+
+	if (fd->revents & POLLHUP)
+		return 0;
+	/* If we read EOF, it usually means the process has exited correctly. */
+	if (fd->revents & POLLIN) {
+		if ((nr = read(self->pipe[0], buf, sizeof (buf))) <= 0)
+			return nr;
+		if (fwrite(buf, 1, nr, self->fp) != nr)
+			return -1;
+	}
+
+	return 1;
+}
+
+enum taskstatus
+task_status(const struct task *self)
+{
+	assert(self);
+
+	return self->status;
+}
+
+struct taskcode
+task_code(const struct task *self)
+{
+	assert(self);
+
+	return (struct taskcode) {
+		.exitcode = self->exitcode,
+		.sigcode = self->sigcode
+	};
+}
+
+void
+task_free(struct task *self)
+{
+	assert(self);
+	assert(self->status != TASKSTATUS_RUNNING);
+
+	if (self->pipe[0])
+		close(self->pipe[0]);
+	if (self->pipe[1])
+		close(self->pipe[1]);
+	if (self->fp)
+		fclose(self->fp);
+	if (self->scriptpath[0])
+		unlink(self->scriptpath);
+
+	free(self->job_tag);
+	free(self->console);
+	free(self);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sciworkerd/task.h	Tue Jul 12 20:20:51 2022 +0200
@@ -0,0 +1,59 @@
+#ifndef SCIWORKERD_TASK_H
+#define SCIWORKERD_TASK_H
+
+#include <sys/types.h>
+
+struct job;
+struct pollfd;
+struct task;
+
+enum taskstatus {
+	TASKSTATUS_PENDING,     /* not started yet. */
+	TASKSTATUS_RUNNING,     /* currently running. */
+	TASKSTATUS_EXITED,      /* process exited normally. */
+	TASKSTATUS_KILLED,      /* process killed killed. */
+	TASKSTATUS_SYNCING      /* was unable to send result to host. */
+};
+
+struct taskcode {
+	int exitcode;
+	int sigcode;
+};
+
+struct task *
+task_new(const struct job *job);
+
+int
+task_setup(struct task *self, const char *script);
+
+pid_t
+task_start(struct task *self);
+
+int
+task_wait(struct task *self);
+
+int
+task_kill(struct task *self);
+
+void
+task_prepare(struct task *self, struct pollfd *fd);
+
+int
+task_sync(struct task *self, const struct pollfd *fd);
+
+time_t
+task_uptime(const struct task *self);
+
+const char *
+task_console(const struct task *self);
+
+enum taskstatus
+task_status(const struct task *self);
+
+struct taskcode
+task_code(const struct task *self);
+
+void
+task_free(struct task *self);
+
+#endif /* !SCIWORKERD_TASK_H */
--- a/types.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,222 +0,0 @@
-/*
- * types.c -- type definitions and conversions
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <assert.h>
-#include <errno.h>
-
-#include "types.h"
-
-typedef json_t * (*packer)(const void *);
-typedef int      (*unpacker)(void *, json_t *);
-
-static inline json_t *
-job_packer(const struct job *job)
-{
-	return json_pack("{si si ss}",
-	    "id",               job->id,
-	    "project_id",       job->project_id,
-	    "tag",              job->tag
-	);
-}
-
-static inline int
-job_unpacker(struct job *job, json_t *doc)
-{
-	return json_unpack(doc, "{si si ss}",
-	    "id",               &job->id,
-	    "project_id",       &job->project_id,
-	    "tag",              &job->tag
-	);
-}
-
-static inline json_t *
-jobresult_packer(const struct jobresult *res)
-{
-	return json_pack("{si si si si ss}",
-	    "id",               res->id,
-	    "job_id",           res->job_id,
-	    "worker_id",        res->worker_id,
-	    "exitcode",         res->exitcode,
-	    "log",              res->log
-	);
-}
-
-static inline int
-jobresult_unpacker(struct jobresult *res, json_t *doc)
-{
-	return json_unpack(doc, "{si si si si ss}",
-	    "id",               &res->id,
-	    "job_id",           &res->job_id,
-	    "worker_id",        &res->worker_id,
-	    "exitcode",         &res->exitcode,
-	    "log",              &res->log
-	);
-}
-
-static inline json_t *
-worker_packer(const struct worker *w)
-{
-	return json_pack("{si ss ss}",
-	    "id",               w->id,
-	    "name",             w->name,
-	    "desc",             w->desc
-	);
-}
-
-static inline int
-worker_unpacker(struct worker *w, json_t *doc)
-{
-	return json_unpack(doc, "{si ss ss}",
-	    "id",               &w->id,
-	    "name",             &w->name,
-	    "desc",             &w->desc
-	);
-}
-
-static inline json_t *
-project_packer(struct project *p)
-{
-	return json_pack("{si ss ss ss ss}",
-	    "id",               p->id,
-	    "name",             p->name,
-	    "desc",             p->desc,
-	    "url",              p->url,
-	    "script",           p->script
-	);
-}
-
-static inline int
-project_unpacker(struct project *p, json_t *doc)
-{
-	return json_unpack(doc, "{si ss ss ss ss}",
-	    "id",               &p->id,
-	    "name",             &p->name,
-	    "desc",             &p->desc,
-	    "url",              &p->url,
-	    "script",           &p->script
-	);
-}
-
-static json_t *
-to(const void *array, size_t arraysz, size_t width, packer fn)
-{
-	json_t *doc;
-
-	if (arraysz == 1)
-		doc = fn(array);
-	else {
-		doc = json_array();
-
-		for (size_t i = 0; i < arraysz; ++i)
-			json_array_append(doc, fn((char *)array + (i * width)));
-	}
-
-	return doc;
-}
-
-static ssize_t
-from(void *array, size_t arraysz, size_t width, json_t *doc, unpacker fn)
-{
-	json_t *val;
-	size_t i, tot = 0;
-
-	if (json_is_array(doc)) {
-		json_array_foreach(doc, i, val) {
-			if (tot >= arraysz)
-				return errno = ERANGE, -1;
-			if (fn((char *)array + (tot++ * width), val) < 0)
-				return errno = EILSEQ, -1;
-		}
-	} else if (json_is_object(doc)) {
-		tot = 1;
-
-		if (fn(array, doc) < 0)
-			return errno = EILSEQ, -1;
-	} else
-		return errno = EINVAL, -1;
-
-	return tot;
-}
-
-json_t *
-job_to(const struct job *jobs, size_t jobsz)
-{
-	assert(jobs);
-
-	return to(jobs, jobsz, sizeof (*jobs), (packer)job_packer);
-}
-
-ssize_t
-job_from(struct job *jobs, size_t jobsz, json_t *doc)
-{
-	assert(jobs);
-	assert(doc);
-
-	return from(jobs, jobsz, sizeof (*jobs), doc, (unpacker)job_unpacker);
-}
-
-json_t *
-jobresult_to(const struct jobresult *res, size_t resz)
-{
-	assert(res);
-
-	return to(res, resz, sizeof (*res), (packer)jobresult_packer);
-}
-
-ssize_t
-jobresult_from(struct jobresult *res, size_t resz, json_t *doc)
-{
-	assert(res);
-	assert(doc);
-
-	return from(res, resz, sizeof (*res), doc, (unpacker)jobresult_unpacker);
-}
-
-json_t *
-worker_to(const struct worker *w, size_t wsz)
-{
-	assert(w);
-
-	return to(w, wsz, sizeof (*w), (packer)worker_packer);
-}
-
-ssize_t
-worker_from(struct worker *w, size_t wsz, json_t *doc)
-{
-	assert(w);
-	assert(doc);
-
-	return from(w, wsz, sizeof (*w), doc, (unpacker)worker_unpacker);
-}
-
-json_t *
-project_to(const struct project *proj, size_t projsz)
-{
-	assert(proj);
-
-	return to(proj, projsz, sizeof (*proj), (packer)project_packer);
-}
-
-ssize_t
-project_from(struct project *proj, size_t projsz, json_t *doc)
-{
-	assert(proj);
-	assert(doc);
-
-	return from(proj, projsz, sizeof (*proj), doc, (unpacker)project_unpacker);
-}
--- a/types.h	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-/*
- * types.h -- type definitions and conversions
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_TYPES_H
-#define SCI_TYPES_H
-
-#include <sys/types.h>
-#include <stddef.h>
-
-#include <jansson.h>
-
-struct job {
-	int id;
-	int project_id;
-	const char *tag;
-};
-
-struct jobresult {
-	int id;
-	int job_id;
-	int worker_id;
-	int exitcode;
-	const char *log;
-};
-
-struct worker {
-	int id;
-	const char *name;
-	const char *desc;
-};
-
-struct project {
-	int id;
-	const char *name;
-	const char *desc;
-	const char *url;
-	const char *script;
-};
-
-json_t *
-job_to(const struct job *, size_t);
-
-ssize_t
-job_from(struct job *, size_t, json_t *);
-
-json_t *
-jobresult_to(const struct jobresult *, size_t);
-
-ssize_t
-jobresult_from(struct jobresult *, size_t, json_t *);
-
-json_t *
-worker_to(const struct worker *, size_t);
-
-ssize_t
-worker_from(struct worker *, size_t, json_t *);
-
-json_t *
-project_to(const struct project *, size_t);
-
-ssize_t
-project_from(struct project *, size_t, json_t *);
-
-#endif /* !SCI_TYPES_H */
--- a/util.c	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,187 +0,0 @@
-/*
- * util.c -- miscellaneous utilities
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/stat.h>
-#include <assert.h>
-#include <err.h>
-#include <fcntl.h>
-#include <libgen.h>
-#include <limits.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "util.h"
-
-void *
-util_malloc(size_t size)
-{
-	void *ret;
-
-	if (!(ret = malloc(size)))
-		err(1, "malloc");
-
-	return ret;
-}
-
-void *
-util_calloc(size_t n, size_t size)
-{
-	void *ret;
-
-	if (!(ret = calloc(n, size)))
-		err(1, "calloc");
-
-	return ret;
-}
-
-void *
-util_realloc(void *ptr, size_t size)
-{
-	void *ret;
-
-	if (!(ret = realloc(ptr, size)) && size)
-		err(1, "realloc");
-
-	return ret;
-}
-
-void *
-util_memdup(const void *ptr, size_t size)
-{
-	void *ret;
-
-	if (!(ret = malloc(size)))
-		err(1, "malloc");
-
-	return memcpy(ret, ptr, size);
-}
-
-char *
-util_strdup(const char *src)
-{
-	char *ret;
-
-	if (!(ret = strdup(src)))
-		err(1, "strdup");
-
-	return ret;
-}
-
-char *
-util_strndup(const char *src, size_t n)
-{
-	assert(src);
-
-	char *ret;
-
-	if (!(ret = strndup(src, n)))
-		err(1, "strndup");
-
-	return ret;
-}
-
-char *
-util_basename(const char *str)
-{
-	static char ret[PATH_MAX];
-	char tmp[PATH_MAX];
-
-	strlcpy(tmp, str, sizeof (tmp));
-	strlcpy(ret, basename(tmp), sizeof (ret));
-
-	return ret;
-}
-
-char *
-util_dirname(const char *str)
-{
-	static char ret[PATH_MAX];
-	char tmp[PATH_MAX];
-
-	strlcpy(tmp, str, sizeof (tmp));
-	strlcpy(ret, dirname(tmp), sizeof (ret));
-
-	return ret;
-}
-
-FILE *
-util_fmemopen(void *buf, size_t size, const char *mode)
-{
-	FILE *fp;
-
-	if (!(fp = fmemopen(buf, size, mode)))
-		err(1, "fmemopen");
-
-	return fp;
-}
-
-char *
-util_printf(char *buf, size_t bufsz, const char *fmt, ...)
-{
-	assert(buf);
-	assert(bufsz);
-	assert(fmt);
-
-	va_list ap;
-
-	va_start(ap, fmt);
-	vsnprintf(buf, bufsz, fmt, ap);
-	va_end(ap);
-
-	return buf;
-}
-
-char *
-util_read(const char *path)
-{
-	int fd;
-	struct stat st;
-	char *ret;
-
-	if ((fd = open(path, O_RDONLY)) < 0)
-		return NULL;
-	if (fstat(fd, &st) < 0)
-		return close(fd), NULL;
-
-	ret = util_calloc(1, st.st_size + 1);
-
-	if (read(fd, ret, st.st_size) != st.st_size) {
-		free(ret);
-		ret = NULL;
-	}
-
-	close(fd);
-
-	return ret;
-}
-
-const char *
-util_path(const char *filename)
-{
-	assert(filename);
-
-	/* Build path to the template file. */
-	static char path[PATH_MAX];
-
-	//snprintf(path, sizeof (path), "%s/%s", config.themedir, filename);
-
-	return path;
-}
--- a/util.h	Wed Oct 06 16:22:23 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/*
- * util.h -- miscellaneous utilities
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_UTIL_H
-#define SCI_UTIL_H
-
-#include <stddef.h>
-#include <stdio.h>
-
-#define UTIL_SIZE(x) (sizeof (x) / sizeof (x[0]))
-
-void *
-util_malloc(size_t);
-
-void *
-util_calloc(size_t, size_t);
-
-void *
-util_realloc(void *, size_t);
-
-void *
-util_reallocarray(void *, size_t, size_t);
-
-void *
-util_memdup(const void *, size_t);
-
-char *
-util_strdup(const char *);
-
-char *
-util_strndup(const char *, size_t);
-
-char *
-util_basename(const char *);
-
-char *
-util_dirname(const char *);
-
-FILE *
-util_fmemopen(void *, size_t, const char *);
-
-char *
-util_printf(char *, size_t, const char *, ...);
-
-char *
-util_read(const char *);
-
-const char *
-util_path(const char *);
-
-#endif /* !SCI_UTIL_H */