changeset 3:215c0c3b3609

misc: use JSON everywhere (scictl/sciwebd)
author David Demelier <markand@malikania.fr>
date Mon, 14 Jun 2021 22:08:24 +0200
parents 5fa3d2f479b2
children 9c4fea43803c
files Makefile base64.c base64.h config.def.h db.c db.h http.c job.c job.h page-api-jobs.c page-api-projects.c page-api-projects.h page-api-script.c page-api-script.h page-api-workers.c page-api-workers.h project.h req.c req.h scictl.c scid.c sciworkerd.c sql/init.sql sql/job-add.sql sql/job-queue.sql sql/job-result-todo.sql sql/job-save.sql sql/job-todo.sql sql/jobresult-add.sql sql/project-add.sql sql/project-find-id.sql sql/project-get.sql sql/project-insert.sql sql/project-list.sql sql/worker-add.sql sql/worker-find-id.sql sql/worker-get.sql sql/worker-insert.sql sql/worker-list.sql types.c types.h util.c util.h worker.h
diffstat 44 files changed, 1640 insertions(+), 1489 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Thu Jun 10 10:39:21 2021 +0200
+++ b/Makefile	Mon Jun 14 22:08:24 2021 +0200
@@ -20,42 +20,44 @@
 
 include config.mk
 
-SCID_SRCS=              base64.c                        \
-                        db.c                            \
-                        job.c                           \
+SCID_SRCS=              db.c                            \
                         util.c                          \
                         scid.c                          \
+                        types.c                         \
                         log.c                           \
                         extern/libsqlite/sqlite3.c
 SCID_DATA=              sql/init.h                      \
-                        sql/job-queue.h                 \
-                        sql/job-result-todo.h           \
-                        sql/job-save.h                  \
+                        sql/job-add.h                   \
+                        sql/job-todo.h                  \
+                        sql/jobresult-add.h             \
+                        sql/project-add.h               \
                         sql/project-find.h              \
-                        sql/project-get.h               \
-                        sql/project-insert.h            \
-                        sql/worker-get.h                \
+                        sql/project-find-id.h           \
+                        sql/project-list.h              \
+                        sql/worker-add.h                \
                         sql/worker-find.h               \
-                        sql/worker-insert.h
+                        sql/worker-find-id.h            \
+                        sql/worker-list.h
 SCID_OBJS=              ${SCID_SRCS:.c=.o}
 SCID_DEPS=              ${SCID_SRCS:.c=.d}
 
-SCIWORKERD_SRCS=        base64.c sciworkerd.c util.c log.c
+SCIWORKERD_SRCS=        sciworkerd.c types.c util.c log.c
 SCIWORKERD_OBJS=        ${SCIWORKERD_SRCS:.c=.o}
 SCIWORKERD_DEPS=        ${SCIWORKERD_SRCS:.c=.d}
 
-SCICTL_SRCS=            base64.c req.c scictl.c util.c
+SCICTL_SRCS=            req.c scictl.c types.c util.c
 SCICTL_OBJS=            ${SCICTL_SRCS:.c=.o}
 SCICTL_DEPS=            ${SCICTL_SRCS:.c=.d}
 
-SCIWEBD_SRCS=           base64.c                        \
-                        http.c                          \
+SCIWEBD_SRCS=           http.c                          \
                         log.c                           \
                         page-api-jobs.c                 \
-                        page-api-script.c               \
+                        page-api-projects.c             \
+                        page-api-workers.c              \
                         page.c                          \
                         req.c                           \
                         sciwebd.c                       \
+                        types.c                         \
                         util.c
 SCIWEBD_OBJS=           ${SCIWEBD_SRCS:.c=.o}
 SCIWEBD_DEPS=           ${SCIWEBD_SRCS:.c=.d}
@@ -77,9 +79,6 @@
 KCGI_INCS=              `pkg-config --cflags kcgi`
 KCGI_LIBS=              `pkg-config --libs kcgi`
 
-ZSTD_INCS=              `pkg-config --cflags libzstd`
-ZSTD_LIBS=              `pkg-config --libs libzstd`
-
 INCS=                   -Iextern/libsqlite
 DEFS=                   -DVARDIR=\"${VARDIR}\" \
                         -DTMPDIR=\"${TMPDIR}\"
@@ -87,10 +86,10 @@
 .SUFFIXES:
 .SUFFIXES: .c .o .sql .h
 
-all: scid scictl sciworkerd sciwebd
+all: scid scictl sciwebd sciworkerd
 
 .c.o:
-	${CC} ${INCS} ${DEFS} ${LIBBSD_INCS} ${KCGI_INCS} ${JANSSON_INCS} ${ZSTD_INCS} ${CFLAGS} -MMD -c $< -o $@
+	${CC} ${INCS} ${DEFS} ${LIBBSD_INCS} ${KCGI_INCS} ${JANSSON_INCS} ${CFLAGS} -MMD -c $< -o $@
 
 .sql.h:
 	./bcc -sc0 $< $< > $@
@@ -108,25 +107,25 @@
 ${SCID_OBJS}: config.h ${SCID_DATA}
 
 scid: ${SCID_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCID_OBJS} ${LIBBSD_LIBS} ${ZSTD_LIBS} ${LDFLAGS}
+	${CC} ${CFLAGS} -o $@ ${SCID_OBJS} ${LIBBSD_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
 
 ${SCIWORKERD_OBJS}: config.h
 
 sciworkerd: ${SCIWORKERD_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCIWORKERD_OBJS} ${LIBBSD_LIBS} ${LIBCURL_LIBS} ${JANSSON_LIBS} ${ZSTD_LIBS} ${LDFLAGS}
+	${CC} ${CFLAGS} -o $@ ${SCIWORKERD_OBJS} ${LIBBSD_LIBS} ${LIBCURL_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
 
 ${SCICTL_OBJS}: config.h
 
 scictl: ${SCICTL_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCICTL_OBJS} ${LIBBSD_LIBS} ${ZSTD_LIBS} ${LDFLAGS}
+	${CC} ${CFLAGS} -o $@ ${SCICTL_OBJS} ${LIBBSD_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
 
 ${SCIWEBD_OBJS}: config.h
 
 sciwebd: ${SCIWEBD_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCIWEBD_OBJS} ${LIBBSD_LIBS} ${KCGI_LIBS} ${JANSSON_LIBS} ${ZSTD_LIBS} ${LDFLAGS}
+	${CC} ${CFLAGS} -o $@ ${SCIWEBD_OBJS} ${LIBBSD_LIBS} ${KCGI_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
 
 clean:
-	rm -f bcc config.h
+	rm -f bcc config.h tags cscope.out
 	rm -f scid ${SCID_OBJS} ${SCID_DEPS} ${SCID_DATA}
 	rm -f scictl ${SCICTL_OBJS} ${SCICTL_DEPS}
 	rm -f sciworkerd ${SCIWORKERD_OBJS} ${SCIWORKERD_DEPS}
--- a/base64.c	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,186 +0,0 @@
-/*
- * base64.h -- base64 encoding and decoding
- *
- * Copyright (c) 2013-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 <ctype.h>
-#include <errno.h>
-#include <string.h>
-
-#include "base64.h"
-
-int
-b64_isbase64(unsigned char ch)
-{
-	return isalnum(ch) || ch == '+' || ch == '-' || ch == '_' || ch == '/';
-}
-
-int
-b64_isvalid(unsigned char ch)
-{
-	return b64_isbase64(ch) || ch == '=';
-}
-
-unsigned char
-b64_lookup(unsigned char value)
-{
-	assert(value < 64);
-
-	static const char *table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
-
-	return table[value];
-}
-
-unsigned char
-b64_rlookup(unsigned char ch)
-{
-	assert(b64_isbase64(ch));
-
-	if (ch >= '0' && ch <= '9')
-		return ch + 4;
-	if (ch >= 'A' && ch <= 'Z')
-		return ch - 65;
-	if (ch >= 'a' && ch <= 'z')
-		return ch - 71;
-
-	/* '-' is base64url support. */
-	return ch == '+' || ch == '-' ? 62U : 63U;
-}
-
-size_t
-b64_encode(const char *src, size_t srcsz, char *dst, size_t dstsz)
-{
-	assert(src);
-	assert(dst);
-
-	size_t nwritten = 0;
-
-	if (srcsz == (size_t)-1)
-		srcsz = strlen(src);
-
-	while (srcsz && dstsz) {
-		char inputbuf[3] = {0};
-		int count = 0;
-
-		while (srcsz && count < 3) {
-			inputbuf[count++] = *src++;
-			--srcsz;
-		}
-
-		if (dstsz < 4) {
-			errno = ERANGE;
-			return -1;
-		}
-
-		*dst++ = b64_lookup(inputbuf[0] >> 2 & 0x3f);
-		*dst++ = b64_lookup((inputbuf[0] << 4 & 0x3f) | (inputbuf[1] >> 4 & 0x0f));
-
-		if (count < 2)
-			*dst++ = '=';
-		else
-			*dst++ = b64_lookup((inputbuf[1] << 2 & 0x3c) | (inputbuf[2] >> 6 & 0x03));
-
-		if (count < 3)
-			*dst++ = '=';
-		else
-			*dst++ = b64_lookup(inputbuf[2] & 0x3f);
-
-		nwritten += 4;
-		dstsz -= 4;
-	}
-
-	/* Not enough room to store '\0'. */
-	if (dstsz == 0) {
-		errno = ERANGE;
-		return -1;
-	}
-
-	*dst = '\0';
-
-	return nwritten;
-}
-
-size_t
-b64_decode(const char *src, size_t srcsz, char *dst, size_t dstsz)
-{
-	assert(src);
-	assert(dst);
-
-	size_t nwritten = 0;
-
-	if (srcsz == (size_t)-1)
-		srcsz = strlen(src);
-
-	while (srcsz && dstsz) {
-		int i = 0, r = 3;
-		unsigned int inputbuf[4] = {0};
-
-		for (; srcsz && i < 4; i++) {
-			if (*src == '=') {
-				/*
-				 * '=' is only allowed in last 2 characters,
-				 * otherwise it means we need less data.
-				 */
-				if (i <= 1)
-					goto eilseq;
-
-				/* Less data required. */
-				--r;
-			} else if (!b64_isvalid(*src))
-				goto eilseq;
-
-			if (b64_isbase64(*src))
-				inputbuf[i] = b64_rlookup(*src);
-
-			++src;
-			--srcsz;
-		}
-
-		/* Make sure we haven't seen AB=Z as well. */
-		if (i != 4 || (src[-2] == '=' && src[-1] != '='))
-			goto eilseq;
-		if ((size_t)r >= dstsz)
-			goto erange;
-
-		*dst++ = ((inputbuf[0] << 2) & 0xfc) |
-		         ((inputbuf[1] >> 4) & 0x03);
-
-		if (r >= 2)
-			*dst++ = ((inputbuf[1] << 4) & 0xf0) | ((inputbuf[2] >> 2) & 0x0f);
-		if (r >= 3)
-			*dst++ = ((inputbuf[2] << 6) & 0xc0) | (inputbuf[3] & 0x3f);
-
-		nwritten += r;
-		dstsz -= r;
-	}
-
-	/* Not enough room to store '\0'. */
-	if (dstsz == 0)
-		goto erange;
-
-	*dst = '\0';
-
-	return nwritten;
-
-eilseq:
-	errno = EILSEQ;
-	return -1;
-
-erange:
-	errno = ERANGE;
-	return -1;
-}
--- a/base64.h	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/*
- * base64.h -- base64 encoding and decoding
- *
- * Copyright (c) 2013-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 BASE64_H
-#define BASE64_H
-
-#include <stddef.h>
-
-#if defined(__cplusplus)
-extern "C" {
-#endif
-
-#define B64_ENCODE_LENGTH(x) (4 * ((x) / 3 + 1))
-#define B64_DECODE_LENGTH(x) (3 * ((x) / 4))
-
-int
-b64_isbase64(unsigned char ch);
-
-int
-b64_isvalid(unsigned char ch);
-
-unsigned char
-b64_lookup(unsigned char value);
-
-unsigned char
-b64_rlookup(unsigned char ch);
-
-size_t
-b64_encode(const char *src, size_t srcsz, char *dst, size_t dstsz);
-
-size_t
-b64_decode(const char *src, size_t srcsz, char *dst, size_t dstsz);
-
-#if defined(__cplusplus)
-}
-#endif
-
-#endif /* !BASE64_H */
--- a/config.def.h	Thu Jun 10 10:39:21 2021 +0200
+++ b/config.def.h	Mon Jun 14 22:08:24 2021 +0200
@@ -20,14 +20,14 @@
 #define SCI_CONFIG_H
 
 /* I/O limits */
-#define SCI_CONSOLE_MAX 1048576                         /* Build log max */
+#define SCI_CONSOLE_MAX 4194304                         /* Build log max (4MB) */
 #define SCI_MSG_MAX     (SCI_CONSOLE_MAX + 1024)        /* Network message max. */
 
 /* Database limits. */
-#define SCI_PROJECT_MAX   64        /* Projects allowed in database. */
-#define SCI_WORKER_MAX    32        /* Workers allowed in database. */
+#define SCI_PROJECT_MAX   64                            /* Projects allowed in database. */
+#define SCI_WORKER_MAX    32                            /* Workers allowed in database. */
 
 /* Usage limits. */
-#define SCI_JOB_LIST_MAX 128        /* Jobs max list size. */
+#define SCI_JOB_LIST_MAX 128                            /* Jobs max list size. */
 
 #endif /* !SCI_CONFIG_H */
--- a/db.c	Thu Jun 10 10:39:21 2021 +0200
+++ b/db.c	Mon Jun 14 22:08:24 2021 +0200
@@ -1,3 +1,4 @@
+#include <sys/queue.h>
 #include <assert.h>
 #include <stdlib.h>
 #include <string.h>
@@ -5,34 +6,117 @@
 #include <sqlite3.h>
 
 #include "db.h"
-#include "job.h"
 #include "log.h"
-#include "project.h"
-#include "worker.h"
+#include "types.h"
+#include "util.h"
 
 #include "sql/init.h"
-#include "sql/job-queue.h"
-#include "sql/job-result-todo.h"
-#include "sql/job-save.h"
-#include "sql/project-insert.h"
-#include "sql/project-get.h"
+#include "sql/job-add.h"
+#include "sql/job-todo.h"
+#include "sql/jobresult-add.h"
+#include "sql/project-add.h"
 #include "sql/project-find.h"
-#include "sql/worker-get.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-insert.h"
+#include "sql/worker-find-id.h"
+#include "sql/worker-list.h"
 
 #define CHAR(v) (const char *)(v)
 
 static sqlite3 *db;
 
-static inline void
-convert_project(struct project *project, sqlite3_stmt *stmt)
+struct str {
+	char *str;
+	SLIST_ENTRY(str) link;
+};
+
+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)
 {
-	project->id = sqlite3_column_int64(stmt, 0);
-	strlcpy(project->name, CHAR(sqlite3_column_text(stmt, 1)), sizeof (project->name));
-	strlcpy(project->desc, CHAR(sqlite3_column_text(stmt, 2)), sizeof (project->desc));
-	strlcpy(project->url, CHAR(sqlite3_column_text(stmt, 3)), sizeof (project->url));
-	strlcpy(project->script, CHAR(sqlite3_column_text(stmt, 4)), sizeof (project->script));
+	struct str *s, *tmp;
+
+	SLIST_FOREACH_SAFE(s, l, link, tmp) {
+		free(s->str);
+		free(s);
+	}
+
+	SLIST_INIT(l);
+}
+
+static inline void
+convert_project(struct db_ctx *ctx, struct project *project, sqlite3_stmt *stmt)
+{
+	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 int
+insert(const char *sql, const char *fmt, ...)
+{
+	assert(sql);
+	assert(fmt);
+
+	sqlite3_stmt *stmt = NULL;
+	va_list ap;
+
+	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
+
+	va_start(ap, fmt);
+
+	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;
+		default:
+			break;
+		}
+	}
+
+	va_end(ap);
+
+	if (sqlite3_step(stmt) != SQLITE_DONE) {
+		log_warn("db: %s", sqlite3_errmsg(db));
+		sqlite3_finalize(stmt);
+		return -1;
+	}
+
+	return sqlite3_last_insert_rowid(db);
 }
 
 int
@@ -64,7 +148,7 @@
 	sqlite3_stmt *stmt = NULL;
 	int ret = -1;
 
-	if (sqlite3_prepare(db, CHAR(sql_project_insert), -1, &stmt, NULL) != SQLITE_OK)
+	if (sqlite3_prepare(db, CHAR(sql_project_add), -1, &stmt, NULL) != SQLITE_OK)
 		goto sqlite3_err;
 
 	sqlite3_bind_text(stmt, 1, pj->name, -1, SQLITE_STATIC);
@@ -86,7 +170,7 @@
 }
 
 ssize_t
