changeset 19:de4bf839b565

misc: revamp SQL
author David Demelier <markand@malikania.fr>
date Fri, 15 Jul 2022 11:11:48 +0200
parents 600204c31bf0
children f98ea578b1ef
files .hgignore Makefile config.def.h config.mk examples/irccd.sh extern/LICENSE.libutlist.txt extern/VERSION.libutlist.txt extern/libutlist/utlist.h lib/apic.c lib/apic.h lib/db.c lib/db.h lib/types.h lib/util.c lib/util.h scictl.c scictl/scictl.c scid/db.c scid/db.h sciworkerd/main.c sciworkerd/sciworkerd.c sciworkerd/sciworkerd.h sciworkerd/task.c sciworkerd/task.h sql/init.sql sql/job-add.sql sql/job-todo.sql sql/jobresult-add.sql sql/project-add.sql sql/project-find-id.sql sql/project-find.sql sql/project-list.sql sql/project-update.sql sql/worker-add.sql sql/worker-find-id.sql sql/worker-find.sql sql/worker-list.sql
diffstat 37 files changed, 2781 insertions(+), 1712 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Tue Jul 12 20:20:51 2022 +0200
+++ b/.hgignore	Fri Jul 15 11:11:48 2022 +0200
@@ -17,9 +17,12 @@
 
 # macOS specific.
 \.DS_Store$
+\.dSYM.*
 
 # executables.
 ^bcc$
+^scid/scid$
+^sciworkerd/sciworkerd$
 
 # tests.
 ^tests/test-db$
--- a/Makefile	Tue Jul 12 20:20:51 2022 +0200
+++ b/Makefile	Fri Jul 15 11:11:48 2022 +0200
@@ -20,7 +20,10 @@
 
 include config.mk
 
-LIBSCI_SRCS=            lib/log.c                       \
+LIBSCI_SRCS=            extern/libsqlite/sqlite3.c      \
+                        lib/apic.c                      \
+                        lib/db.c                        \
+                        lib/log.c                       \
                         lib/types.c                     \
                         lib/util.c
 LIBSCI_OBJS=            ${LIBSCI_SRCS:.c=.o}
@@ -40,9 +43,7 @@
                         sql/worker-find-id.h            \
                         sql/worker-list.h
 
-SCID_SRCS=              extern/libsqlite/sqlite3.c      \
-                        scid/db.c                       \
-                        scid/http.c                     \
+SCID_SRCS=              scid/http.c                     \
                         scid/main.c                     \
                         scid/page-api-jobs.c            \
                         scid/page-api-projects.c        \
@@ -51,7 +52,9 @@
 SCID_OBJS=              ${SCID_SRCS:.c=.o}
 SCID_DEPS=              ${SCID_SRCS:.c=.d}
 
-SCIWORKERD_SRCS=        sciworkerd/main.c sciworkerd/task.c
+SCIWORKERD_SRCS=        sciworkerd/main.c \
+                        sciworkerd/sciworkerd.c \
+                        sciworkerd/task.c
 SCIWORKERD_OBJS=        ${SCIWORKERD_SRCS:.c=.o}
 SCIWORKERD_DEPS=        ${SCIWORKERD_SRCS:.c=.d}
 
@@ -63,9 +66,6 @@
 TESTS_OBJS=             ${TESTS:.c=}
 TESTS_DEPS=             ${TESTS:.c=.d}
 
-LIBBSD_INCS=            `pkg-config --silence-errors --cflags libbsd-overlay`
-LIBBSD_LIBS=            `pkg-config --silence-errors --libs libbsd-overlay`
-
 LIBCURL_INCS=           `pkg-config --cflags libcurl`
 LIBCURL_LIBS=           `pkg-config --libs libcurl`
 
@@ -76,6 +76,7 @@
 KCGI_LIBS=              `pkg-config --libs kcgi`
 
 INCS=                   -Iextern/libsqlite \
+                        -Iextern/libutlist \
                         -Iextern/libgreatest \
                         -Ilib \
                         -I.
@@ -93,11 +94,11 @@
 
 # for unit tests.
 .c:
-	${CC} ${INCS} ${DEFS} ${LIBBSD_INCS} ${KCGI_INCS} ${JANSSON_INCS} \
-		${CFLAGS} -MMD $< -o $@ libsci.a ${LIBBSD_LIBS} ${JANSSON_LIBS}
+	${CC} ${INCS} ${DEFS} ${KCGI_INCS} ${JANSSON_INCS} \
+		${CFLAGS} -MMD $< -o $@ lib/libsci.a ${JANSSON_LIBS}
 
 .c.o:
-	${CC} ${INCS} ${DEFS} ${LIBBSD_INCS} ${KCGI_INCS} ${JANSSON_INCS} \
+	${CC} ${INCS} ${DEFS} ${KCGI_INCS} ${JANSSON_INCS} \
 		${CFLAGS} -MMD -c $< -o $@
 
 .in:
@@ -147,6 +148,8 @@
 	rm -f sciworkerd/sciworkerd ${SCIWORKERD_OBJS} ${SCIWORKERD_DEPS}
 	rm -f ${TESTS_OBJS} ${TESTS_DEPS}
 
+${TESTS_OBJS}: lib/libsci.a
+
 tests: lib/libsci.a ${TESTS_OBJS}
 	for t in ${TESTS_OBJS}; do $$t -v; done
 
--- a/config.def.h	Tue Jul 12 20:20:51 2022 +0200
+++ b/config.def.h	Fri Jul 15 11:11:48 2022 +0200
@@ -19,10 +19,6 @@
 #ifndef SCI_CONFIG_H
 #define SCI_CONFIG_H
 
-/* I/O limits */
-#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. */
@@ -30,4 +26,7 @@
 /* Usage limits. */
 #define SCI_JOB_LIST_MAX 128                            /* Jobs max list size. */
 
+/* Miscellaneous limits. */
+#define SCI_URL_MAX      256
+
 #endif /* !SCI_CONFIG_H */
--- a/config.mk	Tue Jul 12 20:20:51 2022 +0200
+++ b/config.mk	Fri Jul 15 11:11:48 2022 +0200
@@ -1,5 +1,5 @@
 CC=             cc
