//! The process shell: build the writers, collect `argv`, dispatch, return an //! exit code. Every command body lives in `cli.zig`, which takes its writers as //! parameters and is therefore testable without a process. const std = @import("std"); const cli = @import("cli.zig"); const logging = @import("platform/logging.zig"); /// Routes every `std.log` call through the sink. Before `logging.install` /// runs (and always under the test runner, which never installs), the sink /// passes through to the stderr default. pub const std_options: std.Options = .{ .logFn = logging.logFn }; // `src/tests.zig` imports this file, and this is how `cli.zig`'s tests reach // the same runner. `src/tests.zig` is the orchestrator's file, not this // session's. comptime { _ = @import("cli.zig"); } /// Streaming, never positional, and this is load-bearing rather than a default /// worth changing back. /// /// `File.writer` builds a `.positional` writer: it keeps an offset of its own, /// starting at zero, and pwrites there. `std.log` writes to the same descriptor /// directly and shares the descriptor's offset. With stderr redirected to a /// regular file the two disagree about where the end is, so the runner's first /// flush lands on top of whatever the log already wrote at the start and the /// operator loses those lines. `writerStreaming` uses the descriptor's own /// offset, so both writers append. /// /// A terminal and a pipe are unaffected either way, because neither is seekable /// and the positional writer falls back to streaming on them. `2>file` is where /// it shows, and `nxdns run 2>logfile` is an ordinary way to run this program. /// `>>` needs it too: pwrite ignores `O_APPEND`. /// /// Takes the `File` rather than naming stdout and stderr inside, so a test can /// hand it a real file and read back what an operator would have. fn runnerWriter(file: std.Io.File, io: std.Io, buffer: []u8) std.Io.File.Writer { return file.writerStreaming(io, buffer); } pub fn main(init: std.process.Init) u8 { // Both `File.Writer` values are self-referential and must not move, so they // stay in these `var` slots for the whole of `main`. var out_buffer: [4096]u8 = undefined; var err_buffer: [4096]u8 = undefined; var out = runnerWriter(std.Io.File.stdout(), init.io, &out_buffer); var err = runnerWriter(std.Io.File.stderr(), init.io, &err_buffer); const runner: cli.Runner = .{ .io = init.io, .gpa = init.gpa, .out = &out.interface, .err = &err.interface, }; var argv: std.ArrayList([]const u8) = .empty; defer argv.deinit(init.gpa); var args = init.minimal.args.iterate(); _ = args.skip(); while (args.next()) |arg| { argv.append(init.gpa, arg) catch return cli.exit_runtime; } const command = cli.parseArgs(argv.items) catch |e| return cli.runUsageError(runner, e); return switch (command) { .run => |args_| cli.runRun(runner, args_), // `true`: the probe leaves the machine, which is right for an operator // running `nxdns check` and wrong for a test. .check => |args_| cli.runCheck(runner, args_, true), .export_ => |args_| cli.runExport(runner, args_), .import_ => |args_| cli.runImport(runner, args_), .version => cli.runVersion(runner), .help => cli.runHelp(runner), }; } // --------------------------------------------------------------------------- // tests // --------------------------------------------------------------------------- test "a runner writer appends after what std.log already put in a redirected file" { // Reproduced live before this test existed: `nxdns run --config // 2> file` lost `info(migrations): config.db migrated from schema version 0 // to 2`, because the runner's error writer pwrote its first flush at offset // zero, over the line the log had already written. Down a pipe the same run // kept both lines, which is why a terminal never showed this. var threaded: std.Io.Threaded = .init(std.testing.allocator, .{}); defer threaded.deinit(); const io = threaded.io(); const gpa = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var file = try tmp.dir.createFile(io, "stderr.txt", .{}); defer file.close(io); // What the log sink does: a direct write on the shared descriptor, which // moves the descriptor's offset. A positional writer does not see it. const log_line = "info(migrations): config.db migrated from schema version 0 to 2\n"; try file.writeStreamingAll(io, log_line); const runner_line = "nxdns run failed: MissingDefaultGroup\n"; var buffer: [4096]u8 = undefined; var w = runnerWriter(file, io, &buffer); try w.interface.writeAll(runner_line); try w.interface.flush(); const seen = try tmp.dir.readFileAlloc(io, "stderr.txt", gpa, .limited(4096)); defer gpa.free(seen); try std.testing.expectEqualStrings(log_line ++ runner_line, seen); }