milestone 7: serving pipeline, client tracking, pause and lifecycle
This commit is contained in:
+59
-13
@@ -18,12 +18,14 @@ const Writer = std.Io.Writer;
|
||||
const Certificate = std.crypto.Certificate;
|
||||
const tls = std.crypto.tls;
|
||||
|
||||
const app = @import("app.zig");
|
||||
const config_export = @import("config/export.zig");
|
||||
const import = @import("config/import.zig");
|
||||
const model = @import("config/model.zig");
|
||||
const validate = @import("config/validate.zig");
|
||||
const db = @import("storage/db.zig");
|
||||
const migrations = @import("storage/migrations.zig");
|
||||
const querylog_schema = @import("storage/querylog_schema.zig");
|
||||
const doh_client = @import("upstream/doh_client.zig");
|
||||
const dot_client = @import("upstream/dot_client.zig");
|
||||
const pool = @import("upstream/pool.zig");
|
||||
@@ -204,7 +206,9 @@ pub const DataDir = struct {
|
||||
_ = try std.Io.Dir.cwd().createDirPathStatus(io, data_dir, .fromMode(0o700));
|
||||
}
|
||||
|
||||
var dir = try std.Io.Dir.cwd().openDir(io, data_dir, .{});
|
||||
// `.iterate`: the disk monitor sizes the databases by scanning this
|
||||
// directory, and an fd opened without it cannot be read as a directory.
|
||||
var dir = try std.Io.Dir.cwd().openDir(io, data_dir, .{ .iterate = true });
|
||||
errdefer dir.close(io);
|
||||
|
||||
// SQLite opens by path, not by directory handle, so both paths are
|
||||
@@ -243,6 +247,54 @@ pub const DataDir = struct {
|
||||
try db.applyPragmas(&database, .{});
|
||||
return database;
|
||||
}
|
||||
|
||||
/// The first connection to `querylog.db`: `querylog_schema.open` creates the
|
||||
/// file when it is missing and recreates it when it is unusable, so this is
|
||||
/// the call that establishes the schema. `reopenQuerylogDb` is for the
|
||||
/// connections that follow.
|
||||
///
|
||||
/// The 0600 chmod cannot come first the way `openConfigDb` does it — the
|
||||
/// file may not exist yet, and a recreate replaces it — so the main file and
|
||||
/// both WAL sidecars are locked down afterwards instead. A query log holds
|
||||
/// every domain every client asked for, which is as sensitive as anything in
|
||||
/// `config.db`.
|
||||
///
|
||||
/// `querylog_schema.open` resolves the path through SQLite's VFS as well as
|
||||
/// through the directory handle, so it is given `cwd` and the joined path
|
||||
/// rather than `self.dir` and a name (see its doc comment).
|
||||
pub fn openQuerylogDb(self: *const DataDir, io: std.Io) !db.Db {
|
||||
const opened = try querylog_schema.open(io, std.Io.Dir.cwd(), self.querylog_db_path);
|
||||
var database = opened.database;
|
||||
errdefer database.close();
|
||||
try self.restrictQuerylogPermissions(io);
|
||||
return database;
|
||||
}
|
||||
|
||||
/// An additional connection to a `querylog.db` that `openQuerylogDb` has
|
||||
/// already established. Phase 7 needs two — the log writer and the retention
|
||||
/// pass each own one (`retention.zig`'s contract).
|
||||
pub fn reopenQuerylogDb(self: *const DataDir, io: std.Io) !db.Db {
|
||||
_ = io;
|
||||
var database = try db.Db.open(self.querylog_db_path, .{ .mode = .read_write_existing });
|
||||
errdefer database.close();
|
||||
try db.applyPragmas(&database, .{});
|
||||
return database;
|
||||
}
|
||||
|
||||
/// A sidecar that does not exist yet is not a failure: `-wal` and `-shm`
|
||||
/// appear when SQLite first writes, and the next call catches them.
|
||||
fn restrictQuerylogPermissions(self: *const DataDir, io: std.Io) !void {
|
||||
for ([_][]const u8{
|
||||
querylog_db_name,
|
||||
querylog_db_name ++ "-wal",
|
||||
querylog_db_name ++ "-shm",
|
||||
}) |entry| {
|
||||
self.dir.setFilePermissions(io, entry, .fromMode(0o600), .{}) catch |e| switch (e) {
|
||||
error.FileNotFound => {},
|
||||
else => |other| return other,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -320,12 +372,11 @@ pub fn runVersion(r: Runner) u8 {
|
||||
return finish(r, exit_ok);
|
||||
}
|
||||
|
||||
/// Unchanged from milestone 1. Wiring the configuration into the servers is
|
||||
/// Phase 7; there is deliberately no half-built serving path here.
|
||||
/// Serves DNS until SIGINT or SIGTERM. The whole of it lives in `app.zig`,
|
||||
/// which is where the composition root belongs; this stays the entry point so
|
||||
/// that `main` dispatches every command the same way.
|
||||
pub fn runRun(r: Runner, paths: Paths) u8 {
|
||||
_ = paths;
|
||||
r.out.writeAll("not implemented\n") catch return finish(r, exit_runtime);
|
||||
return finish(r, exit_check);
|
||||
return app.run(r, paths);
|
||||
}
|
||||
|
||||
pub fn runExport(r: Runner, args: ExportArgs) u8 {
|
||||
@@ -879,13 +930,8 @@ test "runVersion prints the milestone-1 version lines and exits 0" {
|
||||
try testing.expectEqual(@as(usize, 2), countLines(captured.out.written()));
|
||||
}
|
||||
|
||||
test "runRun still reports that serving is not implemented" {
|
||||
var captured: Captured = .init(testing.allocator);
|
||||
defer captured.deinit();
|
||||
|
||||
try testing.expectEqual(exit_check, runRun(captured.runner(), .{}));
|
||||
try testing.expectEqualStrings("not implemented\n", captured.out.written());
|
||||
}
|
||||
// `runRun` now binds sockets and serves until a signal arrives, so it has no
|
||||
// unit test: booting it is `src/server/phase7_integration_test.zig`'s case 11.
|
||||
|
||||
test "runUsageError names the fault and prints the usage text" {
|
||||
var captured: Captured = .init(testing.allocator);
|
||||
|
||||
Reference in New Issue
Block a user