-CFLAGS=         -g -O0 -Wno-cpp
+CFLAGS=         -g -O0
 #CFLAGS=         -Wall -Wextra -fsanitize=address,undefined -g -O0
 #LDFLAGS=        -fsanitize=address,undefined
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/irccd.sh	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,37 @@
+#!/bin/sh
+
+set -e
+
+readonly wrkdir="$(mktemp -d /tmp/irccd-XXXXXX)"
+readonly repo="http://hg.malikania.fr/irccd"
+
+trap "cleanup" INT TERM EXIT
+
+cleanup()
+{
+	rm -rf $wrkdir
+}
+
+if [ "$#" -ne 1 ]; then
+	echo "abort: $(basename $0) revision" 1>&2
+	exit 1
+fi
+
+case $(uname -s) in
+Darwin)
+	extra_args="-DOPENSSL_ROOT_DIR=/usr/local/opt/openssl"
+	;;
+esac
+
+echo "=> Cloning repository $repo (revision $1) into $wkrdir"
+hg clone -r "$1" "$repo" "$wrkdir"
+cd "$wrkdir"
+
+echo "=> Configuring CMake"
+cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Debug -DIRCCD_WITH_TESTS=On $extra_args
+
+echo "=> Building"
+cmake --build build
+
+echo "=> Running test suite"
+cmake --build build --target test
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extern/LICENSE.libutlist.txt	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,21 @@
+Copyright (c) 2005-2022, Troy D. Hanson  https://troydhanson.github.io/uthash/
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extern/VERSION.libutlist.txt	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,1 @@
+2.3.0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extern/libutlist/utlist.h	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,1073 @@
+/*
+Copyright (c) 2007-2021, Troy D. Hanson   http://troydhanson.github.com/uthash/
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef UTLIST_H
+#define UTLIST_H
+
+#define UTLIST_VERSION 2.3.0
+
+#include <assert.h>
+
+/*
+ * This file contains macros to manipulate singly and doubly-linked lists.
+ *
+ * 1. LL_ macros:  singly-linked lists.
+ * 2. DL_ macros:  doubly-linked lists.
+ * 3. CDL_ macros: circular doubly-linked lists.
+ *
+ * To use singly-linked lists, your structure must have a "next" pointer.
+ * To use doubly-linked lists, your structure must "prev" and "next" pointers.
+ * Either way, the pointer to the head of the list must be initialized to NULL.
+ *
+ * ----------------.EXAMPLE -------------------------
+ * struct item {
+ *      int id;
+ *      struct item *prev, *next;
+ * }
+ *
+ * struct item *list = NULL:
+ *
+ * int main() {
+ *      struct item *item;
+ *      ... allocate and populate item ...
+ *      DL_APPEND(list, item);
+ * }
+ * --------------------------------------------------
+ *
+ * For doubly-linked lists, the append and delete macros are O(1)
+ * For singly-linked lists, append and delete are O(n) but prepend is O(1)
+ * The sort macro is O(n log(n)) for all types of single/double/circular lists.
+ */
+
+/* These macros use decltype or the earlier __typeof GNU extension.
+   As decltype is only available in newer compilers (VS2010 or gcc 4.3+
+   when compiling c++ source) this code uses whatever method is needed
+   or, for VS2008 where neither is available, uses casting workarounds. */
+#if !defined(LDECLTYPE) && !defined(NO_DECLTYPE)
+#if defined(_MSC_VER)   /* MS compiler */
+#if _MSC_VER >= 1600 && defined(__cplusplus)  /* VS2010 or newer in C++ mode */
+#define LDECLTYPE(x) decltype(x)
+#else                   /* VS2008 or older (or VS2010 in C mode) */
+#define NO_DECLTYPE
+#endif
+#elif defined(__BORLANDC__) || defined(__ICCARM__) || defined(__LCC__) || defined(__WATCOMC__)
+#define NO_DECLTYPE
+#else                   /* GNU, Sun and other compilers */
+#define LDECLTYPE(x) __typeof(x)
+#endif
+#endif
+
+/* for VS2008 we use some workarounds to get around the lack of decltype,
+ * namely, we always reassign our tmp variable to the list head if we need
+ * to dereference its prev/next pointers, and save/restore the real head.*/
+#ifdef NO_DECLTYPE
+#define IF_NO_DECLTYPE(x) x
+#define LDECLTYPE(x) char*
+#define UTLIST_SV(elt,list) _tmp = (char*)(list); {char **_alias = (char**)&(list); *_alias = (elt); }
+#define UTLIST_NEXT(elt,list,next) ((char*)((list)->next))
+#define UTLIST_NEXTASGN(elt,list,to,next) { char **_alias = (char**)&((list)->next); *_alias=(char*)(to); }
+/* #define UTLIST_PREV(elt,list,prev) ((char*)((list)->prev)) */
+#define UTLIST_PREVASGN(elt,list,to,prev) { char **_alias = (char**)&((list)->prev); *_alias=(char*)(to); }
+#define UTLIST_RS(list) { char **_alias = (char**)&(list); *_alias=_tmp; }
+#define UTLIST_CASTASGN(a,b) { char **_alias = (char**)&(a); *_alias=(char*)(b); }
+#else
+#define IF_NO_DECLTYPE(x)
+#define UTLIST_SV(elt,list)
+#define UTLIST_NEXT(elt,list,next) ((elt)->next)
+#define UTLIST_NEXTASGN(elt,list,to,next) ((elt)->next)=(to)
+/* #define UTLIST_PREV(elt,list,prev) ((elt)->prev) */
+#define UTLIST_PREVASGN(elt,list,to,prev) ((elt)->prev)=(to)
+#define UTLIST_RS(list)
+#define UTLIST_CASTASGN(a,b) (a)=(b)
+#endif
+
+/******************************************************************************
+ * The sort macro is an adaptation of Simon Tatham's O(n log(n)) mergesort    *
+ * Unwieldy variable names used here to avoid shadowing passed-in variables.  *
+ *****************************************************************************/
+#define LL_SORT(list, cmp)                                                                     \
+    LL_SORT2(list, cmp, next)
+
+#define LL_SORT2(list, cmp, next)                                                              \
+do {                                                                                           \
+  LDECLTYPE(list) _ls_p;                                                                       \
+  LDECLTYPE(list) _ls_q;                                                                       \
+  LDECLTYPE(list) _ls_e;                                                                       \
+  LDECLTYPE(list) _ls_tail;                                                                    \
+  IF_NO_DECLTYPE(LDECLTYPE(list) _tmp;)                                                        \
+  int _ls_insize, _ls_nmerges, _ls_psize, _ls_qsize, _ls_i, _ls_looping;                       \
+  if (list) {                                                                                  \
+    _ls_insize = 1;                                                                            \
+    _ls_looping = 1;                                                                           \
+    while (_ls_looping) {                                                                      \
+      UTLIST_CASTASGN(_ls_p,list);                                                             \
+      (list) = NULL;                                                                           \
+      _ls_tail = NULL;                                                                         \
+      _ls_nmerges = 0;                                                                         \
+      while (_ls_p) {                                                                          \
+        _ls_nmerges++;                                                                         \
+        _ls_q = _ls_p;                                                                         \
+        _ls_psize = 0;                                                                         \
+        for (_ls_i = 0; _ls_i < _ls_insize; _ls_i++) {                                         \
+          _ls_psize++;                                                                         \
+          UTLIST_SV(_ls_q,list); _ls_q = UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list);        \
+          if (!_ls_q) break;                                                                   \
+        }                                                                                      \
+        _ls_qsize = _ls_insize;                                                                \
+        while (_ls_psize > 0 || (_ls_qsize > 0 && _ls_q)) {                                    \
+          if (_ls_psize == 0) {                                                                \
+            _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q =                                      \
+              UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--;                      \
+          } else if (_ls_qsize == 0 || !_ls_q) {                                               \
+            _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p =                                      \
+              UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--;                      \
+          } else if (cmp(_ls_p,_ls_q) <= 0) {                                                  \
+            _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p =                                      \
+              UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--;                      \
+          } else {                                                                             \
+            _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q =                                      \
+              UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--;                      \
+          }                                                                                    \
+          if (_ls_tail) {                                                                      \
+            UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,_ls_e,next); UTLIST_RS(list); \
+          } else {                                                                             \
+            UTLIST_CASTASGN(list,_ls_e);                                                       \
+          }                                                                                    \
+          _ls_tail = _ls_e;                                                                    \
+        }                                                                                      \
+        _ls_p = _ls_q;                                                                         \
+      }                                                                                        \
+      if (_ls_tail) {                                                                          \
+        UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,NULL,next); UTLIST_RS(list);   \
+      }                                                                                        \
+      if (_ls_nmerges <= 1) {                                                                  \
+        _ls_looping=0;                                                                         \
+      }                                                                                        \
+      _ls_insize *= 2;                                                                         \
+    }                                                                                          \
+  }                                                                                            \
+} while (0)
+
+
+#define DL_SORT(list, cmp)                                                                     \
+    DL_SORT2(list, cmp, prev, next)
+
+#define DL_SORT2(list, cmp, prev, next)                                                        \
+do {                                                                                           \
+  LDECLTYPE(list) _ls_p;                                                                       \
+  LDECLTYPE(list) _ls_q;                                                                       \
+  LDECLTYPE(list) _ls_e;                                                                       \
+  LDECLTYPE(list) _ls_tail;                                                                    \
+  IF_NO_DECLTYPE(LDECLTYPE(list) _tmp;)                                                        \
+  int _ls_insize, _ls_nmerges, _ls_psize, _ls_qsize, _ls_i, _ls_looping;                       \
+  if (list) {                                                                                  \
+    _ls_insize = 1;                                                                            \
+    _ls_looping = 1;                                                                           \
+    while (_ls_looping) {                                                                      \
+      UTLIST_CASTASGN(_ls_p,list);                                                             \
+      (list) = NULL;                                                                           \
+      _ls_tail = NULL;                                                                         \
+      _ls_nmerges = 0;                                                                         \
+      while (_ls_p) {                                                                          \
+        _ls_nmerges++;                                                                         \
+        _ls_q = _ls_p;                                                                         \
+        _ls_psize = 0;                                                                         \
+        for (_ls_i = 0; _ls_i < _ls_insize; _ls_i++) {                                         \
+          _ls_psize++;                                                                         \
+          UTLIST_SV(_ls_q,list); _ls_q = UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list);        \
+          if (!_ls_q) break;                                                                   \
+        }                                                                                      \
+        _ls_qsize = _ls_insize;                                                                \
+        while ((_ls_psize > 0) || ((_ls_qsize > 0) && _ls_q)) {                                \
+          if (_ls_psize == 0) {                                                                \
+            _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q =                                      \
+              UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--;                      \
+          } else if ((_ls_qsize == 0) || (!_ls_q)) {                                           \
+            _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p =                                      \
+              UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--;                      \
+          } else if (cmp(_ls_p,_ls_q) <= 0) {                                                  \
+            _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p =                                      \
+              UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--;                      \
+          } else {                                                                             \
+            _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q =                                      \
+              UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--;                      \
+          }                                                                                    \
+          if (_ls_tail) {                                                                      \
+            UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,_ls_e,next); UTLIST_RS(list); \
+          } else {                                                                             \
+            UTLIST_CASTASGN(list,_ls_e);                                                       \
+          }                                                                                    \
+          UTLIST_SV(_ls_e,list); UTLIST_PREVASGN(_ls_e,list,_ls_tail,prev); UTLIST_RS(list);   \
+          _ls_tail = _ls_e;                                                                    \
+        }                                                                                      \
+        _ls_p = _ls_q;                                                                         \
+      }                                                                                        \
+      UTLIST_CASTASGN((list)->prev, _ls_tail);                                                 \
+      UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,NULL,next); UTLIST_RS(list);     \
+      if (_ls_nmerges <= 1) {                                                                  \
+        _ls_looping=0;                                                                         \
+      }                                                                                        \
+      _ls_insize *= 2;                                                                         \
+    }                                                                                          \
+  }                                                                                            \
+} while (0)
+
+#define CDL_SORT(list, cmp)                                                                    \
+    CDL_SORT2(list, cmp, prev, next)
+
+#define CDL_SORT2(list, cmp, prev, next)                                                       \
+do {                                                                                           \
+  LDECLTYPE(list) _ls_p;                                                                       \
+  LDECLTYPE(list) _ls_q;                                                                       \
+  LDECLTYPE(list) _ls_e;                                                                       \
+  LDECLTYPE(list) _ls_tail;                                                                    \
+  LDECLTYPE(list) _ls_oldhead;                                                                 \
+  LDECLTYPE(list) _tmp;                                                                        \
+  int _ls_insize, _ls_nmerges, _ls_psize, _ls_qsize, _ls_i, _ls_looping;                       \
+  if (list) {                                                                                  \
+    _ls_insize = 1;                                                                            \
+    _ls_looping = 1;                                                                           \
+    while (_ls_looping) {                                                                      \
+      UTLIST_CASTASGN(_ls_p,list);                                                             \
+      UTLIST_CASTASGN(_ls_oldhead,list);                                                       \
+      (list) = NULL;                                                                           \
+      _ls_tail = NULL;                                                                         \
+      _ls_nmerges = 0;                                                                         \
+      while (_ls_p) {                                                                          \
+        _ls_nmerges++;                                                                         \
+        _ls_q = _ls_p;                                                                         \
+        _ls_psize = 0;                                                                         \
+        for (_ls_i = 0; _ls_i < _ls_insize; _ls_i++) {                                         \
+          _ls_psize++;                                                                         \
+          UTLIST_SV(_ls_q,list);                                                               \
+          if (UTLIST_NEXT(_ls_q,list,next) == _ls_oldhead) {                                   \
+            _ls_q = NULL;                                                                      \
+          } else {                                                                             \
+            _ls_q = UTLIST_NEXT(_ls_q,list,next);                                              \
+          }                                                                                    \
+          UTLIST_RS(list);                                                                     \
+          if (!_ls_q) break;                                                                   \
+        }                                                                                      \
+        _ls_qsize = _ls_insize;                                                                \
+        while (_ls_psize > 0 || (_ls_qsize > 0 && _ls_q)) {                                    \
+          if (_ls_psize == 0) {                                                                \
+            _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q =                                      \
+              UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--;                      \
+            if (_ls_q == _ls_oldhead) { _ls_q = NULL; }                                        \
+          } else if (_ls_qsize == 0 || !_ls_q) {                                               \
+            _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p =                                      \
+              UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--;                      \
+            if (_ls_p == _ls_oldhead) { _ls_p = NULL; }                                        \
+          } else if (cmp(_ls_p,_ls_q) <= 0) {                                                  \
+            _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p =                                      \
+              UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--;                      \
+            if (_ls_p == _ls_oldhead) { _ls_p = NULL; }                                        \
+          } else {                                                                             \
+            _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q =                                      \
+              UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--;                      \
+            if (_ls_q == _ls_oldhead) { _ls_q = NULL; }                                        \
+          }                                                                                    \
+          if (_ls_tail) {                                                                      \
+            UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,_ls_e,next); UTLIST_RS(list); \
+          } else {                                                                             \
+            UTLIST_CASTASGN(list,_ls_e);                                                       \
+          }                                                                                    \
+          UTLIST_SV(_ls_e,list); UTLIST_PREVASGN(_ls_e,list,_ls_tail,prev); UTLIST_RS(list);   \
+          _ls_tail = _ls_e;                                                                    \
+        }                                                                                      \
+        _ls_p = _ls_q;                                                                         \
+      }                                                                                        \
+      UTLIST_CASTASGN((list)->prev,_ls_tail);                                                  \
+      UTLIST_CASTASGN(_tmp,list);                                                              \
+      UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,_tmp,next); UTLIST_RS(list);     \
+      if (_ls_nmerges <= 1) {                                                                  \
+        _ls_looping=0;                                                                         \
+      }                                                                                        \
+      _ls_insize *= 2;                                                                         \
+    }                                                                                          \
+  }                                                                                            \
+} while (0)
+
+/******************************************************************************
+ * singly linked list macros (non-circular)                                   *
+ *****************************************************************************/
+#define LL_PREPEND(head,add)                                                                   \
+    LL_PREPEND2(head,add,next)
+
+#define LL_PREPEND2(head,add,next)                                                             \
+do {                                                                                           \
+  (add)->next = (head);                                                                        \
+  (head) = (add);                                                                              \
+} while (0)
+
+#define LL_CONCAT(head1,head2)                                                                 \
+    LL_CONCAT2(head1,head2,next)
+
+#define LL_CONCAT2(head1,head2,next)                                                           \
+do {                                                                                           \
+  LDECLTYPE(head1) _tmp;                                                                       \
+  if (head1) {                                                                                 \
+    _tmp = (head1);                                                                            \
+    while (_tmp->next) { _tmp = _tmp->next; }                                                  \
+    _tmp->next=(head2);                                                                        \
+  } else {                                                                                     \
+    (head1)=(head2);                                                                           \
+  }                                                                                            \
+} while (0)
+
+#define LL_APPEND(head,add)                                                                    \
+    LL_APPEND2(head,add,next)
+
+#define LL_APPEND2(head,add,next)                                                              \
+do {                                                                                           \
+  LDECLTYPE(head) _tmp;                                                                        \
+  (add)->next=NULL;                                                                            \
+  if (head) {                                                                                  \
+    _tmp = (head);                                                                             \
+    while (_tmp->next) { _tmp = _tmp->next; }                                                  \
+    _tmp->next=(add);                                                                          \
+  } else {                                                                                     \
+    (head)=(add);                                                                              \
+  }                                                                                            \
+} while (0)
+
+#define LL_INSERT_INORDER(head,add,cmp)                                                        \
+    LL_INSERT_INORDER2(head,add,cmp,next)
+
+#define LL_INSERT_INORDER2(head,add,cmp,next)                                                  \
+do {                                                                                           \
+  LDECLTYPE(head) _tmp;                                                                        \
+  if (head) {                                                                                  \
+    LL_LOWER_BOUND2(head, _tmp, add, cmp, next);                                               \
+    LL_APPEND_ELEM2(head, _tmp, add, next);                                                    \
+  } else {                                                                                     \
+    (head) = (add);                                                                            \
+    (head)->next = NULL;                                                                       \
+  }                                                                                            \
+} while (0)
+
+#define LL_LOWER_BOUND(head,elt,like,cmp)                                                      \
+    LL_LOWER_BOUND2(head,elt,like,cmp,next)
+
+#define LL_LOWER_BOUND2(head,elt,like,cmp,next)                                                \
+  do {                                                                                         \
+    if ((head) == NULL || (cmp(head, like)) >= 0) {                                            \
+      (elt) = NULL;                                                                            \
+    } else {                                                                                   \
+      for ((elt) = (head); (elt)->next != NULL; (elt) = (elt)->next) {                         \
+        if (cmp((elt)->next, like) >= 0) {                                                     \
+          break;                                                                               \
+        }                                                                                      \
+      }                                                                                        \
+    }                                                                                          \
+  } while (0)
+
+#define LL_DELETE(head,del)                                                                    \
+    LL_DELETE2(head,del,next)
+
+#define LL_DELETE2(head,del,next)                                                              \
+do {                                                                                           \
+  LDECLTYPE(head) _tmp;                                                                        \
+  if ((head) == (del)) {                                                                       \
+    (head)=(head)->next;                                                                       \
+  } else {                                                                                     \
+    _tmp = (head);                                                                             \
+    while (_tmp->next && (_tmp->next != (del))) {                                              \
+      _tmp = _tmp->next;                                                                       \
+    }                                                                                          \
+    if (_tmp->next) {                                                                          \
+      _tmp->next = (del)->next;                                                                \
+    }                                                                                          \
+  }                                                                                            \
+} while (0)
+
+#define LL_COUNT(head,el,counter)                                                              \
+    LL_COUNT2(head,el,counter,next)                                                            \
+
+#define LL_COUNT2(head,el,counter,next)                                                        \
+do {                                                                                           \
+  (counter) = 0;                                                                               \
+  LL_FOREACH2(head,el,next) { ++(counter); }                                                   \
+} while (0)
+
+#define LL_FOREACH(head,el)                                                                    \
+    LL_FOREACH2(head,el,next)
+
+#define LL_FOREACH2(head,el,next)                                                              \
+    for ((el) = (head); el; (el) = (el)->next)
+
+#define LL_FOREACH_SAFE(head,el,tmp)                                                           \
+    LL_FOREACH_SAFE2(head,el,tmp,next)
+
+#define LL_FOREACH_SAFE2(head,el,tmp,next)                                                     \
+  for ((el) = (head); (el) && ((tmp) = (el)->next, 1); (el) = (tmp))
+
+#define LL_SEARCH_SCALAR(head,out,field,val)                                                   \
+    LL_SEARCH_SCALAR2(head,out,field,val,next)
+
+#define LL_SEARCH_SCALAR2(head,out,field,val,next)                                             \
+do {                                                                                           \
+    LL_FOREACH2(head,out,next) {                                                               \
+      if ((out)->field == (val)) break;                                                        \
+    }                                                                                          \
+} while (0)
+
+#define LL_SEARCH(head,out,elt,cmp)                                                            \
+    LL_SEARCH2(head,out,elt,cmp,next)
+
+#define LL_SEARCH2(head,out,elt,cmp,next)                                                      \
+do {                                                                                           \
+    LL_FOREACH2(head,out,next) {                                                               \
+      if ((cmp(out,elt))==0) break;                                                            \
+    }                                                                                          \
+} while (0)
+
+#define LL_REPLACE_ELEM2(head, el, add, next)                                                  \
+do {                                                                                           \
+ LDECLTYPE(head) _tmp;                                                                         \
+ assert((head) != NULL);                                                                       \
+ assert((el) != NULL);                                                                         \
+ assert((add) != NULL);                                                                        \
+ (add)->next = (el)->next;                                                                     \
+ if ((head) == (el)) {                                                                         \
+  (head) = (add);                                                                              \
+ } else {                                                                                      \
+  _tmp = (head);                                                                               \
+  while (_tmp->next && (_tmp->next != (el))) {                                                 \
+   _tmp = _tmp->next;                                                                          \
+  }                                                                                            \
+  if (_tmp->next) {                                                                            \
+    _tmp->next = (add);                                                                        \
+  }                                                                                            \
+ }                                                                                             \
+} while (0)
+
+#define LL_REPLACE_ELEM(head, el, add)                                                         \
+    LL_REPLACE_ELEM2(head, el, add, next)
+
+#define LL_PREPEND_ELEM2(head, el, add, next)                                                  \
+do {                                                                                           \
+ if (el) {                                                                                     \
+  LDECLTYPE(head) _tmp;                                                                        \
+  assert((head) != NULL);                                                                      \
+  assert((add) != NULL);                                                                       \
+  (add)->next = (el);                                                                          \
+  if ((head) == (el)) {                                                                        \
+   (head) = (add);                                                                             \
+  } else {                                                                                     \
+   _tmp = (head);                                                                              \
+   while (_tmp->next && (_tmp->next != (el))) {                                                \
+    _tmp = _tmp->next;                                                                         \
+   }                                                                                           \
+   if (_tmp->next) {                                                                           \
+     _tmp->next = (add);                                                                       \
+   }                                                                                           \
+  }                                                                                            \
+ } else {                                                                                      \
+  LL_APPEND2(head, add, next);                                                                 \
+ }                                                                                             \
+} while (0)                                                                                    \
+
+#define LL_PREPEND_ELEM(head, el, add)                                                         \
+    LL_PREPEND_ELEM2(head, el, add, next)
+
+#define LL_APPEND_ELEM2(head, el, add, next)                                                   \
+do {                                                                                           \
+ if (el) {                                                                                     \
+  assert((head) != NULL);                                                                      \
+  assert((add) != NULL);                                                                       \
+  (add)->next = (el)->next;                                                                    \
+  (el)->next = (add);                                                                          \
+ } else {                                                                                      \
+  LL_PREPEND2(head, add, next);                                                                \
+ }                                                                                             \
+} while (0)                                                                                    \
+
+#define LL_APPEND_ELEM(head, el, add)                                                          \
+    LL_APPEND_ELEM2(head, el, add, next)
+
+#ifdef NO_DECLTYPE
+/* Here are VS2008 / NO_DECLTYPE replacements for a few functions */
+
+#undef LL_CONCAT2
+#define LL_CONCAT2(head1,head2,next)                                                           \
+do {                                                                                           \
+  char *_tmp;                                                                                  \
+  if (head1) {                                                                                 \
+    _tmp = (char*)(head1);                                                                     \
+    while ((head1)->next) { (head1) = (head1)->next; }                                         \
+    (head1)->next = (head2);                                                                   \
+    UTLIST_RS(head1);                                                                          \
+  } else {                                                                                     \
+    (head1)=(head2);                                                                           \
+  }                                                                                            \
+} while (0)
+
+#undef LL_APPEND2
+#define LL_APPEND2(head,add,next)                                                              \
+do {                                                                                           \
+  if (head) {                                                                                  \
+    (add)->next = head;     /* use add->next as a temp variable */                             \
+    while ((add)->next->next) { (add)->next = (add)->next->next; }                             \
+    (add)->next->next=(add);                                                                   \
+  } else {                                                                                     \
+    (head)=(add);                                                                              \
+  }                                                                                            \
+  (add)->next=NULL;                                                                            \
+} while (0)
+
+#undef LL_INSERT_INORDER2
+#define LL_INSERT_INORDER2(head,add,cmp,next)                                                  \
+do {                                                                                           \
+  if ((head) == NULL || (cmp(head, add)) >= 0) {                                               \
+    (add)->next = (head);                                                                      \
+    (head) = (add);                                                                            \
+  } else {                                                                                     \
+    char *_tmp = (char*)(head);                                                                \
+    while ((head)->next != NULL && (cmp((head)->next, add)) < 0) {                             \
+      (head) = (head)->next;                                                                   \
+    }                                                                                          \
+    (add)->next = (head)->next;                                                                \
+    (head)->next = (add);                                                                      \
+    UTLIST_RS(head);                                                                           \
+  }                                                                                            \
+} while (0)
+
+#undef LL_DELETE2
+#define LL_DELETE2(head,del,next)                                                              \
+do {                                                                                           \
+  if ((head) == (del)) {                                                                       \
+    (head)=(head)->next;                                                                       \
+  } else {                                                                                     \
+    char *_tmp = (char*)(head);                                                                \
+    while ((head)->next && ((head)->next != (del))) {                                          \
+      (head) = (head)->next;                                                                   \
+    }                                                                                          \
+    if ((head)->next) {                                                                        \
+      (head)->next = ((del)->next);                                                            \
+    }                                                                                          \
+    UTLIST_RS(head);                                                                           \
+  }                                                                                            \
+} while (0)
+
+#undef LL_REPLACE_ELEM2
+#define LL_REPLACE_ELEM2(head, el, add, next)                                                  \
+do {                                                                                           \
+  assert((head) != NULL);                                                                      \
+  assert((el) != NULL);                                                                        \
+  assert((add) != NULL);                                                                       \
+  if ((head) == (el)) {                                                                        \
+    (head) = (add);                                                                            \
+  } else {                                                                                     \
+    (add)->next = head;                                                                        \
+    while ((add)->next->next && ((add)->next->next != (el))) {                                 \
+      (add)->next = (add)->next->next;                                                         \
+    }                                                                                          \
+    if ((add)->next->next) {                                                                   \
+      (add)->next->next = (add);                                                               \
+    }                                                                                          \
+  }                                                                                            \
+  (add)->next = (el)->next;                                                                    \
+} while (0)
+
+#undef LL_PREPEND_ELEM2
+#define LL_PREPEND_ELEM2(head, el, add, next)                                                  \
+do {                                                                                           \
+  if (el) {                                                                                    \
+    assert((head) != NULL);                                                                    \
+    assert((add) != NULL);                                                                     \
+    if ((head) == (el)) {                                                                      \
+      (head) = (add);                                                                          \
+    } else {                                                                                   \
+      (add)->next = (head);                                                                    \
+      while ((add)->next->next && ((add)->next->next != (el))) {                               \
+        (add)->next = (add)->next->next;                                                       \
+      }                                                                                        \
+      if ((add)->next->next) {                                                                 \
+        (add)->next->next = (add);                                                             \
+      }                                                                                        \
+    }                                                                                          \
+    (add)->next = (el);                                                                        \
+  } else {                                                                                     \
+    LL_APPEND2(head, add, next);                                                               \
+  }                                                                                            \
+} while (0)                                                                                    \
+
+#endif /* NO_DECLTYPE */
+
+/******************************************************************************
+ * doubly linked list macros (non-circular)                                   *
+ *****************************************************************************/
+#define DL_PREPEND(head,add)                                                                   \
+    DL_PREPEND2(head,add,prev,next)
+
+#define DL_PREPEND2(head,add,prev,next)                                                        \
+do {                                                                                           \
+ (add)->next = (head);                                                                         \
+ if (head) {                                                                                   \
+   (add)->prev = (head)->prev;                                                                 \
+   (head)->prev = (add);                                                                       \
+ } else {                                                                                      \
+   (add)->prev = (add);                                                                        \
+ }                                                                                             \
+ (head) = (add);                                                                               \
+} while (0)
+
+#define DL_APPEND(head,add)                                                                    \
+    DL_APPEND2(head,add,prev,next)
+
+#define DL_APPEND2(head,add,prev,next)                                                         \
+do {                                                                                           \
+  if (head) {                                                                                  \
+      (add)->prev = (head)->prev;                                                              \
+      (head)->prev->next = (add);                                                              \
+      (head)->prev = (add);                                                                    \
+      (add)->next = NULL;                                                                      \
+  } else {                                                                                     \
+      (head)=(add);                                                                            \
+      (head)->prev = (head);                                                                   \
+      (head)->next = NULL;                                                                     \
+  }                                                                                            \
+} while (0)
+
+#define DL_INSERT_INORDER(head,add,cmp)                                                        \
+    DL_INSERT_INORDER2(head,add,cmp,prev,next)
+
+#define DL_INSERT_INORDER2(head,add,cmp,prev,next)                                             \
+do {                                                                                           \
+  LDECLTYPE(head) _tmp;                                                                        \
+  if (head) {                                                                                  \
+    DL_LOWER_BOUND2(head, _tmp, add, cmp, next);                                               \
+    DL_APPEND_ELEM2(head, _tmp, add, prev, next);                                              \
+  } else {                                                                                     \
+    (head) = (add);                                                                            \
+    (head)->prev = (head);                                                                     \
+    (head)->next = NULL;                                                                       \
+  }                                                                                            \
+} while (0)
+
+#define DL_LOWER_BOUND(head,elt,like,cmp)                                                      \
+    DL_LOWER_BOUND2(head,elt,like,cmp,next)
+
+#define DL_LOWER_BOUND2(head,elt,like,cmp,next)                                                \
+do {                                                                                           \
+  if ((head) == NULL || (cmp(head, like)) >= 0) {                                              \
+    (elt) = NULL;                                                                              \
+  } else {                                                                                     \
+    for ((elt) = (head); (elt)->next != NULL; (elt) = (elt)->next) {                           \
+      if ((cmp((elt)->next, like)) >= 0) {                                                     \
+        break;                                                                                 \
+      }                                                                                        \
+    }                                                                                          \
+  }                                                                                            \
+} while (0)
+
+#define DL_CONCAT(head1,head2)                                                                 \
+    DL_CONCAT2(head1,head2,prev,next)
+
+#define DL_CONCAT2(head1,head2,prev,next)                                                      \
+do {                                                                                           \
+  LDECLTYPE(head1) _tmp;                                                                       \
+  if (head2) {                                                                                 \
+    if (head1) {                                                                               \
+        UTLIST_CASTASGN(_tmp, (head2)->prev);                                                  \
+        (head2)->prev = (head1)->prev;                                                         \
+        (head1)->prev->next = (head2);                                                         \
+        UTLIST_CASTASGN((head1)->prev, _tmp);                                                  \
+    } else {                                                                                   \
+        (head1)=(head2);                                                                       \
+    }                                                                                          \
+  }                                                                                            \
+} while (0)
+
+#define DL_DELETE(head,del)                                                                    \
+    DL_DELETE2(head,del,prev,next)
+
+#define DL_DELETE2(head,del,prev,next)                                                         \
+do {                                                                                           \
+  assert((head) != NULL);                                                                      \
+  assert((del)->prev != NULL);                                                                 \
+  if ((del)->prev == (del)) {                                                                  \
+      (head)=NULL;                                                                             \
+  } else if ((del)==(head)) {                                                                  \
+      (del)->next->prev = (del)->prev;                                                         \
+      (head) = (del)->next;                                                                    \
+  } else {                                                                                     \
+      (del)->prev->next = (del)->next;                                                         \
+      if ((del)->next) {                                                                       \
+          (del)->next->prev = (del)->prev;                                                     \
+      } else {                                                                                 \
+          (head)->prev = (del)->prev;                                                          \
+      }                                                                                        \
+  }                                                                                            \
+} while (0)
+
+#define DL_COUNT(head,el,counter)                                                              \
+    DL_COUNT2(head,el,counter,next)                                                            \
+
+#define DL_COUNT2(head,el,counter,next)                                                        \
+do {                                                                                           \
+  (counter) = 0;                                                                               \
+  DL_FOREACH2(head,el,next) { ++(counter); }                                                   \
+} while (0)
+
+#define DL_FOREACH(head,el)                                                                    \
+    DL_FOREACH2(head,el,next)
+
+#define DL_FOREACH2(head,el,next)                                                              \
+    for ((el) = (head); el; (el) = (el)->next)
+
+/* this version is safe for deleting the elements during iteration */
+#define DL_FOREACH_SAFE(head,el,tmp)                                                           \
+    DL_FOREACH_SAFE2(head,el,tmp,next)
+
+#define DL_FOREACH_SAFE2(head,el,tmp,next)                                                     \
+  for ((el) = (head); (el) && ((tmp) = (el)->next, 1); (el) = (tmp))
+
+/* these are identical to their singly-linked list counterparts */
+#define DL_SEARCH_SCALAR LL_SEARCH_SCALAR
+#define DL_SEARCH LL_SEARCH
+#define DL_SEARCH_SCALAR2 LL_SEARCH_SCALAR2
+#define DL_SEARCH2 LL_SEARCH2
+
+#define DL_REPLACE_ELEM2(head, el, add, prev, next)                                            \
+do {                                                                                           \
+ assert((head) != NULL);                                                                       \
+ assert((el) != NULL);                                                                         \
+ assert((add) != NULL);                                                                        \
+ if ((head) == (el)) {                                                                         \
+  (head) = (add);                                                                              \
+  (add)->next = (el)->next;                                                                    \
+  if ((el)->next == NULL) {                                                                    \
+   (add)->prev = (add);                                                                        \
+  } else {                                                                                     \
+   (add)->prev = (el)->prev;                                                                   \
+   (add)->next->prev = (add);                                                                  \
+  }                                                                                            \
+ } else {                                                                                      \
+  (add)->next = (el)->next;                                                                    \
+  (add)->prev = (el)->prev;                                                                    \
+  (add)->prev->next = (add);                                                                   \
+  if ((el)->next == NULL) {                                                                    \
+   (head)->prev = (add);                                                                       \
+  } else {                                                                                     \
+   (add)->next->prev = (add);                                                                  \
+  }                                                                                            \
+ }                                                                                             \
+} while (0)
+
+#define DL_REPLACE_ELEM(head, el, add)                                                         \
+    DL_REPLACE_ELEM2(head, el, add, prev, next)
+
+#define DL_PREPEND_ELEM2(head, el, add, prev, next)                                            \
+do {                                                                                           \
+ if (el) {                                                                                     \
+  assert((head) != NULL);                                                                      \
+  assert((add) != NULL);                                                                       \
+  (add)->next = (el);                                                                          \
+  (add)->prev = (el)->prev;                                                                    \
+  (el)->prev = (add);                                                                          \
+  if ((head) == (el)) {                                                                        \
+   (head) = (add);                                                                             \
+  } else {                                                                                     \
+   (add)->prev->next = (add);                                                                  \
+  }                                                                                            \
+ } else {                                                                                      \
+  DL_APPEND2(head, add, prev, next);                                                           \
+ }                                                                                             \
+} while (0)                                                                                    \
+
+#define DL_PREPEND_ELEM(head, el, add)                                                         \
+    DL_PREPEND_ELEM2(head, el, add, prev, next)
+
+#define DL_APPEND_ELEM2(head, el, add, prev, next)                                             \
+do {                                                                                           \
+ if (el) {                                                                                     \
+  assert((head) != NULL);                                                                      \
+  assert((add) != NULL);                                                                       \
+  (add)->next = (el)->next;                                                                    \
+  (add)->prev = (el);                                                                          \
+  (el)->next = (add);                                                                          \
+  if ((add)->next) {                                                                           \
+   (add)->next->prev = (add);                                                                  \
+  } else {                                                                                     \
+   (head)->prev = (add);                                                                       \
+  }                                                                                            \
+ } else {                                                                                      \
+  DL_PREPEND2(head, add, prev, next);                                                          \
+ }                                                                                             \
+} while (0)                                                                                    \
+
+#define DL_APPEND_ELEM(head, el, add)                                                          \
+   DL_APPEND_ELEM2(head, el, add, prev, next)
+
+#ifdef NO_DECLTYPE
+/* Here are VS2008 / NO_DECLTYPE replacements for a few functions */
+
+#undef DL_INSERT_INORDER2
+#define DL_INSERT_INORDER2(head,add,cmp,prev,next)                                             \
+do {                                                                                           \
+  if ((head) == NULL) {                                                                        \
+    (add)->prev = (add);                                                                       \
+    (add)->next = NULL;                                                                        \
+    (head) = (add);                                                                            \
+  } else if ((cmp(head, add)) >= 0) {                                                          \
+    (add)->prev = (head)->prev;                                                                \
+    (add)->next = (head);                                                                      \
+    (head)->prev = (add);                                                                      \
+    (head) = (add);                                                                            \
+  } else {                                                                                     \
+    char *_tmp = (char*)(head);                                                                \
+    while ((head)->next && (cmp((head)->next, add)) < 0) {                                     \
+      (head) = (head)->next;                                                                   \
+    }                                                                                          \
+    (add)->prev = (head);                                                                      \
+    (add)->next = (head)->next;                                                                \
+    (head)->next = (add);                                                                      \
+    UTLIST_RS(head);                                                                           \
+    if ((add)->next) {                                                                         \
+      (add)->next->prev = (add);                                                               \
+    } else {                                                                                   \
+      (head)->prev = (add);                                                                    \
+    }                                                                                          \
+  }                                                                                            \
+} while (0)
+#endif /* NO_DECLTYPE */
+
+/******************************************************************************
+ * circular doubly linked list macros                                         *
+ *****************************************************************************/
+#define CDL_APPEND(head,add)                                                                   \
+    CDL_APPEND2(head,add,prev,next)
+
+#define CDL_APPEND2(head,add,prev,next)                                                        \
+do {                                                                                           \
+ if (head) {                                                                                   \
+   (add)->prev = (head)->prev;                                                                 \
+   (add)->next = (head);                                                                       \
+   (head)->prev = (add);                                                                       \
+   (add)->prev->next = (add);                                                                  \
+ } else {                                                                                      \
+   (add)->prev = (add);                                                                        \
+   (add)->next = (add);                                                                        \
+   (head) = (add);                                                                             \
+ }                                                                                             \
+} while (0)
+
+#define CDL_PREPEND(head,add)                                                                  \
+    CDL_PREPEND2(head,add,prev,next)
+
+#define CDL_PREPEND2(head,add,prev,next)                                                       \
+do {                                                                                           \
+ if (head) {                                                                                   \
+   (add)->prev = (head)->prev;                                                                 \
+   (add)->next = (head);                                                                       \
+   (head)->prev = (add);                                                                       \
+   (add)->prev->next = (add);                                                                  \
+ } else {                                                                                      \
+   (add)->prev = (add);                                                                        \
+   (add)->next = (add);                                                                        \
+ }                                                                                             \
+ (head) = (add);                                                                               \
+} while (0)
+
+#define CDL_INSERT_INORDER(head,add,cmp)                                                       \
+    CDL_INSERT_INORDER2(head,add,cmp,prev,next)
+
+#define CDL_INSERT_INORDER2(head,add,cmp,prev,next)                                            \
+do {                                                                                           \
+  LDECLTYPE(head) _tmp;                                                                        \
+  if (head) {                                                                                  \
+    CDL_LOWER_BOUND2(head, _tmp, add, cmp, next);                                              \
+    CDL_APPEND_ELEM2(head, _tmp, add, prev, next);                                             \
+  } else {                                                                                     \
+    (head) = (add);                                                                            \
+    (head)->next = (head);                                                                     \
+    (head)->prev = (head);                                                                     \
+  }                                                                                            \
+} while (0)
+
+#define CDL_LOWER_BOUND(head,elt,like,cmp)                                                     \
+    CDL_LOWER_BOUND2(head,elt,like,cmp,next)
+
+#define CDL_LOWER_BOUND2(head,elt,like,cmp,next)                                               \
+do {                                                                                           \
+  if ((head) == NULL || (cmp(head, like)) >= 0) {                                              \
+    (elt) = NULL;                                                                              \
+  } else {                                                                                     \
+    for ((elt) = (head); (elt)->next != (head); (elt) = (elt)->next) {                         \
+      if ((cmp((elt)->next, like)) >= 0) {                                                     \
+        break;                                                                                 \
+      }                                                                                        \
+    }                                                                                          \
+  }                                                                                            \
+} while (0)
+
+#define CDL_DELETE(head,del)                                                                   \
+    CDL_DELETE2(head,del,prev,next)
+
+#define CDL_DELETE2(head,del,prev,next)                                                        \
+do {                                                                                           \
+  if (((head)==(del)) && ((head)->next == (head))) {                                           \
+      (head) = NULL;                                                                           \
+  } else {                                                                                     \
+     (del)->next->prev = (del)->prev;                                                          \
+     (del)->prev->next = (del)->next;                                                          \
+     if ((del) == (head)) (head)=(del)->next;                                                  \
+  }                                                                                            \
+} while (0)
+
+#define CDL_COUNT(head,el,counter)                                                             \
+    CDL_COUNT2(head,el,counter,next)                                                           \
+
+#define CDL_COUNT2(head, el, counter,next)                                                     \
+do {                                                                                           \
+  (counter) = 0;                                                                               \
+  CDL_FOREACH2(head,el,next) { ++(counter); }                                                  \
+} while (0)
+
+#define CDL_FOREACH(head,el)                                                                   \
+    CDL_FOREACH2(head,el,next)
+
+#define CDL_FOREACH2(head,el,next)                                                             \
+    for ((el)=(head);el;(el)=(((el)->next==(head)) ? NULL : (el)->next))
+
+#define CDL_FOREACH_SAFE(head,el,tmp1,tmp2)                                                    \
+    CDL_FOREACH_SAFE2(head,el,tmp1,tmp2,prev,next)
+
+#define CDL_FOREACH_SAFE2(head,el,tmp1,tmp2,prev,next)                                         \
+  for ((el) = (head), (tmp1) = (head) ? (head)->prev : NULL;                                   \
+       (el) && ((tmp2) = (el)->next, 1);                                                       \
+       (el) = ((el) == (tmp1) ? NULL : (tmp2)))
+
+#define CDL_SEARCH_SCALAR(head,out,field,val)                                                  \
+    CDL_SEARCH_SCALAR2(head,out,field,val,next)
+
+#define CDL_SEARCH_SCALAR2(head,out,field,val,next)                                            \
+do {                                                                                           \
+    CDL_FOREACH2(head,out,next) {                                                              \
+      if ((out)->field == (val)) break;                                                        \
+    }                                                                                          \
+} while (0)
+
+#define CDL_SEARCH(head,out,elt,cmp)                                                           \
+    CDL_SEARCH2(head,out,elt,cmp,next)
+
+#define CDL_SEARCH2(head,out,elt,cmp,next)                                                     \
+do {                                                                                           \
+    CDL_FOREACH2(head,out,next) {                                                              \
+      if ((cmp(out,elt))==0) break;                                                            \
+    }                                                                                          \
+} while (0)
+
+#define CDL_REPLACE_ELEM2(head, el, add, prev, next)                                           \
+do {                                                                                           \
+ assert((head) != NULL);                                                                       \
+ assert((el) != NULL);                                                                         \
+ assert((add) != NULL);                                                                        \
+ if ((el)->next == (el)) {                                                                     \
+  (add)->next = (add);                                                                         \
+  (add)->prev = (add);                                                                         \
+  (head) = (add);                                                                              \
+ } else {                                                                                      \
+  (add)->next = (el)->next;                                                                    \
+  (add)->prev = (el)->prev;                                                                    \
+  (add)->next->prev = (add);                                                                   \
+  (add)->prev->next = (add);                                                                   \
+  if ((head) == (el)) {                                                                        \
+   (head) = (add);                                                                             \
+  }                                                                                            \
+ }                                                                                             \
+} while (0)
+
+#define CDL_REPLACE_ELEM(head, el, add)                                                        \
+    CDL_REPLACE_ELEM2(head, el, add, prev, next)
+
+#define CDL_PREPEND_ELEM2(head, el, add, prev, next)                                           \
+do {                                                                                           \
+  if (el) {                                                                                    \
+    assert((head) != NULL);                                                                    \
+    assert((add) != NULL);                                                                     \
+    (add)->next = (el);                                                                        \
+    (add)->prev = (el)->prev;                                                                  \
+    (el)->prev = (add);                                                                        \
+    (add)->prev->next = (add);                                                                 \
+    if ((head) == (el)) {                                                                      \
+      (head) = (add);                                                                          \
+    }                                                                                          \
+  } else {                                                                                     \
+    CDL_APPEND2(head, add, prev, next);                                                        \
+  }                                                                                            \
+} while (0)
+
+#define CDL_PREPEND_ELEM(head, el, add)                                                        \
+    CDL_PREPEND_ELEM2(head, el, add, prev, next)
+
+#define CDL_APPEND_ELEM2(head, el, add, prev, next)                                            \
+do {                                                                                           \
+ if (el) {                                                                                     \
+  assert((head) != NULL);                                                                      \
+  assert((add) != NULL);                                                                       \
+  (add)->next = (el)->next;                                                                    \
+  (add)->prev = (el);                                                                          \
+  (el)->next = (add);                                                                          \
+  (add)->next->prev = (add);                                                                   \
+ } else {                                                                                      \
+  CDL_PREPEND2(head, add, prev, next);                                                         \
+ }                                                                                             \
+} while (0)
+
+#define CDL_APPEND_ELEM(head, el, add)                                                         \
+    CDL_APPEND_ELEM2(head, el, add, prev, next)
+
+#ifdef NO_DECLTYPE
+/* Here are VS2008 / NO_DECLTYPE replacements for a few functions */
+
+#undef CDL_INSERT_INORDER2
+#define CDL_INSERT_INORDER2(head,add,cmp,prev,next)                                            \
+do {                                                                                           \
+  if ((head) == NULL) {                                                                        \
+    (add)->prev = (add);                                                                       \
+    (add)->next = (add);                                                                       \
+    (head) = (add);                                                                            \
+  } else if ((cmp(head, add)) >= 0) {                                                          \
+    (add)->prev = (head)->prev;                                                                \
+    (add)->next = (head);                                                                      \
+    (add)->prev->next = (add);                                                                 \
+    (head)->prev = (add);                                                                      \
+    (head) = (add);                                                                            \
+  } else {                                                                                     \
+    char *_tmp = (char*)(head);                                                                \
+    while ((char*)(head)->next != _tmp && (cmp((head)->next, add)) < 0) {                      \
+      (head) = (head)->next;                                                                   \
+    }                                                                                          \
+    (add)->prev = (head);                                                                      \
+    (add)->next = (head)->next;                                                                \
+    (add)->next->prev = (add);                                                                 \
+    (head)->next = (add);                                                                      \
+    UTLIST_RS(head);                                                                           \
+  }                                                                                            \
+} while (0)
+#endif /* NO_DECLTYPE */
+
+#endif /* UTLIST_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/apic.c	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,157 @@
+#include <assert.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <curl/curl.h>
+
+#include "apic.h"
+#include "util.h"
+
+struct curlpack {
+	CURL *curl;
+	CURLcode code;
+	struct curl_slist *headers;
+};
+
+static size_t
+writer(char *in, size_t n, size_t w, FILE *fp)
+{
+	if (fwrite(in, n, w, fp) != w)
+		return 0;
+
+	return w;
+}
+
+static inline char *
+create_url(const char *fmt, va_list args)
+{
+	static _Thread_local char ret[256];
+	va_list ap;
+
+	ret[0] = 0;
+	va_copy(ap, args);
+	vsnprintf(ret, sizeof (ret), fmt, ap);
+	va_end(ap);
+
+	return ret;
+}
+
+static inline FILE *
+create_file(char **buf, size_t *bufsz)
+{
+	FILE *fp;
+
+	*buf = NULL;
+	*bufsz = 0;
+
+	if (!(fp = open_memstream(buf, bufsz)))
+		util_die("open_memstream: %s\n", strerror(errno));
+
+	return fp;
+}
+
+static struct curlpack
+create_curl(FILE *fp, const char *body, const char *url)
+{
+	struct curlpack pack = {0};
+
+	pack.headers = curl_slist_append(pack.headers, "Content-Type: application/json");
+	pack.curl = curl_easy_init();
+	curl_easy_setopt(pack.curl, CURLOPT_HTTPHEADER, pack.headers);
+	curl_easy_setopt(pack.curl, CURLOPT_URL, url);
+	curl_easy_setopt(pack.curl, CURLOPT_FOLLOWLOCATION, 1L);
+	curl_easy_setopt(pack.curl, CURLOPT_TIMEOUT, 3L);
+	curl_easy_setopt(pack.curl, CURLOPT_WRITEFUNCTION, writer);
+	curl_easy_setopt(pack.curl, CURLOPT_WRITEDATA, fp);
+	curl_easy_setopt(pack.curl, CURLOPT_NOSIGNAL, 1L);
+
+	/* Assume POST request if there is a body. */
+	if (body) {
+		curl_easy_setopt(pack.curl, CURLOPT_POSTFIELDS, body);
+		curl_easy_setopt(pack.curl, CURLOPT_POSTFIELDSIZE, strlen(body));
+	}
+
+	pack.code = curl_easy_perform(pack.curl);
+
+	return pack;
+}
+
+static int
+perform(struct apicreq *req, const char *body, const char *fmt, va_list ap)
+{
+	FILE *fp;
+	char *response, *url;
+	size_t responsesz;
+	json_error_t error;
+	int ret = -1;
+	struct curlpack curl;
+
+	memset(req, 0, sizeof (*req));
+
+	url  = create_url(fmt, ap);
+	fp   = create_file(&response, &responsesz);
+	curl = create_curl(fp, body, url);
+
+	/* Perform that request now. */
+	fclose(fp);
+
+	if (curl.code != CURLE_OK)
+		snprintf(req->error, sizeof (req->error), "%s", curl_easy_strerror(curl.code));
+	else {
+		curl_easy_getinfo(curl.curl, CURLINFO_RESPONSE_CODE, &req->status);
+
+		if (req->status != 200)
+			snprintf(req->error, sizeof (req->error), "HTTP returned %ld", req->status);
+		if (response[0] && !(req->doc = json_loads(response, 0, &error)))
+			snprintf(req->error, sizeof (req->error), "JSON parse error: %s", error.text);
+		else
+			ret = 0;
+	}
+
+	curl_easy_cleanup(curl.curl);
+	curl_slist_free_all(curl.headers);
+
+	free(response);
+
+	return ret;
+}
+
+int
+apic_get(struct apicreq *req, const char *fmt, ...)
+{
+	assert(req);
+	assert(fmt);
+
+	va_list ap;
+	int ret;
+
+	va_start(ap, fmt);
+	ret = perform(req, NULL, fmt, ap);
+	va_end(ap);
+
+	return ret;
+}
+
+int
+apic_post(struct apicreq *req, const json_t *doc, const char *fmt, ...)
+{
+	assert(req);
+	assert(fmt);
+
+	va_list ap;
+	int ret;
+	char *body;
+
+	if (!(body = json_dumps(doc, JSON_COMPACT)))
+		util_die("%s", strerror(ENOMEM));
+
+	va_start(ap, fmt);
+	ret = perform(req, body, fmt, ap);
+	va_end(ap);
+
+	free(body);
+
+	return ret;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/apic.h	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,20 @@
+#ifndef SCI_APIC_H
+#define SCI_APIC_H
+
+#include <jansson.h>
+
+#define APIC_ERR_MAX 128
+
+struct apicreq {
+	json_t *doc;
+	char error[APIC_ERR_MAX];
+	long status;
+};
+
+int
+apic_get(struct apicreq *req, const char *url, ...);
+
+int
+apic_post(struct apicreq *req, const json_t *body, const char *url, ...);
+
+#endif /* !SCI_APIC_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/db.c	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,404 @@
+/*
+ * db.c -- scid database access
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sqlite3.h>
+
+#include <utlist.h>
+
+#include "db.h"
+#include "log.h"
+#include "types.h"
+#include "util.h"
+
+#include "sql/init.h"
+#include "sql/job-add.h"
+#include "sql/job-todo.h"
+#include "sql/jobresult-add.h"
+#include "sql/project-add.h"
+#include "sql/project-update.h"
+#include "sql/project-find.h"
+#include "sql/project-find-id.h"
+#include "sql/project-list.h"
+#include "sql/worker-add.h"
+#include "sql/worker-find.h"
+#include "sql/worker-find-id.h"
+#include "sql/worker-list.h"
+
+#define CHAR(v) (const char *)(v)
+
+static sqlite3 *db;
+
+typedef void (*unpacker)(sqlite3_stmt *, struct db_ctx *, void *);
+
+struct str {
+	char *str;
+	struct str *next;
+};
+
+struct list {
+	unpacker unpack;
+	void *data;
+	size_t datasz;
+	size_t elemwidth;
+	struct db_ctx *ctx;
+};
+
+static const char *
+strlist_add(struct db_ctx *ctx, const char *text)
+{
+	struct str *s, *list = ctx->handle;
+
+	s = util_calloc(1, sizeof (*s));
+	s->str = util_strdup(text);
+	LL_APPEND(list, s);
+
+	return s->str;
+}
+
+static void
+strlist_free(struct db_ctx *ctx)
+{
+	struct str *s, *tmp, *list = ctx->handle;
+
+	LL_FOREACH_SAFE(list, s, tmp) {
+		free(s->str);
+		free(s);
+	}
+
+	ctx->handle = NULL;
+}
+
+static void
+project_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct project *project)
+{
+	project->id = sqlite3_column_int(stmt, 0);
+	project->name = strlist_add(ctx, CHAR(sqlite3_column_text(stmt, 1)));
+	project->desc = strlist_add(ctx, CHAR(sqlite3_column_text(stmt, 2)));
+	project->url = strlist_add(ctx, CHAR(sqlite3_column_text(stmt, 3)));
+	project->script = strlist_add(ctx, CHAR(sqlite3_column_text(stmt, 4)));
+}
+
+static void
+worker_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct worker *w)
+{
+	w->id = sqlite3_column_int(stmt, 0);
+	w->name = strlist_add(ctx, CHAR(sqlite3_column_text(stmt, 1)));
+	w->desc = strlist_add(ctx, CHAR(sqlite3_column_text(stmt, 2)));
+}
+
+static void
+job_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct job *job)
+{
+	job->id = sqlite3_column_int(stmt, 0);
+	job->tag = strlist_add(ctx, CHAR(sqlite3_column_text(stmt, 1)));
+	job->project_id = sqlite3_column_int(stmt, 2);
+}
+
+static void
+vbind(sqlite3_stmt *stmt, const char *fmt, va_list ap)
+{
+	for (int index = 1; *fmt; ++fmt) {
+		switch (*fmt) {
+		case 'i':
+			sqlite3_bind_int(stmt, index++, va_arg(ap, int));
+			break;
+		case 's':
+			sqlite3_bind_text(stmt, index++, va_arg(ap, const char *), -1, SQLITE_STATIC);
+			break;
+		case 'z':
+			sqlite3_bind_int64(stmt, index++, va_arg(ap, size_t));
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+static int
+insert(const char *sql, const char *fmt, ...)
+{
+	assert(sql);
+	assert(fmt);
+
+	sqlite3_stmt *stmt = NULL;
+	va_list ap;
+	int ret = -1;
+
+	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
+
+	va_start(ap, fmt);
+	vbind(stmt, fmt, ap);
+	va_end(ap);
+
+	if (sqlite3_step(stmt) != SQLITE_DONE)
+		log_warn("db: %s", sqlite3_errmsg(db));
+	else
+		ret = sqlite3_last_insert_rowid(db);
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+}
+
+static int
+update(const char *sql, const char *fmt, ...)
+{
+	assert(sql);
+	assert(fmt);
+
+	sqlite3_stmt *stmt = NULL;
+	va_list ap;
+	int ret = 1;
+
+	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
+
+	va_start(ap, fmt);
+	vbind(stmt, fmt, ap);
+	va_end(ap);
+
+	if (sqlite3_step(stmt) != SQLITE_DONE)
+		log_warn("db: %s", sqlite3_errmsg(db));
+	else
+		ret = 0;
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+}
+
+static ssize_t
+list(struct list *sel, const char *sql, const char *args, ...)
+{
+	sqlite3_stmt *stmt = NULL;
+
+	va_list ap;
+	int step;
+	ssize_t ret = -1;
+	size_t tot = 0;
+
+	sel->ctx->handle = NULL;
+
+	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
+
+	va_start(ap, args);
+	vbind(stmt, args, ap);
+	va_end(ap);
+
+	while (tot < sel->datasz && (step = sqlite3_step(stmt)) == SQLITE_ROW)
+		sel->unpack(stmt, sel->ctx, (unsigned char *)sel->data + (tot++ * sel->elemwidth));
+
+	if (step == SQLITE_OK || step == SQLITE_DONE || step == SQLITE_ROW)
+		ret = tot;
+	else {
+		memset(sel->data, 0, sel->datasz * sel->elemwidth);
+		strlist_free(sel->ctx->handle);
+		sel->ctx->handle = NULL;
+	}
+
+	sqlite3_finalize(stmt);
+
+	return ret;
+}
+
+int
+db_open(const char *path)
+{
+	assert(path);
+
+	if (sqlite3_open(path, &db) != SQLITE_OK)
+		return log_warn("db: open error: %s", sqlite3_errmsg(db)), -1;
+
+	/* Wait for 30 seconds to lock the database. */
+	sqlite3_busy_timeout(db, 30000);
+
+	if (sqlite3_exec(db, CHAR(sql_init), NULL, NULL, NULL) != SQLITE_OK)
+		return log_warn("db: initialization error: %s", sqlite3_errmsg(db)), -1;
+
+	return 0;
+}
+
+int
+db_project_add(struct project *p)
+{
+	return (p->id = insert(CHAR(sql_project_add), "ssss", p->name, p->desc,
+	    p->url, p->script)) < 0 ? -1 : 0;
+}
+
+int
+db_project_update(const struct project *p)
+{
+	assert(p);
+
+	return update(CHAR(sql_project_update), "ssssi", p->name, p->desc,
+	    p->url, p->script, p->id);
+}
+
+ssize_t
+db_project_list(struct db_ctx *ctx, struct project *projects, size_t projectsz)
+{
+	struct list sel = {
+		.unpack = (unpacker)project_unpacker,
+		.data = projects,
+		.datasz = projectsz,
+		.elemwidth = sizeof (*projects),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_project_list), "z", projectsz);
+}
+
+int
+db_project_find(struct db_ctx *ctx, struct project *project)
+{
+	struct list sel = {
+		.unpack = (unpacker)project_unpacker,
+		.data = project,
+		.datasz = 1,
+		.elemwidth = sizeof (*project),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_project_find), "s", project->name) == 1 ? 0 : -1;
+}
+
+int
+db_project_find_id(struct db_ctx *ctx, struct project *project)
+{
+	struct list sel = {
+		.unpack = (unpacker)project_unpacker,
+		.data = project,
+		.datasz = 1,
+		.elemwidth = sizeof (*project),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_project_find_id), "i", project->id) == 1 ? 0 : -1;
+}
+
+int
+db_worker_add(struct worker *wk)
+{
+	assert(wk);
+
+	return (wk->id = insert(CHAR(sql_worker_add), "ss", wk->name, wk->desc)) < 0 ? -1 : 0;
+}
+
+ssize_t
+db_worker_list(struct db_ctx *ctx, struct worker *wk, size_t wksz)
+{
+	assert(ctx);
+	assert(wk);
+
+	struct list sel = {
+		.unpack = (unpacker)worker_unpacker,
+		.data = wk,
+		.datasz = wksz,
+		.elemwidth = sizeof (*wk),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_worker_list), "z", wksz);
+}
+
+int
+db_worker_find(struct db_ctx *ctx, struct worker *wk)
+{
+	struct list sel = {
+		.unpack = (unpacker)worker_unpacker,
+		.data = wk,
+		.datasz = 1,
+		.elemwidth = sizeof (*wk),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_worker_find), "s", wk->name) == 1 ? 0 : -1;
+}
+
+int
+db_worker_find_id(struct db_ctx *ctx, struct worker *wk)
+{
+	struct list sel = {
+		.unpack = (unpacker)worker_unpacker,
+		.data = wk,
+		.datasz = 1,
+		.elemwidth = sizeof (*wk),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_worker_find_id), "i", wk->id) == 1 ? 0 : -1;
+}
+
+int
+db_job_add(struct job *job)
+{
+	assert(job);
+
+	return (job->id = insert(CHAR(sql_job_add),
+	    "si", job->tag, job->project_id)) < 0 ? -1 : 0;
+}
+
+ssize_t
+db_job_todo(struct db_ctx *ctx, struct job *jobs, size_t jobsz, int worker_id)
+{
+	assert(ctx);
+	assert(jobs);
+
+	struct list sel = {
+		.unpack = (unpacker)job_unpacker,
+		.data = jobs,
+		.datasz = jobsz,
+		.elemwidth = sizeof (*jobs),
+		.ctx = ctx
+	};
+
+	return list(&sel, CHAR(sql_job_todo), "iiz", worker_id, worker_id, jobsz);
+}
+
+int
+db_jobresult_add(struct jobresult *r)
+{
+	assert(r);
+
+	return (r->id = insert(CHAR(sql_jobresult_add), "iiis", r->job_id,
+	    r->worker_id, r->exitcode, r->log)) < 0 ? -1 : 0;
+}
+
+void
+db_finish(void)
+{
+	if (db) {
+		sqlite3_close(db);
+		db = NULL;
+	}
+}
+
+void
+db_ctx_finish(struct db_ctx *ctx)
+{
+	if (ctx->handle) {
+		strlist_free(ctx->handle);
+		ctx->handle = NULL;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/db.h	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,79 @@
+/*
+ * db.h -- scid database access
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_DB_H
+#define SCI_DB_H
+
+#include <sys/types.h>
+#include <stddef.h>
+
+struct project;
+struct worker;
+struct job;
+struct jobresult;
+
+struct db_ctx {
+	void *handle;
+};
+
+int
+db_open(const char *);
+
+int
+db_job_add(struct job *);
+
+ssize_t
+db_job_todo(struct db_ctx *, struct job *, size_t, int);
+
+int
+db_jobresult_add(struct jobresult *);
+
+int
+db_project_add(struct project *);
+
+int
+db_project_update(const struct project *);
+
+ssize_t
+db_project_list(struct db_ctx *, struct project *, size_t);
+
+int
+db_project_find(struct db_ctx *, struct project *);
+
+int
+db_project_find_id(struct db_ctx *, struct project *);
+
+int
+db_worker_add(struct worker *);
+
+ssize_t
+db_worker_list(struct db_ctx *, struct worker *, size_t);
+
+int
+db_worker_find(struct db_ctx *, struct worker *);
+
+int
+db_worker_find_id(struct db_ctx *, struct worker *);
+
+void
+db_finish(void);
+
+void
+db_ctx_finish(struct db_ctx *);
+
+#endif /* !SCI_DB_H */
--- a/lib/types.h	Tue Jul 12 20:20:51 2022 +0200
+++ b/lib/types.h	Fri Jul 15 11:11:48 2022 +0200
@@ -21,17 +21,18 @@
 
 #include <sys/types.h>
 #include <stddef.h>
+#include <stdint.h>
 
 #include <jansson.h>
 
 struct job {
-	int id;
+	intmax_t id;
 	int project_id;
 	const char *tag;
 };
 
 struct jobresult {
-	int id;
+	intmax_t id;
 	int job_id;
 	int worker_id;
 	int exitcode;
@@ -39,13 +40,13 @@
 };
 
 struct worker {
-	int id;
+	intmax_t id;
 	const char *name;
 	const char *desc;
 };
 
 struct project {
-	int id;
+	intmax_t id;
 	const char *name;
 	const char *desc;
 	const char *url;
--- a/lib/util.c	Tue Jul 12 20:20:51 2022 +0200
+++ b/lib/util.c	Fri Jul 15 11:11:48 2022 +0200
@@ -185,3 +185,16 @@
 
 	return path;
 }
+
+void
+util_die(const char *fmt, ...)
+{
+	assert(fmt);
+
+	va_list ap;
+
+	va_start(ap, fmt);
+	vfprintf(stderr, fmt, ap);
+	va_end(ap);
+	exit(1);
+}
--- a/lib/util.h	Tue Jul 12 20:20:51 2022 +0200
+++ b/lib/util.h	Fri Jul 15 11:11:48 2022 +0200
@@ -63,4 +63,7 @@
 const char *
 util_path(const char *);
 
+void
+util_die(const char *, ...);
+
 #endif /* !SCI_UTIL_H */
--- a/scictl.c	Tue Jul 12 20:20:51 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,424 +0,0 @@
-/*
- * scictl.c -- main scictl(8) utility file
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <err.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <stdnoreturn.h>
-#include <unistd.h>
-
-#include "config.h"
-#include "req.h"
-#include "types.h"
-#include "util.h"
-
-noreturn static void
-usage(void)
-{
-	fprintf(stderr, "usage: %s [-s sock] command [args...]\n", getprogname());
-	exit(1);
-}
-
-noreturn static void
-help(void)
-{
-	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-info name\n", getprogname());
-	fprintf(stderr, "       %s project-list\n", getprogname());
-	fprintf(stderr, "       %s project-update name key value\n", getprogname());
-	fprintf(stderr, "       %s worker-add name desc\n", getprogname());
-	fprintf(stderr, "       %s worker-list\n", getprogname());
-	exit(0);
-}
-
-static char *
-readfile(const char *path)
-{
-	FILE *fp, *str;
-	char buf[BUFSIZ], *console;
-	size_t nr;
-
-	if (strcmp(path, "-") == 0)
-		fp = stdin;
-	else if (!(fp = fopen(path, "r")))
-		err(1, "%s", path);
-
-	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))) {
-		free(console);
-		console = NULL;
-	}
-
-	fclose(str);
-	fclose(fp);
-
-	return console;
-}
-
-static size_t
-extract(char *s, size_t w, size_t n, void *data)
-{
-	return fwrite(s, w, n, data);
-}
-
-static json_t *
-parse(const char *data)
-{
-	json_t *doc;
-	json_error_t err;
-
-	if (!(json_loads(doc, 0, &err)))
-		die("abort: unable to parse JSON: %s\n", err.text);
-
-	return doc;
-}
-
-static json_t *
-get(const char *url)
-{
-	CURL *curl;
-	CURLcode code;
-	FILE *fp;
-	char buf[HTTP_BUF_MAX];
-	long ret;
-
-	if (!(fp = fmemopen(buf, sizeof (buf), "w")))
-		die("abort: %s", strerror(errno));
-
-	curl = curl_easy_init();
-	curl_easy_setopt(curl, CURLOPT_URL, url);
-	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, extract);
-	curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
-	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
-	code = curl_easy_perform(curl);
-
-	if (code != CURLE_OK)
-		die("abort: %s", curl_easy_strerror(code));
-
-	curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &ret);
-
-	if (ret != 200)
-		die("abort: HTTP %ld\n", ret);
-
-	curl_easy_cleanup(curl);
-	fclose(fp);
-
-	return parse(buf);
-}
-
-static struct req
-cmd_job_add(int argc, char **argv)
-{
-	struct job job = {0};
-	struct project project = {0};
-
-	if (argc < 2)
-		usage();
-
-	project.name = argv[0];
-
-	if (_project_find(&project, argv[0])).status)
-		return rp;
-
-	job.project_id = project.id;
-	job.tag = argv[1];
-
-	rj = req_job_add(&job);
-	req_finish(&rp);
-
-	return rj;
-}
-
-static struct req
-cmd_job_todo(int argc, char **argv)
-{
-	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();
-
-	/* First retrieve projects for a better listing. */
-	if ((rp = req_project_list(projects, &projectsz)).status)
-		return rp;
-
-	if ((rj = req_job_todo(jobs, &jobsz, argv[0])).status) {
-		req_finish(&rp);
-		return rj;
-	}
-
-	for (size_t i = 0; i < jobsz; ++i) {
-		const char *project = "unknown";
-
-		/* Find project if exists (it should). */
-		for (size_t p = 0; p < projectsz; ++p) {
-			if (projects[p].id == jobs[i].project_id) {
-				project = projects[p].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");
-	}
-
-	req_finish(&rp);
-	
-	return rj;
-}
-
-static struct req
-cmd_jobresult_add(int argc, char **argv)
-{
-	struct jobresult res = {0};
-	struct worker wk = {0};
-	struct req rw, rj;
-	char *log;
-
-	if (argc < 5)
-		usage();
-
-	/* Find worker id. */
-	if ((rw = req_worker_find(&wk, argv[1])).status)
-		return rw;
-
-	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 = {0};
-	struct req res;
-	char *script;
-
-	if (argc < 4)
-		usage();
-
-	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;
-}
-
-static struct req
-cmd_project_update(int argc, char **argv)
-{
-	struct project pc;
-	struct req rget, rsend;
-	char *script = NULL;
-
-	if (argc < 3)
-		help();
-
-	if ((rget = req_project_find(&pc, argv[0])).status)
-		return rget;
-
-	if (strcmp(argv[1], "name") == 0)
-		pc.name = argv[2];
-	else if (strcmp(argv[1], "desc") == 0)
-		pc.desc = argv[2];
-	else if (strcmp(argv[1], "url") == 0)
-		pc.url = argv[2];
-	else if (strcmp(argv[1], "script") == 0)
-		pc.script = script = readfile(argv[2]);
-
-	rsend = req_project_update(&pc);
-
-	req_finish(&rget);
-	free(script);
-
-	return rsend;
-}
-
-static struct req
-cmd_project_info(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%d\n", "id:", project.id);
-	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
-cmd_project_list(int argc, char **argv)
-{
-	(void)argc;
-	(void)argv;
-
-	struct project projects[SCI_PROJECT_MAX] = {0};
-	struct req req;
-	size_t projectsz = UTIL_SIZE(projects);
-
-	if ((req = req_project_list(projects, &projectsz)).status)
-		return req;
-
-	for (size_t i = 0; i < projectsz; ++i) {
-		printf("%-16s%d\n", "id:", projects[i].id);
-		printf("%-16s%s\n", "name:", projects[i].name);
-		printf("%-16s%s\n", "desc:", projects[i].desc);
-		printf("%-16s%s\n", "url:", projects[i].url);
-
-		if (i + 1 < projectsz)
-			printf("\n");
-	}
-
-	return req;
-}
-
-static struct req
-cmd_worker_add(int argc, char **argv)
-{
-	struct worker wk = {0};
-
-	if (argc < 2)
-		usage();
-
-	wk.name = argv[0];
-	wk.desc = argv[1];
-
-	return req_worker_add(&wk);
-}
-
-static struct req
-cmd_worker_list(int argc, char **argv)
-{
-	(void)argc;
-	(void)argv;
-
-	struct worker wk[SCI_WORKER_MAX];
-	struct req req;
-	size_t wksz = UTIL_SIZE(wk);
-
-	if ((req = req_worker_list(wk, &wksz)).status)
-		return req;
-
-	for (size_t i = 0; i < wksz; ++i) {
-		printf("%-16s%d\n", "id:", wk[i].id);
-		printf("%-16s%s\n", "name:", wk[i].name);
-		printf("%-16s%s\n", "desc:", wk[i].desc);
-
-		if (i + 1 < wksz)
-			printf("\n");
-	}
-
-	return req;
-}
-
-static struct {
-	const char *name;
-	struct req (*exec)(int, char **);
-} commands[] = {
-	{ "job-add",            cmd_job_add             },
-	{ "job-todo",           cmd_job_todo            },
-	{ "jobresult-add",      cmd_jobresult_add       },
-	{ "project-add",        cmd_project_add         },
-	{ "project-info",       cmd_project_info        },
-	{ "project-list",       cmd_project_list        },
-	{ "project-update",     cmd_project_update      },
-	{ "worker-add",         cmd_worker_add          },
-	{ "worker-list",        cmd_worker_list         },
-	{ NULL,                 NULL                    }
-};
-
-int
-main(int argc, char **argv)
-{
-	int ch, cmdfound = 0;
-
-	setprogname("scictl");
-
-	while ((ch = getopt(argc, argv, "s:")) != -1) {
-		switch (ch) {
-		case 's':
-			req_set_path(optarg);
-			break;
-		default:
-			break;
-		}
-	}
-
-	argc -= optind;
-	argv += optind;
-
-	if (argc <= 0)
-		usage();
-	if (strcmp(argv[0], "help") == 0)
-		help();
-
-	for (size_t i = 0; commands[i].name; ++i) {
-		struct req res;
-
-		if (strcmp(commands[i].name, argv[0]) == 0) {
-			res = commands[i].exec(--argc, ++argv);
-			cmdfound = 1;
-
-			if (res.status)
-				warnx("%s", json_string_value(json_object_get(res.msg, "error")));
-
-			req_finish(&res);
-			break;
-		}
-	}
-
-	if (!cmdfound)
-		errx(1, "abort: command %s not found", argv[0]);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scictl/scictl.c	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,424 @@
+/*
+ * scictl.c -- main scictl(8) utility file
+ *
+ * Copyright (c) 2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdnoreturn.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "req.h"
+#include "types.h"
+#include "util.h"
+
+noreturn static void
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-s sock] command [args...]\n", getprogname());
+	exit(1);
+}
+
+noreturn static void
+help(void)
+{
+	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-info name\n", getprogname());
+	fprintf(stderr, "       %s project-list\n", getprogname());
+	fprintf(stderr, "       %s project-update name key value\n", getprogname());
+	fprintf(stderr, "       %s worker-add name desc\n", getprogname());
+	fprintf(stderr, "       %s worker-list\n", getprogname());
+	exit(0);
+}
+
+static char *
+readfile(const char *path)
+{
+	FILE *fp, *str;
+	char buf[BUFSIZ], *console;
+	size_t nr;
+
+	if (strcmp(path, "-") == 0)
+		fp = stdin;
+	else if (!(fp = fopen(path, "r")))
+		err(1, "%s", path);
+
+	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))) {
+		free(console);
+		console = NULL;
+	}
+
+	fclose(str);
+	fclose(fp);
+
+	return console;
+}
+
+static size_t
+extract(char *s, size_t w, size_t n, void *data)
+{
+	return fwrite(s, w, n, data);
+}
+
+static json_t *
+parse(const char *data)
+{
+	json_t *doc;
+	json_error_t err;
+
+	if (!(json_loads(doc, 0, &err)))
+		die("abort: unable to parse JSON: %s\n", err.text);
+
+	return doc;
+}
+
+static json_t *
+get(const char *url)
+{
+	CURL *curl;
+	CURLcode code;
+	FILE *fp;
+	char buf[HTTP_BUF_MAX];
+	long ret;
+
+	if (!(fp = fmemopen(buf, sizeof (buf), "w")))
+		die("abort: %s", strerror(errno));
+
+	curl = curl_easy_init();
+	curl_easy_setopt(curl, CURLOPT_URL, url);
+	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, extract);
+	curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
+	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
+	code = curl_easy_perform(curl);
+
+	if (code != CURLE_OK)
+		die("abort: %s", curl_easy_strerror(code));
+
+	curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &ret);
+
+	if (ret != 200)
+		die("abort: HTTP %ld\n", ret);
+
+	curl_easy_cleanup(curl);
+	fclose(fp);
+
+	return parse(buf);
+}
+
+static struct req
+cmd_job_add(int argc, char **argv)
+{
+	struct job job = {0};
+	struct project project = {0};
+
+	if (argc < 2)
+		usage();
+
+	project.name = argv[0];
+
+	if (_project_find(&project, argv[0])).status)
+		return rp;
+
+	job.project_id = project.id;
+	job.tag = argv[1];
+
+	rj = req_job_add(&job);
+	req_finish(&rp);
+
+	return rj;
+}
+
+static struct req
+cmd_job_todo(int argc, char **argv)
+{
+	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();
+
+	/* First retrieve projects for a better listing. */
+	if ((rp = req_project_list(projects, &projectsz)).status)
+		return rp;
+
+	if ((rj = req_job_todo(jobs, &jobsz, argv[0])).status) {
+		req_finish(&rp);
+		return rj;
+	}
+
+	for (size_t i = 0; i < jobsz; ++i) {
+		const char *project = "unknown";
+
+		/* Find project if exists (it should). */
+		for (size_t p = 0; p < projectsz; ++p) {
+			if (projects[p].id == jobs[i].project_id) {
+				project = projects[p].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");
+	}
+
+	req_finish(&rp);
+	
+	return rj;
+}
+
+static struct req
+cmd_jobresult_add(int argc, char **argv)
+{
+	struct jobresult res = {0};
+	struct worker wk = {0};
+	struct req rw, rj;
+	char *log;
+
+	if (argc < 5)
+		usage();
+
+	/* Find worker id. */
+	if ((rw = req_worker_find(&wk, argv[1])).status)
+		return rw;
+
+	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 = {0};
+	struct req res;
+	char *script;
+
+	if (argc < 4)
+		usage();
+
+	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;
+}
+
+static struct req
+cmd_project_update(int argc, char **argv)
+{
+	struct project pc;
+	struct req rget, rsend;
+	char *script = NULL;
+
+	if (argc < 3)
+		help();
+
+	if ((rget = req_project_find(&pc, argv[0])).status)
+		return rget;
+
+	if (strcmp(argv[1], "name") == 0)
+		pc.name = argv[2];
+	else if (strcmp(argv[1], "desc") == 0)
+		pc.desc = argv[2];
+	else if (strcmp(argv[1], "url") == 0)
+		pc.url = argv[2];
+	else if (strcmp(argv[1], "script") == 0)
+		pc.script = script = readfile(argv[2]);
+
+	rsend = req_project_update(&pc);
+
+	req_finish(&rget);
+	free(script);
+
+	return rsend;
+}
+
+static struct req
+cmd_project_info(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%d\n", "id:", project.id);
+	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
+cmd_project_list(int argc, char **argv)
+{
+	(void)argc;
+	(void)argv;
+
+	struct project projects[SCI_PROJECT_MAX] = {0};
+	struct req req;
+	size_t projectsz = UTIL_SIZE(projects);
+
+	if ((req = req_project_list(projects, &projectsz)).status)
+		return req;
+
+	for (size_t i = 0; i < projectsz; ++i) {
+		printf("%-16s%d\n", "id:", projects[i].id);
+		printf("%-16s%s\n", "name:", projects[i].name);
+		printf("%-16s%s\n", "desc:", projects[i].desc);
+		printf("%-16s%s\n", "url:", projects[i].url);
+
+		if (i + 1 < projectsz)
+			printf("\n");
+	}
+
+	return req;
+}
+
+static struct req
+cmd_worker_add(int argc, char **argv)
+{
+	struct worker wk = {0};
+
+	if (argc < 2)
+		usage();
+
+	wk.name = argv[0];
+	wk.desc = argv[1];
+
+	return req_worker_add(&wk);
+}
+
+static struct req
+cmd_worker_list(int argc, char **argv)
+{
+	(void)argc;
+	(void)argv;
+
+	struct worker wk[SCI_WORKER_MAX];
+	struct req req;
+	size_t wksz = UTIL_SIZE(wk);
+
+	if ((req = req_worker_list(wk, &wksz)).status)
+		return req;
+
+	for (size_t i = 0; i < wksz; ++i) {
+		printf("%-16s%d\n", "id:", wk[i].id);
+		printf("%-16s%s\n", "name:", wk[i].name);
+		printf("%-16s%s\n", "desc:", wk[i].desc);
+
+		if (i + 1 < wksz)
+			printf("\n");
+	}
+
+	return req;
+}
+
+static struct {
+	const char *name;
+	struct req (*exec)(int, char **);
+} commands[] = {
+	{ "job-add",            cmd_job_add             },
+	{ "job-todo",           cmd_job_todo            },
+	{ "jobresult-add",      cmd_jobresult_add       },
+	{ "project-add",        cmd_project_add         },
+	{ "project-info",       cmd_project_info        },
+	{ "project-list",       cmd_project_list        },
+	{ "project-update",     cmd_project_update      },
+	{ "worker-add",         cmd_worker_add          },
+	{ "worker-list",        cmd_worker_list         },
+	{ NULL,                 NULL                    }
+};
+
+int
+main(int argc, char **argv)
+{
+	int ch, cmdfound = 0;
+
+	setprogname("scictl");
+
+	while ((ch = getopt(argc, argv, "s:")) != -1) {
+		switch (ch) {
+		case 's':
+			req_set_path(optarg);
+			break;
+		default:
+			break;
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc <= 0)
+		usage();
+	if (strcmp(argv[0], "help") == 0)
+		help();
+
+	for (size_t i = 0; commands[i].name; ++i) {
+		struct req res;
+
+		if (strcmp(commands[i].name, argv[0]) == 0) {
+			res = commands[i].exec(--argc, ++argv);
+			cmdfound = 1;
+
+			if (res.status)
+				warnx("%s", json_string_value(json_object_get(res.msg, "error")));
+
+			req_finish(&res);
+			break;
+		}
+	}
+
+	if (!cmdfound)
+		errx(1, "abort: command %s not found", argv[0]);
+}
--- a/scid/db.c	Tue Jul 12 20:20:51 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,419 +0,0 @@
-/*
- * db.c -- scid database access
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/queue.h>
-#include <assert.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include <sqlite3.h>
-
-#include "db.h"
-#include "log.h"
-#include "types.h"
-#include "util.h"
-
-#include "sql/init.h"
-#include "sql/job-add.h"
-#include "sql/job-todo.h"
-#include "sql/jobresult-add.h"
-#include "sql/project-add.h"
-#include "sql/project-update.h"
-#include "sql/project-find.h"
-#include "sql/project-find-id.h"
-#include "sql/project-list.h"
-#include "sql/worker-add.h"
-#include "sql/worker-find.h"
-#include "sql/worker-find-id.h"
-#include "sql/worker-list.h"
-
-#define CHAR(v) (const char *)(v)
-
-static sqlite3 *db;
-
-typedef void (*unpacker)(sqlite3_stmt *, struct db_ctx *, void *);
-
-struct str {
-	char *str;
-	SLIST_ENTRY(str) link;
-};
-
-struct list {
-	unpacker unpack;
-	void *data;
-	size_t datasz;
-	size_t elemwidth;
-	struct db_ctx *ctx;
-};
-
-SLIST_HEAD(strlist, str);
-
-static struct strlist *
-strlist_new(void)
-{
-	struct strlist *l;
-
-	l = util_calloc(1, sizeof (*l));
-	SLIST_INIT(l);
-
-	return l;
-}
-
-static const char *
-strlist_add(struct strlist *l, const char *text)
-{
-	struct str *s;
-
-	s = util_calloc(1, sizeof (*s));
-	s->str = util_strdup(text);
-
-	SLIST_INSERT_HEAD(l, s, link);
-
-	return s->str;
-}
-
-static void
-strlist_free(struct strlist *l)
-{
-	struct str *s, *tmp;
-
-	SLIST_FOREACH_SAFE(s, l, link, tmp) {
-		free(s->str);
-		free(s);
-	}
-
-	free(l);
-}
-
-static void
-project_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct project *project)
-{
-	project->id = sqlite3_column_int(stmt, 0);
-	project->name = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 1)));
-	project->desc = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 2)));
-	project->url = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 3)));
-	project->script = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 4)));
-}
-
-static void
-worker_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct worker *w)
-{
-	w->id = sqlite3_column_int(stmt, 0);
-	w->name = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 1)));
-	w->desc = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 2)));
-}
-
-static void
-job_unpacker(sqlite3_stmt *stmt, struct db_ctx *ctx, struct job *job)
-{
-	job->id = sqlite3_column_int(stmt, 0);
-	job->tag = strlist_add(ctx->handle, CHAR(sqlite3_column_text(stmt, 1)));
-	job->project_id = sqlite3_column_int(stmt, 2);
-}
-
-static void
-vbind(sqlite3_stmt *stmt, const char *fmt, va_list ap)
-{
-	for (int index = 1; *fmt; ++fmt) {
-		switch (*fmt) {
-		case 'i':
-			sqlite3_bind_int(stmt, index++, va_arg(ap, int));
-			break;
-		case 's':
-			sqlite3_bind_text(stmt, index++, va_arg(ap, const char *), -1, SQLITE_STATIC);
-			break;
-		case 'z':
-			sqlite3_bind_int64(stmt, index++, va_arg(ap, size_t));
-			break;
-		default:
-			break;
-		}
-	}
-}
-
-static int
-insert(const char *sql, const char *fmt, ...)
-{
-	assert(sql);
-	assert(fmt);
-
-	sqlite3_stmt *stmt = NULL;
-	va_list ap;
-	int ret = -1;
-
-	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
-		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
-
-	va_start(ap, fmt);
-	vbind(stmt, fmt, ap);
-	va_end(ap);
-
-	if (sqlite3_step(stmt) != SQLITE_DONE)
-		log_warn("db: %s", sqlite3_errmsg(db));
-	else
-		ret = sqlite3_last_insert_rowid(db);
-
-	sqlite3_finalize(stmt);
-
-	return ret;
-}
-
-static int
-update(const char *sql, const char *fmt, ...)
-{
-	assert(sql);
-	assert(fmt);
-
-	sqlite3_stmt *stmt = NULL;
-	va_list ap;
-	int ret = 1;
-
-	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
-		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
-
-	va_start(ap, fmt);
-	vbind(stmt, fmt, ap);
-	va_end(ap);
-
-	if (sqlite3_step(stmt) != SQLITE_DONE)
-		log_warn("db: %s", sqlite3_errmsg(db));
-	else
-		ret = 0;
-
-	sqlite3_finalize(stmt);
-
-	return ret;
-}
-
-static ssize_t
-list(struct list *sel, const char *sql, const char *args, ...)
-{
-	sqlite3_stmt *stmt = NULL;
-
-	va_list ap;
-	int step;
-	ssize_t ret = -1;
-	size_t tot = 0;
-
-	sel->ctx->handle = NULL;
-
-	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
-		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
-
-	va_start(ap, args);
-	vbind(stmt, args, ap);
-	va_end(ap);
-
-	sel->ctx->handle = strlist_new();
-
-	while (tot < sel->datasz && (step = sqlite3_step(stmt)) == SQLITE_ROW)
-		sel->unpack(stmt, sel->ctx, (unsigned char *)sel->data + (tot++ * sel->elemwidth));
-
-	if (step == SQLITE_OK || step == SQLITE_DONE || step == SQLITE_ROW)
-		ret = tot;
-	else {
-		memset(sel->data, 0, sel->datasz * sel->elemwidth);
-		strlist_free(sel->ctx->handle);
-		sel->ctx->handle = NULL;
-	}
-
-	sqlite3_finalize(stmt);
-
-	return ret;
-}
-
-int
-db_open(const char *path)
-{
-	assert(path);
-
-	if (sqlite3_open(path, &db) != SQLITE_OK)
-		return log_warn("db: open error: %s", sqlite3_errmsg(db)), -1;
-
-	/* Wait for 30 seconds to lock the database. */
-	sqlite3_busy_timeout(db, 30000);
-
-	if (sqlite3_exec(db, CHAR(sql_init), NULL, NULL, NULL) != SQLITE_OK)
-		return log_warn("db: initialization error: %s", sqlite3_errmsg(db)), -1;
-
-	return 0;
-}
-
-int
-db_project_add(struct project *p)
-{
-	return (p->id = insert(CHAR(sql_project_add), "ssss", p->name, p->desc,
-	    p->url, p->script)) < 0 ? -1 : 0;
-}
-
-int
-db_project_update(const struct project *p)
-{
-	assert(p);
-
-	return update(CHAR(sql_project_update), "ssssi", p->name, p->desc,
-	    p->url, p->script, p->id);
-}
-
-ssize_t
-db_project_list(struct db_ctx *ctx, struct project *projects, size_t projectsz)
-{
-	struct list sel = {
-		.unpack = (unpacker)project_unpacker,
-		.data = projects,
-		.datasz = projectsz,
-		.elemwidth = sizeof (*projects),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_project_list), "z", projectsz);
-}
-
-int
-db_project_find(struct db_ctx *ctx, struct project *project)
-{
-	struct list sel = {
-		.unpack = (unpacker)project_unpacker,
-		.data = project,
-		.datasz = 1,
-		.elemwidth = sizeof (*project),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_project_find), "s", project->name) == 1 ? 0 : -1;
-}
-
-int
-db_project_find_id(struct db_ctx *ctx, struct project *project)
-{
-	struct list sel = {
-		.unpack = (unpacker)project_unpacker,
-		.data = project,
-		.datasz = 1,
-		.elemwidth = sizeof (*project),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_project_find_id), "i", project->id) == 1 ? 0 : -1;
-}
-
-int
-db_worker_add(struct worker *wk)
-{
-	assert(wk);
-
-	return (wk->id = insert(CHAR(sql_worker_add), "ss", wk->name, wk->desc)) < 0 ? -1 : 0;
-}
-
-ssize_t
-db_worker_list(struct db_ctx *ctx, struct worker *wk, size_t wksz)
-{
-	assert(ctx);
-	assert(wk);
-
-	struct list sel = {
-		.unpack = (unpacker)worker_unpacker,
-		.data = wk,
-		.datasz = wksz,
-		.elemwidth = sizeof (*wk),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_worker_list), "z", wksz);
-}
-
-int
-db_worker_find(struct db_ctx *ctx, struct worker *wk)
-{
-	struct list sel = {
-		.unpack = (unpacker)worker_unpacker,
-		.data = wk,
-		.datasz = 1,
-		.elemwidth = sizeof (*wk),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_worker_find), "s", wk->name) == 1 ? 0 : -1;
-}
-
-int
-db_worker_find_id(struct db_ctx *ctx, struct worker *wk)
-{
-	struct list sel = {
-		.unpack = (unpacker)worker_unpacker,
-		.data = wk,
-		.datasz = 1,
-		.elemwidth = sizeof (*wk),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_worker_find_id), "i", wk->id) == 1 ? 0 : -1;
-}
-
-int
-db_job_add(struct job *job)
-{
-	assert(job);
-
-	return (job->id = insert(CHAR(sql_job_add),
-	    "si", job->tag, job->project_id)) < 0 ? -1 : 0;
-}
-
-ssize_t
-db_job_todo(struct db_ctx *ctx, struct job *jobs, size_t jobsz, int worker_id)
-{
-	assert(ctx);
-	assert(jobs);
-
-	struct list sel = {
-		.unpack = (unpacker)job_unpacker,
-		.data = jobs,
-		.datasz = jobsz,
-		.elemwidth = sizeof (*jobs),
-		.ctx = ctx
-	};
-
-	return list(&sel, CHAR(sql_job_todo), "iiz", worker_id, worker_id, jobsz);
-}
-
-int
-db_jobresult_add(struct jobresult *r)
-{
-	assert(r);
-
-	return (r->id = insert(CHAR(sql_jobresult_add), "iiis", r->job_id,
-	    r->worker_id, r->exitcode, r->log)) < 0 ? -1 : 0;
-}
-
-void
-db_finish(void)
-{
-	if (db) {
-		sqlite3_close(db);
-		db = NULL;
-	}
-}
-
-void
-db_ctx_finish(struct db_ctx *ctx)
-{
-	if (ctx->handle) {
-		strlist_free(ctx->handle);
-		ctx->handle = NULL;
-	}
-}
--- a/scid/db.h	Tue Jul 12 20:20:51 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-/*
- * db.h -- scid database access
- *
- * Copyright (c) 2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_DB_H
-#define SCI_DB_H
-
-#include <sys/types.h>
-#include <stddef.h>
-
-struct project;
-struct worker;
-struct job;
-struct jobresult;
-
-struct db_ctx {
-	void *handle;
-};
-
-int
-db_open(const char *);
-
-int
-db_job_add(struct job *);
-
-ssize_t
-db_job_todo(struct db_ctx *, struct job *, size_t, int);
-
-int
-db_jobresult_add(struct jobresult *);
-
-int
-db_project_add(struct project *);
-
-int
-db_project_update(const struct project *);
-
-ssize_t
-db_project_list(struct db_ctx *, struct project *, size_t);
-
-int
-db_project_find(struct db_ctx *, struct project *);
-
-int
-db_project_find_id(struct db_ctx *, struct project *);
-
-int
-db_worker_add(struct worker *);
-
-ssize_t
-db_worker_list(struct db_ctx *, struct worker *, size_t);
-
-int
-db_worker_find(struct db_ctx *, struct worker *);
-
-int
-db_worker_find_id(struct db_ctx *, struct worker *);
-
-void
-db_finish(void);
-
-void
-db_ctx_finish(struct db_ctx *);
-
-#endif /* !SCI_DB_H */
--- a/sciworkerd/main.c	Tue Jul 12 20:20:51 2022 +0200
+++ b/sciworkerd/main.c	Fri Jul 15 11:11:48 2022 +0200
@@ -16,719 +16,49 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#if 0
-
-#include <sys/queue.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <assert.h>
-#include <err.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <limits.h>
-#include <poll.h>
-#include <signal.h>
 #include <stdio.h>
 #include <stdlib.h>
-#include <stdnoreturn.h>
-#include <string.h>
 #include <unistd.h>
 
-#include <curl/curl.h>
-#include <jansson.h>
-
-#include "config.h"
-#include "log.h"
-#include "types.h"
-#include "util.h"
-
-#define TAG_MAX 256
-
-struct task {
-	enum taskst status;
-	pid_t child;
-	int pipe[2];
-	int exitcode;
-	int job_id;
-	int project_id;
-	char job_tag[TAG_MAX];
-	char out[SCI_CONSOLE_MAX];
-	char script[PATH_MAX];
-	int scriptfd;
-	TAILQ_ENTRY(task) link;
-};
-
-TAILQ_HEAD(tasks, task);
-
-struct fds {
-	struct pollfd *list;
-	size_t listsz;
-};
-
-struct fetch {
-	char buf[SCI_MSG_MAX];
-	FILE *bufp;
-};
-
-static struct {
-	char *url;
-	char *worker;
-	int maxbuilds;
-} config = {
-	.url = "http://localhost",
-	.worker = "default",
-	.maxbuilds = 4
-};
-
-static struct tasks tasks = TAILQ_HEAD_INITIALIZER(tasks);
-static struct worker worker;
-static int alive = 1;
-
-/*
- * Show usage and exit with code 1.
- */
-noreturn static void
-usage(void)
-{
-	fprintf(stderr, "usage: %s [-m maxbuild] [-u url] [-w worker]\n", getprogname());
-	exit(1);
-}
-
-/*
- * Find a task by its id.
- */
-static inline struct task *
-find_by_fd(int fd)
-{
-	struct task *tk;
-
-	TAILQ_FOREACH(tk, &tasks, link)
-		if (tk->pipe[0] == fd)
-			return tk;
-
-	return NULL;
-}
-
-/*
- * Find a task by its pid number.
- */
-static inline struct task *
-find_by_pid(pid_t pid)
-{
-	struct task *t;
-
-	TAILQ_FOREACH(t, &tasks, link)
-		if (t->child == pid)
-			return t;
-
-	return NULL;
-}
-
-/*
- * Destroy a task entirely.
- */
-static void
-destroy(struct task *tk)
-{
-	log_debug("destroying task %d", tk->job_id);
-	unlink(tk->script);
-
-	if (tk->pipe[0])
-		close(tk->pipe[0]);
-	if (tk->pipe[1])
-		close(tk->pipe[1]);
-	if (tk->scriptfd)
-		close(tk->scriptfd);
-
-	TAILQ_REMOVE(&tasks, tk, link);
-	memset(tk, 0, sizeof (*tk));
-	free(tk);
-}
-
-static const char *
-makeurl(const char *fmt, ...)
-{
-	assert(fmt);
-
-	static char url[256];
-	char page[128] = {0};
-	va_list ap;
-
-	va_start(ap, fmt);
-	vsnprintf(page, sizeof (page), fmt, ap);
-	va_end(ap);
-
-	snprintf(url, sizeof (url), "%s/%s", config.url, page);
-
-	return url;
-}
-
-static void
-complete(int signum, siginfo_t *sinfo, void *ctx)
-{
-	(void)ctx;
-	(void)signum;
-
-	struct task *tk;
-
-	if (waitpid(sinfo->si_pid, NULL, 0) < 0)
-		log_warn("waitpid: %s", strerror(errno));
-
-	if ((tk = find_by_pid(sinfo->si_pid))) {
-		log_debug("process %d terminated (exitcode=%d)",
-		    (int)sinfo->si_pid, sinfo->si_status);
-
-		close(tk->pipe[1]);
-		tk->status = TASKST_COMPLETED;
-		tk->exitcode = sinfo->si_status;
-		tk->pipe[1] = 0;
-	}
-}
-
-static void
-stop(int signum)
-{
-	log_warn("exiting on signal %d", signum);
-	alive = 0;
-}
-
-static char *
-uploadenc(const struct task *tk)
-{
-	json_t *doc;
-
-	struct jobresult res = {0};
-	char *dump;
-
-	res.job_id = tk->job_id;
-	res.exitcode = tk->exitcode;
-	res.log = tk->out;
-	res.worker_id = worker.id;
-
-	doc = jobresult_to(&res, 1);
-	dump = json_dumps(doc, JSON_COMPACT);
-
-	json_decref(doc);
-
-	return dump;
-}
-
-static size_t
-getcb(char *in, size_t n, size_t w, FILE *fp)
-{
-	if (fwrite(in, n, w, fp) != w)
-		return log_warn("get: %s", strerror(errno)), 0;
-
-	return w;
-}
-
-static json_t *
-get(const char *topic, const char *url)
-{
-	CURL *curl;
-	CURLcode code;
-
-	json_t *doc;
-	json_error_t error;
-
-	char buf[SCI_MSG_MAX];
-	long status;
-	FILE *fp;
-
-	curl = curl_easy_init();
-
-	if (!(fp = fmemopen(buf, sizeof (buf), "w")))
-		err(1, "fmemopen");
-
-	curl_easy_setopt(curl, CURLOPT_URL, url);
-	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
-	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
-	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, getcb);
-	curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
-
-	if ((code = curl_easy_perform(curl)) != CURLE_OK)
-		log_warn("%s: %s", topic, curl_easy_strerror(code));
-
-	curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
-	curl_easy_cleanup(curl);
-
-	fclose(fp);
-
-	if (code != CURLE_OK)
-		return log_warn("%s: %s", topic, curl_easy_strerror(code)), NULL;
-	if (status != 200)
-		return log_warn("%s: unexpected status code %ld", topic, status), NULL;
-	if (!(doc = json_loads(buf, 0, &error)))
-		return log_warn("%s: %s", topic, error.text), NULL;
-
-	return doc;
-}
-
-static size_t
-silent(char *in, size_t n, size_t w, void *data)
-{
-	(void)in;
-	(void)n;
-	(void)data;
-
-	return w;
-}
+#include "sciworkerd.h"
 
 static void