-db_project_get(struct project *projects, size_t projectsz)
+db_project_list(struct db_ctx *ctx, struct project *projects, size_t projectsz)
 {
 	assert(projects);
 
@@ -94,15 +178,16 @@
 	struct project *p = projects;
 	ssize_t ret = 0;
 
-	if (sqlite3_prepare(db, CHAR(sql_project_get), -1, &stmt, NULL) != SQLITE_OK) {
+	if (sqlite3_prepare(db, CHAR(sql_project_list), -1, &stmt, NULL) != SQLITE_OK) {
 		log_warn("db: %s", sqlite3_errmsg(db));
 		return -1;
 	}
 
-	sqlite3_bind_int64(stmt, 1, projectsz);
+	sqlite3_bind_int(stmt, 1, projectsz);
+	ctx->handle = strlist_new();
 
 	for (; sqlite3_step(stmt) == SQLITE_ROW && (size_t)ret < projectsz; ++ret, ++p)
-		convert_project(p, stmt);
+		convert_project(ctx, p, stmt);
 
 	if (stmt)
 		sqlite3_finalize(stmt);
@@ -111,13 +196,16 @@
 }
 
 int
-db_project_find(struct project *project)
+db_project_find(struct db_ctx *ctx, struct project *project)
 {
+	assert(ctx);
 	assert(project);
 
 	sqlite3_stmt *stmt = NULL;
 	int ret = -1;
 
+	ctx->handle = NULL;
+
 	if (sqlite3_prepare(db, CHAR(sql_project_find), -1, &stmt, NULL) != SQLITE_OK)
 		goto sqlite3_err;
 
@@ -127,11 +215,52 @@
 		goto sqlite3_err;
 
 	ret = 0;
-	convert_project(project, stmt);
+	ctx->handle = strlist_new();
+	convert_project(ctx, project, stmt);
 
 sqlite3_err:
-	if (ret < 0)
+	if (ret < 0) {
+		if (ctx->handle)
+			db_ctx_finish(ctx);
+
 		log_warn("db: %s", sqlite3_errmsg(db));
+	}
+	if (stmt)
+		sqlite3_finalize(stmt);
+
+	return ret;
+}
+
+int
+db_project_find_id(struct db_ctx *ctx, struct project *project)
+{
+	assert(ctx);
+	assert(project);
+
+	sqlite3_stmt *stmt = NULL;
+	int ret = -1;
+
+	ctx->handle = NULL;
+
+	if (sqlite3_prepare(db, CHAR(sql_project_find_id), -1, &stmt, NULL) != SQLITE_OK)
+		goto sqlite3_err;
+
+	sqlite3_bind_int(stmt, 1, project->id);
+
+	if (sqlite3_step(stmt) != SQLITE_ROW)
+		goto sqlite3_err;
+
+	ret = 0;
+	ctx->handle = strlist_new();
+	convert_project(ctx, project, stmt);
+
+sqlite3_err:
+	if (ret < 0) {
+		if (ctx->handle)
+			db_ctx_finish(ctx);
+
+		log_warn("db: %s", sqlite3_errmsg(db));
+	}
 	if (stmt)
 		sqlite3_finalize(stmt);
 
@@ -146,7 +275,7 @@
 	sqlite3_stmt *stmt = NULL;
 	int ret = -1;
 
-	if (sqlite3_prepare(db, CHAR(sql_worker_insert), -1, &stmt, NULL) != SQLITE_OK)
+	if (sqlite3_prepare(db, CHAR(sql_worker_add), -1, &stmt, NULL) != SQLITE_OK)
 		goto sqlite3_err;
 
 	sqlite3_bind_text(stmt, 1, wk->name, -1, SQLITE_STATIC);
@@ -166,28 +295,36 @@
 }
 
 ssize_t
-db_worker_get(struct worker *wk, size_t wksz)
+db_worker_list(struct db_ctx *ctx, struct worker *wk, size_t wksz)
 {
+	assert(ctx);
 	assert(wk);
 
 	sqlite3_stmt *stmt = NULL;
 	struct worker *w = wk;
 	ssize_t ret = -1;
 
-	if (sqlite3_prepare(db, CHAR(sql_worker_get), -1, &stmt, NULL) != SQLITE_OK)
+	ctx->handle = NULL;
+
+	if (sqlite3_prepare(db, CHAR(sql_worker_list), -1, &stmt, NULL) != SQLITE_OK)
 		goto sqlite3_err;
 
-	sqlite3_bind_int64(stmt, 1, wksz);
+	sqlite3_bind_int(stmt, 1, wksz);
+	ctx->handle = strlist_new();
 
 	for (ret = 0; sqlite3_step(stmt) == SQLITE_ROW && (size_t)ret < wksz; ++ret, ++w) {
-		w->id = sqlite3_column_int64(stmt, 0);
-		strlcpy(w->name, CHAR(sqlite3_column_text(stmt, 1)), sizeof (w->name));
-		strlcpy(w->desc, CHAR(sqlite3_column_text(stmt, 2)), sizeof (w->desc));
+		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)));
 	}
 
 sqlite3_err:
-	if (ret < 0)
+	if (ret < 0) {
+		if (ctx->handle)
+			db_ctx_finish(ctx);
+
 		log_warn("db: %s", sqlite3_errmsg(db));
+	}
 	if (stmt)
 		sqlite3_finalize(stmt);
 
@@ -195,13 +332,16 @@
 }
 
 int
-db_worker_find(struct worker *w)
+db_worker_find(struct db_ctx *ctx, struct worker *w)
 {
+	assert(ctx);
 	assert(w);
 
 	sqlite3_stmt *stmt = NULL;
 	int ret = -1;
 
+	ctx->handle = NULL;
+
 	if (sqlite3_prepare(db, CHAR(sql_worker_find), -1, &stmt, NULL) != SQLITE_OK)
 		goto sqlite3_err;
 
@@ -211,13 +351,18 @@
 		goto sqlite3_err;
 
 	ret = 0;
-	w->id = sqlite3_column_int64(stmt, 0);
-	strlcpy(w->name, CHAR(sqlite3_column_text(stmt, 1)), sizeof (w->name));
-	strlcpy(w->desc, CHAR(sqlite3_column_text(stmt, 2)), sizeof (w->desc));
+	ctx->handle = strlist_new();
+	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)));
 
 sqlite3_err:
-	if (ret < 0)
+	if (ret < 0) {
+		if (ctx->handle)
+			db_ctx_finish(ctx);
+
 		log_warn("db: %s", sqlite3_errmsg(db));
+	}
 	if (stmt)
 		sqlite3_finalize(stmt);
 
@@ -225,61 +370,37 @@
 }
 
 int
