commit 02c164882396f4f8b3dfff6ea90e8d19c8b869bf
Author: Jeffrey C. Ollie <jeff@ocjtech.us>
Date:   Sat Mar 22 12:03:01 2025 -0500

    first

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a4aa3da
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/zig-out
+/.zig-cache
+/result*
+
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..bae698b
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,34 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+    const target = b.standardTargetOptions(.{});
+    const optimize = b.standardOptimizeOption(.{});
+
+    // We will also create a module for our other entry point, 'main.zig'.
+    const exe_mod = b.createModule(.{
+        .root_source_file = b.path("src/main.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+
+    const exe = b.addExecutable(.{
+        .name = "zig_logging_wrapper",
+        .root_module = exe_mod,
+    });
+    b.installArtifact(exe);
+    const run_cmd = b.addRunArtifact(exe);
+    run_cmd.step.dependOn(b.getInstallStep());
+    if (b.args) |args| {
+        run_cmd.addArgs(args);
+    }
+    const run_step = b.step("run", "Run the app");
+    run_step.dependOn(&run_cmd.step);
+
+    const exe_unit_tests = b.addTest(.{
+        .root_module = exe_mod,
+    });
+
+    const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
+    const test_step = b.step("test", "Run unit tests");
+    test_step.dependOn(&run_exe_unit_tests.step);
+}
diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000..c80576f
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,14 @@
+.{
+    .name = .zig_logging_wrapper,
+
+    .version = "0.0.0",
+    .fingerprint = 0x259c6307f5768bcf, // Changing this has security and trust implications.
+    .minimum_zig_version = "0.14.0",
+    .dependencies = .{},
+
+    .paths = .{
+        "build.zig",
+        "build.zig.zon",
+        "src",
+    },
+}
diff --git a/src/PipeReader.zig b/src/PipeReader.zig
new file mode 100644
index 0000000..8c0ee0d
--- /dev/null
+++ b/src/PipeReader.zig
@@ -0,0 +1,56 @@
+const PipeReader = @This();
+
+const std = @import("std");
+
+const log = std.log.scoped(.pipe_reader);
+
+spawn_failed: *bool,
+exe: *std.process.Child,
+pipe: enum { stdout, stderr },
+sem: std.Thread.Semaphore = .{},
+thread: std.Thread = undefined,
+
+pub fn spawn(self: *PipeReader) !void {
+    self.thread = try std.Thread.spawn(.{}, reader, .{self});
+}
+
+pub fn wait(self: *PipeReader) void {
+    self.sem.wait();
+}
+
+pub fn join(self: *PipeReader) void {
+    self.thread.join();
+}
+
+fn reader(self: *PipeReader) void {
+    const fd: std.fs.File = fd: {
+        defer self.sem.post();
+        while (!self.spawn_failed.*) {
+            switch (self.pipe) {
+                .stdout => if (self.exe.stdout) |fd| break :fd fd,
+                .stderr => if (self.exe.stderr) |fd| break :fd fd,
+            }
+            std.time.sleep(10 * std.time.ns_per_ms);
+        }
+        self.sem.post();
+        return;
+    };
+
+    const rdr = fd.reader();
+
+    while (true) {
+        var buf: [2048]u8 = undefined;
+        const line = rdr.readUntilDelimiter(&buf, '\n') catch |err| switch (err) {
+            error.EndOfStream => return,
+            error.NotOpenForReading => return,
+            else => {
+                log.err("{s}: {}", .{ @tagName(self.pipe), err });
+
+                return;
+            },
+        };
+        const ts = std.time.nanoTimestamp();
+        if (line.len == 0) break;
+        log.info("{s}: {d} {s}", .{ @tagName(self.pipe), ts, line });
+    }
+}
diff --git a/src/UnixSocketReader.zig b/src/UnixSocketReader.zig
new file mode 100644
index 0000000..d4ed6e8
--- /dev/null
+++ b/src/UnixSocketReader.zig
@@ -0,0 +1,129 @@
+const UnixSocketReader = @This();
+
+const std = @import("std");
+
+const log = std.log.scoped(.socket_reader);
+
+sock: std.posix.socket_t = undefined,
+thread: std.Thread = undefined,
+
+pub fn init(self: *UnixSocketReader) !void {
+    const path: []const u8 = "/tmp/wrapper";
+
+    var dir = try std.fs.openDirAbsolute(std.fs.path.dirname(path) orelse return error.BadPath, .{});
+    defer dir.close();
+    dir.deleteFile(std.fs.path.basename(path)) catch {};
+
+    var addr = std.mem.zeroes(std.posix.sockaddr.un);
+    addr.family = std.posix.AF.UNIX;
+    @memcpy(addr.path[0..path.len], path);
+
+    self.sock = try std.posix.socket(std.posix.AF.UNIX, std.posix.SOCK.DGRAM, 0);
+    try std.posix.bind(self.sock, @ptrCast(&addr), @sizeOf(std.posix.sockaddr.un));
+}
+
+pub fn spawn(self: *UnixSocketReader) !void {
+    self.thread = try std.Thread.spawn(.{}, reader, .{self});
+    self.thread.detach();
+}
+
+const Severity = enum(u8) {
+    EMERG = 0,
+    ALERT = 1,
+    CRIT = 2,
+    ERR = 3,
+    WARNING = 4,
+    NOTICE = 5,
+    INFO = 6,
+    DEBUG = 7,
+};
+
+const Facility = enum(u8) {
+    KERN = (0 << 3),
+    USER = (1 << 3),
+    MAIL = (2 << 3),
+    DAEMON = (3 << 3),
+    AUTH = (4 << 3),
+    SYSLOG = (5 << 3),
+    LPR = (6 << 3),
+    NEWS = (7 << 3),
+    UUCP = (8 << 3),
+    CRON = (9 << 3),
+    AUTHPRIV = (10 << 3),
+    FTP = (11 << 3),
+    NTP = (12 << 3),
+    AUDIT = (13 << 3),
+    ALERT = (14 << 3),
+    CLOCK = (15 << 3),
+    LOCAL0 = (16 << 3),
+    LOCAL1 = (17 << 3),
+    LOCAL2 = (18 << 3),
+    LOCAL3 = (19 << 3),
+    LOCAL4 = (20 << 3),
+    LOCAL5 = (21 << 3),
+    LOCAL6 = (22 << 3),
+    LOCAL7 = (23 << 3),
+};
+
+pub fn reader(self: *UnixSocketReader) void {
+    while (true) {
+        var buf: [4096]u8 = undefined;
+        var addr: std.posix.sockaddr = undefined;
+        var addrlen: std.posix.socklen_t = @sizeOf(std.posix.sockaddr);
+        const len = std.posix.recvfrom(self.sock, &buf, 0, &addr, &addrlen) catch |err| {
+            log.info("socket reader: {}", .{err});
+            return;
+        };
+        const line = buf[0..len];
+        syslog: {
+            if (line.len < 3) {
+                log.warn("data too short to be syslog", .{});
+                break :syslog;
+            }
+            const pri_start_char = std.mem.indexOfScalar(u8, line, '<') orelse {
+                log.warn("syslog: can't find '<'", .{});
+                break :syslog;
+            };
+            if (pri_start_char != 0) {
+                log.warn("syslog does not start with '<'", .{});
+                break :syslog;
+            }
+            const pri_end_char = std.mem.indexOfScalarPos(u8, line, 1, '>') orelse {
+                log.warn("syslog: can't find '>'", .{});
+                break :syslog;
+            };
+            const pri = line[pri_start_char + 1 .. pri_end_char];
+            if (pri.len < 1) {
+                log.warn("pri is too short", .{});
+                break :syslog;
+            }
+            if (pri.len > 3) {
+                log.warn("pri is too long", .{});
+                break :syslog;
+            }
+            for (pri) |ch| if (!std.ascii.isDigit(ch)) {
+                log.warn("invalid digit in pri", .{});
+                break :syslog;
+            };
+            const prival = std.fmt.parseUnsigned(u8, pri, 10) catch |err| {
+                log.warn("unable to convert pri to an integer: {}", .{err});
+                break :syslog;
+            };
+            if (prival > 191) {
+                log.warn("prival out of range", .{});
+                break :syslog;
+            }
+            log.info("pri: \'{s}\'", .{pri});
+            const severity = std.meta.intToEnum(Severity, prival & 0b00000111) catch {
+                log.warn("unable to convert severity", .{});
+                break :syslog;
+            };
+            const facility = std.meta.intToEnum(Facility, prival & 0b11111000) catch {
+                log.warn("unable to convert facility", .{});
+                break :syslog;
+            };
+            log.info("severity: {s} facility: {s}", .{ @tagName(severity), @tagName(facility) });
+        }
+        log.info("{s}", .{line});
+    }
+}
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..eaf2cad
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,85 @@
+const std = @import("std");
+const builtin = @import("builtin");
+
+const log = std.log.scoped(.wrapper);
+
+const PipeReader = @import("PipeReader.zig");
+const SocketReader = @import("SocketReader.zig");
+
+var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
+
+pub fn main() !void {
+    const alloc, const is_debug = switch (builtin.mode) {
+        .Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true },
+        .ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false },
+    };
+    defer if (is_debug) {
+        _ = debug_allocator.deinit();
+    };
+
+    var socket: SocketReader = .{};
+    try socket.init();
+    try socket.spawn();
+
+    var exe = std.process.Child.init(
+        &.{
+            "logger",
+            "--stderr",
+            "--unix=/tmp/wrapper",
+            "hello",
+        },
+        alloc,
+    );
+
+    exe.stdin_behavior = .Ignore;
+    exe.stdout_behavior = .Pipe;
+    exe.stderr_behavior = .Pipe;
+
+    var spawn_failed: bool = false;
+
+    var stdout: PipeReader = .{
+        .spawn_failed = &spawn_failed,
+        .exe = &exe,
+        .pipe = .stdout,
+    };
+    try stdout.spawn();
+    defer stdout.join();
+
+    var stderr: PipeReader = .{
+        .spawn_failed = &spawn_failed,
+        .exe = &exe,
+        .pipe = .stderr,
+    };
+    try stderr.spawn();
+    defer stderr.join();
+
+    exe.spawn() catch |err| {
+        log.err("err: {}", .{err});
+        spawn_failed = true;
+        return;
+    };
+
+    stdout.wait();
+    stderr.wait();
+
+    const rc = try exe.wait();
+
+    switch (rc) {
+        .Exited => |code| {
+            if (code != 0) {
+                log.warn("exited with error code {d}", .{code});
+            }
+        },
+        .Signal => |signal| {
+            log.warn("terminated with signal {}", .{signal});
+        },
+        .Stopped => |signal| {
+            log.warn("stopped with signal {}", .{signal});
+        },
+        .Unknown => |code| {
+            log.warn("unknown error {}", .{code});
+        },
+    }
+
+    log.info("finished!", .{});
+}