docs: rewrite for people running nxdns; query-history refusals name their remedy and a refused start gives no wrong hints

This commit is contained in:
2026-10-21 17:03:00 +02:00
parent fbc8fd7882
commit c21e5a9542
31 changed files with 1186 additions and 3877 deletions
+37 -14
View File
@@ -89,17 +89,8 @@ const download_budget_s = 300;
pub fn run(runner: cli.Runner, args: cli.RunArgs) u8 {
const code = serve(runner, args) catch |err| code: {
runner.err.print("nxdns run failed: {s}\n", .{@errorName(err)}) catch {};
const mapped = failureExitCode(err);
if (mapped == cli.exit_check) {
runner.err.writeAll("run `nxdns check` to see the configuration in full\n") catch {};
// Ruling 6: after bootstrap seeding died, a fresh database fails
// validation naturally (`NoUsableUpstreams`) and the operator needs
// to be told how a database gets a configuration at all. In file
// mode they already have a file, and the diagnostics above name what
// is wrong with it.
if (args.config == null) cli.writeDbSourceHint(runner.err);
}
break :code mapped;
writeFailureHints(runner.err, err, args.config != null);
break :code failureExitCode(err);
};
// Output the operator never received is not output, so a failed flush
@@ -109,6 +100,18 @@ pub fn run(runner: cli.Runner, args: cli.RunArgs) u8 {
return code;
}
/// What to do next, for a failure the operator has to fix. A schema refusal is
/// not about the configuration: its log line already names the remedy, and
/// `nxdns check` has nothing to add. `NoUsableUpstreams` in database mode is an
/// unconfigured database, which needs telling how a database gets a
/// configuration at all; in file mode the diagnostics above name what is wrong
/// with the file.
fn writeFailureHints(w: *Writer, err: anyerror, file_mode: bool) void {
if (!faults.isConfigFault(err) or faults.isSchemaRefusal(err)) return;
w.writeAll("run `nxdns check` to see the configuration in full\n") catch {};
if (!file_mode and err == error.NoUsableUpstreams) cli.writeDbSourceHint(w);
}
/// The one classification, from `config/faults.zig`. This file keeps no list of
/// its own: `run` exiting 1 on a seed file `check` and `import` exit 2 on was
/// exactly the cost of the second list that used to be here.
@@ -1656,6 +1659,26 @@ test "a recreate under a long data directory keeps the watermark and a usable na
try deep.access(io, name, .{});
}
test "a failed run points at the next step only where it applies" {
const Case = struct { err: anyerror, file_mode: bool, check: bool, import: bool };
const cases = [_]Case{
.{ .err = error.NoUsableUpstreams, .file_mode = false, .check = true, .import = true },
.{ .err = error.NoUsableUpstreams, .file_mode = true, .check = true, .import = false },
.{ .err = error.MissingDefaultGroup, .file_mode = false, .check = true, .import = false },
.{ .err = error.SchemaTooNew, .file_mode = false, .check = false, .import = false },
.{ .err = error.MigrationFailed, .file_mode = true, .check = false, .import = false },
.{ .err = error.AddressInUse, .file_mode = false, .check = false, .import = false },
};
for (cases) |case| {
var buf: [512]u8 = undefined;
var w: Writer = .fixed(&buf);
writeFailureHints(&w, case.err, case.file_mode);
const text = w.buffered();
try testing.expectEqual(case.check, std.mem.indexOf(u8, text, "nxdns check") != null);
try testing.expectEqual(case.import, std.mem.indexOf(u8, text, cli.db_source_hint) != null);
}
}
test "parseBind refuses a bind address of the wrong family" {
var out_buf: [8]u8 = undefined;
var err_buf: [256]u8 = undefined;
@@ -1760,9 +1783,9 @@ test "a configuration whose blocklist source is in no group imports and checks c
const io = threaded.io();
const gpa = std.testing.allocator;
// A source is created before it is attached — `docs/tutorial/first-run.md`
// POSTs the blocklist and then PUTs the group's sources — so an unattached
// source is a legal intermediate state on every write path. It warns; it
// A source is created before it is attached — the Protection page adds the
// source and then saves the group's sources — so an unattached source is a
// legal intermediate state on every write path. It warns; it
// never fails.
const source: [:0]const u8 =
\\.{