From b6ffd2c8c6ee7d7781d35b005cb4fa6efcb1cfbe Mon Sep 17 00:00:00 2001 From: Marvin Friedrich Date: Sun, 18 Oct 2026 18:27:34 +0200 Subject: [PATCH] Add basic tools --- meson.build | 5 + meson.options | 7 + toolchain/aarch64.cross-file | 19 +++ toolchain/riscv64.cross-file | 19 +++ toolchain/x86_64.cross-file | 19 +++ tools/console.c | 42 ++++++ tools/devd.c | 180 ++++++++++++++++++++++ tools/insertmod.c | 21 +++ tools/kmsg.c | 283 +++++++++++++++++++++++++++++++++++ tools/meson.build | 6 + tools/mount.c | 21 +++ tools/umount.c | 16 ++ 12 files changed, 638 insertions(+) create mode 100644 toolchain/aarch64.cross-file create mode 100644 toolchain/riscv64.cross-file create mode 100644 toolchain/x86_64.cross-file create mode 100644 tools/console.c create mode 100644 tools/devd.c create mode 100644 tools/insertmod.c create mode 100644 tools/kmsg.c create mode 100644 tools/meson.build create mode 100644 tools/mount.c create mode 100644 tools/umount.c diff --git a/meson.build b/meson.build index 6782975..06144ff 100644 --- a/meson.build +++ b/meson.build @@ -26,6 +26,11 @@ c_compiler = meson.get_compiler('c') # We require a compiler that supports the GNU C dialect. assert(c_compiler.has_define('__GNUC__')) +if get_option('tools_only') + subdir('tools') + subdir_done() +endif + add_project_arguments( '-Wno-unused', '-Wno-unused-parameter', diff --git a/meson.options b/meson.options index c2159ca..389704e 100644 --- a/meson.options +++ b/meson.options @@ -5,6 +5,13 @@ option( description: 'Only install headers', ) +option( + 'tools_only', + type: 'boolean', + value: false, + description: 'Only build userspace tools', +) + option( 'config_path', type: 'string', diff --git a/toolchain/aarch64.cross-file b/toolchain/aarch64.cross-file new file mode 100644 index 0000000..7441a97 --- /dev/null +++ b/toolchain/aarch64.cross-file @@ -0,0 +1,19 @@ +[binaries] +c = 'clang' +cpp = 'clang++' +ar = 'llvm-ar' +strip = 'llvm-strip' +objcopy = 'llvm-objcopy' +ld = 'ld.lld' + +[built-in options] +c_args = ['-target', 'aarch64-unknown-none'] +cpp_args = ['-target', 'aarch64-unknown-none'] +c_link_args = ['--target=aarch64-unknown-linux-gnu', '-fuse-ld=lld'] +cpp_link_args = ['--target=aarch64-unknown-linux-gnu', '-fuse-ld=lld'] + +[host_machine] +system = 'linux' +cpu_family = 'aarch64' +cpu = 'aarch64' +endian = 'little' diff --git a/toolchain/riscv64.cross-file b/toolchain/riscv64.cross-file new file mode 100644 index 0000000..a2a5fa0 --- /dev/null +++ b/toolchain/riscv64.cross-file @@ -0,0 +1,19 @@ +[binaries] +c = 'clang' +cpp = 'clang++' +ar = 'llvm-ar' +strip = 'llvm-strip' +objcopy = 'llvm-objcopy' +ld = 'ld.lld' + +[built-in options] +c_args = ['-target', 'riscv64-unknown-none'] +cpp_args = ['-target', 'riscv64-unknown-none'] +c_link_args = ['--target=riscv64-unknown-linux-gnu', '-fuse-ld=lld'] +cpp_link_args = ['--target=riscv64-unknown-linux-gnu', '-fuse-ld=lld'] + +[host_machine] +system = 'linux' +cpu_family = 'riscv64' +cpu = 'riscv64' +endian = 'little' diff --git a/toolchain/x86_64.cross-file b/toolchain/x86_64.cross-file new file mode 100644 index 0000000..ae80724 --- /dev/null +++ b/toolchain/x86_64.cross-file @@ -0,0 +1,19 @@ +[binaries] +c = 'clang' +cpp = 'clang++' +ar = 'llvm-ar' +strip = 'llvm-strip' +objcopy = 'llvm-objcopy' +ld = 'ld.lld' + +[built-in options] +c_args = ['-target', 'x86_64-unknown-none'] +cpp_args = ['-target', 'x86_64-unknown-none'] +c_link_args = ['--target=x86_64-unknown-linux-gnu', '-fuse-ld=lld'] +cpp_link_args = ['--target=x86_64-unknown-linux-gnu', '-fuse-ld=lld'] + +[host_machine] +system = 'linux' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' diff --git a/tools/console.c b/tools/console.c new file mode 100644 index 0000000..a6bb2b4 --- /dev/null +++ b/tools/console.c @@ -0,0 +1,42 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include + +int main(int argc, char** argv) { + if (argc < 3) { + fprintf(stderr, "usage: %s [args...]\n", argv[0]); + return 2; + } + + const char* tty = argv[1]; + + if (setsid() < 0 && getpid() != getsid(0)) { + perror("setsid"); + return 1; + } + + int fd = open(tty, O_RDWR); + if (fd < 0) { + perror(tty); + return 1; + } + + if (ioctl(fd, TIOCSCTTY, 0) != 0) + perror("TIOCSCTTY"); + + if (dup2(fd, STDIN_FILENO) < 0 || dup2(fd, STDOUT_FILENO) < 0 || dup2(fd, STDERR_FILENO) < 0) { + perror("dup2"); + return 1; + } + + if (fd > STDERR_FILENO) + close(fd); + + execvp(argv[2], &argv[2]); + perror(argv[2]); + return 1; +} diff --git a/tools/devd.c b/tools/devd.c new file mode 100644 index 0000000..ae60189 --- /dev/null +++ b/tools/devd.c @@ -0,0 +1,180 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEVCTL_PATH "/dev/devctl" +#define SOCKET_PATH "/var/run/devd.pipe" +#define MAX_CLIENTS 32 +#define DEVCTL_BUF 4096 +#define LINE_BUF 1024 + +static int clients[MAX_CLIENTS]; +static int nclients = 0; + +// Accumulates a partial line read from /dev/devctl across reads. +static char line[LINE_BUF]; +static size_t line_len = 0; + +static void add_client(int fd) { + if (nclients >= MAX_CLIENTS) { + close(fd); + return; + } + clients[nclients++] = fd; +} + +static void remove_client(int idx) { + close(clients[idx]); + clients[idx] = clients[--nclients]; +} + +// Broadcast a single notification line (including its trailing newline). +static void broadcast(const char* buf, size_t len) { + for (int i = 0; i < nclients;) { + ssize_t off = 0; + int dead = 0; + while (off < (ssize_t)len) { + ssize_t n = write(clients[i], buf + off, len - off); + if (n < 0) { + if (errno == EINTR) + continue; + dead = 1; + break; + } + off += n; + } + if (dead) + remove_client(i); + else + i++; + } +} + +// Feed raw bytes from /dev/devctl. +static void feed(const char* data, size_t len) { + for (size_t i = 0; i < len; i++) { + char c = data[i]; + if (line_len < sizeof(line) - 1) + line[line_len++] = c; + if (c == '\n') { + broadcast(line, line_len); + line_len = 0; + } + } + // Drop an overlong line that never terminated, to avoid wedging. + if (line_len >= sizeof(line) - 1) + line_len = 0; +} + +static int open_devctl(void) { + int fd = open(DEVCTL_PATH, O_RDONLY | O_CLOEXEC); + if (fd < 0) + perror("devd: open " DEVCTL_PATH); + return fd; +} + +static int make_listener(void) { + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + perror("devd: socket"); + return -1; + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); + + unlink(SOCKET_PATH); + if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("devd: bind " SOCKET_PATH); + close(fd); + return -1; + } + chmod(SOCKET_PATH, 0666); + + if (listen(fd, 8) < 0) { + perror("devd: listen"); + close(fd); + return -1; + } + return fd; +} + +int main(void) { + // A client going away must not kill us. + signal(SIGPIPE, SIG_IGN); + + int devctl = open_devctl(); + if (devctl < 0) + return 1; + + int listener = make_listener(); + if (listener < 0) + return 1; + + printf("devd: listening on %s, forwarding from %s\n", SOCKET_PATH, DEVCTL_PATH); + fflush(stdout); + + for (;;) { + struct pollfd pfds[2 + MAX_CLIENTS]; + pfds[0].fd = devctl; + pfds[0].events = POLLIN; + pfds[1].fd = listener; + pfds[1].events = POLLIN; + for (int i = 0; i < nclients; i++) { + pfds[2 + i].fd = clients[i]; + pfds[2 + i].events = 0; // Only watch for hangup. + } + + int n = poll(pfds, 2 + nclients, -1); + if (n < 0) { + if (errno == EINTR) + continue; + perror("devd: poll"); + break; + } + + // New notifications from the kernel. + if (pfds[0].revents & POLLIN) { + char buf[DEVCTL_BUF]; + ssize_t r = read(devctl, buf, sizeof(buf)); + if (r > 0) { + feed(buf, (size_t)r); + } else if (r == 0 || (r < 0 && errno != EINTR && errno != EAGAIN)) { + fprintf(stderr, "devd: %s closed, exiting\n", DEVCTL_PATH); + break; + } + } + + // New client connections. + if (pfds[1].revents & POLLIN) { + int c = accept(listener, NULL, NULL); + if (c >= 0) { + fcntl(c, F_SETFD, FD_CLOEXEC); + add_client(c); + } + } + + // Reap clients that hung up. + for (int i = nclients - 1; i >= 0; i--) { + if (pfds[2 + i].revents & (POLLHUP | POLLERR | POLLNVAL)) + remove_client(i); + } + } + + close(devctl); + close(listener); + unlink(SOCKET_PATH); + return 1; +} diff --git a/tools/insertmod.c b/tools/insertmod.c new file mode 100644 index 0000000..220141d --- /dev/null +++ b/tools/insertmod.c @@ -0,0 +1,21 @@ +#include +#include +#include +#include + +int main(int argc, char** argv) { + if (argc < 2) { + fprintf(stderr, "usage: %s [cmdline]\n", argv[0]); + return 2; + } + + const char* path = argv[1]; + const char* cmdline = argc >= 3 ? argv[2] : NULL; + + if (insertmod(path, cmdline) != 0) { + perror("insertmod"); + return 1; + } + + return 0; +} diff --git a/tools/kmsg.c b/tools/kmsg.c new file mode 100644 index 0000000..e2548fe --- /dev/null +++ b/tools/kmsg.c @@ -0,0 +1,283 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr size_t LINE_MAX_LEN = 8192; + +static const char* const level_names[] = { + "emerg", + "alert", + "crit", + "err", + "warn", + "notice", + "info", + "debug", +}; + +#define COLOR_RESET "\033[0m" +#define COLOR_TIME "\033[32m" +#define COLOR_SUBSYS "\033[33m" + +static const char* const level_colors[] = { + "\033[7;31m", + "\033[7;31m", + "\033[1;31m", + "\033[31m", + "\033[1m", + "", + "", + "", +}; + +struct options { + bool follow; + bool raw; + bool decode; + bool color; + unsigned int level_mask; +}; + +static int hex_value(char c) { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + return -1; +} + +static size_t unescape(const char* in, size_t len, char* out) { + size_t n = 0; + for (size_t i = 0; i < len; i++) { + if (in[i] == '\\' && i + 3 < len && in[i + 1] == 'x') { + const int hi = hex_value(in[i + 2]); + const int lo = hex_value(in[i + 3]); + if (hi >= 0 && lo >= 0) { + out[n++] = (char)(hi << 4 | lo); + i += 3; + continue; + } + } + out[n++] = in[i]; + } + return n; +} + +static const char* find_caller(char* header) { + char* save; + for (char* field = strtok_r(header, ",", &save); field; field = strtok_r(nullptr, ",", &save)) { + if (!strncmp(field, "caller=", 7)) + return field + 7; + } + return nullptr; +} + +static size_t subsystem_len(const char* text, size_t len) { + for (size_t i = 0; i + 1 < len; i++) { + if (text[i] == ':' && (text[i + 1] == ' ' || text[i + 1] == '\t')) + return i + 1; + } + return 0; +} + +static void print_record(const struct options* opts, char* record, size_t len) { + if (opts->raw) { + printf("%.*s\n", (int)len, record); + return; + } + + char* msg = memchr(record, ';', len); + if (!msg) + return; + *msg++ = '\0'; + + unsigned int prio; + uint64_t seq; + uint64_t usec; + if (sscanf(record, "%u,%" SCNu64 ",%" SCNu64, &prio, &seq, &usec) != 3) + return; + + const unsigned int level = prio & 7; + if (!(opts->level_mask & (1u << level))) + return; + + const char* caller = find_caller(record); + + static char text[LINE_MAX_LEN]; + const size_t text_len = unescape(msg, len - (size_t)(msg - record), text); + + if (opts->decode) + printf("%-6s: ", level_names[level]); + + if (opts->color) + printf(COLOR_TIME); + printf("[%5" PRIu64 ".%06" PRIu64 "]", usec / 1'000'000, usec % 1'000'000); + if (opts->color) + printf(COLOR_RESET); + putchar(' '); + + if (caller) + printf("[%6s] ", caller); + + const char* text_pos = text; + size_t remaining = text_len; + const size_t subsys = subsystem_len(text, text_len); + if (subsys) { + if (opts->color) + printf(COLOR_SUBSYS "%.*s" COLOR_RESET, (int)subsys, text); + else + printf("%.*s", (int)subsys, text); + text_pos += subsys; + remaining -= subsys; + } + + const char* color = opts->color ? level_colors[level] : ""; + if (*color) + printf("%s%.*s" COLOR_RESET "\n", color, (int)remaining, text_pos); + else + printf("%.*s\n", (int)remaining, text_pos); +} + +static int parse_levels(const char* list, unsigned int* mask) { + char* copy = strdup(list); + if (!copy) + return -1; + + *mask = 0; + char* save; + for (char* tok = strtok_r(copy, ",", &save); tok; tok = strtok_r(nullptr, ",", &save)) { + bool found = false; + for (unsigned int i = 0; i < 8; i++) { + if (!strcmp(tok, level_names[i])) { + *mask |= 1u << i; + found = true; + break; + } + } + if (!found) { + fprintf(stderr, "kmsg: unknown level '%s'\n", tok); + free(copy); + return -1; + } + } + free(copy); + return 0; +} + +static void usage(const char* prog) { + fprintf( + stderr, + "usage: %s [-w] [-r] [-x] [-l level[,level...]] [--color=auto|always|never]\n" + " -w, --follow wait for new messages\n" + " -r, --raw print raw /dev/kmsg records\n" + " -x, --decode show the log level of each message\n" + " -l, --level only show the given levels (emerg,alert,crit,err,warn,notice,info,debug)\n" + " -L, --color colorize output (auto, always, never)\n", + prog + ); +} + +int main(int argc, char** argv) { + struct options opts = { + .level_mask = 0xFF, + }; + int color_mode = -1; + + static const struct option long_opts[] = { + {"follow", no_argument, nullptr, 'w'}, + {"raw", no_argument, nullptr, 'r'}, + {"decode", no_argument, nullptr, 'x'}, + {"level", required_argument, nullptr, 'l'}, + {"color", optional_argument, nullptr, 'L'}, + {"help", no_argument, nullptr, 'h'}, + {}, + }; + + int c; + while ((c = getopt_long(argc, argv, "wrxl:L::h", long_opts, nullptr)) != -1) { + switch (c) { + case 'w': + opts.follow = true; + break; + case 'r': + opts.raw = true; + break; + case 'x': + opts.decode = true; + break; + case 'l': + if (parse_levels(optarg, &opts.level_mask)) + return 1; + break; + case 'L': + if (!optarg || !strcmp(optarg, "always")) + color_mode = 1; + else if (!strcmp(optarg, "never")) + color_mode = 0; + else if (!strcmp(optarg, "auto")) + color_mode = -1; + else { + usage(argv[0]); + return 1; + } + break; + case 'h': + usage(argv[0]); + return 0; + default: + usage(argv[0]); + return 1; + } + } + + opts.color = color_mode < 0 ? isatty(STDOUT_FILENO) : color_mode; + + const int fd = open("/dev/kmsg", O_RDONLY | (opts.follow ? 0 : O_NONBLOCK)); + if (fd < 0) { + perror("kmsg: /dev/kmsg"); + return 1; + } + + static char record[LINE_MAX_LEN]; + while (true) { + const ssize_t n = read(fd, record, sizeof(record) - 1); + if (n < 0) { + if (errno == EINTR) + continue; + if (errno == EPIPE) + continue; + if (errno == EAGAIN) + break; + perror("kmsg: read"); + close(fd); + return 1; + } + if (n == 0) + break; + record[n] = '\0'; + for (char* line = record; line < record + n;) { + char* end = memchr(line, '\n', (size_t)(record + n - line)); + if (!end) + end = record + n; + *end = '\0'; + if (*line != ' ') + print_record(&opts, line, (size_t)(end - line)); + line = end + 1; + } + if (opts.follow) + fflush(stdout); + } + + close(fd); + return 0; +} diff --git a/tools/meson.build b/tools/meson.build new file mode 100644 index 0000000..37d87d4 --- /dev/null +++ b/tools/meson.build @@ -0,0 +1,6 @@ +executable('console', 'console.c', install: true) +executable('devd', 'devd.c', install: true) +executable('insertmod', 'insertmod.c', install: true) +executable('kmsg', 'kmsg.c', install: true) +executable('mount', 'mount.c', install: true) +executable('umount', 'umount.c', install: true) diff --git a/tools/mount.c b/tools/mount.c new file mode 100644 index 0000000..9c3d6c4 --- /dev/null +++ b/tools/mount.c @@ -0,0 +1,21 @@ +#include +#include +#include + +int main(int argc, char** argv) { + if (argc < 3) { + fprintf(stderr, "usage: %s [source]\n", argv[0]); + return 2; + } + + const char* fstype = argv[1]; + const char* target = argv[2]; + const char* source = argc >= 4 ? argv[3] : NULL; + + if (mount(fstype, target, 0, (void*)source) != 0) { + perror("mount"); + return 1; + } + + return 0; +} diff --git a/tools/umount.c b/tools/umount.c new file mode 100644 index 0000000..1511eba --- /dev/null +++ b/tools/umount.c @@ -0,0 +1,16 @@ +#include +#include + +int main(int argc, char** argv) { + if (argc < 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + + if (unmount(argv[1], 0) != 0) { + perror("umount"); + return 1; + } + + return 0; +}