-upload(struct task *tk)
-{
-	CURL *curl;
-	CURLcode code;
-	struct curl_slist *headers = NULL;
-	long status;
-	char *dump;
-
-	curl = curl_easy_init();
-	headers = curl_slist_append(headers, "Content-Type: application/json");
-	curl_easy_setopt(curl, CURLOPT_URL, makeurl("api/v1/jobs"));
-	//curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:4000");
-	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
-	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
-	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, silent);
-	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, (dump = uploadenc(tk)));
-	curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(dump));
-	curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
-	code = curl_easy_perform(curl);
-	curl_slist_free_all(headers);
-
-	/*
-	 * If we fail to upload data, we put the result into syncing mode so
-	 * that we retry later without redoing the job over and over
-	 */
-	tk->status = TASKST_SYNCING;
-
-	if (code != CURLE_OK)
-		log_warn("upload: %s", curl_easy_strerror(code));
-	else {
-		curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
-
-		if (status != 200)
-			log_warn("upload: unexpected return code: %ld", status);
-		else
-			destroy(tk);
-	}
-
-	free(dump);
-	curl_easy_cleanup(curl);
-}
-
-static inline int
-pending(int id)
-{
-	struct task *t;
-
-	TAILQ_FOREACH(t, &tasks, link)
-		if (t->job_id == id)
-			return 1;
-
-	return 0;
-}
-
-static void
-queue(int id, int project_id, const char *tag)
-{
-	struct task *tk;
-
-	log_info("queued job build (%d) for tag %s\n", id, tag);
-
-	tk = util_calloc(1, sizeof (*tk));
-	tk->job_id = id;
-	tk->project_id = project_id;
-	strlcpy(tk->job_tag, tag, sizeof (tk->job_tag));
-
-	TAILQ_INSERT_TAIL(&tasks, tk, link);
-}
-
-static void
-merge(json_t *doc)
+env(void)
 {
-	struct job jobs[SCI_JOB_LIST_MAX];
-	ssize_t jobsz;
-
-	if ((jobsz = job_from(jobs, UTIL_SIZE(jobs), doc)) < 0)
-		log_warn("fetchjobs: %s", strerror(errno));
-	else {
-		for (ssize_t i = 0; i < jobsz; ++i) {
-			if (!pending(jobs[i].id))
-				queue(jobs[i].id, jobs[i].project_id, jobs[i].tag);
-		}
-	}
-
-	json_decref(doc);
-}
-
-static void
-fetchjobs(void)
-{
-	json_t *doc;
-
-	if (!(doc = get("fetch", makeurl("api/v1/jobs/%s", config.worker))))
-		log_warn("unable to retrieve jobs");
-	else
-		merge(doc);
-}
-
-/*
- * This function reads stdout/stderr pipe from child and optionally remove them
- * if they have completed.
- */
-static void
-readall(struct fds *fds)
-{
-	struct task *tk;
-	char buf[BUFSIZ];
-	ssize_t nr;
-
-	for (size_t i = 0; i < fds->listsz; ++i) {
-		if (fds->list[i].revents == 0)
-			continue;
-		if (!(tk = find_by_fd(fds->list[i].fd)))
-			continue;
-
-		/* Read stdout/stderr from children pipe. */
-		if ((nr = read(fds->list[i].fd, buf, sizeof (buf) - 1)) <= 0)
-			tk->status = TASKST_SYNCING;
-		else {
-			buf[nr] = 0;
-			strlcat(tk->out, buf, sizeof (tk->out));
-		}
-	}
-}
-
-/*
- * Retrieve status code from spawned process complete or upload again if they
- * failed to sync.
- */
-static void
-flushall(void)
-{
-	struct task *tk, *tmp;
-
-	TAILQ_FOREACH_SAFE(tk, &tasks, link, tmp)
-		if (tk->status == TASKST_SYNCING)
-			upload(tk);
-}
-
-static int
-extract(struct task *tk, json_t *doc)
-{
-	struct project proj;
-	size_t len;
-
-	if (project_from(&proj, 1, doc) < 0) {
-		json_decref(doc);
-		log_warn("fetchproject: %s", strerror(errno));
-		return -1;
-	}
-
-	len = strlen(proj.script);
-
-	if ((size_t)write(tk->scriptfd, proj.script, len) != len) {
-		json_decref(doc);
-		log_warn("fetchproject: %s", strerror(errno));
-		return -1;
-	}
-
-	/* Close so we can finally spawn it. */
-	close(tk->scriptfd);
-	tk->scriptfd = 0;
+	const char *env;
 
-	return 0;
-}
-
-static int
-fetchproject(struct task *tk)
-{
-	json_t *doc;
-
-	if (!(doc = get("fetchproject", makeurl("api/v1/projects/%d", tk->project_id))))
-		return -1;
-
-	return extract(tk, doc);
-}
-
-/*
- * Create a task to run the script. This will retrieve the project script code
- * at this moment and put it in a temporary file.
- */
-static void
-createtask(struct task *tk)
-{
-	if (tk->status != TASKST_PENDING)
-		return;
-
-	log_debug("creating task (id=%d, tag=%s)", tk->job_id, tk->job_tag);
-	snprintf(tk->script, sizeof (tk->script), "/tmp/sciworkerd-%d-XXXXXX", tk->job_id);
-
-	if ((tk->scriptfd = mkstemp(tk->script)) < 0 ||
-	    fchmod(tk->scriptfd, S_IRUSR | S_IWUSR | S_IXUSR) < 0) {
-		unlink(tk->script);
-		log_warn("%s", strerror(errno));
-		return;
-	}
-
-	if (fetchproject(tk) < 0) {
-		unlink(tk->script);
-		close(tk->scriptfd);
-		tk->scriptfd = 0;
-	} else
-		spawn(tk);
-}
-
-/*
- * Start all pending tasks if the limit of running tasks is not reached.
- */
-static void
-startall(void)
-{
-	size_t nrunning = 0;
-	struct task *tk;
-
-	TAILQ_FOREACH(tk, &tasks, link)
-		if (tk->status == TASKST_RUNNING)
-			++nrunning;
-
-	if (nrunning >= (size_t)config.maxbuilds)
-		log_debug("not spawning new process because limit is reached");
-	else {
-		tk = TAILQ_FIRST(&tasks);
-
-		while (tk && nrunning++ < (size_t)config.maxbuilds) {
-			createtask(tk);
-			tk = TAILQ_NEXT(tk, link);
-		}
-	}
-}
-
-static void
-fetchworker(void)
-{
-	json_t *doc;
-
-	if (!(doc = get("fetchworker", makeurl("api/v1/workers/%s", config.worker))) ||
-	    worker_from(&worker, 1, doc) < 0)
-		errx(1, "unable to retrieve worker id");
-
-	log_info("worker id: %d", worker.id);
-	log_info("worker name: %s", worker.name);
-	log_info("worker description: %s", worker.desc);
-
-	json_decref(doc);
-}
-
-static void
-init(void)
-{
-	struct sigaction sa;
-
-	sa.sa_flags = SA_SIGINFO | SA_RESTART;
-	sa.sa_sigaction = complete;
-	sigemptyset(&sa.sa_mask);
-
-	if (sigaction(SIGCHLD, &sa, NULL) < 0)
-		err(1, "sigaction");
-
-	sa.sa_flags = SA_RESTART;
-	sa.sa_handler = stop;
-	sigemptyset(&sa.sa_mask);
-
-	if (sigaction(SIGTERM, &sa, NULL) < 0 || sigaction(SIGINT, &sa, NULL) < 0)
-		err(1, "sigaction");
-
-	log_open("sciworkerd");
-	fetchworker();
-}
-
-static struct fds
-prepare(void)
-{
-	struct fds fds = {0};
-	struct task *tk;
-	size_t i = 0;
-
-	TAILQ_FOREACH(tk, &tasks, link)
-		if (tk->status == TASKST_RUNNING || tk->status == TASKST_COMPLETED)
-			fds.listsz++;
-
-	fds.list = util_calloc(fds.listsz, sizeof (*fds.list));
-
-	TAILQ_FOREACH(tk, &tasks, link) {
-		if (tk->status == TASKST_RUNNING || tk->status == TASKST_COMPLETED) {
-			fds.list[i].fd = tk->pipe[0];
-			fds.list[i++].events = POLLIN | POLLPRI;
-		}
-	}
-
-	return fds;
-}
-
-static void
-run(void)
-{
-	struct fds fds;
-
-	fds = prepare();
-
-	if (poll(fds.list, fds.listsz, 5000) < 0 && errno != EINTR)
-		err(1, "poll");
-
-	fetchjobs();
-	readall(&fds);
-	startall();
-	flushall();
-}
-
-static void
-finish(void)
-{
-	size_t tot = 0;
-	struct task *tk, *tmp;
-
-	TAILQ_FOREACH(tk, &tasks, link)
-		tot++;
-
-	signal(SIGCHLD, SIG_IGN);
-	log_debug("killing remaining %zu tasks", tot);
-
-	TAILQ_FOREACH_SAFE(tk, &tasks, link, tmp) {
-		if (tk->status == TASKST_RUNNING) {
-			kill(tk->child, SIGTERM);
-			waitpid(tk->child, NULL, 0);
-		}
-
-		destroy(tk);
-	}
+	if ((env = getenv("SCI_URL")))
+		snprintf(sciworkerd.url, sizeof (sciworkerd.url), "%s", optarg);
+	if ((env = getenv("SCI_WORKER")))
+		snprintf(sciworkerd.name, sizeof (sciworkerd.name), "%s", optarg);
 }
 
 int
 main(int argc, char **argv)
 {
-	int ch;
-	const char *errstr;
+	int ch, val;
 
-	setprogname("sciworkerd");
+	env();
+	opterr = 0;
 
-	while ((ch = getopt(argc, argv, "m:u:w:")) != -1) {
+	while ((ch = getopt(argc, argv, "j:t:u:w:")) != -1) {
 		switch (ch) {
-		case 'm':
-			config.maxbuilds = strtonum(optarg, 0, INT_MAX, &errstr);
-
-			if (errstr)
-				errx(1, "%s: %s", optarg, errstr);
-
+		case 'j':
+			if ((val = atoi(optarg)) > 0)
+				sciworkerd.maxjobs = val;
+			break;
+		case 't':
+			if ((val = atoi(optarg)) > 0)
+				sciworkerd.timeout = val;
 			break;
 		case 'u':
-			config.url = optarg;
+			snprintf(sciworkerd.url, sizeof (sciworkerd.url), "%s", optarg);
 			break;
 		case 'w':
-			config.worker = optarg;
+			snprintf(sciworkerd.name, sizeof (sciworkerd.name), "%s", optarg);
 			break;
 		default:
-			usage();
 			break;
 		}
 	}
-
-	init();
-
-	while (alive)
-		run();
-
-	finish();
 }
-#endif
-
-
-
-
-
-
-
-
-
-
-#include <err.h>
-#include <errno.h>
-#include <poll.h>
-#include <signal.h>
-#include <string.h>
-#include <time.h>
-#include <unistd.h>
-
-#include "types.h"
-#include "task.h"
-
-#define SCRIPT \
-	"#!/bin/sh\n" \
-	"echo yes\n" \
-	"sleep 10\n" \
-	"echo no 1>&2\n" \
-	"sleep 1\n" \
-	"exit 1"
-
-int
-main(void)
-{
-	struct job job = {
-		.project_id = 10,
-		.id = 10,
-		.tag = "1234"
-	};
-	struct sigaction sa = {0};
-	struct pollfd fd;
-	struct task *t;
-	int run = 1;
-
-	t = task_new(&job);
-
-	if (task_setup(t, SCRIPT) < 0)
-		err(1, "task_set_script");
-	if (task_start(t) < 0)
-		err(1, "task_start");
-
-	while (run) {
-		if (difftime(time(NULL), task_uptime(t)) >= 3) {
-			printf("task timeout !\n");
-			task_kill(t);
-			task_wait(t);
-			break;
-		}
-
-		task_prepare(t, &fd);
-
-		if (poll(&fd, 1, 250) < 0 && errno != EINTR)
-			err(1, "poll");
-
-		switch (task_sync(t, &fd)) {
-		case -1:
-			err(1, "task_sync");
-		case 0:
-			run = 0;
-			task_wait(t);
-			break;
-		default:
-			/* Keep going... */
-			break;
-		}
-	}
-
-	switch (task_status(t)) {
-	case TASKSTATUS_EXITED:
-		printf("process exited with code: %d\n", task_code(t).exitcode);
-		break;
-	case TASKSTATUS_KILLED:
-		printf("process killed with signal %d\n", task_code(t).sigcode);
-		break;
-	default:
-		break;
-	}
-
-	printf("== console ==\n%s==\n", task_console(t));
-	task_free(t);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sciworkerd/sciworkerd.c	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,393 @@
+#include <errno.h>
+#include <poll.h>
+#include <signal.h>
+#include <string.h>
+#include <time.h>
+
+#include <utlist.h>
+
+#include "apic.h"
+#include "log.h"
+#include "sciworkerd.h"
+#include "task.h"
+#include "types.h"
+#include "util.h"
+
+#define TAG "sigworkerd: "
+
+struct taskentry {
+	struct task *task;
+	struct job job;
+	struct taskentry *next;
+};
+
+static struct taskentry *taskpending;
+static struct taskentry *tasks;
+static struct taskentry *taskfinished;
+static struct worker worker;
+static int run = 1;
+
+struct sciworkerd sciworkerd = {
+	.fetchinterval = 300,
+	.maxjobs = 4,
+	.timeout = 600
+};
+
+static inline void
+taskentry_free(struct taskentry *entry)
+{
+	if (task_status(entry->task) == TASKSTATUS_RUNNING) {
+		if (task_kill(entry->task) == 0)
+			task_wait(entry->task);
+	}
+
+	task_free(entry->task);
+	free(entry);
+}
+
+static void
+stop(int sign)
+{
+	log_info(TAG "exiting on signal %d\n", sign);
+	run = 0;
+}
+
+static inline int
+pending(int id)
+{
+	const struct taskentry *iter;
+
+	LL_FOREACH(taskpending, iter)
+		if (iter->job.id == id)
+			return 1;
+
+	return 0;
+}
+
+static inline void
+queue(const struct job *job)
+{
+	struct taskentry *tk;
+
+	log_info(TAG "queued job build (%d) for tag %s\n", job->id, job->tag);
+
+	tk = util_calloc(1, sizeof (*tk));
+	tk->task = task_new(job->tag);
+	memcpy(&tk->job, job, sizeof (*job));
+	LL_APPEND(taskpending, tk);
+}
+
+static void
+merge(json_t *doc)
+{
+	struct job jobs[SCI_JOB_LIST_MAX];
+	ssize_t jobsz;
+	size_t total = 0;
+
+	if ((jobsz = job_from(jobs, UTIL_SIZE(jobs), doc)) < 0)
+		log_warn(TAG "error while parsing jobs: %s", strerror(errno));
+	else {
+		for (ssize_t i = 0; i < jobsz; ++i) {
+			if (!pending(jobs[i].id)) {
+				queue(&jobs[i]);
+				total++;
+			}
+		}
+
+		log_info(TAG "added %zu new pending tasks", total);
+	}
+}
+
+/*
+ * Fetch jobs periodically, depending on the user setting.
+ */
+static void
+fetch_jobs(void)
+{
+	static time_t startup;
+	time_t now;
+	struct apicreq req;
+
+	if (!startup)
+		startup = time(NULL);
+
+	if (difftime((now = time(NULL)), startup) >= sciworkerd.fetchinterval) {
+		startup = now;
+
+		if (apic_get(&req, "%s/api/v1/%s", sciworkerd.url, sciworkerd.name) < 0)
+			log_warn(TAG "unable to fetch jobs: %s", req.error);
+		if (req.doc) {
+			merge(req.doc);
+			json_decref(req.doc);
+		}
+	}
+}
+
+/*
+ * Fetch information about myself.
+ */
+static void
+fetch_worker(void)
+{
+	struct apicreq req;
+
+	if (apic_get(&req, "%s/api/v1/workers/%s", sciworkerd.url, sciworkerd.name) < 0)
+		log_warn(TAG "unable to fetch worker info: %s", req.error);
+	if (!req.doc)
+		log_die(TAG "empty worker response");
+	if (worker_from(&worker, 1, req.doc) < 0)
+		log_die(TAG "unable to parse worker", strerror(errno));
+
+	log_info("worker id: %d", worker.id);
+	log_info("worker name: %s", worker.name);
+	log_info("worker description: %s", worker.desc);
+
+	json_decref(req.doc);
+}
+
+/*
+ * Fetch information about a project.
+ */
+static int
+fetch_project(struct project *project, int id)
+{
+	struct apicreq req;
+
+	if (apic_get(&req, "%s/api/v1/projects/%d", id) < 0)
+		return log_warn(TAG "unable to fetch project info: %s", req.error), -1;
+	if (!req.doc)
+		return log_warn(TAG "empty project response"), -1;
+	if (project_from(project, 1, req.doc) < 0)
+		return log_warn(TAG "unable to parse project: %s", strerror(errno)), -1;
+
+	return 0;
+}
+
+static inline size_t
+count(const struct taskentry *head)
+{
+	const struct taskentry *iter;
+	size_t tot = 0;
+
+	LL_FOREACH(head, iter)
+		tot++;
+
+	return tot;
+}
+
+/*
+ * Start a task. We fetch its script code and then create the task with that
+ * script.
+ */
+static int
+start(struct taskentry *entry)
+{
+	struct project project;
+	pid_t pid;
+
+	if (fetch_project(&project, entry->job.project_id) < 0)
+		return log_warn(TAG "unable to fetch project, dropping task"), -1;
+	if (task_setup(entry->task, project.script) < 0)
+		return log_warn(TAG "unable to setup script code: %s, dropping task", strerror(errno)), -1;
+	if ((pid = task_start(entry->task)) < 0)
+		return log_warn(TAG "unable to spawn task process: %s", strerror(errno)), -1;
+
+	log_info(TAG "task %lld spawned", (long long int)pid);
+
+	return 0;
+}
+
+static inline void
+delete(struct taskentry *entry)
+{
+	LL_DELETE(taskpending, entry);
+	task_free(entry->task);
+	free(entry);
+}
+
+static void
+start_all(void)
+{
+	size_t running = count(tasks);
+	struct taskentry *entry;
+
+	while (running-- > 0 && (entry = taskpending)) {
+		if (start(entry) < 0)
+			delete(entry);
+		else {
+			LL_DELETE(taskpending, entry);
+			LL_APPEND(tasks, entry);
+		}
+	}
+}
+
+static void
+process_all(void)
+{
+	struct taskentry *iter, *next;
+	struct taskcode code;
+	struct pollfd *fds;
+	size_t fdsz, i = 0;
+	int ret;
+
+	/* First, read every pipes. */
+	if (!(fdsz = count(tasks)))
+		return;
+
+	fds = util_calloc(fdsz, sizeof (*fds));
+
+	for (iter = tasks; iter; iter = iter->next)
+		task_prepare(iter->task, &fds[i++]);
+
+	if (poll(fds, fdsz, 5000) < 0)
+		log_warn("poll: %s", strerror(errno));
+
+	for (iter = tasks, i = 0; i < fdsz; ++i) {
+		next = iter->next;
+
+		/*
+		 *  0: EOF         [wait]
+		 * -1: error       [kill + wait]
+		 * >0: keep going  [nothing]
+		 */
+		if ((ret = task_sync(iter->task, &fds[i])) < 0) {
+			log_warn(TAG "pipe error: %s, killing task", strerror(errno));
+
+			if (task_kill(iter->task) < 0)
+				log_warn(TAG "task kill error: %s", strerror(errno));
+		}
+
+		/* Now wait for the task to complete. */
+		if (ret <= 0) {
+			if (task_wait(iter->task) < 0)
+				log_warn(TAG "task wait error: %s", strerror(errno));
+			else {
+				code = task_code(iter->task);
+
+				switch (task_status(iter->task)) {
+				case TASKSTATUS_EXITED:
+					log_info(TAG "task %lld exited with code %d",
+					    (long long int)task_pid(iter->task), code.exitcode);
+					break;
+				case TASKSTATUS_KILLED:
+					log_info(TAG "task %lld killed with signal %d",
+					    (long long int)task_pid(iter->task), code.sigcode);
+					break;
+				default:
+					break;
+				}
+			}
+
+			/* Remove that task and push to the outgoing queue. */
+			next = iter->next;
+			LL_DELETE(tasks, iter);
+			LL_APPEND(taskfinished, iter);
+		}
+	}
+
+	free(fds);
+}
+
+/*
+ * Kill all tasks that have been running for too long.
+ */
+static void
+ghost_all(void)
+{
+	struct taskentry *iter, *tmp;
+	time_t now;
+
+	LL_FOREACH_SAFE(tasks, iter, tmp) {
+		if (difftime(time(NULL), task_uptime(iter->task)) < sciworkerd.timeout)
+			continue;
+
+		/* Do not attempt to wait if kill failed to avoid lock. */
+		log_info(TAG "task timeout, killing");
+
+		if (task_kill(iter->task) == 0)
+			task_wait(iter->task);
+
+		LL_DELETE(tasks, iter);
+		LL_APPEND(taskfinished, iter);
+	}
+}
+
+static int
+publish(struct taskentry *iter)
+{
+	// TODO: add sigcode.
+	struct taskcode code = task_code(iter->task);
+	struct jobresult res = {
+		.job_id = iter->job.id,
+		.exitcode = code.exitcode,
+		.log = task_console(iter->task),
+		.worker_id = worker.id
+	};
+	struct apicreq req;
+	json_t *doc;
+	int ret; 
+
+	doc = jobresult_to(&res, 1);
+	ret = apic_post(&req, doc, "%s/api/v1/jobs", sciworkerd.url);
+	json_decref(doc);
+
+	if (ret)
+		log_warn(TAG "unable to publish task: %s", req.error);
+	else
+		log_info(TAG "task successfully published");
+
+	return ret;
+}
+
+static void
+publish_all(void)
+{
+	struct taskentry *iter, *tmp;
+
+	LL_FOREACH_SAFE(taskfinished, iter, tmp) {
+		if (publish(iter) == 0) {
+			LL_DELETE(taskfinished, iter);
+			taskentry_free(iter);
+		}
+	}
+}
+
+void
+sciworkerd_init(void)
+{
+	struct sigaction sa = {0};
+
+	log_open("sigworkerd");
+
+	sigemptyset(&sa.sa_mask);
+	sa.sa_handler = stop;
+
+	if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, NULL) < 0)
+		log_die(TAG "sigaction: %s", strerror(errno));
+
+	fetch_worker();
+}
+
+void
+sciworkerd_run(void)
+{
+	while (run) {
+		fetch_jobs();
+		process_all();
+		ghost_all();
+		publish_all();
+	}
+}
+
+void
+sciworkerd_finish(void)
+{
+	struct taskentry *iter, *tmp;
+
+	LL_FOREACH_SAFE(taskpending, iter, tmp)
+		taskentry_free(iter);
+	LL_FOREACH_SAFE(tasks, iter, tmp)
+		taskentry_free(iter);
+	LL_FOREACH_SAFE(taskfinished, iter, tmp)
+		taskentry_free(iter);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sciworkerd/sciworkerd.h	Fri Jul 15 11:11:48 2022 +0200
@@ -0,0 +1,23 @@
+#ifndef SCIWORKERD_H
+#define SCIWORKERD_H
+
+#include "config.h"
+
+extern struct sciworkerd {
+	char url[SCI_URL_MAX];
+	char name[SCI_WORKER_MAX];
+	unsigned int fetchinterval;
+	unsigned int maxjobs;
+	unsigned int timeout;
+} sciworkerd;
+
+void
+sciworkerd_init(void);
+
+void
+sciworkerd_run(void);
+
+void
+sciworkerd_finish(void);
+
+#endif /* !SCIWORKERD_H */
--- a/sciworkerd/task.c	Tue Jul 12 20:20:51 2022 +0200
+++ b/sciworkerd/task.c	Fri Jul 15 11:11:48 2022 +0200
@@ -24,8 +24,6 @@
 	int pipe[2];
 	int exitcode;
 	int sigcode;
-	int job_id;
-	int project_id;
 	char *job_tag;
 	FILE *fp;
 	char *console;
@@ -36,14 +34,12 @@
 };
 
 struct task *