-db_job_queue(struct job *job)
+db_worker_find_id(struct db_ctx *ctx, struct worker *w)
 {
-	assert(job);
+	assert(ctx);
+	assert(w);
 
 	sqlite3_stmt *stmt = NULL;
 	int ret = -1;
 
-	if (sqlite3_prepare(db, CHAR(sql_job_queue), -1, &stmt, NULL) != SQLITE_OK)
+	ctx->handle = NULL;
+
+	if (sqlite3_prepare(db, CHAR(sql_worker_find_id), -1, &stmt, NULL) != SQLITE_OK)
 		goto sqlite3_err;
 
-	sqlite3_bind_text(stmt, 1, job->tag, -1, SQLITE_STATIC);
-	sqlite3_bind_int64(stmt, 2, job->project.id);
+	sqlite3_bind_int(stmt, 1, w->id);
 
-	if (sqlite3_step(stmt) != SQLITE_DONE)
+	if (sqlite3_step(stmt) != SQLITE_ROW)
 		goto sqlite3_err;
 
-	job->id = sqlite3_last_insert_rowid(db);
 	ret = 0;
+	ctx->handle = strlist_new();
+	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)));
 
 sqlite3_err:
-	if (ret < 0)
-		log_warn("db: %s", sqlite3_errmsg(db));
-	if (stmt)
-		sqlite3_finalize(stmt);
-
-	return ret;
-}
+	if (ret < 0) {
+		if (ctx->handle)
+			db_ctx_finish(ctx);
 
-ssize_t
-db_job_result_todo(struct job_result *re, size_t resz, int64_t worker_id)
-{
-	assert(re);
-
-	sqlite3_stmt *stmt = NULL;
-	ssize_t ret = 0;
-
-	if (sqlite3_prepare(db, CHAR(sql_job_result_todo), -1, &stmt, NULL) != SQLITE_OK) {
 		log_warn("db: %s", sqlite3_errmsg(db));
-		return -1;
 	}
-
-	sqlite3_bind_int64(stmt, 1, worker_id);
-	sqlite3_bind_int64(stmt, 2, resz);
-
-	while (sqlite3_step(stmt) == SQLITE_ROW && (size_t)ret++ < resz) {
-		memset(re, 0, sizeof (*re));
-		re->job.id = sqlite3_column_int64(stmt, 0);
-		strlcpy(re->job.tag, CHAR(sqlite3_column_text(stmt, 1)),
-		    sizeof (re->job.tag));
-		strlcpy(re->job.project.name, CHAR(sqlite3_column_text(stmt, 2)),
-		    sizeof (re->job.project.name));
-
-		++re;
-	};
-
 	if (stmt)
 		sqlite3_finalize(stmt);
 
@@ -287,35 +408,53 @@
 }
 
 int
-db_job_save(struct job_result *r)
+db_job_add(struct job *job)
+{
+	assert(job);
+
+	job->id = insert(CHAR(sql_job_add), "si", job->tag, job->project_id);
+
+	return job->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);
+
+	sqlite3_stmt *stmt = NULL;
+	ssize_t ret = 0;
+
+	if (sqlite3_prepare(db, CHAR(sql_job_todo), -1, &stmt, NULL) != SQLITE_OK) {
+		log_warn("db: %s", sqlite3_errmsg(db));
+		return -1;
+	}
+
+	sqlite3_bind_int(stmt, 1, worker_id);
+	sqlite3_bind_int(stmt, 2, jobsz);
+	ctx->handle = strlist_new();
+
+	while (sqlite3_step(stmt) == SQLITE_ROW && (size_t)ret++ < jobsz) {
+		jobs->id = sqlite3_column_int(stmt, 0);
+		jobs->tag = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 1)));
+		jobs++->project_id = sqlite3_column_int(stmt, 2);
+	};
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+}
+
+int
+db_jobresult_add(struct jobresult *r)
 {
 	assert(r);
 
-	sqlite3_stmt *stmt = NULL;
-	int ret = -1;
-
-	if (sqlite3_prepare(db, CHAR(sql_job_save), -1, &stmt, NULL) != SQLITE_OK)
-		goto sqlite3_err;
-
-	sqlite3_bind_int64(stmt, 1, r->job.id);
-	sqlite3_bind_int64(stmt, 2, r->worker.id);
-	sqlite3_bind_int(stmt, 3, r->status);
-	sqlite3_bind_int(stmt, 4, r->retcode);
-	sqlite3_bind_text(stmt, 5, r->console, -1, SQLITE_STATIC);
+	r->id = insert(CHAR(sql_jobresult_add), "iiis", r->job_id,
+	    r->worker_id, r->exitcode, r->log);
 
-	if (sqlite3_step(stmt) != SQLITE_DONE)
-		goto sqlite3_err;
-
-	ret = 0;
-	r->id = sqlite3_last_insert_rowid(db);
-
-sqlite3_err:
-	if (ret < 0)
-		log_warn("db: %s", sqlite3_errmsg(db));
-	if (stmt)
-		sqlite3_finalize(stmt);
-
-	return ret;
+	return r->id < 0 ? -1 : 0;
 }
 
 void
@@ -326,3 +465,12 @@
 		db = NULL;
 	}
 }
+
+void
+db_ctx_finish(struct db_ctx *ctx)
+{
+	if (ctx->handle) {
+		strlist_free(ctx->handle);
+		ctx->handle = NULL;
+	}
+}
--- a/db.h	Thu Jun 10 10:39:21 2021 +0200
+++ b/db.h	Mon Jun 14 22:08:24 2021 +0200
@@ -3,44 +3,56 @@
 
 #include <sys/types.h>
 #include <stddef.h>
-#include <stdint.h>
 
 struct project;
 struct worker;
 struct job;
-struct job_result;
+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 *);
 
 ssize_t
-db_project_get(struct project *, size_t);
+db_project_list(struct db_ctx *, struct project *, size_t);
 
 int
-db_project_find(struct project *);
+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_get(struct worker *, size_t);
-
-int
-db_worker_find(struct worker *);
+db_worker_list(struct db_ctx *, struct worker *, size_t);
 
 int
-db_job_queue(struct job *);
-
-ssize_t
-db_job_result_todo(struct job_result *, size_t, int64_t);
+db_worker_find(struct db_ctx *, struct worker *);
 
 int
-db_job_save(struct job_result *);
+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/http.c	Thu Jun 10 10:39:21 2021 +0200
+++ b/http.c	Mon Jun 14 22:08:24 2021 +0200
@@ -11,7 +11,8 @@
 #include "log.h"
 #include "page.h"
 #include "page-api-jobs.h"
-#include "page-api-script.h"
+#include "page-api-projects.h"
+#include "page-api-workers.h"
 #include "req.h"
 
 enum page {
@@ -26,27 +27,17 @@
 		const char *prefix;
 		void (*handler)(struct kreq *);
 	} apis[] = {
-		{ "v1/jobs",    page_api_v1_jobs        },
-		{ "v1/script",  page_api_v1_script      },
-		{ NULL,         NULL                    }
+		{ "v1/jobs",            page_api_v1_jobs        },
+		{ "v1/projects",        page_api_v1_projects    },
+		{ "v1/workers",         page_api_v1_workers     },
+		{ NULL,                 NULL                    }
 	};
 
