summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorVito Caputo <vcaputo@pengaru.com>2020-11-25 17:58:50 -0800
committerVito Caputo <vcaputo@pengaru.com>2020-11-25 18:07:05 -0800
commit8ec1588722809a35a7d149bff10de56424e2658a (patch)
treedac9aca6321b92f84505293227c5151193dd8ee6 /src
parent7b15a68d12e2df7f45c4311d0281ff9a78693e81 (diff)
src: initial commit of jio WIP source
This is a very quick and dirty experimental hack written in some sort of bastard continuation-passing style in C w/io_uring using journal-file introspection and manipulation duty as an excuse for its existence. Consider this unfinished prototype quality code.
Diffstat (limited to 'src')
-rw-r--r--src/bootid.c99
-rw-r--r--src/bootid.h10
-rw-r--r--src/humane.c82
-rw-r--r--src/humane.h12
-rw-r--r--src/jio.c139
-rw-r--r--src/journals.c731
-rw-r--r--src/journals.h41
-rw-r--r--src/machid.c72
-rw-r--r--src/machid.h10
-rw-r--r--src/op.h13
-rw-r--r--src/readfile.c96
-rw-r--r--src/readfile.h10
-rw-r--r--src/reclaim-tail-waste.c169
-rw-r--r--src/reclaim-tail-waste.h8
-rw-r--r--src/report-tail-waste.c137
-rw-r--r--src/report-tail-waste.h8
-rw-r--r--src/report-usage.c131
-rw-r--r--src/report-usage.h8
18 files changed, 1776 insertions, 0 deletions
diff --git a/src/bootid.c b/src/bootid.c
new file mode 100644
index 0000000..c2f9d24
--- /dev/null
+++ b/src/bootid.c
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <fcntl.h>
+#include <liburing.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <iou.h>
+#include <thunk.h>
+
+#include "bootid.h"
+#include "readfile.h"
+
+/* Implements iou-based bootid retrieval.
+ *
+ * Note there's not really any reason for doing this async vio iou, it just
+ * served as the initial test/use case while iou was fleshed out, so here it
+ * is, no harm in using it. I guess it's kind of convenient to have a little
+ * kicking of the tires iou/io_uring user at the start of the program anyways,
+ * rather than having something more complex to debug io_uring issues out of
+ * the hole. At least if the bootid is retrieved there's some evidence of the
+ * basic machinery working.
+ */
+
+
+#define BOOTID_PATH "/proc/sys/kernel/random/boot_id"
+
+
+/* perform in-place removal of hyphens from str */
+static void dehyphen(char *str)
+{
+ char *dest;
+
+ for (dest = str; *str; str++) {
+ if (*str == '-')
+ continue;
+
+ *dest++ = *str;
+ }
+
+ *dest = '\0';
+}
+
+/* call user-supplied closure now that buf is populated, after some baking of the data */
+THUNK_DEFINE_STATIC(have_bootid, iou_t *, iou, char *, buf, size_t *, size, char **, res_ptr, thunk_t *, closure)
+{
+ assert(iou);
+ assert(closure);
+
+ /* FIXME: I'm just assuming it's a proper nl-terminated bootid for now, null terminate it */
+ buf[*size - 1] = '\0';
+
+ dehyphen(buf); /* replicating what systemd does with the bootid */
+
+ *res_ptr = strdup(buf);
+ if (!*res_ptr)
+ return -ENOMEM;
+
+ return thunk_dispatch(closure);
+}
+
+
+/* get a bootid via iou, scheduling closure once gotten */
+/* returns < 0 on error, 0 on successful queueing of operation */
+THUNK_DEFINE(bootid_get, iou_t *, iou, char **, res_ptr, thunk_t *, closure)
+{
+ thunk_t *bootid_thunk;
+ struct {
+ char buf[4096];
+ size_t len;
+ } *buf;
+
+ /* we need a buffer for readfiles, it only needs to last until have_bootid,
+ * where it gets null terminated and strdup()d, so allocae it as part of
+ * the have_bootid thunk. The THUNK_ALLOC/THUNK_INIT interfaces are still
+ * evolving.
+ */
+ bootid_thunk = THUNK_ALLOC(have_bootid, (void **)&buf, sizeof(*buf));
+ buf->len = sizeof(buf->buf);
+ THUNK_INIT(have_bootid(bootid_thunk, iou, buf->buf, &buf->len, res_ptr, closure));
+
+ return readfile(iou, BOOTID_PATH, buf->buf, &buf->len, bootid_thunk);
+}
diff --git a/src/bootid.h b/src/bootid.h
new file mode 100644
index 0000000..15ee9be
--- /dev/null
+++ b/src/bootid.h
@@ -0,0 +1,10 @@
+#ifndef _JIO_BOOTID_H
+#define _JIO_BOOTID_H
+
+#include "thunk.h"
+
+typedef struct iou_t iou_t;
+
+THUNK_DECLARE(bootid_get, iou_t *, iou, char **, res_ptr, thunk_t *, closure);
+
+#endif
diff --git a/src/humane.c b/src/humane.c
new file mode 100644
index 0000000..3fdce03
--- /dev/null
+++ b/src/humane.c
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "humane.h"
+
+/* Print bytes as a "human-readable" string in Si storage units into humane->buf and return it. */
+char * humane_bytes(humane_t *humane, uint64_t bytes)
+{
+ double z = bytes;
+
+ static const char * units[] = {
+ "B",
+ "KiB",
+ "MiB",
+ "GiB",
+ "TiB",
+ "PiB",
+ "EiB",
+ };
+ int order = 0;
+
+ while (z >= 1024) {
+ order++;
+ z /= 1024;
+ }
+
+ /* FIXME: isn't there a format specifier for adaptive precision? where %.2 means
+ * use a maximum of two digits but only extend a non-zero fraction up to that limit,
+ * i.e. don't produce outputs like 1.00. but produce 1.1 or 1.01, but 1.00 should be 1.
+ * I can't remember if there's a double format to do that, and can't waste more time
+ * reading the printf(3) man page.
+ */
+ snprintf(humane->buf, sizeof(humane->buf), "%.2f %s", z, units[order]);
+
+ return humane->buf;
+}
+
+
+#if 0
+/* TODO: when/if unit tests become a thing in this tree, turn this into one of them and assert the
+ * stringified "humane" outputs match expectations.
+ */
+
+#define U64(x) UINT64_C(x)
+int main(int argc, char *argv[])
+{
+ uint64_t nums[] = {
+ 0,
+ U64(1),
+ U64(512),
+ U64(1024),
+ U64(1024) + U64(512),
+ U64(1024) * U64(1024),
+ U64(1024) * U64(1024) * U64(1024),
+ U64(1024) * U64(1024) * U64(1024) * U64(1024),
+ U64(1024) * U64(1024) * U64(1024) * U64(1024) * U64(1024),
+ U64(1024) * U64(1024) * U64(1024) * U64(1024) * U64(1024) * U64(1024),
+ UINT64_MAX,
+ };
+ humane_t humane = {};
+
+ for (int i = 0; i < sizeof(nums) / sizeof(*nums); i++)
+ printf("%"PRIu64" humane: %s\n", nums[i], humane_bytes(&humane, nums[i]));
+}
+#endif
diff --git a/src/humane.h b/src/humane.h
new file mode 100644
index 0000000..959589e
--- /dev/null
+++ b/src/humane.h
@@ -0,0 +1,12 @@
+#ifndef _JIO_HUMANE_H
+#define _JIO_HUMANE_H
+
+#include <stdint.h>
+
+typedef struct humane_t {
+ char buf[sizeof("1023.99 EiB")];
+} humane_t;
+
+char * humane_bytes(humane_t *humane, uint64_t bytes);
+
+#endif
diff --git a/src/jio.c b/src/jio.c
new file mode 100644
index 0000000..2e98545
--- /dev/null
+++ b/src/jio.c
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include <iou.h>
+
+#include "reclaim-tail-waste.h"
+#include "report-tail-waste.h"
+#include "report-usage.h"
+
+#include "upstream/journal-def.h"
+
+/* jio - journal-file input/output tool */
+
+/* Copyright (c) 2020 Vito Caputo <vcaputo@pengaru.com> */
+
+/* XXX: This is a WIP experiment, use at your own risk! XXX */
+
+int main(int argc, char *argv[])
+{
+ iou_t *iou;
+ int r;
+
+ if (argc < 2) {
+ printf("Usage: %s {help,reclaim,report} [subcommand-args]\n", argv[0]);
+ return 0;
+ }
+
+ iou = iou_new(8);
+ if (!iou)
+ return 1;
+
+ /* FIXME TODO This is ad-hoc open-coded jank for now */
+ if (!strcmp(argv[1], "help")) {
+ printf(
+ "\n"
+ " help show this help\n"
+ " license print license header\n"
+ " reclaim [subcmd] reclaim space from journal files\n"
+ " tail-waste reclaim wasted space from tails of archives\n"
+ "\n"
+ " report [subcmd] report statistics about journal files\n"
+ " usage report space used by various object types\n"
+ " tail-waste report extra space allocated onto tails\n"
+ " version print jio version\n"
+ "\n"
+ );
+ return 0;
+ } else if (!strcmp(argv[1], "license")) {
+ printf(
+ "\n"
+ " Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com>\n"
+ "\n"
+ " This program is free software: you can redistribute it and/or modify it\n"
+ " under the terms of the GNU General Public License version 3 as published\n"
+ " by the Free Software Foundation.\n"
+ "\n"
+ " This program is distributed in the hope that it will be useful,\n"
+ " but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
+ " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
+ " GNU General Public License for more details.\n"
+ "\n"
+ " You should have received a copy of the GNU General Public License\n"
+ " along with this program. If not, see <http://www.gnu.org/licenses/>.\n"
+ "\n"
+ );
+ return 0;
+ } else if (!strcmp(argv[1], "reclaim")) {
+ if (argc < 3) {
+ printf("Usage: %s reclaim {tail-waste}\n", argv[0]);
+ return 0;
+ }
+
+ if (!strcmp(argv[2], "tail-waste")) {
+ r = jio_reclaim_tail_waste(iou, argc, argv);
+ if (r < 0) {
+ fprintf(stderr, "failed to reclaim tail waste: %s\n", strerror(-r));
+ return 1;
+ }
+ } else {
+ fprintf(stderr, "Unsupported reclaim subcommand: \"%s\"\n", argv[2]);
+ return 1;
+ }
+ } else if (!strcmp(argv[1], "report")) {
+ if (argc < 3) {
+ printf("Usage: %s report {usage,tail-waste}\n", argv[0]);
+ return 0;
+ }
+
+ if (!strcmp(argv[2], "tail-waste")) {
+ r = jio_report_tail_waste(iou, argc, argv);
+ if (r < 0) {
+ fprintf(stderr, "failed to report tail waste: %s\n", strerror(-r));
+ return 1;
+ }
+ } else if (!strcmp(argv[2], "usage")) {
+ r = jio_report_usage(iou, argc, argv);
+ if (r < 0) {
+ fprintf(stderr, "failed to report usage: %s\n", strerror(-r));
+ return 1;
+ }
+ } else {
+ fprintf(stderr, "Unsupported report subcommand: \"%s\"\n", argv[2]);
+ return 1;
+ }
+ } else if (!strcmp(argv[1], "version")) {
+ puts("jio version " VERSION);
+ return 0;
+ } else {
+ fprintf(stderr, "Unsupported subcommand: \"%s\"\n", argv[1]);
+ return 1;
+ }
+
+ if (!r) {
+ /* XXX: note this is a successful noop if there's no outstanding io */
+ r = iou_run(iou);
+ if (r < 0)
+ fprintf(stderr, "iou error: %s\n", strerror(-r));
+ }
+
+ iou_free(iou);
+
+ return r != 0;
+}
diff --git a/src/journals.c b/src/journals.c
new file mode 100644
index 0000000..7ad60d4
--- /dev/null
+++ b/src/journals.c
@@ -0,0 +1,731 @@
+/*
+ * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <liburing.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <iou.h>
+#include <thunk.h>
+
+#include "journals.h"
+#include "op.h"
+
+#include "upstream/journal-def.h"
+
+
+#define PERSISTENT_PATH "/var/log/journal"
+
+
+typedef struct journals_t {
+ int dirfd;
+ size_t n_journals, n_allocated, n_opened;
+ journal_t journals[];
+} journals_t;
+
+
+/* an open on journal->name was attempted, result in op->result.
+ * bump *journals->n_opened, when it matches *journals->n_journals, dispatch closure
+ */
+THUNK_DEFINE_STATIC(opened_journal, iou_t *, iou, iou_op_t *, op, journals_t *, journals, journal_t *, journal, thunk_t *, closure)
+{
+ assert(iou);
+ assert(op);
+ assert(journals);
+ assert(journal);
+ assert(closure);
+
+ /* note n_opened is a count of open calls, not successes, the closure only gets called
+ * when all the opens have been performed hence the need to count them.
+ */
+ journals->n_opened++;
+
+ if (op->result < 0 ) {
+ if (op->result != -EPERM && op->result != -EACCES)
+ return op->result;
+
+ fprintf(stderr, "Permission denied opening \"%s\", ignoring\n", journal->name);
+ journal->fd = -1;
+ } else {
+ journal->fd = op->result;
+ }
+
+ if (journals->n_opened == journals->n_journals)
+ return thunk_dispatch(closure);
+
+ return 0;
+}
+
+
+THUNK_DEFINE_STATIC(get_journals, iou_t *, iou, iou_op_t *, op, journals_t **, journals, int, flags, thunk_t *, closure)
+{
+ struct dirent *dent;
+ DIR *jdir;
+ size_t n = 0;
+ journals_t *j;
+ int r;
+
+ assert(iou);
+ assert(op);
+ assert(journals);
+ assert(closure);
+
+ if (op->result < 0)
+ return op->result;
+
+ /* I don't see any readdir/getdents ops for io_uring, so just do the opendir/readdir
+ * synchronously here before queueing the opening of all those paths.
+ */
+ jdir = fdopendir(op->result);
+ if (jdir == NULL) {
+ int r = errno;
+
+ close(op->result); /* only on successful fdopendir is the fd owned and closed by jdir */
+
+ return r;
+ }
+
+ while ((dent = readdir(jdir))) {
+ if (dent->d_name[0] == '.') /* just skip dot files and "." ".." */
+ continue;
+
+ n++;
+ }
+ rewinddir(jdir);
+
+ if (!n) /* no journals! */
+ return 0;
+
+ j = calloc(1, sizeof(journals_t) + sizeof(j->journals[0]) * n);
+ if (!j) {
+ closedir(jdir);
+ return -ENOMEM;
+ }
+
+ j->n_allocated = n;
+
+ while ((dent = readdir(jdir))) {
+ if (dent->d_name[0] == '.') /* just skip dot files and "." ".." */
+ continue;
+
+ j->journals[j->n_journals++].name = strdup(dent->d_name);
+
+ assert(j->n_journals <= j->n_allocated);// FIXME this defends against the race, but the race
+ // is a normal thing and should be handled gracefully,
+ // or just stop doing this two pass dance.
+ }
+
+ /* duplicate the dirfd for openat use later on, since closedir() will close op->result */
+ j->dirfd = dup(op->result);
+ closedir(jdir);
+
+ /* to keep things relatively simple, let's ensure there's enough queue space for at least one
+ * operation per journal.
+ */
+ r = iou_resize(iou, j->n_journals);
+ if (r < 0)
+ return r; /* TODO: cleanup j */
+
+ /* we have a list of journal names, now queue requests to open them all */
+ for (size_t i = 0; i < j->n_journals; i++) {
+ iou_op_t *oop;
+
+ oop = iou_op_new(iou);
+ if (!oop)
+ return -ENOMEM; // FIXME: cleanup
+
+ io_uring_prep_openat(oop->sqe, j->dirfd, j->journals[i].name, flags, 0);
+ op_queue(iou, oop, THUNK(opened_journal(iou, oop, j, &j->journals[i], closure)));
+ }
+
+ iou_flush(iou);
+
+ /* stow the journals where they can be found, but note they aren't opened yet. */
+ *journals = j;
+
+ return 0;
+}
+
+
+THUNK_DEFINE_STATIC(var_opened, iou_t *, iou, iou_op_t *, op, char **, machid, journals_t **, journals, int, flags, thunk_t *, closure)
+{
+ iou_op_t *mop;
+
+ if (op->result < 0)
+ return op->result;
+
+ /* PERSISTENT_PATH is opened, req open ./$machid */
+ mop = iou_op_new(iou);
+ if (!mop)
+ return -ENOMEM;
+
+ /* req iou to open dir, passing the req to read and open its contents */
+ io_uring_prep_openat(mop->sqe, op->result, *machid, O_DIRECTORY|O_RDONLY, 0);
+ op_queue(iou, mop, THUNK(get_journals(iou, mop, journals, flags, closure)));
+
+ return 0;
+}
+
+
+/* requesting opening the journals via iou, allocating the resulting journals_t @ *journals */
+/* returns < 0 on error, 0 on successful queueing of operation */
+THUNK_DEFINE(journals_open, iou_t *, iou, char **, machid, int, flags, journals_t **, journals, thunk_t *, closure)
+{
+ iou_op_t *op;
+
+ assert(iou);
+ assert(machid);
+ assert(journals);
+ assert(closure);
+
+ op = iou_op_new(iou);
+ if (!op)
+ return -ENOMEM;
+
+ /* req iou to open PERSISTENT_PATH, passing on to var_opened */
+ io_uring_prep_openat(op->sqe, 0, PERSISTENT_PATH, O_DIRECTORY|O_RDONLY, 0);
+ op_queue(iou, op, THUNK(var_opened(iou, op, machid, journals, flags, closure)));
+
+ return 0;
+}
+
+
+
+THUNK_DEFINE_STATIC(got_iter_object_header, iou_t *, iou, iou_op_t *, op, journal_t *, journal, uint64_t *, iter_offset, ObjectHeader *, iter_object_header, thunk_t *, closure)
+{
+ assert(iou);
+ assert(journal);
+ assert(iter_offset);
+ assert(iter_object_header);
+ assert(closure);
+
+ if (op->result < 0)
+ return op->result;
+
+ if (op->result != sizeof(ObjectHeader))
+ return -EINVAL;
+
+ iter_object_header->size = le64toh(iter_object_header->size);
+
+ return thunk_dispatch_keep(closure);
+}
+
+
+/* Queues IO on iou for getting the next object from (*journal)->fd
+ * addressed by state in {iter_offset,iter_object_header} storing it @
+ * *iter_object_header which must already have space allocated to accomodate
+ * sizeof(ObjectHeader), registering closure for dispatch when completed. This
+ * only performs a single step, advancing the offset in *iter_offset by
+ * *iter_object_header->size, ready for continuation in a subsequent call.
+ *
+ * When (*iter_offset == 0) it's considered the initial iteration, as this is
+ * an invalid object offset (0=the journal header), the first object of the
+ * journal will be loaded into *iter_object_header without advancing anything -
+ * ignoring the initial contents of *iter_object_header.
+ *
+ * When closure gets dispatched with (*iter_offset == 0), there's no more
+ * objects in journal, and *iter_object_header will have been unmodified.
+ * Hence closure should always check if (*iter_offset == 0) before accessing
+ * *iter_object_header, and do whatever is appropriate upon reaching the end of
+ * the journal. If journal_iter_next_object() recurs after reaching this
+ * point, it will restart iterating from the first object of the journal.
+ *
+ * Currently closures before the end of journal are dispatched w/the
+ * non-freeing variant thunk_dispatch_keep(). Only the last dispatch
+ * w/(*iter_offset == 0) is dispatched with the freeing thunk_dispatch(). This
+ * feels clunky, but it works for now. I might extend thunk.h to let closures
+ * control wether their dispatch should free or not via the return value. TODO
+ */
+THUNK_DEFINE(journal_iter_next_object, iou_t *, iou, journal_t **, journal, Header *, header, uint64_t *, iter_offset, ObjectHeader *, iter_object_header, thunk_t *, closure)
+{
+ iou_op_t *op;
+
+ assert(iou);
+ assert(journal);
+ assert(iter_offset);
+ assert(iter_object_header);
+ assert(closure);
+
+ /* restart iterating when entered with (*iter_offset == 0) */
+ if (!(*iter_offset))
+ *iter_offset = header->header_size;
+ else {
+ if (iter_object_header->size)
+ *iter_offset += ALIGN64(iter_object_header->size);
+ else {
+ fprintf(stderr, "Encountered zero-sized object, journal \"%s\" appears corrupt, ignoring remainder\n", (*journal)->name);
+ *iter_offset = header->tail_object_offset + 1;
+ }
+ }
+
+ /* final dispatch if past tail object */
+ if (*iter_offset > header->tail_object_offset) {
+ *iter_offset = 0;
+ return thunk_dispatch(closure);
+ }
+
+ op = iou_op_new(iou);
+ if (!op)
+ return -ENOMEM;
+
+ io_uring_prep_read(op->sqe, (*journal)->fd, iter_object_header, sizeof(ObjectHeader), *iter_offset);
+ op_queue(iou, op, THUNK(got_iter_object_header(iou, op, *journal, iter_offset, iter_object_header, closure)));
+
+ return 0;
+}
+
+
+/* Helper for the journal_for_each() simple objects iteration. */
+THUNK_DEFINE_STATIC(journal_for_each_dispatch, iou_t *, iou, journal_t **, journal, Header *, header, uint64_t *, iter_offset, ObjectHeader *, iter_object_header, thunk_t *, closure)
+{
+ int r;
+
+ if (!(*iter_offset))
+ return thunk_dispatch(closure);
+
+ r = thunk_dispatch_keep(closure);
+ if (r < 0)
+ return r;
+
+ return journal_iter_next_object(iou, journal, header, iter_offset, iter_object_header, THUNK(
+ journal_for_each_dispatch(iou, journal, header, iter_offset, iter_object_header, closure)));
+}
+
+
+/* Convenience journal object iterator, dispatches thunk for every object in the journal.
+ *
+ * Note in this wrapper there's no way to control/defer the iterator, your
+ * closure is simply dispatched in a loop, no continuation is passed to it for
+ * resuming iteration, which tends to limit its use to simple situations.
+ */
+THUNK_DEFINE(journal_for_each, iou_t *, iou, journal_t **, journal, Header *, header, uint64_t *, iter_offset, ObjectHeader *, iter_object_header, thunk_t *, closure)
+{
+ assert(iter_offset);
+
+ *iter_offset = 0;
+
+ return journal_iter_next_object(iou, journal, header, iter_offset, iter_object_header, THUNK(
+ journal_for_each_dispatch(iou, journal, header, iter_offset, iter_object_header, closure)));
+}
+
+
+THUNK_DEFINE_STATIC(got_hash_table_iter_object_header, iou_t *, iou, iou_op_t *, op, journal_t *, journal, HashItem *, hash_table, uint64_t, nbuckets, uint64_t *, iter_bucket, uint64_t *, iter_offset, HashedObjectHeader *, iter_object_header, size_t, iter_object_size, thunk_t *, closure)
+{
+ assert(iou);
+ assert(journal);
+ assert(hash_table);
+ assert(iter_bucket && *iter_bucket < nbuckets);
+ assert(iter_offset);
+ assert(iter_object_header);
+ assert(closure);
+
+ if (op->result < 0)
+ return op->result;
+
+ if (op->result != iter_object_size)
+ return -EINVAL;
+
+ iter_object_header->object.size = le64toh(iter_object_header->object.size);
+ iter_object_header->hash = le64toh(iter_object_header->hash);
+ iter_object_header->next_hash_offset = le64toh(iter_object_header->next_hash_offset);
+
+ if (iter_object_size > sizeof(HashedObjectHeader)) {
+ /* The caller can iterate either the field or data hash tables,
+ * so just introspect and handle those two... using a size >
+ * than the minimum HashedObjectHeader as a heuristic.
+ * Scenarios that only need what's available in HashedObjectHeader
+ * can bypass all this and only read in the HashedObjectHeader
+ * while iterating.
+ */
+ switch (iter_object_header->object.type) {
+ case OBJECT_DATA: {
+ DataObject *data_object = (DataObject *)iter_object_header;
+
+ assert(iter_object_size >= sizeof(DataObject));
+
+ data_object->next_field_offset = le64toh(data_object->next_field_offset);
+ data_object->entry_offset = le64toh(data_object->entry_offset);
+ data_object->entry_array_offset = le64toh(data_object->entry_array_offset);
+ data_object->n_entries = le64toh(data_object->n_entries);
+ break;
+ }
+ case OBJECT_FIELD: {
+ FieldObject *field_object = (FieldObject *)iter_object_header;
+
+ assert(iter_object_size >= sizeof(FieldObject));
+
+ field_object->head_data_offset = le64toh(field_object->head_data_offset);
+ break;
+ }
+ default:
+ assert(0);
+ }
+ }
+
+ return thunk_dispatch_keep(closure);
+}
+
+
+/* Queues IO on iou for getting the next object from hash_table{_size}
+ * positioned by state {iter_bucket,iter_offset,iter_object_header} storing it
+ * @ *iter_object_header which must already have space allocated to accomodate
+ * iter_object_size, registering closure for dispatch when completed. This
+ * only performs a single step, advancing the state in
+ * {iter_bucket,iter_offset} appropriate for continuing via another call.
+ *
+ * When (*iter_offset == 0) it's considered the initial iteration, as this is
+ * an invalid object offset (0=the journal header), and the first object of the
+ * table will be loaded into *iter_object_header without advancing anything.
+ *
+ * When closure gets dispatched with (*iter_offset == 0), there's no more
+ * objects in hash_table, and *iter_object_header will have been unmodified.
+ * Hence closure should always check if (*iter_offset == 0) before using
+ * *iter_object_header, and do whatever is appropriate upon reaching the end of
+ * the hash table. If journal_hash_table_iter_next_object() recurs after
+ * reaching this point, it will restart iterating from the first object of the
+ * table.
+ *
+ * Currently closures before the end of hash_table are dispatched w/the
+ * non-freeing variant thunk_dispatch_keep(). Only the last dispatch
+ * w/(*iter_offset == 0) is dispatched with the freeing thunk_dispatch().
+ * This feels clunky, but it works for now. I might extend thunk.h to let
+ * closures control wether their dispatch should free or not via the return
+ * value. TODO
+ */
+THUNK_DEFINE(journal_hash_table_iter_next_object, iou_t *, iou, journal_t **, journal, HashItem **, hash_table, uint64_t *, hash_table_size, uint64_t *, iter_bucket, uint64_t *, iter_offset, HashedObjectHeader *, iter_object_header, size_t, iter_object_size, thunk_t *, closure)
+{
+ size_t nbuckets;
+ iou_op_t *op;
+
+ assert(iou);
+ assert(journal);
+ assert(hash_table);
+ assert(hash_table_size && *hash_table_size >= sizeof(HashItem));
+ assert(iter_bucket);
+ assert(iter_offset);
+ assert(iter_object_header);
+ assert(iter_object_size >= sizeof(HashedObjectHeader));
+ assert(closure);
+
+ nbuckets = *hash_table_size / sizeof(HashItem);
+
+ assert(*iter_bucket < nbuckets);
+
+ /* restart iterating when entered with (*iter_offset == 0) */
+ if (!*iter_offset)
+ *iter_bucket = 0;
+
+ if (*iter_offset && *iter_offset != (*hash_table)[*iter_bucket].tail_hash_offset) {
+ *iter_offset = iter_object_header->next_hash_offset;
+ } else {
+ do {
+ (*iter_bucket)++;
+ if (*iter_bucket >= nbuckets) {
+ *iter_offset = 0;
+ return thunk_dispatch(closure);
+ }
+ (*iter_offset) = (*hash_table)[*iter_bucket].head_hash_offset;
+ } while (!(*iter_offset));
+ }
+
+ op = iou_op_new(iou);
+ if (!op)
+ return -ENOMEM;
+
+ io_uring_prep_read(op->sqe, (*journal)->fd, iter_object_header, iter_object_size, *iter_offset);
+ op_queue(iou, op, THUNK(got_hash_table_iter_object_header(iou, op, *journal, *hash_table, *hash_table_size, iter_bucket, iter_offset, iter_object_header, iter_object_size, closure)));
+
+ return 0;
+}
+
+
+/* Helper for the journal_hash_table_for_each() simple hash table iteration. */
+THUNK_DEFINE_STATIC(journal_hash_table_for_each_dispatch, iou_t *, iou, journal_t **, journal, HashItem **, hash_table, uint64_t *, hash_table_size, uint64_t *, iter_bucket, uint64_t *, iter_offset, HashedObjectHeader *, iter_object_header, size_t, iter_object_size, thunk_t *, closure)
+{
+ int r;
+
+ if (!*iter_offset)
+ return thunk_dispatch(closure);
+
+ r = thunk_dispatch_keep(closure);
+ if (r < 0)
+ return r;
+
+ return journal_hash_table_iter_next_object(iou, journal, hash_table, hash_table_size, iter_bucket, iter_offset, iter_object_header, iter_object_size, THUNK(
+ journal_hash_table_for_each_dispatch(iou, journal, hash_table, hash_table_size, iter_bucket, iter_offset, iter_object_header, iter_object_size, closure)));
+}
+
+
+/* Convenience hash table iterator, dispatches thunk for every object in the hash table.
+ * iter_object_size would typically be sizeof(DataObject) or sizeof(FieldObject), as
+ * these are the two hash table types currently handled. The size is supplied rather
+ * than a type, because one may iterate by only loading the HashedObjectHeader portion.
+ * The size is introspected to determine if more than just the HashedObjectHeader is being
+ * loaded, and its type asserted to fit in the supplied space.
+ *
+ * Note in this wrapper there's no way to control/defer the iterator, your
+ * closure is simply dispatched in a loop, no continuation is passed to it for
+ * resuming iteration, which tends to limit its use to simple situations.
+ */
+THUNK_DEFINE(journal_hash_table_for_each, iou_t *, iou, journal_t **, journal, HashItem **, hash_table, uint64_t *, hash_table_size, uint64_t *, iter_bucket, uint64_t *, iter_offset, HashedObjectHeader *, iter_object_header, size_t, iter_object_size, thunk_t *, closure)
+{
+ assert(iter_offset);
+
+ *iter_offset = 0;
+
+ return journal_hash_table_iter_next_object(iou, journal, hash_table, hash_table_size, iter_bucket, iter_offset, iter_object_header, iter_object_size, THUNK(
+ journal_hash_table_for_each_dispatch(iou, journal, hash_table, hash_table_size, iter_bucket, iter_offset, iter_object_header, iter_object_size, closure)));
+}
+
+
+THUNK_DEFINE_STATIC(got_hashtable, iou_t *, iou, iou_op_t *, op, HashItem *, table, uint64_t, size, HashItem **, res_hash_table, thunk_t *, closure)
+{
+ assert(iou);
+ assert(op);
+ assert(table);
+ assert(res_hash_table);
+ assert(closure);
+
+ if (op->result < 0)
+ return op->result;
+
+ if (op->result != size)
+ return -EINVAL;
+
+ for (uint64_t i = 0; i < size / sizeof(HashItem); i++) {
+ table[i].head_hash_offset = le64toh(table[i].head_hash_offset);
+ table[i].tail_hash_offset = le64toh(table[i].tail_hash_offset);
+ }
+ /* TODO: validation/sanity checks? */
+
+ *res_hash_table = table;
+
+ return thunk_dispatch(closure);
+}
+
+
+/* Queue IO on iou for loading a journal's hash table from *journal into memory allocated and stored @ *res_hash_table.
+ * Registers closure for dispatch when completed.
+ */
+THUNK_DEFINE(journal_get_hash_table, iou_t *, iou, journal_t **, journal, uint64_t *, hash_table_offset, uint64_t *, hash_table_size, HashItem **, res_hash_table, thunk_t *, closure)
+{
+ iou_op_t *op;
+ HashItem *table;
+
+ assert(iou);
+ assert(journal);
+ assert(hash_table_offset);
+ assert(hash_table_size);
+ assert(res_hash_table);
+ assert(closure);
+
+ op = iou_op_new(iou);
+ if (!op)
+ return -ENOMEM;
+
+ table = malloc(*hash_table_size);
+ if (!table)
+ return -ENOMEM;
+
+ io_uring_prep_read(op->sqe, (*journal)->fd, table, *hash_table_size, *hash_table_offset);
+ op_queue(iou, op, THUNK(got_hashtable(iou, op, table, *hash_table_size, res_hash_table, closure)));
+
+ return 0;
+}
+
+
+/* Validate and prepare journal header loaded via journal_get_header @ header, dispatch closure. */
+THUNK_DEFINE_STATIC(got_header, iou_t *, iou, iou_op_t *, op, journal_t *, journal, Header *, header, thunk_t *, closure)
+{
+ assert(iou);
+ assert(op);
+ assert(journal);
+ assert(header);
+ assert(closure);
+
+ if (op->result < 0)
+ return op->result;
+
+ if (op->result < sizeof(*header))
+ return -EINVAL;
+
+ header->compatible_flags = le32toh(header->compatible_flags);
+ header->incompatible_flags = le32toh(header->incompatible_flags);
+ header->header_size = le64toh(header->header_size);
+ header->arena_size = le64toh(header->arena_size);
+ header->data_hash_table_offset = le64toh(header->data_hash_table_offset);
+ header->data_hash_table_size = le64toh(header->data_hash_table_size);
+ header->field_hash_table_offset = le64toh(header->field_hash_table_offset);
+ header->field_hash_table_size = le64toh(header->field_hash_table_size);
+ header->tail_object_offset = le64toh(header->tail_object_offset);
+ header->n_objects = le64toh(header->n_objects);
+ header->n_entries = le64toh(header->n_entries);
+ header->tail_entry_seqnum = le64toh(header->tail_entry_seqnum);
+ header->head_entry_seqnum = le64toh(header->head_entry_seqnum);
+ header->entry_array_offset = le64toh(header->entry_array_offset);
+ header->head_entry_realtime = le64toh(header->head_entry_realtime);
+ header->tail_entry_realtime = le64toh(header->tail_entry_realtime);
+ header->tail_entry_monotonic = le64toh(header->tail_entry_monotonic);
+ header->n_data = le64toh(header->n_data);
+ header->n_fields = le64toh(header->n_fields);
+ header->n_tags = le64toh(header->n_tags);
+ header->n_entry_arrays = le64toh(header->n_entry_arrays);
+ header->data_hash_chain_depth = le64toh(header->data_hash_chain_depth);
+ header->field_hash_chain_depth = le64toh(header->field_hash_chain_depth);
+ /* TODO: validation/sanity checks? */
+
+ return thunk_dispatch(closure);
+}
+
+
+/* Queue IO on iou for loading a journal's header from *journal into *header.
+ * Registers closure for dispatch when completed.
+ */
+THUNK_DEFINE(journal_get_header, iou_t *, iou, journal_t **, journal, Header *, header, thunk_t *, closure)
+{
+ iou_op_t *op;
+
+ assert(iou);
+ assert(journal);
+ assert(closure);
+
+ op = iou_op_new(iou);
+ if (!op)
+ return -ENOMEM;
+
+ io_uring_prep_read(op->sqe, (*journal)->fd, header, sizeof(*header), 0);
+ op_queue(iou, op, THUNK(got_header(iou, op, *journal, header, closure)));
+
+ return 0;
+}
+
+
+/* Validate and prepare object header loaded via journal_get_object_header @ object_header, dispatch closure. */
+THUNK_DEFINE_STATIC(got_object_header, iou_t *, iou, iou_op_t *, op, ObjectHeader *, object_header, thunk_t *, closure)
+{
+ assert(iou);
+ assert(op);
+ assert(object_header);
+ assert(closure);
+
+ if (op->result < 0)
+ return op->result;
+
+ if (op->result < sizeof(*object_header))
+ return -EINVAL;
+
+ object_header->size = le64toh(object_header->size);
+ /* TODO: validation/sanity checks? */
+
+ return thunk_dispatch(closure);
+}
+
+
+/* Queue IO on iou for loading an object header from *journal @ offset *offset, into *object_header.
+ * Registers closure for dispatch on the io when completed.
+ */
+THUNK_DEFINE(journal_get_object_header, iou_t *, iou, journal_t **, journal, uint64_t *, offset, ObjectHeader *, object_header, thunk_t *, closure)
+{
+ iou_op_t *op;
+
+ assert(iou);
+ assert(journal);
+ assert(offset);
+ assert(object_header);
+ assert(closure);
+
+ op = iou_op_new(iou);
+ if (!op)
+ return -ENOMEM;
+
+ io_uring_prep_read(op->sqe, (*journal)->fd, object_header, sizeof(*object_header), *offset);
+ op_queue(iou, op, THUNK(got_object_header(iou, op, object_header, closure)));
+
+ return 0;
+}
+
+
+/* for every open journal in *journals, store the journal in *journal_iter and dispatch closure */
+/* closure must expect to be dispatched multiple times; once per journal, and will be freed once at end */
+THUNK_DEFINE(journals_for_each, journals_t **, journals, journal_t **, journal_iter, thunk_t *, closure)
+{
+ journals_t *j;
+
+ assert(journals && *journals);
+ assert(journal_iter);
+ assert(closure);
+
+ j = *journals;
+
+ for (size_t i = 0; i < j->n_journals; i++) {
+ int r;
+
+ if (j->journals[i].fd < 0)
+ continue;
+
+ (*journal_iter) = &j->journals[i];
+
+ r = thunk_dispatch_keep(closure);
+ if (r < 0) {
+ free(closure);
+ return r;
+ }
+ }
+
+ free(closure);
+
+ return 0;
+}
+
+
+const char * journal_object_type_str(ObjectType type)
+{
+ static const char *names[] = {
+ "UNUSED",
+ "Data",
+ "Field",
+ "Entry",
+ "DataHashTable",
+ "FieldHashTable",
+ "EntryArray",
+ "Tag",
+ };
+
+ if (type < 0 || type >= sizeof(names) / sizeof(*names))
+ return "UNKNOWN";
+
+ return names[type];
+}
+
+
+const char * journal_state_str(JournalState state)
+{
+ static const char *names[] = {
+ "Offline",
+ "Online",
+ "Archived",
+ };
+
+ if (state < 0 || state >= sizeof(names) / sizeof(*names))
+ return "UNKNOWN";
+
+ return names[state];
+}
diff --git a/src/journals.h b/src/journals.h
new file mode 100644
index 0000000..1cbe660
--- /dev/null
+++ b/src/journals.h
@@ -0,0 +1,41 @@
+#ifndef _JIO_JOURNALS_H
+#define _JIO_JOURNALS_H
+
+#include <stdint.h>
+
+/* open() includes since journals_open() reuses open() flags */
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+
+#include "thunk.h"
+
+#include "upstream/journal-def.h"
+
+typedef struct iou_t iou_t;
+
+typedef struct journal_t {
+ char *name;
+ int fd;
+} journal_t;
+
+typedef struct journals_t journals_t;
+
+THUNK_DECLARE(journals_open, iou_t *, iou, char **, machid, int, flags, journals_t **, journals, thunk_t *, closure);
+THUNK_DECLARE(journal_get_header, iou_t *, iou, journal_t **, journal, Header *, header, thunk_t *, closure);
+
+THUNK_DECLARE(journal_iter_next_object, iou_t *, iou, journal_t **, journal, Header *, header, uint64_t *, iter_offset, ObjectHeader *, iter_object_header, thunk_t *, closure);
+THUNK_DECLARE(journal_for_each, iou_t *, iou, journal_t **, journal, Header *, header, uint64_t *, iter_offset, ObjectHeader *, iter_object_header, thunk_t *, closure);
+
+THUNK_DECLARE(journal_get_hash_table, iou_t *, iou, journal_t **, journal, uint64_t *, hash_table_offset, uint64_t *, hash_table_size, HashItem **, res_hash_table, thunk_t *, closure);
+THUNK_DECLARE(journal_hash_table_iter_next_object, iou_t *, iou, journal_t **, journal, HashItem **, hash_table, uint64_t *, hash_table_size, uint64_t *, iter_bucket, uint64_t *, iter_offset, HashedObjectHeader *, iter_object_header, size_t, iter_object_size, thunk_t *, closure);
+THUNK_DECLARE(journal_hash_table_for_each, iou_t *, iou, journal_t **, journal, HashItem **, hash_table, uint64_t *, hash_table_size, uint64_t *, iter_bucket, uint64_t *, iter_offset, HashedObjectHeader *, iter_object_header, size_t, iter_object_size, thunk_t *, closure);
+
+THUNK_DECLARE(journal_get_object_header, iou_t *, iou, journal_t **, journal, uint64_t *, offset, ObjectHeader *, object_header, thunk_t *, closure);
+THUNK_DECLARE(journals_for_each, journals_t **, journals, journal_t **, journal_iter, thunk_t *, closure);
+
+const char * journal_object_type_str(ObjectType type);
+const char * journal_state_str(JournalState state);
+
+#endif
diff --git a/src/machid.c b/src/machid.c
new file mode 100644
index 0000000..721a4ee
--- /dev/null
+++ b/src/machid.c
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <fcntl.h>
+#include <liburing.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <iou.h>
+#include <thunk.h>
+
+#include "machid.h"
+#include "readfile.h"
+
+/* Implements iou-based machine-id retrieval.
+ *
+ * this is basically copy & paste from bootid.c,
+ * which suggests this should just be readfile.c
+ */
+
+
+#define MACHID_PATH "/etc/machine-id"
+
+
+/* call user-supplied closure now that buf is populated */
+THUNK_DEFINE_STATIC(have_machid, iou_t *, iou, char *, buf, size_t *, size, char **, res_ptr, thunk_t *, closure)
+{
+ assert(iou);
+ assert(closure);
+
+ /* FIXME: I'm just assuming it's a proper nl-terminated machid for now, null terminate it */
+ buf[*size - 1] = '\0';
+
+ *res_ptr = strdup(buf);
+ if (!*res_ptr)
+ return -ENOMEM;
+
+ return thunk_dispatch(closure);
+}
+
+
+/* get a machid via iou, scheduling closure once gotten */
+/* returns < 0 on error, 0 on successful queueing of operation */
+THUNK_DEFINE(machid_get, iou_t *, iou, char **, res_ptr, thunk_t *, closure)
+{
+ thunk_t *machid_thunk;
+ struct {
+ char buf[4096];
+ size_t len;
+ } *buf;
+
+ machid_thunk = THUNK_ALLOC(have_machid, (void **)&buf, sizeof(*buf));
+ buf->len = sizeof(buf->buf);
+ THUNK_INIT(have_machid(machid_thunk, iou, buf->buf, &buf->len, res_ptr, closure));
+
+ return readfile(iou, MACHID_PATH, buf->buf, &buf->len, machid_thunk);
+}
diff --git a/src/machid.h b/src/machid.h
new file mode 100644
index 0000000..6aa972b
--- /dev/null
+++ b/src/machid.h
@@ -0,0 +1,10 @@
+#ifndef _JIO_MACHID_H
+#define _JIO_MACHID_H
+
+#include "thunk.h"
+
+typedef struct iou_t iou_t;
+
+THUNK_DECLARE(machid_get, iou_t *, iou, char **, res_ptr, thunk_t *, closure);
+
+#endif
diff --git a/src/op.h b/src/op.h
new file mode 100644
index 0000000..ccb1932
--- /dev/null
+++ b/src/op.h
@@ -0,0 +1,13 @@
+#ifndef _JIO_OP_H
+#define _JIO_OP_H
+
+#include <iou.h>
+#include <thunk.h>
+
+/* ergonomic helper for submitting a thunk_t as a cb+cb_data pair to iou_op_queue() */
+static inline void op_queue(iou_t *iou, iou_op_t *op, thunk_t *thunk)
+{
+ return iou_op_queue(iou, op, (int (*)(void *))thunk->dispatch, thunk);
+}
+
+#endif
diff --git a/src/readfile.c b/src/readfile.c
new file mode 100644
index 0000000..8af7438
--- /dev/null
+++ b/src/readfile.c
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <fcntl.h>
+#include <liburing.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <iou.h>
+#include <thunk.h>
+
+#include "readfile.h"
+#include "op.h"
+
+/* Implements iou-based file read into a supplied buffer.
+ */
+
+
+/* call user-supplied closure now that buf is populated */
+THUNK_DEFINE_STATIC(read_done, iou_t *, iou, iou_op_t *, op, int, fd, char *, buf, size_t *, size, thunk_t *, closure)
+{
+ assert(iou);
+ assert(op);
+ assert(closure);
+
+ if (op->result < 0)
+ return op->result;
+
+ (void) close(fd);
+
+ *size = op->result;
+
+ return thunk_dispatch(closure);
+}
+
+
+/* ask iou to read from the opened fd into buf */
+THUNK_DEFINE_STATIC(open_done, iou_t *, iou, iou_op_t *, op, char *, buf, size_t *, size, thunk_t *, closure)
+{
+ thunk_t *read_closure;
+ iou_op_t *read_op;
+
+ assert(iou);
+ assert(op);
+
+ if (op->result < 0)
+ return op->result;
+
+ read_op = iou_op_new(iou);
+ if (!read_op)
+ return -ENOMEM;
+
+ read_closure = THUNK(read_done(iou, read_op, op->result, buf, size, closure));
+ if (!read_closure)
+ return -ENOMEM;
+
+ io_uring_prep_read(read_op->sqe, op->result, buf, *size, 0);
+ op_queue(iou, read_op, read_closure);
+
+ return 0;
+}
+
+
+/* read a file into a buffer via iou, dispatching closure when complete */
+/* size specifies the buf size, and will also get the length of what's read
+ * written to it upon completion.
+ */
+/* returns < 0 on error, 0 on successful queueing of operation */
+THUNK_DEFINE(readfile, iou_t *, iou, const char *, path, char *, buf, size_t *, size, thunk_t *, closure)
+{
+ iou_op_t *op;
+
+ op = iou_op_new(iou);
+ if (!op)
+ return -ENOMEM;
+
+ /* req iou to open path, passing the req to read its contents */
+ io_uring_prep_openat(op->sqe, 0, path, 0, O_RDONLY);
+ op_queue(iou, op, THUNK(open_done(iou, op, buf, size, closure)));
+
+ return 0;
+}
diff --git a/src/readfile.h b/src/readfile.h
new file mode 100644
index 0000000..2e414b3
--- /dev/null
+++ b/src/readfile.h
@@ -0,0 +1,10 @@
+#ifndef _JIO_READFILE_H
+#define _JIO_READFILE_H
+
+#include "thunk.h"
+
+typedef struct iou_t iou_t;
+
+THUNK_DECLARE(readfile, iou_t *, iou, const char *, path, char *, buf, size_t *, size, thunk_t *, closure);
+
+#endif
diff --git a/src/reclaim-tail-waste.c b/src/reclaim-tail-waste.c
new file mode 100644
index 0000000..fd3cb13
--- /dev/null
+++ b/src/reclaim-tail-waste.c
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <iou.h>
+#include <thunk.h>
+
+#include "bootid.h"
+#include "humane.h"
+#include "journals.h"
+#include "machid.h"
+#include "reclaim-tail-waste.h"
+
+#include "upstream/journal-def.h"
+
+
+typedef struct tail_waste_t {
+ unsigned n_journals, n_reclaimed, n_ignored, n_errored, n_mules;
+ uint64_t reclaimed_bytes, ignored_bytes, errored_bytes;
+} tail_waste_t;
+
+
+THUNK_DEFINE_STATIC(reclaim_tail_waste, journal_t **, journal, Header *, journal_header, ObjectHeader *, tail_object_header, tail_waste_t *, tail_waste)
+{
+ uint64_t sz, tail;
+ struct stat st;
+ int r;
+ humane_t h1;
+
+ assert(journal);
+ assert(journal_header);
+ assert(tail_object_header);
+ assert(tail_waste);
+
+ r = fstat((*journal)->fd, &st);
+ if (r < 0)
+ return r;
+
+ tail_waste->n_journals++;
+
+ sz = st.st_size;
+ tail = journal_header->tail_object_offset + ALIGN64(tail_object_header->size);
+
+ if (sz == tail) {
+ tail_waste->n_mules++;
+
+ return 0;
+ }
+
+ if (journal_header->state != STATE_ARCHIVED) {
+ printf("Ignoring %s of tail-waste on \"%s\" for not being archived (state=%s)\n",
+ humane_bytes(&h1, sz - tail),
+ (*journal)->name,
+ journal_state_str(journal_header->state));
+
+ tail_waste->n_ignored++;
+ tail_waste->ignored_bytes += sz - tail;
+
+ return 0;
+ }
+
+ if (ftruncate((*journal)->fd, tail) < 0) {
+ fprintf(stderr, "Unable to truncate \"%s\" to %"PRIu64", ignoring: %s\n",
+ (*journal)->name,
+ tail,
+ strerror(errno));
+
+ tail_waste->n_errored++;
+ tail_waste->errored_bytes += sz - tail;
+
+ return 0;
+ }
+
+ tail_waste->n_reclaimed++;
+ tail_waste->reclaimed_bytes += sz - tail;
+
+ return 0;
+}
+
+
+THUNK_DEFINE_STATIC(per_journal_tail_waste, iou_t *, iou, journal_t **, journal_iter, tail_waste_t *, tail_waste)
+{
+ struct {
+ journal_t *journal;
+ Header header;
+ ObjectHeader tail_object_header;
+ } *foo;
+
+ thunk_t *closure;
+
+ assert(iou);
+ assert(journal_iter);
+
+ closure = THUNK_ALLOC(reclaim_tail_waste, (void **)&foo, sizeof(*foo));
+ foo->journal = *journal_iter;
+
+ return journal_get_header(iou, &foo->journal, &foo->header, THUNK(
+ journal_get_object_header(iou, &foo->journal, &foo->header.tail_object_offset, &foo->tail_object_header, THUNK_INIT(
+ reclaim_tail_waste(closure, &foo->journal, &foo->header, &foo->tail_object_header, tail_waste)))));
+}
+
+
+/* print the size of wasted space between each journal's tail object and EOF, and a sum total. */
+int jio_reclaim_tail_waste(iou_t *iou, int argc, char *argv[])
+{
+ char *machid;
+ journals_t *journals;
+ journal_t *journal_iter;
+ tail_waste_t tail_waste = {};
+ int r;
+ humane_t h1;
+
+ r = machid_get(iou, &machid, THUNK(
+ journals_open(iou, &machid, O_RDWR, &journals, THUNK(
+ journals_for_each(&journals, &journal_iter, THUNK(
+ per_journal_tail_waste(iou, &journal_iter, &tail_waste)))))));
+ if (r < 0)
+ return r;
+
+ printf("\nReclaiming tail-waste...\n");
+ r = iou_run(iou);
+ if (r < 0)
+ return r;
+
+ printf("\nSummary:\n");
+ if (!tail_waste.n_journals)
+ printf("\tNo journal files opened!\n");
+
+ if (tail_waste.n_mules)
+ printf("\tSkipped %u journal files free of tail-waste\n",
+ tail_waste.n_mules);
+
+ if (tail_waste.n_ignored)
+ printf("\tIgnored %u unarchived journal files totalling %s of tail-waste\n",
+ tail_waste.n_ignored,
+ humane_bytes(&h1, tail_waste.ignored_bytes));
+
+ if (tail_waste.n_reclaimed)
+ printf("\tReclaimed %s from %u journal files\n",
+ humane_bytes(&h1, tail_waste.reclaimed_bytes),
+ tail_waste.n_reclaimed);
+
+ if (tail_waste.n_errored)
+ printf("\tFailed to relcaim %s from %u journal files due to errors\n",
+ humane_bytes(&h1, tail_waste.errored_bytes),
+ tail_waste.n_errored);
+
+ return 0;
+}
diff --git a/src/reclaim-tail-waste.h b/src/reclaim-tail-waste.h
new file mode 100644
index 0000000..06e403e
--- /dev/null
+++ b/src/reclaim-tail-waste.h
@@ -0,0 +1,8 @@
+#ifndef _JIO_RECLAIM_TAIL_WASTE
+#define _JIO_RECLAIM_TAIL_WASTE
+
+typedef struct iou_t iou_t;
+
+int jio_reclaim_tail_waste(iou_t *iou, int argc, char *argv[]);
+
+#endif
diff --git a/src/report-tail-waste.c b/src/report-tail-waste.c
new file mode 100644
index 0000000..fdd52cb
--- /dev/null
+++ b/src/report-tail-waste.c
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdint.h>
+
+#include <iou.h>
+#include <thunk.h>
+
+#include "bootid.h"
+#include "humane.h"
+#include "journals.h"
+#include "machid.h"
+#include "report-tail-waste.h"
+
+#include "upstream/journal-def.h"
+
+
+typedef struct tail_waste_t {
+ unsigned per_state_counts[_STATE_MAX];
+ uint64_t per_state_bytes[_STATE_MAX];
+ uint64_t total, total_file_size;
+ unsigned n_journals;
+} tail_waste_t;
+
+
+THUNK_DEFINE_STATIC(print_tail_waste, journal_t **, journal, Header *, journal_header, ObjectHeader *, tail_object_header, tail_waste_t *, tail_waste)
+{
+ uint64_t sz, tail;
+ struct stat st;
+ int r;
+ humane_t h1, h2;
+
+ assert(journal);
+ assert(journal_header);
+ assert(tail_object_header);
+ assert(tail_waste);
+
+ r = fstat((*journal)->fd, &st);
+ if (r < 0)
+ return r;
+
+ sz = st.st_size;
+ tail = journal_header->tail_object_offset + ALIGN64(tail_object_header->size);
+
+ printf("\t%s: %s, size: %s, tail-waste: %s\n",
+ journal_state_str(journal_header->state),
+ (*journal)->name,
+ humane_bytes(&h1, sz),
+ humane_bytes(&h2, sz - tail));
+
+ tail_waste->per_state_bytes[journal_header->state] += sz - tail;
+ tail_waste->total += sz - tail;
+ tail_waste->total_file_size += sz;
+ tail_waste->per_state_counts[journal_header->state]++;
+ tail_waste->n_journals++;
+
+ return 0;
+}
+
+
+THUNK_DEFINE_STATIC(per_journal_tail_waste, iou_t *, iou, journal_t **, journal_iter, tail_waste_t *, tail_waste)
+{
+ struct {
+ journal_t *journal;
+ Header header;
+ ObjectHeader tail_object_header;
+ } *foo;
+
+ thunk_t *closure;
+
+ assert(iou);
+ assert(journal_iter);
+
+ closure = THUNK_ALLOC(print_tail_waste, (void **)&foo, sizeof(*foo));
+ foo->journal = *journal_iter;
+
+ return journal_get_header(iou, &foo->journal, &foo->header, THUNK(
+ journal_get_object_header(iou, &foo->journal, &foo->header.tail_object_offset, &foo->tail_object_header, THUNK_INIT(
+ print_tail_waste(closure, &foo->journal, &foo->header, &foo->tail_object_header, tail_waste)))));
+}
+
+
+/* print the size of wasted space between each journal's tail object and EOF, and a sum total. */
+int jio_report_tail_waste(iou_t *iou, int argc, char *argv[])
+{
+ tail_waste_t tail_waste = {};
+ char *machid;
+ journals_t *journals;
+ journal_t *journal_iter;
+ humane_t h1, h2;
+ int r;
+
+ r = machid_get(iou, &machid, THUNK(
+ journals_open(iou, &machid, O_RDONLY, &journals, THUNK(
+ journals_for_each(&journals, &journal_iter, THUNK(
+ per_journal_tail_waste(iou, &journal_iter, &tail_waste)))))));
+ if (r < 0)
+ return r;
+
+ printf("\nPer-journal:\n");
+ r = iou_run(iou);
+ if (r < 0)
+ return r;
+
+ printf("\nTotals:\n");
+ printf("\tTail-waste by state:\n");
+ for (int i = 0; i < _STATE_MAX; i++) {
+ printf("\t\t%10s [%u]: %s, %"PRIu64"%% of all tail-waste\n",
+ journal_state_str(i),
+ tail_waste.per_state_counts[i],
+ humane_bytes(&h1, tail_waste.per_state_bytes[i]),
+ tail_waste.total >= 100 ? tail_waste.per_state_bytes[i] / (tail_waste.total / 100) : 0);
+ }
+
+ printf("\n\tAggregate tail-waste: %s, %"PRIu64"%% of %s spanning %u journal files\n",
+ humane_bytes(&h1, tail_waste.total),
+ tail_waste.total_file_size >= 100 ? tail_waste.total / (tail_waste.total_file_size / 100) : 0,
+ humane_bytes(&h2, tail_waste.total_file_size),
+ tail_waste.n_journals);
+
+ return 0;
+}
diff --git a/src/report-tail-waste.h b/src/report-tail-waste.h
new file mode 100644
index 0000000..7810399
--- /dev/null
+++ b/src/report-tail-waste.h
@@ -0,0 +1,8 @@
+#ifndef _JIO_REPORT_TAIL_WASTE
+#define _JIO_REPORT_TAIL_WASTE
+
+typedef struct iou_t iou_t;
+
+int jio_report_tail_waste(iou_t *iou, int argc, char *argv[]);
+
+#endif
diff --git a/src/report-usage.c b/src/report-usage.c
new file mode 100644
index 0000000..0cadeb7
--- /dev/null
+++ b/src/report-usage.c
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include <iou.h>
+#include <thunk.h>
+
+#include "bootid.h"
+#include "humane.h"
+#include "journals.h"
+#include "machid.h"
+#include "report-usage.h"
+
+#include "upstream/journal-def.h"
+
+
+typedef struct usage_t {
+ uint64_t count_per_type[_OBJECT_TYPE_MAX];
+ uint64_t use_per_type[_OBJECT_TYPE_MAX];
+ uint64_t use_total;
+ uint64_t file_size, file_count;
+} usage_t;
+
+
+THUNK_DEFINE_STATIC(per_data_object, ObjectHeader *, iter_object_header, usage_t *, usage, usage_t *, total_usage)
+{
+ assert(iter_object_header);
+ assert(usage);
+ assert(total_usage);
+
+ usage->count_per_type[iter_object_header->type]++;
+ usage->use_per_type[iter_object_header->type] += iter_object_header->size;
+ usage->use_total += iter_object_header->size;
+
+ total_usage->count_per_type[iter_object_header->type]++;
+ total_usage->use_per_type[iter_object_header->type] += iter_object_header->size;
+ total_usage->use_total += iter_object_header->size;
+
+ return 0;
+}
+
+
+THUNK_DEFINE_STATIC(per_journal, iou_t *, iou, journal_t **, journal_iter, usage_t *, total_usage, unsigned *, n_journals)
+{
+ struct {
+ journal_t *journal;
+ Header header;
+ usage_t usage;
+ uint64_t iter_offset;
+ ObjectHeader iter_object_header;
+ } *foo;
+
+ thunk_t *closure;
+ struct stat st;
+ int r;
+
+ assert(iou);
+ assert(journal_iter);
+ assert(total_usage);
+
+ /* XXX: io_uring has a STATX opcode, so this too could be async */
+ r = fstat((*journal_iter)->fd, &st);
+ if (r < 0)
+ return r;
+
+ closure = THUNK_ALLOC(per_data_object, (void **)&foo, sizeof(*foo));
+ foo->journal = *journal_iter;
+ foo->usage.file_size = st.st_size;
+
+ total_usage->file_size += st.st_size;
+ (*n_journals)++;
+
+ return journal_get_header(iou, &foo->journal, &foo->header, THUNK(
+ journal_for_each(iou, &foo->journal, &foo->header, &foo->iter_offset, &foo->iter_object_header, THUNK_INIT(
+ per_data_object(closure, &foo->iter_object_header, &foo->usage, total_usage)))));
+}
+
+
+/* print the amount of space used by various objects per journal, and sum totals */
+int jio_report_usage(iou_t *iou, int argc, char *argv[])
+{
+ usage_t aggregate_usage = {};
+ char *machid;
+ journals_t *journals;
+ journal_t *journal_iter;
+ unsigned n_journals = 0;
+ humane_t h1, h2;
+ int r;
+
+ r = machid_get(iou, &machid, THUNK(
+ journals_open(iou, &machid, O_RDONLY, &journals, THUNK(
+ journals_for_each(&journals, &journal_iter, THUNK(
+ per_journal(iou, &journal_iter, &aggregate_usage, &n_journals)))))));
+ if (r < 0)
+ return r;
+
+ r = iou_run(iou);
+ if (r < 0)
+ return r;
+
+ printf("Per-object-type usage:\n");
+ for (int i = 0; i < _OBJECT_TYPE_MAX; i++)
+ printf("%16s: [%"PRIu64"] %s\n",
+ journal_object_type_str(i),
+ aggregate_usage.count_per_type[i],
+ humane_bytes(&h1,
+ aggregate_usage.use_per_type[i]));
+
+ printf("Aggregate object usage: %s of %s spanning %u journal files\n",
+ humane_bytes(&h1, aggregate_usage.use_total),
+ humane_bytes(&h2, aggregate_usage.file_size),
+ n_journals);
+
+ return 0;
+}
diff --git a/src/report-usage.h b/src/report-usage.h
new file mode 100644
index 0000000..ce5a6e1
--- /dev/null
+++ b/src/report-usage.h
@@ -0,0 +1,8 @@
+#ifndef _JIO_REPORT_USAGE
+#define _JIO_REPORT_USAGE
+
+typedef struct iou_t iou_t;
+
+int jio_report_usage(iou_t *iou, int argc, char *argv[]);
+
+#endif
© All Rights Reserved