-task_new(const struct job *job)
+task_new(const char *tag)
 {
 	struct task *task;
 
 	task = util_calloc(1, sizeof (*task));
-	task->project_id = job->project_id;
-	task->job_id = job->id;
-	task->job_tag = util_strdup(job->tag);
+	task->job_tag = util_strdup(tag);
 	task->pipe[0] = task->pipe[1] = -1;
 	task->child = -1;
 
@@ -63,8 +59,7 @@
 
 	const size_t len = strlen(script);
 
-	snprintf(self->scriptpath, sizeof (self->scriptpath),
-	    "/tmp/sciworkerd-%d-XXXXXX", self->job_id);
+	snprintf(self->scriptpath, sizeof (self->scriptpath), "/tmp/sciworkerd-XXXXXX");
 
 	if ((self->scriptfd = mkstemp(self->scriptpath)) < 0)
 		goto failed;
@@ -166,6 +161,15 @@
 	return self->startup;
 }
 
+pid_t
+task_pid(const struct task *self)
+{
+	assert(self);
+	assert(self->status == TASKSTATUS_RUNNING);
+
+	return self->child;
+}
+
 const char *
 task_console(const struct task *self)
 {
--- a/sciworkerd/task.h	Tue Jul 12 20:20:51 2022 +0200
+++ b/sciworkerd/task.h	Fri Jul 15 11:11:48 2022 +0200
@@ -3,7 +3,6 @@
 
 #include <sys/types.h>
 
-struct job;
 struct pollfd;
 struct task;
 
@@ -11,8 +10,7 @@
 	TASKSTATUS_PENDING,     /* not started yet. */
 	TASKSTATUS_RUNNING,     /* currently running. */
 	TASKSTATUS_EXITED,      /* process exited normally. */
-	TASKSTATUS_KILLED,      /* process killed killed. */
-	TASKSTATUS_SYNCING      /* was unable to send result to host. */
+	TASKSTATUS_KILLED       /* process killed killed. */
 };
 
 struct taskcode {
@@ -21,7 +19,7 @@
 };
 
 struct task *