-	if (req_connect(VARDIR "/run/sci.sock") < 0) {
-		page(req, NULL, KHTTP_500, KMIME_TEXT_HTML, "pages/500.html");
-		return;
-	}
-
-	for (size_t i = 0; apis[i].prefix; ++i) {
-		if (strncmp(req->path, apis[i].prefix, strlen(apis[i].prefix)) == 0) {
-			apis[i].handler(req);
-			goto finish;
-		}
-	}
+	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");
-
-finish:
-	req_finish();
 }
 
 static const char *pages[] = {
--- a/job.c	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-/*
- * job.c -- job description and result
- *
- * 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 <stdlib.h>
-#include <string.h>
-
-#include "job.h"
-
-void
-job_result_finish(struct job_result *res)
-{
-	assert(res);
-
-	free(res->console);
-	memset(res, 0, sizeof (*res));
-}
--- a/job.h	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/*
- * job.h -- job description and result
- *
- * 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_JOB_H
-#define SCI_JOB_H
-
-#include <stdint.h>
-
-#define JOB_TAG_MAX 128
-
-#include "project.h"
-#include "worker.h"
-
-enum job_status {
-	JOB_STATUS_TODO,
-	JOB_STATUS_SUCCESS,
-	JOB_STATUS_FAIL
-};
-
-struct job {
-	int64_t id;
-	struct project project;
-	char tag[JOB_TAG_MAX];
-};
-
-struct job_result {
-	int64_t id;
-	struct job job;
-	struct worker worker;
-	enum job_status status;
-	int retcode;
-	char *console;
-};
-
-void
-job_result_finish(struct job_result *);
-
-#endif /* !SCI_JOB_H */
--- a/page-api-jobs.c	Thu Jun 10 10:39:21 2021 +0200
+++ b/page-api-jobs.c	Mon Jun 14 22:08:24 2021 +0200
@@ -8,97 +8,78 @@
 #include <jansson.h>
 
 #include "config.h"
-#include "job.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_result *jobs, size_t jobsz)
+list(struct kreq *r, const struct job *jobs, size_t jobsz)
 {
-	json_t *array, *obj;
+	json_t *doc;
+	char *dump;
 
-	array = json_array();
+	doc = job_to(jobs, jobsz);
+	dump = json_dumps(doc, JSON_COMPACT);
+
+	khttp_puts(r, dump);
+	free(dump);
+	json_decref(doc);
+}
 
-	for (size_t i = 0; i < jobsz; ++i) {
-		obj = json_object();
-		json_object_set(obj, "id", json_integer(jobs[i].job.id));
-		json_object_set(obj, "tag", json_string(jobs[i].job.tag));
-		json_object_set(obj, "project", json_string(jobs[i].job.project.name));
-		json_array_append(array, obj);
-	}
+static int
+save(const char *json)
+{
+	struct req req = {0};
+	struct jobresult res = {0};
+	int ret = -1;
+
+	json_t *doc;
+	json_error_t err;
 
-	khttp_puts(r, json_dumps(array, JSON_COMPACT));
+	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_result jobs[SCI_JOB_LIST_MAX];
+	struct job jobs[SCI_JOB_LIST_MAX];
 	size_t jobsz = UTIL_SIZE(jobs);
 	const char *worker = util_basename(r->path);
 
-	if ((req = req_job_list(jobs, &jobsz, worker)).status)
+	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 int
-parse(struct job_result *res, const char *json)
-{
-	json_t *doc, *code, *id, *retcode;
-	json_error_t err;
-
-	if (!(doc = json_loads(json, 0, &err)))
-		return log_warn("api/post: invalid JSON input: %s", err.text), -1;
-	if (!json_is_object(doc) ||
-	    !json_is_string((code = json_object_get(doc, "code"))) ||
-	    !json_is_integer((id = json_object_get(doc, "id"))) ||
-	    !json_is_integer((retcode = json_object_get(doc, "retcode")))) {
-		log_warn("api/post: invalid JSON input");
-		json_decref(doc);
-		return -1;
-	}
-
-	res->job.id = json_integer_value(id);
-	res->retcode = json_integer_value(retcode);
-	res->console = util_strdup(json_string_value(code));
-	json_decref(doc);
-
-	return 0;
-}
-
-static int
-save(struct job_result *res)
-{
-	struct req req;
-
-	if ((req = req_job_save(res)).status) {
-		log_warn("api/post: save error: %s", strerror(req.status));
-		return -1;
-	}
-
-	return 0;
-}
-
 static void
 post(struct kreq *r)
 {
-	struct job_result res = {0};
-	const char *worker = util_basename(r->path);
-
-	strlcpy(res.worker.name, worker, sizeof (res.worker.name));
-	log_info("data=%s", r->fields[0].key);
-
-	if (r->fieldsz < 1 || parse(&res, r->fields[0].key) || save(&res) < 0)
+	if (r->fieldsz < 1)
+		page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL);
+	else if (save(r->fields[0].key) < 0)
 		page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL);
 	else {
 		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
@@ -106,8 +87,6 @@
 		khttp_body(r);
 		khttp_free(r);
 	}
-
-	free(res.console);
 }
 
 void
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/page-api-projects.c	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,118 @@
+#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;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/page-api-projects.h	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,9 @@
+#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-script.c	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-#include <sys/types.h>
-#include <assert.h>
-#include <stdarg.h>
-#include <stdint.h>
-
-#include <kcgi.h>
-#include <jansson.h>
-
-#include "config.h"
-#include "page.h"
-#include "req.h"
-#include "util.h"
-
-static void
-content(struct kreq *r, const char *code)
-{
-	json_t *doc;
-
-	doc = json_object();
-	json_object_set(doc, "code", json_string(code));
-	khttp_puts(r, json_dumps(doc, JSON_COMPACT));
-	json_decref(doc);
-}
-
-static void
-get(struct kreq *r)
-{
-	struct req req;
-	char script[SCI_MSG_MAX];
-	const char *project = util_basename(r->path);
-
-	if ((req = req_script_get(project, script, sizeof (script))).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);
-		content(r, script);
-		khttp_free(r);
-	}
-}
-
-void
-page_api_v1_script(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-script.h	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-#ifndef SCI_PAGE_API_SCRIPT_H
-#define SCI_PAGE_API_SCRIPT_H
-
-struct kreq;
-
-void
-page_api_v1_script(struct kreq *);
-
-#endif /* !SCI_PAGE_API_SCRIPT_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/page-api-workers.c	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,113 @@
+#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;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/page-api-workers.h	Mon Jun 14 22:08:24 2021 +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 */
--- a/project.h	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-#ifndef SCI_PROJECT_H
-#define SCI_PROJECT_H
-
-#include <limits.h>
-#include <stdint.h>
-
-#define PROJECT_NAME_MAX 32
-#define PROJECT_DESC_MAX 256
-#define PROJECT_URL_MAX  128
-
-struct project {
-	int64_t id;
-	char name[PROJECT_NAME_MAX];
-	char desc[PROJECT_DESC_MAX];
-	char url[PROJECT_URL_MAX];
-	char script[PATH_MAX];
-};
-
-#endif /* !SCI_PROJECT_H */
--- a/req.c	Thu Jun 10 10:39:21 2021 +0200
+++ b/req.c	Mon Jun 14 22:08:24 2021 +0200
@@ -4,80 +4,26 @@
 #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 "job.h"
-#include "project.h"
+#include "config.h"
 #include "req.h"
+#include "types.h"
 #include "util.h"
-#include "worker.h"
-
-static int sock;
-
-static struct req
-ask(const char *fmt, ...)
-{
-	assert(fmt);
-
-	struct req res = {0};
-	char buf[1024], *p;
-	va_list ap;
-	ssize_t bufsz = 0, nr;
-
-	va_start(ap, fmt);
-	vsnprintf(buf, sizeof (buf), fmt, ap);
-	va_end(ap);
-
-	if (strlcat(buf, "\r\n\r\n", sizeof (buf)) >= sizeof (buf)) {
-		res.status = EMSGSIZE;
-		return res;
-	}
-
-	if (send(sock, buf, strlen(buf), MSG_NOSIGNAL) < 0) {
-		res.status = errno;
-		return res;
-	}
 
-	while ((nr = recv(sock, buf + bufsz, sizeof (buf) - bufsz, 0)) > 0) {
-		bufsz += nr;
-
-		if ((size_t)bufsz >= sizeof (buf)) {
-			bufsz = 0;
-			res.status = EMSGSIZE;
-			break;
-		}
-	}
-
-	buf[bufsz] = '\0';
-
-	/* Remove final '\r\n\r\n' */
-	if ((p = strstr(buf, "\r\n\r\n")))
-		*p = '\0';
+static char path[PATH_MAX] = VARDIR "/run/sci.sock";
 
-	/* Check and remove status. */
-	if (strncmp(buf, "ERR", 3) == 0)
-		res.status = -1;
-	if ((p = strchr(buf, '\n')))
-		++p;
-	else
-		p = buf;
-
-	strlcat(res.msg, p, sizeof (res.msg));
-
-	return res;
-}
-
-int
-req_connect(const char *path)
+static int
+attach(const char *path)
 {
-	assert(path);
-
 	struct sockaddr_un sun;
 	struct timeval tv = { .tv_sec = 3 };
+	int sock;
 
 	if ((sock = socket(PF_LOCAL, SOCK_STREAM, 0)) < 0)
 		return -1;
@@ -86,67 +32,117 @@
 	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)
+	    setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof (tv)) < 0 ||
+	    connect(sock, (const struct sockaddr *)&sun, sizeof (sun)) < 0) {
+		close(sock);
 		return -1;
-	if (connect(sock, (const struct sockaddr *)&sun, sizeof (sun)) < 0)
-		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);
 
-	return 0;
+	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_queue(const struct job *job)
+req_job_add(const struct job *job)
 {
 	assert(job);
 
-	return ask("job-queue %s|%s", job->project.name, job->tag);
+	return exchange("job-add", job_to(job, 1));
 }
 
 struct req
