From e540ab40c0efe6360e9a6e3d9f34b0e1b420fdde Mon Sep 17 00:00:00 2001 From: Vito Caputo Date: Wed, 20 Feb 2019 20:21:08 -0800 Subject: codex: initial commit !!! This code is a very quick hack I made in an evening of !!! inspired curiosity, the code should be considered of junk !!! quality. Codex aspires to be a read-only FUSE interface to the journal provided by systemd. At this point it only implements simple reading of the journal through a single match filter selected via filesystem path. With codex mounting the journal @ /mnt, you can see what fields are available for matching against via: `ls /mnt/by-field` This returns a list of fields as directories, which you can then list the contents of to see all the unique values present for the repsective field, returned as regular files: `ls /mnt/by-field/UNIT` Then by reading one of the returned files, you read the contents of the journal filtered on the respective field and value, e.g.: `cat /mnt/by-field/UNIT/apt-daily.timer` and you'll get a stream of all the data in the journal with a filter of UNIT=apt-daily.timer With some work, this could be made more advanced supporting combined filters, perhaps in the form of: `cat /mnt/by-field/UNIT/apt-daily.timer/and/by-field/PRIORITY/3/all` As-is, the path by-field/UNIT/apt-daily.timer is a regular file, so no such constructions are possible. But if it were changed to be another directory, where you can select further matches by traversing yet another and/by-field subpath, where all the fields are available (or maybe only the matching subset of fields if feasible), then more complex uses could be satisfied. Imagine or/by-field and and/by-field being available under every value's directory. Note this commit includes a barely functioning PoC. It is not intended for production, but you can already use it to navigate journals in a way I find more pleasant than journalctl. I think it'd be great to have a tool like this built out with all the bells and whistles. It may make sense to roll something similar into journalctl like `journalctl --mount /mnt/point`. --- src/Makefile.am | 2 + src/codex.c | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 src/Makefile.am create mode 100644 src/codex.c (limited to 'src') diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..29477df --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,2 @@ +bin_PROGRAMS = codex +codex_SOURCES = codex.c diff --git a/src/codex.c b/src/codex.c new file mode 100644 index 0000000..451f646 --- /dev/null +++ b/src/codex.c @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2019 - Vito Caputo - + * + * 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 . + */ + +#define FUSE_USE_VERSION 26 + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef MIN +#define MIN(_a, _b) ((_a) < (_b)) ? (_a) : (_b) +#endif + +static sd_journal *sdj; + +static int codex_getattr(const char *path, struct stat *stbuf) +{ + int res = 0; + + memset(stbuf, 0, sizeof(struct stat)); + if (strcmp(path, "/") == 0) { + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 2; + } else if (!strcmp(path, "/by-field")) { + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 2; + } else if (!strncmp(path, "/by-field/", 10)) { + const char *value = strchr(&path[10], '/'); + + if (!value) { + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 2; + } else { + stbuf->st_mode = S_IFREG | 0444; + stbuf->st_nlink = 1; + } + } else if (!strcmp(path, "/by-boot")) { + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 2; + } else + res = -ENOENT; + + return res; +} + + +/* TODO: this is a temp hack, there's no unescaping, it's the bare minimum + * to avoid producing dentry names with slashes. + */ +static void escape(char *buf, size_t size) +{ + if (!size) + size = strlen(buf); + + for (size_t i = 0; i < size; i++) { + if (buf[i] == '/') + buf[i] = '-'; + } +} + + +static void unescape(char *buf, size_t size) +{ + if (!size) + size = strlen(buf); + + for (size_t i = 0; i < size; i++) { + if (buf[i] == '-') + buf[i] = '/'; + } +} + + +static int codex_readdir(const char *path, void *buf, fuse_fill_dir_t filler, + off_t offset, struct fuse_file_info *fi) +{ + if (!strcmp(path, "/by-field")) { + const char *field; + + filler(buf, ".", NULL, 0); + filler(buf, "..", NULL, 0); + + SD_JOURNAL_FOREACH_FIELD(sdj, field) + filler(buf, field, NULL, 0); + + } else if (!strncmp(path, "/by-field/", 10)) { + char name[NAME_MAX]; + const void *data; + size_t size; + + filler(buf, ".", NULL, 0); + filler(buf, "..", NULL, 0); + + sd_journal_query_unique(sdj, &path[10]); + SD_JOURNAL_FOREACH_UNIQUE(sdj, data, size) { + const void *eq; + + eq = memchr(data, '=', size); + if (eq) + snprintf(name, sizeof(name), "%.*s", (int) (size - ((const uint8_t*) eq - (const uint8_t*) data + 1)), (const char*) eq + 1); + else + snprintf(name, sizeof(name), "%.*s", (int) size, (const char*) data); + + /* TODO: use proper escaping */ + escape(name, strlen(name)); + + filler(buf, name, NULL, 0); + } + + } else if (!strcmp(path, "/by-boot")) { + filler(buf, ".", NULL, 0); + filler(buf, "..", NULL, 0); + + /* TODO: enumerate boots in a more readable fashion like journalctl --list-boots */ + filler(buf, "IMPLEMENT ME", NULL, 0); + } else if (!strcmp(path, "/")) { + filler(buf, ".", NULL, 0); + filler(buf, "..", NULL, 0); + + filler(buf, "by-field", NULL, 0); + filler(buf, "by-boot", NULL, 0); + } else { + return -ENOENT; + } + + return 0; +} + + +static int codex_open(const char *path, struct fuse_file_info *fi) +{ + if ((fi->flags & 3) != O_RDONLY) + return -EACCES; + + return 0; +} + + +static int codex_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) +{ + static size_t data_len, data_off; + static int needs_nl; + const char *field, *value; + char *data_ptr; + int cnt = 0; + + if (strncmp(path, "/by-field/", 10)) + return -ENOENT; + + field = &path[10]; + value = strchr(field, '/'); + if (!value) + return -ENOENT; + value++; + + /* This is all a dirty hack, zero offset is assumed to be new open. + * XXX: no support for concurrent multiple file accesses or threding etc. + */ + if (!offset) { + char buf[1024]; + int r; + + sd_journal_flush_matches(sdj); + sd_journal_seek_head(sdj); + + snprintf(buf, sizeof(buf), "%.*s=%s", (int)(value - field - 1), field, value); + /* TODO: use proper escaping */ + unescape(&buf[value - field], 0); /* XXX: overflow risk */ + + r = sd_journal_add_match(sdj, buf, 0); + if (r < 0) + return r; + + sd_journal_next(sdj); + needs_nl = data_len = data_off = 0; + } + + while (cnt < size) { + size_t len; + + if (data_off >= data_len) { + data_off = 0; + while (!sd_journal_enumerate_data(sdj, (void *)&data_ptr, &data_len) || !data_len) { + if (!sd_journal_next(sdj)) { + data_len = 0; + if (needs_nl) + break; + + return cnt; + } + } + } + + if (needs_nl) { + buf[cnt++] = '\n'; + needs_nl = 0; + continue; + } + + len = MIN(size - cnt, data_len - data_off); + + memcpy(&buf[cnt], &data_ptr[data_off], len); + + cnt += len; + data_off += len; + + if (data_off == data_len) + needs_nl = 1; + } + + return cnt; +} + + +static struct fuse_operations codex_ops = { + .getattr = codex_getattr, + .readdir = codex_readdir, + .open = codex_open, + .read = codex_read, +}; + +int main(int argc, char *argv[]) +{ + int r; + + r = sd_journal_open(&sdj, 0); + if (r < 0) { + fprintf(stderr, "Unable to open sd-journal: %s", strerror(-r)); + return EXIT_FAILURE; + } + + puts("TODO/FIXME: Note these flags are required to work: -o direct_io -f -s"); + + return fuse_main(argc, argv, &codex_ops, NULL); +} -- cgit v1.2.3