-task_new(const struct job *job);
+task_new(const char *tag);
 
 int
 task_setup(struct task *self, const char *script);
@@ -44,6 +42,9 @@
 time_t
 task_uptime(const struct task *self);
 
+pid_t
+task_pid(const struct task *self);
+
 const char *
 task_console(const struct task *self);
 
--- a/sql/init.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/init.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -17,38 +17,34 @@
 --
 
 CREATE TABLE IF NOT EXISTS project(
-	id INTEGER PRIMARY KEY AUTOINCREMENT,
-	name TEXT NOT NULL UNIQUE,
-	desc TEXT NOT NULL,
-	url TEXT NOT NULL,
-	script TEXT NOT NULL,
-	date INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
+	`name` TEXT NOT NULL UNIQUE,
+	`desc` TEXT NOT NULL,
+	`url` TEXT NOT NULL,
+	`script` TEXT NOT NULL,
+	`date` INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
 );
 
 CREATE TABLE IF NOT EXISTS worker(
-	id INTEGER PRIMARY KEY AUTOINCREMENT,
-	name TEXT NOT NULL UNIQUE,
-	desc TEXT NOT NULL,
-	date INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
+	`name` TEXT NOT NULL UNIQUE,
+	`desc` TEXT NOT NULL,
+	`date` INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
 );
 
 CREATE TABLE IF NOT EXISTS job(
-	id INTEGER PRIMARY KEY AUTOINCREMENT,
-	tag TEXT NOT NULL UNIQUE,
-	project_id INTEGER NOT NULL REFERENCES project (id),
-	date INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
+	`tag` TEXT NOT NULL UNIQUE,
+	`project_id` INTEGER NOT NULL REFERENCES project (id),
+	`date` INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
 );
 
 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),