-req_job_list(struct job_result *jobs, size_t *jobsz, const char *project)
+req_job_todo(struct job *jobs, size_t *jobsz, const char *worker)
 {
 	assert(jobs);
 	assert(jobsz);
+	assert(worker);
 
-	struct req req;
-	char fmt[128], *token, *p = req.msg;
-	size_t tot = 0;
-
-	if ((req = ask("job-list %s", project)).status)
-		return req;
+	struct req st;
+	struct worker wk;
 
-	snprintf(fmt, sizeof (fmt), "%%zd|%%%zu[^|]|%%%zu[^|]\n",
-	    sizeof (jobs->job.tag), sizeof (jobs->job.project.name));
+	/*
+	 * 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;
 
-	while ((token = strtok_r(p, "\n", &p)) && tot < *jobsz) {
-		if (sscanf(token, fmt, &jobs->job.id, jobs->job.tag, jobs->job.project.name) == 3) {
-			++jobs;
-			++tot;
-		}
+	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;
 	}
 
-	*jobsz = tot;
-
-	return req;
+	return st;
 }
 
 struct req
-req_job_save(const struct job_result *res)
+req_jobresult_add(const struct jobresult *res)
 {
 	assert(res);
 
-	char *b64;
-	struct req req = {0};
-
-	if (!(b64 = util_zbase64_enc(res->console)))
-		req.status = errno;
-	else {
-		req = ask("job-save %lld|%s|%s|%s|%s", (long long int)res->job.id,
-		    res->worker.name, res->status, res->retcode, b64);
-		free(b64);
-	}
-
-	return req;
+	return exchange("jobresult-add", jobresult_to(res, 1));
 }
 
 struct req
@@ -154,35 +150,66 @@
 {
 	assert(p);
 
-	return ask("project-add %s|%s|%s|%s", p->name, p->desc, p->url, p->script);
+	return exchange("project-add", 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_list(struct project *pc, size_t *pcsz)
+req_project_find_id(struct project *p, int id)
 {
-	assert(pc);
-	assert(pcsz);
+	assert(p);
 
-	struct req req;
-	char fmt[128], *token, *p = req.msg;
-	size_t tot = 0;
+	struct req st;
 
-	if ((req = ask("project-list")).status)
-		return req;
-
-	snprintf(fmt, sizeof (fmt), "%%%zu[^|]|%%%zu[^|]|%%%zu[^|]|%%%zu[^\n]\n",
-	    sizeof (pc->name), sizeof (pc->desc), sizeof (pc->url), sizeof (pc->script));
+	if ((st = exchange("project-find-id", json_pack("{si}", "id", id))).status)
+		return st;
 
-	while ((token = strtok_r(p, "\n", &p)) && tot < *pcsz) {
-		if (sscanf(token, fmt, pc->name, pc->desc, pc->url, pc->script) == 4) {
-			++pc;
-			++tot;
-		}
+	if (project_from(p, 1, st.msg) < 0) {
+		json_decref(st.msg);
+		st.msg = NULL;
+		st.status = errno;
 	}
 
-	*pcsz = tot;
+	return st;
+}
+
+struct req
+req_project_list(struct project *projects, size_t *projectsz)
+{
+	assert(projects);
+	assert(projectsz);
+
+	struct req st;
 
-	return req;
+	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
@@ -190,7 +217,44 @@
 {
 	assert(w);
 
-	return ask("worker-add %s|%s", w->name, w->desc);
+	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
@@ -199,54 +263,25 @@
 	assert(wk);
 	assert(wksz);
 
-	struct req req;
-	char fmt[128], *token, *p = req.msg;
-	size_t tot = 0;
+	struct req st;
 
-	if ((req = ask("worker-list")).status)
-		return req;
+	if ((st = exchange("worker-list", json_object())).status)
+		return st;
 
-	snprintf(fmt, sizeof (fmt), "%%%zu[^|]|%%%zu[^\n]\n",
-	    sizeof (wk->name), sizeof (wk->desc));
-
-	while ((token = strtok_r(p, "\n", &p)) && tot < *wksz) {
-		if (sscanf(token, fmt, wk->name, wk->desc) == 2) {
-			wk++;
-			tot++;
-		}
+	if ((*wksz = worker_from(wk, *wksz, st.msg)) == (size_t)-1) {
+		json_decref(st.msg);
+		st.msg = NULL;
+		st.status = errno;
 	}
 
-	*wksz = tot;
-
-	return req;
-}
-
-struct req
-req_script_get(const char *project, char *out, size_t outsz)
-{
-	assert(out);
-
-	struct req req;
-	char *script;
-
-	if ((req = ask("script-get %s", project)).status)
-		return req;
-	if (!(script = util_zbase64_dec(req.msg))) {
-		req.status = EINVAL;
-		return req;
-	}
-
-	if (strlcpy(out, script, outsz) >= outsz)
-		req.status = errno;
-
-	free(script);
-
-	return req;
+	return st;
 }
 
 void
-req_finish(void)
+req_finish(struct req *st)
 {
-	close(sock);
-	sock = 0;
+	if (st->msg)
+		json_decref(st->msg);
+
+	memset(st, 0, sizeof (*st));
 }
--- a/req.h	Thu Jun 10 10:39:21 2021 +0200
+++ b/req.h	Mon Jun 14 22:08:24 2021 +0200
@@ -3,44 +3,55 @@
 
 #include <stddef.h>
 
+#include <jansson.h>
+
+struct job;
+struct jobresult;
+struct project;
+struct worker;
+
 struct req {
 	int status;
-	char msg[1024];
+	json_t *msg;
 };
 
-struct worker;
-struct project;
-struct job;
-struct job_result;
-
-int
-req_connect(const char *);
+void
+req_set_path(const char *path);
 
 struct req
-req_job_queue(const struct job *);
+req_job_add(const struct job *);
 
 struct req
-req_job_list(struct job_result *, size_t *, const char *);
+req_job_todo(struct job *, size_t *, const char *);
 
 struct req
-req_job_save(const struct job_result *);
+req_jobresult_add(const struct jobresult *);
 
 struct req
 req_project_add(const struct project *);
 
 struct req
-req_project_list(struct project *, size_t *);
+req_project_find(struct project *, const char *);
 
 struct req
-req_script_get(const char *, char *, size_t);
+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(void);
+req_finish(struct req *);
 
 #endif /* !SCI_REQ_H */
--- a/scictl.c	Thu Jun 10 10:39:21 2021 +0200
+++ b/scictl.c	Mon Jun 14 22:08:24 2021 +0200
@@ -1,4 +1,3 @@
-#define _BSD_SOURCE
 #include <err.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -7,11 +6,9 @@
 #include <unistd.h>
 
 #include "config.h"
-#include "project.h"
-#include "job.h"
 #include "req.h"
+#include "types.h"
 #include "util.h"
-#include "worker.h"
 
 noreturn static void
 usage(void)
@@ -23,12 +20,11 @@
 noreturn static void
 help(void)
 {
-	fprintf(stderr, "usage: %s job-queue project tag\n", getprogname());
-	fprintf(stderr, "       %s job-list worker\n", getprogname());
-	fprintf(stderr, "       %s job-save id worker status retcode console\n", getprogname());
+	fprintf(stderr, "usage: %s job-add project tag\n", getprogname());
+	fprintf(stderr, "       %s job-todo worker\n", getprogname());
+	fprintf(stderr, "       %s jobresult-add id worker exitcode console\n", getprogname());
 	fprintf(stderr, "       %s project-add name desc url script\n", getprogname());
 	fprintf(stderr, "       %s project-list\n", getprogname());
-	fprintf(stderr, "       %s script-get project\n", getprogname());
 	fprintf(stderr, "       %s worker-add name desc\n", getprogname());
 	fprintf(stderr, "       %s worker-list\n", getprogname());
 	exit(0);
@@ -38,8 +34,7 @@
 readfile(const char *path)
 {
 	FILE *fp, *str;
-	static char console[SCI_MSG_MAX];
-	char buf[BUFSIZ], *ret = console;
+	char buf[BUFSIZ], *console;
 	size_t nr;
 
 	if (strcmp(path, "-") == 0)
@@ -47,90 +42,157 @@
 	else if (!(fp = fopen(path, "r")))
 		err(1, "%s", path);
 
-	if (!(str = fmemopen(console, sizeof (console), "w")))
+	console = util_calloc(1, SCI_MSG_MAX);
+
+	if (!(str = fmemopen(console, SCI_MSG_MAX, "w")))
 		err(1, "fmemopen");
 
 	while ((nr = fread(buf, 1, sizeof (buf), fp)) > 0)
 		fwrite(buf, 1, nr, str);
 
-	if ((ferror(fp) && !feof(fp)) || (ferror(str) && !feof(str)))
-		ret = NULL;
+	if ((ferror(fp) && !feof(fp)) || (ferror(str) && !feof(str))) {
+		free(console);
+		console = NULL;
+	}
 
 	fclose(str);
 	fclose(fp);
 
-	return ret;
+	return console;
 }
 
 static struct req
-cmd_job_queue(int argc, char **argv)
+cmd_job_add(int argc, char **argv)
 {
 	struct job job = {0};
+	struct project project = {0};
+	struct req rp, rj;
 
 	if (argc < 2)
 		usage();
 
-	strlcpy(job.project.name, argv[0], sizeof (job.project.name));
-	strlcpy(job.tag, argv[1], sizeof (job.tag));
+	if ((rp = req_project_find(&project, argv[0])).status)
+		return rp;
 
-	return req_job_queue(&job);
+	job.project_id = project.id;
+	job.tag = argv[1];
+
+	rj = req_job_add(&job);
+	req_finish(&rp);
+
+	return rj;
 }
 
 static struct req
-cmd_job_list(int argc, char **argv)
+cmd_job_todo(int argc, char **argv)
 {
-	struct job_result jobs[SCI_JOB_LIST_MAX];
-	size_t jobsz = UTIL_SIZE(jobs);
-	struct req req;
+	struct project projects[SCI_PROJECT_MAX] = {0};
+	struct job jobs[SCI_JOB_LIST_MAX] = {0};
+	struct req rp, rj;
+	size_t projectsz = UTIL_SIZE(projects), jobsz = UTIL_SIZE(jobs);
 
 	if (argc < 1)
 		usage();
 
-	if ((req = req_job_list(jobs, &jobsz, argv[0])).status)
-		return req;
+	/* First retrieve projects for a better listing. */
+	if ((rp = req_project_list(projects, &projectsz)).status)
+		return rp;
 
-	printf("%-16s%-16s%s\n", "ID", "TAG", "PROJECT");
+	if ((rj = req_job_todo(jobs, &jobsz, argv[0])).status) {
+		req_finish(&rp);
+		return rj;
+	}
 
 	for (size_t i = 0; i < jobsz; ++i) {
-		printf("%-16lld%-16s%s\n", (long long int)jobs[i].job.id,
-		    jobs[i].job.tag, jobs[i].job.project.name);
+		const char *project = "unknown";
+
+		/* Find project if exists (it should). */
+		for (size_t i = 0; i < projectsz; ++i) {
+			if (projects[i].id == jobs[i].project_id) {
+				project = projects[i].name;
+				break;
+			}
+		}
+
+		printf("%-16s%d\n", "id:", jobs[i].id);
+		printf("%-16s%s\n", "tag:", jobs[i].tag);
+		printf("%-16s%s\n", "project:", project);
+
+		if (i + 1 < jobsz)
+			printf("\n");
 	}
 
-	return req;
+	req_finish(&rp);
+	
+	return rj;
 }
 
 static struct req
-cmd_job_save(int argc, char **argv)
+cmd_jobresult_add(int argc, char **argv)
 {
-	struct job_result res = {0};
+	struct jobresult res = {0};
+	struct worker wk = {0};
+	struct req rw, rj;
+	char *log;
 
 	if (argc < 5)
 		usage();
 
-	res.job.id = strtoll(argv[0], NULL, 10);
-	res.status = strtoll(argv[2], NULL, 10);
-	res.retcode = strtoll(argv[3], NULL, 10);
-	res.console = readfile(argv[4]);
-	strlcpy(res.worker.name, argv[1], sizeof (res.worker.name));
+	/* Find worker id. */
+	if ((rw = req_worker_find(&wk, argv[1])).status)
+		return rw;
 
-	return req_job_save(&res);
+	res.job_id = strtoll(argv[0], NULL, 10);
+	res.exitcode = strtoll(argv[2], NULL, 10);
+	res.worker_id = wk.id;
+	res.log = log = readfile(argv[3]);
+	rj = req_jobresult_add(&res);
+
+	free(log);
+	req_finish(&rw);
+
+	return rj;
 }
 
 static struct req
 cmd_project_add(int argc, char **argv)
 {
-	struct project pc;
+	struct project pc = {0};
+	struct req res;
+	char *script;
 
 	if (argc < 4)
 		usage();
 
-	memset(&pc, 0, sizeof (pc));
-	strlcpy(pc.name, argv[0], sizeof (pc.name));
-	strlcpy(pc.desc, argv[1], sizeof (pc.desc));
-	strlcpy(pc.url, argv[2], sizeof (pc.url));
-	strlcpy(pc.script, argv[3], sizeof (pc.script));
+	pc.name = argv[0];
+	pc.desc = argv[1];
+	pc.url = argv[2];
+	pc.script = script = readfile(argv[3]);
+	res = req_project_add(&pc);
+
+	free(script);
+
+	return res;
+}
 
-	return req_project_add(&pc);
+static struct req
+cmd_project_find(int argc, char **argv)
+{
+	struct project project = {0};
+	struct req req;
+
+	if (argc < 1)
+		usage();
+	if ((req = req_project_find(&project, argv[0])).status)
+		return req;
+
+	printf("%-16s%s\n", "name:", project.name);
+	printf("%-16s%s\n", "desc:", project.desc);
+	printf("%-16s%s\n", "url:", project.url);
+	printf("\n");
+	printf("%s", project.script);
+
+	return req;
 }
 
 static struct req
@@ -139,43 +201,21 @@
 	(void)argc;
 	(void)argv;
 
-	struct project pc[SCI_PROJECT_MAX];
+	struct project projects[SCI_PROJECT_MAX] = {0};
 	struct req req;
-	size_t pcsz = UTIL_SIZE(pc);
+	size_t projectsz = UTIL_SIZE(projects);
 
-	memset(pc, 0, sizeof (pc));
-
-	if ((req = req_project_list(pc, &pcsz)).status)
+	if ((req = req_project_list(projects, &projectsz)).status)
 		return req;
 
-	printf("%-16s%-24s%-20s%s\n", "NAME", "DESCRIPTION", "URL", "SCRIPT");
-
-	for (size_t i = 0; i < pcsz; ++i)
-		printf("%-16s%-24s%-20s%s\n", pc[i].name, pc[i].desc,
-		    pc[i].url, pc[i].script);
-
-	return req;
-}
+	for (size_t i = 0; i < projectsz; ++i) {
+		printf("%-16s%s\n", "name:", projects[i].name);
+		printf("%-16s%s\n", "desc:", projects[i].desc);
+		printf("%-16s%s\n", "url:", projects[i].url);
 
-static struct req
-cmd_script_get(int argc, char **argv)
-{
-	char script[SCI_MSG_MAX];
-	struct req req;
-
-	if (argc < 1)
-		usage();
-	if ((req = req_script_get(argv[0], script, sizeof (script))).status)
-		return req;
-
-	printf("%s", script);
-
-	/*
-	 * Don't break up the terminal output if the script does not contain a
-	 * final new line.
-	 */
-	if (script[strlen(script) - 1] != '\n')
-		printf("\n");
+		if (i + 1 < projectsz)
+			printf("\n");
+	}
 
 	return req;
 }
@@ -183,14 +223,13 @@
 static struct req
 cmd_worker_add(int argc, char **argv)
 {
-	struct worker wk;
+	struct worker wk = {0};
 
 	if (argc < 2)
 		usage();
 
-	memset(&wk, 0, sizeof (wk));
-	strlcpy(wk.name, argv[0], sizeof (wk.name));
-	strlcpy(wk.desc, argv[1], sizeof (wk.desc));
+	wk.name = argv[0];
+	wk.desc = argv[1];
 
 	return req_worker_add(&wk);
 }
@@ -208,10 +247,13 @@
 	if ((req = req_worker_list(wk, &wksz)).status)
 		return req;
 
-	printf("%-16s%s\n", "NAME", "DESCRIPTION");
+	for (size_t i = 0; i < wksz; ++i) {
+		printf("%-16s%s\n", "name:", wk[i].name);
+		printf("%-16s%s\n", "desc:", wk[i].desc);
 
-	for (size_t i = 0; i < wksz; ++i)
-		printf("%-16s%s\n",  wk[i].name, wk[i].desc);
+		if (i + 1 < wksz)
+			printf("\n");
+	}
 
 	return req;
 }
@@ -220,12 +262,12 @@
 	const char *name;
 	struct req (*exec)(int, char **);
 } commands[] = {
-	{ "job-queue",          cmd_job_queue           },
-	{ "job-list",           cmd_job_list            },
-	{ "job-save",           cmd_job_save            },
+	{ "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-list",       cmd_project_list        },
-	{ "script-get",         cmd_script_get          },
 	{ "worker-add",         cmd_worker_add          },
 	{ "worker-list",        cmd_worker_list         },
 	{ NULL,                 NULL                    }
@@ -234,7 +276,6 @@
 int
 main(int argc, char **argv)
 {
-	const char *sock = VARDIR "/run/sci.sock";
 	int ch, cmdfound = 0;
 
 	setprogname("scictl");
@@ -242,7 +283,7 @@
 	while ((ch = getopt(argc, argv, "s:")) != -1) {
 		switch (ch) {
 		case 's':
-			sock = optarg;
+			req_set_path(optarg);
 			break;
 		default:
 			break;
@@ -256,8 +297,6 @@
 		usage();
 	if (strcmp(argv[0], "help") == 0)
 		help();
-	if (req_connect(sock) < 0)
-		err(1, "%s", sock);
 
 	for (size_t i = 0; commands[i].name; ++i) {
 		struct req res;
@@ -266,15 +305,16 @@
 			res = commands[i].exec(--argc, ++argv);
 			cmdfound = 1;
 
+#if 0
 			if (res.status)
 				warnx("%s", res.msg);
+#endif
 
+			req_finish(&res);
 			break;
 		}
 	}
 
 	if (!cmdfound)
 		errx(1, "abort: command %s not found", argv[0]);
-
-	req_finish();
 }
--- a/scid.c	Thu Jun 10 10:39:21 2021 +0200
+++ b/scid.c	Mon Jun 14 22:08:24 2021 +0200
@@ -23,6 +23,7 @@
 #include <err.h>
 #include <errno.h>
 #include <fcntl.h>
+#include <limits.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <stdnoreturn.h>
@@ -31,17 +32,15 @@
 
 #include "config.h"
 #include "db.h"
-#include "job.h"
 #include "log.h"
-#include "project.h"
+#include "types.h"
 #include "util.h"
-#include "worker.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 void
+noreturn static inline void
 usage(void)
 {
 	fprintf(stderr, "usage: %s [-d database]\n", getprogname());
@@ -76,384 +75,274 @@
 		err(1, "unable to open database");
 }
 
-static size_t
-split(char *line, char **args, size_t max)
+static int
+answer(int fd, json_t *json)
 {
-	char *token, *ptr;
-	size_t i = 0;
+	char *str;
+	int ret = 0;
+
+	str = json_dumps(json, JSON_COMPACT);
 
-	for (ptr = line; i < max && (token = strtok_r(ptr, "|", &ptr)); ++i)
-		args[i] = token;
+	if (send(fd, str, strlen(str), 0) < 0 || send(fd, "\r\n", 2, 0) < 0)
+		ret = -1;
 
-	return i;
+	free(str);
+	json_decref(json);
+
+	return ret;
 }
 
-static int
-answer(int fd, const char *fmt, ...)
+static inline int
+ok(int fd)
 {
-	char buf[1024] = {0};
+	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);
-	strlcat(buf, "\r\n\r\n", sizeof (buf));
 	va_end(ap);
 
-	send(fd, buf, strlen(buf), MSG_NOSIGNAL);
+	return answer(fd, json_pack("{ss ss}",
+	    "status", "error",
+	    "error", buf
+	));
+}
 
-	return 0;
+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
-ok(int fd)
+cmd_job_todo(int fd, json_t *doc)
 {
-	send(fd, "OK", 2, MSG_NOSIGNAL);
+	struct db_ctx ctx;
+	struct job jobs[SCI_JOB_LIST_MAX];
+	ssize_t jobsz;
+	int worker_id, ret;
 
-	return 0;
+	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;
 }
 
-/*
- * Request
- * -------
- *
- * job-queue project|tag
- *
- * Errors
- * ------
- *
- * - Already pending
- * - Internal database error
- */
 static int
-cmd_job_queue(int fd, char *cmd)
+cmd_jobresult_add(int fd, json_t *doc)
 {
-	char *args[2] = {0};
-	struct job job = {0};
-	struct project project;
+	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);
 
-	if (split(cmd, args, 2) != 2) {
-		log_warn("invalid job-queue invocation");
-		return EINVAL;
-	}
-
-	strlcpy(project.name, args[0], sizeof (project.name));
+	return ok(fd);
+}
 
-	if (db_project_find(&project) < 0) {
-		log_warn("project %s not found", args[0]);
-		return ENOENT;
-	}
+static int
+cmd_project_add(int fd, json_t *doc)
+{
+	struct project proj = {0};
 
-	job.project.id = project.id;
-	strlcpy(job.tag, args[1], sizeof (job.tag));
+	if (project_from(&proj, 1, doc) < 0)
+		return EINVAL;
+	if (db_project_add(&proj) < 0)
+		return error(fd, "unable to create project");
 
-	if (db_job_queue(&job) < 0)
-		return answer(fd, "ERR unable to create job");
-
-	log_info("queued new job (%lld), for project %s with tag %s",
-	    (long long int)job.id, args[0], args[1]);
+	log_info("created new project (%d) %s", proj.id, proj.name);
 
 	return ok(fd);
 }
 
 static int
-cmd_job_list(int fd, char *cmd)
+cmd_project_find(int fd, json_t *doc)
 {
-	char *args[1] = {0}, buf[SCI_MSG_MAX];
-	struct job_result jobs[SCI_JOB_LIST_MAX];
-	struct worker worker;
-	ssize_t n;
-	FILE *fp;
-
-	if (split(cmd, args, 1) != 1) {
-		log_warn("invalid job-list invocation");
-		return EINVAL;
-	}
-	if (!(fp = fmemopen(buf, sizeof (buf), "w")))
-		return ENOMEM;
-
-	strlcpy(worker.name, args[0], sizeof (worker.name));
+	struct db_ctx ctx;
+	struct project proj = {0};
+	int ret;
 
-	if (db_worker_find(&worker) < 0) {
-		log_warn("worker %s not found", args[0]);
-		return ENOENT;
-	}
-
-	if ((n = db_job_result_todo(jobs, UTIL_SIZE(jobs), worker.id)) < 0)
-		return answer(fd, "ERR unable to retrieve jobs list");
+	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");
 
-	fprintf(fp, "OK\n");
+	ret = answer(fd, project_to(&proj, 1));
+	db_ctx_finish(&ctx);
 
-	for (ssize_t i = 0; i < n; ++i)
-		fprintf(fp, "%lld|%s|%s\n", (long long int)jobs[i].job.id,
-		    jobs[i].job.tag, jobs[i].job.project.name);
-
-	fclose(fp);
-
-	return answer(fd, "%s", buf);
+	return ret;
 }
 
-/*
- * Request
- * -------
- *
- * job-save id|worker|status|retcode|console
- */
 static int
-cmd_job_save(int fd, char *cmd)
+cmd_project_find_id(int fd, json_t *doc)
 {
-	char *args[5] = {0};
-	struct job_result res;
-
-	if (split(cmd, args, 5) != 5) {
-		log_warn("invalid job-save invocation");
-		return EINVAL;
-	}
-
-	strlcpy(res.worker.name, args[1], sizeof (res.worker.name));
+	struct db_ctx ctx;
+	struct project proj = {0};
+	int ret;
 
-	if (db_worker_find(&res.worker) < 0) {
-		log_warn("worker %s not found", args[1]);
-		return ENOENT;
-	}
-
-	res.job.id = strtoll(args[0], NULL, 10);
-	res.status = strtoll(args[2], NULL, 10);
-	res.retcode = strtoll(args[3], NULL, 10);
-	res.console = util_zbase64_dec(args[4]);
-
-	if (!res.console) {
-		log_warn("failed to decode console data");
+	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");
 
-	if (db_job_save(&res) < 0) {
-		log_warn("failed to save job result");
-		return EINVAL;
-	}
+	ret = answer(fd, project_to(&proj, 1));
+	db_ctx_finish(&ctx);
 
-	log_info("save job info (%lld, status=%d retcode=%d)",
-	    (long long int)res.id, res.status, res.retcode);
-	free(res.console);
-
-	return ok(fd);
+	return ret;
 }
 
-/*
- * Request
- * -------
- *
- * project-add name|desc|url|script
- *
- * Errors
- * ------
- *
- * - Already exists
- * - Internal database error
- */
 static int
-cmd_project_add(int fd, char *cmd)
+cmd_project_list(int fd, json_t *doc)
 {
-	char *args[4] = {0};
-	struct project proj = {0};
+	(void)doc;
 
-	if (split(cmd, args, 4) != 4) {
-		log_warn("invalid project-add invocation");
+	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);
 
-	strlcpy(proj.name, args[0], sizeof (proj.name));
-	strlcpy(proj.desc, args[1], sizeof (proj.desc));
-	strlcpy(proj.url, args[2], sizeof (proj.url));
-	strlcpy(proj.script, args[3], sizeof (proj.script));
+	return ret;
+}
 
-	if (db_project_add(&proj) < 0)
-		return answer(fd, "ERR unable to create project");
+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);
 }
 
-/*
- * Request
- * -------
- *
- * project-list
- *
- * Answer
- * ------
- *
- * OK
- * name|desc|url|script
- * ...
- *
- * Errors
- * ------
- *
- * - Internal database error
- */
 static int
-cmd_project_list(int fd, char *cmd)
-{
-	(void)cmd;
-
-	char buf[SCI_MSG_MAX];
-	struct project projects[SCI_PROJECT_MAX];
-	ssize_t np;
-	FILE *fp;
-
-	if ((np = db_project_get(projects, SCI_PROJECT_MAX)) < 0)
-		return EINVAL;
-	if (!(fp = fmemopen(buf, sizeof (buf), "w")))
-		return ENOMEM;
-
-	fprintf(fp, "OK\n");
-
-	for (ssize_t i = 0; i < np; ++i)
-		fprintf(fp, "%s|%s|%s|%s\n", projects[i].name, projects[i].desc,
-		    projects[i].url, projects[i].script);
-
-	fclose(fp);
-
-	return answer(fd, "%s", buf);
-}
-
-/*
- * Request
- * -------
- *
- * worker-add name|desc
- *
- * Errors
- * ------
- *
- * - Internal database error
- */
-static int
-cmd_worker_add(int fd, char *cmd)
+cmd_worker_find(int fd, json_t *doc)
 {
-	char *lines[2];
-	struct worker wk;
-
-	if (split(cmd, lines, 2) != 2) {
-		log_warn("invalid worker-add invocation");
-		return EINVAL;
-	}
-
-	strlcpy(wk.name, lines[0], sizeof (wk.name));
-	strlcpy(wk.desc, lines[1], sizeof (wk.desc));
-
-	if (db_worker_add(&wk) < 0)
-		return answer(fd, "ERR unable to create project");
-
-	return ok(fd);
-}
+	struct worker wk = {0};
+	struct db_ctx ctx;
+	int ret;
 
-/*
- * Request
- * -------
- *
- * worker-list
- *
- * Answer
- * ------
- *
- * OK
- * name|desc
- * ...
- *
- * Errors
- * ------
- *
- * - Internal database error
- */
-static int
-cmd_worker_list(int fd, char *cmd)
-{
-	(void)cmd;
+	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");
 
-	char buf[SCI_MSG_MAX];
-	struct worker wk[SCI_WORKER_MAX];
-	ssize_t np;
-	FILE *fp;
+	ret = answer(fd, worker_to(&wk, 1));
+	db_ctx_finish(&ctx);
 
-	if ((np = db_worker_get(wk, SCI_WORKER_MAX)) < 0)
-		return EINVAL;
-	if (!(fp = fmemopen(buf, sizeof (buf), "w")))
-		return ENOMEM;
-
-	fprintf(fp, "OK\n");
-
-	for (ssize_t i = 0; i < np; ++i)
-		fprintf(fp, "%s|%s\n", wk[i].name, wk[i].desc);
-
-	fclose(fp);
-
-	return answer(fd, "%s", buf);
+	return ret;
 }
 
 static int
-cmd_script_get(int fd, char *cmd)
+cmd_worker_find_id(int fd, json_t *doc)
 {
-	char buf[SCI_MSG_MAX], *b64;
-	struct project project = {0};
-	int filed, ret;
-	ssize_t nr;
+	struct worker wk = {0};
+	struct db_ctx ctx;
+	int ret;
 
-	strlcpy(project.name, cmd, sizeof (project.name));
+	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);
 