-	exitcode INTEGER DEFAULT 0,
-	console TEXT DEFAULT NULL,
-	date INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
+	`job_id` INTEGER NOT NULL REFERENCES job (id),
+	`worker_id` INTEGER NOT NULL REFERENCES worker (id),
+	`exitcode` INTEGER DEFAULT 0,
+	`console` TEXT DEFAULT NULL,
+	`date` INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
 );
 
 CREATE TABLE IF NOT EXISTS property(
-	key TEXT PRIMARY KEY NOT NULL,
-	value TEXT
+	`key` TEXT PRIMARY KEY NOT NULL,
+	`value` TEXT
 );
--- a/sql/job-add.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/job-add.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -17,6 +17,6 @@
 --
 
 INSERT INTO job(
-  tag,
-  project_id
+  `tag`,
+  `project_id`
 ) VALUES (?, ?)
--- a/sql/job-todo.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/job-todo.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -21,19 +21,19 @@
 -- otherwise when adding a new worker it would need to run potentially a very
 -- high number of jobs
 --
-SELECT job.id
-     , job.tag
-     , job.project_id
-  FROM job
- WHERE job.id
+SELECT `job`.`rowid`
+     , `job`.`tag`
+     , `job`.`project_id`
+  FROM `job`
+ WHERE `job`.`rowid`
 NOT IN (
-       SELECT jobresult.job_id
-         FROM jobresult
-        WHERE jobresult.worker_id = ?
+       SELECT `jobresult`.`job_id`
+         FROM `jobresult`
+        WHERE `jobresult`.`worker_id` = ?
        )