-	if (db_project_find(&project) < 0)
-		return ENOENT;
-	if ((filed = open(project.script, O_RDONLY)) < 0)
-		return errno;
-	if ((nr = read(filed, buf, sizeof (buf) - 1)) <= 0) {
-		close(filed);
-		return errno;
-	}
+	return ret;
+}
+
+static int
+cmd_worker_list(int fd, json_t *doc)
+{
+	(void)doc;
 
-	buf[nr] = 0;
-	close(filed);
+	struct db_ctx ctx;
+	struct worker workers[SCI_WORKER_MAX];
+	ssize_t workersz;
+	int ret;
 
-	if (!(b64 = util_zbase64_enc(buf)))
-		return errno;
+	if ((workersz = db_worker_list(&ctx, workers, UTIL_SIZE(workers))) < 0)
+		return error(fd, "unable to retrieve worker list");
 
-	ret = answer(fd, "OK\n%s", b64);
-	free(b64);
+	ret = answer(fd, worker_to(workers, workersz));
+	db_ctx_finish(&ctx);
 
 	return ret;
 }
 
 static void
-dispatch(int fd, char *cmd)
+dispatch(int fd, const char *msg)
 {
-	assert(cmd);
-
 	static const struct {
 		const char *name;
-		int (*exec)(int, char *);
+		int (*exec)(int, json_t *);
 	} cmds[] = {
-		{ "job-queue",          cmd_job_queue           },
-		{ "job-list",           cmd_job_list            },
-		{ "job-save",           cmd_job_save            },
+		{ "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        },
 		{ "worker-add",         cmd_worker_add          },
+		{ "worker-find",        cmd_worker_find         },
+		{ "worker-find-id",     cmd_worker_find_id      },
 		{ "worker-list",        cmd_worker_list         },
-		{ "script-get",         cmd_script_get          },
 		{ NULL,                 NULL                    }
 	};
 
-	for (size_t i = 0; cmds[i].name; ++i) {
-		size_t len = strlen(cmds[i].name);
-		int ret;
-
-		if (strncmp(cmds[i].name, cmd, len) == 0) {
-			cmd += len;
+	json_t *doc, *cmd;
+	json_error_t err;
 
-			while (*cmd && isspace(*cmd))
-				cmd++;
+	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");
+	}
 
-			if ((ret = cmds[i].exec(fd, cmd)) != 0)
-				answer(fd, "ERR %s", strerror(ret));
+	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;
 		}
 	}
@@ -463,9 +352,10 @@
 run(void)
 {
 	int client;
-	char buf[1024] = {0}, *endl;
-	ssize_t bufsz = 0, nr;
-	struct timeval tv = { .tv_sec = 3 };
+	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");
@@ -474,24 +364,22 @@
 	if (setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof (tv)) < 0)
 		err(1, "setsockopt");
 
-	while (!strstr(buf, "\r\n\r\n") && (nr = recv(client, buf + bufsz, sizeof (buf) - bufsz, 0)) > 0) {
-		bufsz += nr;
+	fp = util_fmemopen((msg = util_calloc(1, SCI_MSG_MAX)), SCI_MSG_MAX, "w");
+	setbuf(fp, NULL);
 
-		if ((size_t)bufsz >= sizeof (buf))
-			goto end;
-	}
+	while (!strstr(msg, "\r\n") && (nr = recv(client, buf, sizeof (buf), 0)) > 0)
+		if (fwrite(buf, 1, nr, fp) != (size_t)nr)
+			warn("fwrite");
 
 	if (nr < 0)
 		warn("recv");
-
-	buf[bufsz] = '\0';
-
-	if ((endl = strstr(buf, "\r\n\r\n")))
+	if ((endl = strstr(msg, "\r\n")))
 		*endl = 0;
 
-	dispatch(client, buf);
-end:
+	fclose(fp);
+	dispatch(client, msg);
 	close(client);
+	free(msg);
 }
 
 int
--- a/sciworkerd.c	Thu Jun 10 10:39:21 2021 +0200
+++ b/sciworkerd.c	Mon Jun 14 22:08:24 2021 +0200
@@ -18,11 +18,12 @@
 #include <jansson.h>
 
 #include "config.h"
-#include "job.h"
 #include "log.h"
-#include "project.h"
+#include "types.h"
 #include "util.h"
 
+#define TAG_MAX 256
+
 enum taskst {
 	TASKST_PENDING,         /* not started yet. */
 	TASKST_RUNNING,         /* currently running. */
@@ -34,8 +35,10 @@
 	enum taskst status;
 	pid_t child;
 	int pipe[2];
-	int retcode;
-	struct job job;
+	int exitcode;
+	int job_id;
+	int project_id;
+	char job_tag[TAG_MAX];
 	char out[SCI_CONSOLE_MAX];
 	char script[PATH_MAX];
 	int scriptfd;
@@ -70,6 +73,7 @@
 };
 
 static struct tasks tasks = TAILQ_HEAD_INITIALIZER(tasks);
+static struct worker worker;
 
 #if 0
 static int sigpipe[2];
@@ -109,7 +113,7 @@
 static void
 destroy(struct task *tk)
 {
-	log_debug("destroying task %lld", tk->job.id);
+	log_debug("destroying task %d", tk->job_id);
 
 	if (tk->pipe[0])
 		close(tk->pipe[0]);
@@ -141,11 +145,12 @@
 		dup2(tk->pipe[1], STDERR_FILENO);
 		close(tk->pipe[0]);
 		close(tk->pipe[1]);
-		log_debug("spawn: running process (%lld) %s", tk->child, tk->script);
+		log_debug("spawn: running process (%lld) %s",
+		    (long long int)tk->child, tk->script);
 
 		tk->status = TASKST_RUNNING;
 
-		if (execl(tk->script, tk->script, tk->job.tag, NULL) < 0) {
+		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);
@@ -213,7 +218,7 @@
 		log_debug("process %lld completed", (long long int)sinfo->si_pid);
 		close(tk->pipe[1]);
 		tk->status = TASKST_COMPLETED;
-		tk->retcode = status;
+		tk->exitcode = status;
 		tk->pipe[1] = 0;
 	}
 
@@ -228,73 +233,25 @@
 #endif
 }
 
-static void
-init(void)
-{
-	struct sigaction sa;
-
-	sa.sa_flags = SA_SIGINFO;
-	sa.sa_sigaction = complete;
-	sigemptyset(&sa.sa_mask);
-
-	if (sigaction(SIGCHLD, &sa, NULL) < 0)
-		err(1, "sigaction");
-
-	log_open("sciworkerd");
-
-#if 0
-	if (pipe(sigpipe) < 0)
-		err(1, "pipe");
-	if ((flags = fcntl(sigpipe[1], F_GETFL, 0)) < 0 ||
-	    fcntl(sigpipe[1], F_SETFL, flags | O_NONBLOCK) < 0)
-		err(1, "fcntl");
-#endif
-}
-
-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));
-
-#if 0
-	fds.list[0].fd = sigpipe[0];
-	fds.list[0].events = POLLIN;
-#endif
-	printf("fd => %zu\n", fds.listsz);
-
-	TAILQ_FOREACH(tk, &tasks, link) {
-		if (tk->status == TASKST_RUNNING || tk->status == TASKST_COMPLETED) {
-			printf("adding %d to pollin\n", tk->pipe[0]);
-			fds.list[i].fd = tk->pipe[0];
-			fds.list[i++].events = POLLIN | POLLPRI;
-		}
-	}
-
-	return fds;
-}
-
 static const char *
 uploadenc(const struct task *tk)
 {
-	static char json[SCI_MSG_MAX];
-	json_t *object;
+	json_t *doc;
+
+	struct jobresult res = {0};
+	char *dump;
 
-	object = json_object();
-	json_object_set(object, "code", json_string(tk->out));
-	json_object_set(object, "id", json_integer(tk->job.id));
-	json_object_set(object, "retcode", json_integer(tk->retcode));
-	strlcpy(json, json_dumps(object, JSON_COMPACT), sizeof (json));
-	json_decref(object);
+	res.job_id = tk->job_id;
+	res.exitcode = tk->exitcode;
+	res.log = tk->out;
+	res.worker_id = worker.id;
 
-	return json;
+	doc = jobresult_to(&res, 1);
+	dump = json_dumps(doc, JSON_COMPACT);
+
+	json_decref(doc);
+
+	return dump;
 }
 
 static size_t
@@ -306,12 +263,16 @@
 	return w;
 }
 
-static const char *
+static json_t *
 get(const char *topic, const char *url)
 {
 	CURL *curl;
 	CURLcode code;
-	static char buf[SCI_MSG_MAX];
+
+	json_t *doc;
+	json_error_t error;
+
+	char buf[SCI_MSG_MAX];
 	long status;
 	FILE *fp;
 
@@ -320,9 +281,6 @@
 	if (!(fp = fmemopen(buf, sizeof (buf), "w")))
 		err(1, "fmemopen");
 
-#if 0
-	curl_easy_setopt(curl, CURLOPT_URL, makeurl("api/v1/script/%s", tk->job.project.name));
-#endif
 	curl_easy_setopt(curl, CURLOPT_URL, url);
 	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
 	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
@@ -341,8 +299,10 @@
 		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 buf;
+	return doc;
 }
 
 static size_t
@@ -363,7 +323,7 @@
 	long status;
 
 	curl = curl_easy_init();
-	curl_easy_setopt(curl, CURLOPT_URL, makeurl("api/v1/jobs/%s", config.worker));
+	curl_easy_setopt(curl, CURLOPT_URL, makeurl("api/v1/jobs"));
 	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
 	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
 	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, silent);
@@ -393,83 +353,66 @@
 static inline void
 finished(struct task *tk)
 {
-	log_info("task %d: completed with exit code %d", tk->child, tk->retcode);
+	log_info("task %d: completed with exit code %d", tk->child, tk->exitcode);
 	printf("== OUTPUT ==\n");
 	puts(tk->out);
 	upload(tk);
 }
 
 static inline int
-pending(int64_t id)
+pending(int id)
 {
 	struct task *t;
 
 	TAILQ_FOREACH(t, &tasks, link)
-		if (t->job.id == id)
+		if (t->job_id == id)
 			return 1;
 
 	return 0;
 }
 
 static void