-   AND job.date >= (
-                 SELECT worker.date
-                   FROM worker
-                  WHERE worker.id = ?
+   AND `job`.`date` >= (
+                 SELECT `worker`.`date`
+                   FROM `worker`
+                  WHERE `worker`.`rowid` = ?
                  )
  LIMIT ?
--- a/sql/jobresult-add.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/jobresult-add.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -17,8 +17,8 @@
 --
 
 INSERT INTO jobresult(
-	job_id,
-	worker_id,
-	exitcode,
-	console
+	`job_id`,
+	`worker_id`,
+	`exitcode`,
+	`console`
 ) VALUES (?, ?, ?, ?)
--- a/sql/project-add.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/project-add.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -17,8 +17,8 @@
 --
 
 INSERT INTO project(
-	name,
-	desc,
-	url,
-	script
+	`name`,
+	`desc`,
+	`url`,
+	`script`
 ) VALUES (?, ?, ?, ?)
--- a/sql/project-find-id.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/project-find-id.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -16,7 +16,8 @@
 -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 --
 
-SELECT *
-  FROM project
- WHERE id = ?
+SELECT rowid
+     , *
+  FROM `project`
+ WHERE `rowid` = ?
  LIMIT 1
--- a/sql/project-find.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/project-find.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -16,7 +16,8 @@
 -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 --
 
-SELECT *
-  FROM project
- WHERE name = ?
+SELECT rowid
+     , *
+  FROM `project`
+ WHERE `name` = ?
  LIMIT 1
--- a/sql/project-list.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/project-list.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -16,6 +16,7 @@
 -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 --
 
-SELECT *
-  FROM project
+SELECT rowid
+     , *
+  FROM `project`
  LIMIT ?
--- a/sql/project-update.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/project-update.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -16,9 +16,9 @@
 -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 --
 
-UPDATE project
-   SET name = ?
-     , desc = ?
-     , url = ?
-     , script = ?
- WHERE id = ?
+UPDATE `project`
+   SET `name` = ?
+     , `desc` = ?
+     , `url` = ?
+     , `script` = ?
+ WHERE `rowid` = ?
--- a/sql/worker-add.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/worker-add.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -17,6 +17,6 @@
 --
 
 INSERT INTO worker(
-	name,
-	desc
+	`name`,
+	`desc`
 ) VALUES (?, ?)
--- a/sql/worker-find-id.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/worker-find-id.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -16,7 +16,8 @@
 -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 --
 
-SELECT *
-  FROM worker
- WHERE id = ?
+SELECT rowid
+     , *
+  FROM `worker`
+ WHERE `rowid` = ?
  LIMIT 1
--- a/sql/worker-find.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/worker-find.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -16,7 +16,8 @@
 -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 --
 
-SELECT *
-  FROM worker
- WHERE name = ?
+SELECT rowid
+     , *
+  FROM `worker`
+ WHERE `name` = ?
  LIMIT 1
--- a/sql/worker-list.sql	Tue Jul 12 20:20:51 2022 +0200
+++ b/sql/worker-list.sql	Fri Jul 15 11:11:48 2022 +0200
@@ -16,6 +16,7 @@
 -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 --
 
-SELECT *
-  FROM worker
+SELECT rowid
+     , *
+  FROM `worker`
  LIMIT ?