-push(int64_t id, const char *tag, const char *project)
+queue(int id, int project_id, const char *tag)
 {
 	struct task *tk;
 
-	log_info("queued job build (%lld) for project %s, tag %s\n", id, project, tag);
+	log_info("queued job build (%d) for tag %s\n", id, tag);
 
 	tk = util_calloc(1, sizeof (*tk));
-	tk->job.id = id;
-	strlcpy(tk->job.tag, tag, sizeof (tk->job.tag));
-	strlcpy(tk->job.project.name, project, sizeof (tk->job.project.name));
+	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(const char *str)
+merge(json_t *doc)
 {
-	json_t *array, *obj, *id, *tag, *project;
-	json_error_t err;
-	size_t i;
+	struct job jobs[SCI_JOB_LIST_MAX];
+	ssize_t jobsz;
 
-	if (!(array = json_loads(str, 0, &err))) {
-		log_warn("fetch: failed to decode JSON: %s", err.text);
-		return;
-	}
-	if (!json_is_array(array))
-		goto invalid;
-
-	json_array_foreach(array, i, obj) {
-		if (!json_is_object(obj) ||
-		    !json_is_integer((id = json_object_get(obj, "id"))) ||
-		    !json_is_string((tag = json_object_get(obj, "tag"))) ||
-		    !json_is_string((project = json_object_get(obj, "project"))))
-			goto invalid;
-
-		if (!pending(json_integer_value(id)))
-			push(json_integer_value(id), json_string_value(tag),
-			    json_string_value(project));
+	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(array);
-
-	return;
-
-invalid:
-	log_warn("fetch: invalid JSON input");
-	json_decref(array);
+	json_decref(doc);
 }
 
 static void
 fetchjobs(void)
 {
-	const char *json;
+	json_t *doc;
 
-	if (!(json = get("fetch", makeurl("api/v1/jobs/%s", config.worker))))
+	if (!(doc = get("fetch", makeurl("api/v1/jobs/%s", config.worker))))
 		log_warn("unable to retrieve jobs");
 	else
-		merge(json);
+		merge(doc);
 }
 
 /*
@@ -514,26 +457,22 @@
 }
 
 static int
-extract(struct task *tk, const char *json)
+extract(struct task *tk, json_t *doc)
 {
-	json_t *doc, *code;
-	json_error_t err;
+	struct project proj;
 	size_t len;
 
-	if (!(doc = json_loads(json, 0, &err))) {
-		log_warn("fetchscript: failed to decode JSON: %s", err.text);
+	if (project_from(&proj, 1, doc) < 0) {
+		json_decref(doc);
+		log_warn("fetchproject: %s", strerror(errno));
 		return -1;
 	}
-	if (!json_is_object(doc) ||
-	    !json_is_string((code = json_object_get(doc, "code"))))
-		goto invalid;
+
+	len = strlen(proj.script);
 
-	len = strlen(json_string_value(code));
-
-	if ((size_t)write(tk->scriptfd, json_string_value(code), len) != len) {
-		log_warn("fetchscript: %s", strerror(errno));
+	if ((size_t)write(tk->scriptfd, proj.script, len) != len) {
 		json_decref(doc);
-
+		log_warn("fetchproject: %s", strerror(errno));
 		return -1;
 	}
 
@@ -542,36 +481,31 @@
 	tk->scriptfd = 0;
 
 	return 0;
-
-invalid:
-	log_warn("fetchscript: invalid JSON");
-	json_decref(doc);
-
-	return -1;
 }
 
 static int
-fetchscript(struct task *tk)
+fetchproject(struct task *tk)
 {
-	const char *json;
+	json_t *doc;
 
-	if (!(json = get("fetchscript", makeurl("api/v1/script/%s", tk->job.project.name))))
+	if (!(doc = get("fetchproject", makeurl("api/v1/projects/%d", tk->project_id))))
 		return -1;
 
-	return extract(tk, json);
+	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=%lld, project=%s, tag=%s)",
-	    tk->job.id, tk->job.project.name, tk->job.tag);
-
-	snprintf(tk->script, sizeof (tk->script), "/tmp/sciworkerd-%s-XXXXXX",
-	    tk->job.project.name);
+	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) {
@@ -580,7 +514,7 @@
 		return;
 	}
 
-	if (fetchscript(tk) < 0) {
+	if (fetchproject(tk) < 0) {
 		unlink(tk->script);
 		close(tk->scriptfd);
 		tk->scriptfd = 0;
@@ -588,6 +522,9 @@
 		spawn(tk);
 }
 
+/*
+ * Start all pending tasks if the limit of running tasks is not reached.
+ */
 static void
 startall(void)
 {
@@ -598,9 +535,9 @@
 		if (tk->status == TASKST_RUNNING)
 			++nrunning;
 
-	if (nrunning >= (size_t)config.maxbuilds) {
+	if (nrunning >= (size_t)config.maxbuilds)
 		log_debug("not spawning new process because limit is reached");
-	} else {
+	else {
 		tk = TAILQ_FIRST(&tasks);
 
 		while (tk && nrunning++ < (size_t)config.maxbuilds) {
@@ -611,6 +548,74 @@
 }
 
 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.sa_sigaction = complete;
+	sigemptyset(&sa.sa_mask);
+
+	if (sigaction(SIGCHLD, &sa, NULL) < 0)
+		err(1, "sigaction");
+
+	log_open("sciworkerd");
+	fetchworker();
+
+#if 0
+	if (pipe(sigpipe) < 0)
+		err(1, "pipe");
+	if ((flags = fcntl(sigpipe[1], F_GETFL, 0)) < 0 ||
+	    fcntl(sigpipe[1], F_SETFL, flags | O_NONBLOCK) < 0)
+		err(1, "fcntl");
+#endif
+}
+
+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));
+
+#if 0
+	fds.list[0].fd = sigpipe[0];
+	fds.list[0].events = POLLIN;
+#endif
+
+	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;
--- a/sql/init.sql	Thu Jun 10 10:39:21 2021 +0200
+++ b/sql/init.sql	Mon Jun 14 22:08:24 2021 +0200
@@ -18,7 +18,7 @@
 	project_id INTEGER NOT NULL REFERENCES project (id)
 );
 
-CREATE TABLE IF NOT EXISTS job_result(
+CREATE TABLE IF NOT EXISTS jobresult(
 	id INTEGER PRIMARY KEY AUTOINCREMENT,
 	job_id INTEGER NOT NULL REFERENCES job (id),
 	worker_id INTEGER NOT NULL REFERENCES worker (id),
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/job-add.sql	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,4 @@
+INSERT INTO job(
+  tag,
+  project_id
+) VALUES (?, ?)
--- a/sql/job-queue.sql	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-INSERT INTO job(
-  tag,
-  project_id
-) VALUES (?, ?)
--- a/sql/job-result-todo.sql	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-     SELECT job.id
-          , job.tag
-          , project.name
-       FROM job, project
-      WHERE job.project_id = project.id
-        AND job.id
-     NOT IN (
-            SELECT job_result.job_id
-              FROM job_result
-             WHERE job_result.worker_id = ?
-     )
-      LIMIT ?
--- a/sql/job-save.sql	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-INSERT INTO job_result(
-	job_id,
-	worker_id,
-	status,
-	retcode,
-	console
-) VALUES (?, ?, ?, ?, ?)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/job-todo.sql	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,11 @@
+     SELECT job.id
+          , job.tag
+          , job.project_id
+       FROM job
+      WHERE job.id
+     NOT IN (
+            SELECT job_result.job_id
+              FROM job_result
+             WHERE job_result.worker_id = ?
+     )
+      LIMIT ?
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/jobresult-add.sql	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,7 @@
+INSERT INTO job_result(
+	job_id,
+	worker_id,
+	status,
+	retcode,
+	console
+) VALUES (?, ?, ?, ?, ?)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/project-add.sql	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,6 @@
+INSERT INTO project(
+  name,
+  desc,
+  url,
+  script
+) VALUES (?, ?, ?, ?)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/project-find-id.sql	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,4 @@
+SELECT *
+  FROM project
+ WHERE id = ?
+ LIMIT 1
--- a/sql/project-get.sql	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-SELECT *
-  FROM project
- LIMIT ?
--- a/sql/project-insert.sql	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-INSERT INTO project(
-  name,
-  desc,
-  url,
-  script
-) VALUES (?, ?, ?, ?)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/project-list.sql	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,3 @@
+SELECT *
+  FROM project
+ LIMIT ?
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/worker-add.sql	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,4 @@
+INSERT INTO worker(
+  name,
+  desc
+) VALUES (?, ?)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/worker-find-id.sql	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,4 @@
+SELECT *
+  FROM worker
+ WHERE id = ?
+ LIMIT 1
--- a/sql/worker-get.sql	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-SELECT *
-  FROM worker
- LIMIT ?
--- a/sql/worker-insert.sql	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-INSERT INTO worker(
-  name,
-  desc
-) VALUES (?, ?)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sql/worker-list.sql	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,3 @@
+SELECT *
+  FROM worker
+ LIMIT ?
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/types.c	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,204 @@
+#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/types.h	Mon Jun 14 22:08:24 2021 +0200
@@ -0,0 +1,61 @@
+#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	Thu Jun 10 10:39:21 2021 +0200
+++ b/util.c	Mon Jun 14 22:08:24 2021 +0200
@@ -16,18 +16,18 @@
  * 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 <zstd.h>
-
-#include "base64.h"
 #include "util.h"
 
 void *
@@ -122,28 +122,15 @@
 	return ret;
 }
 
-size_t
-util_split(char *line, const char **args, size_t max, char delim)
+FILE *
+util_fmemopen(void *buf, size_t size, const char *mode)
 {
-	size_t idx;
-
-	if (!*line)
-		return 0;
-
-	for (idx = 0; idx < max; ++idx) {
-		char *sp = strchr(line, delim);
+	FILE *fp;
 
-		if (!sp || idx + 1 >= max) {
-			args[idx++] = line;
-			break;
-		}
+	if (!(fp = fmemopen(buf, size, mode)))
+		err(1, "fmemopen");
 
-		*sp = '\0';
-		args[idx] = line;
-		line = sp + 1;
-	}
-
-	return idx;
+	return fp;
 }
 
 char *
@@ -163,67 +150,27 @@
 }
 
 char *
-util_zbase64_enc(const char *src)
+util_read(const char *path)
 {
-	assert(src);
-
-	char *zstd, *b64;
-	size_t zstdsz, b64sz, len;
+	int fd;
+	struct stat st;
+	char *ret;
 
-	len = strlen(src);
-	zstdsz = ZSTD_compressBound(len);
-	zstd = util_malloc(zstdsz);
+	if ((fd = open(path, O_RDONLY)) < 0)
+		return NULL;
+	if (fstat(fd, &st) < 0)
+		return close(fd), NULL;
 
-	if (ZSTD_isError(zstdsz = ZSTD_compress(zstd, zstdsz, src, len, 18))) {
-		free(zstd);
-		return NULL;
+	ret = util_calloc(1, st.st_size + 1);
+
+	if (read(fd, ret, st.st_size) != st.st_size) {
+		free(ret);
+		ret = NULL;
 	}
 
-	b64sz = B64_ENCODE_LENGTH(zstdsz);
-	b64 = util_calloc(1, b64sz + 1);
-	b64_encode(zstd, zstdsz, b64, b64sz);
-	free(zstd);
-
-	return b64;
-}
-
-char *
-util_zbase64_dec(const char *src)
-{
-	assert(src);
-
-	char *zstd, *text;
-	size_t zstdsz, textsz, len;
-
-	len = strlen(src);
-	zstdsz = B64_DECODE_LENGTH(len) + 1;
-	zstd = util_calloc(1, zstdsz);
+	close(fd);
 
-	if ((zstdsz = b64_decode(src, len, zstd, zstdsz)) == (size_t)-1) {
-		free(zstd);
-		return NULL;
-	}
-
-	switch ((textsz = ZSTD_getFrameContentSize(zstd, zstdsz))) {
-	case ZSTD_CONTENTSIZE_UNKNOWN:
-	case ZSTD_CONTENTSIZE_ERROR:
-		free(zstd);
-		return NULL;
-	default:
-		break;
-	}
-
-	text = util_calloc(1, textsz + 1);
-
-	if (ZSTD_isError((textsz = ZSTD_decompress(text, textsz, zstd, zstdsz)))) {
-		free(zstd);
-		free(text);
-		return NULL;
-	}
-
-	free(zstd);
-
-	return text;
+	return ret;
 }
 
 const char *
--- a/util.h	Thu Jun 10 10:39:21 2021 +0200
+++ b/util.h	Mon Jun 14 22:08:24 2021 +0200
@@ -20,6 +20,7 @@
 #define SCI_UTIL_H
 
 #include <stddef.h>
+#include <stdio.h>
 
 #define UTIL_SIZE(x) (sizeof (x) / sizeof (x[0]))
 
@@ -50,17 +51,14 @@
 char *
 util_dirname(const char *);
 
-size_t
-util_split(char *, const char **, size_t, char);
+FILE *
+util_fmemopen(void *, size_t, const char *);
 
 char *
 util_printf(char *, size_t, const char *, ...);
 
 char *
-util_zbase64_enc(const char *);
-
-char *
-util_zbase64_dec(const char *);
+util_read(const char *);
 
 const char *
 util_path(const char *);
--- a/worker.h	Thu Jun 10 10:39:21 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-#ifndef SCI_WORKER_H
-#define SCI_WORKER_H
-
-#include <stdint.h>
-
-#define WORKER_NAME_MAX 32
-#define WORKER_DESC_MAX 256
-
-struct worker {
-	int64_t id;
-	char name[WORKER_NAME_MAX];
-	char desc[WORKER_DESC_MAX];
-};
-
-#endif /* !SCI_WORKER_H */