//! `zit daemon -R STORE`: the verb around `daemon/server.zig`. It resolves
//! what only the verb layer can (the store's path, the socket's default
//! place, the settings tiers, the elision classifier) and hands the rest to
//! the server, which the library could run without any of this.
const std = @import("std");
const zit = @import("zit");
const cli = zit.cli;
const server = zit.daemon_server;
const main = @import("../main.zig");
const cmd_sync = @import("sync.zig");
const cmd_checkout = @import("checkout.zig");
/// `$TMPDIR`, read in `main`: where the default socket directory goes.
pub var tmpdir_env: ?[]const u8 = null;
/// `ZIT_DAEMON_FAULT`, read in `main`: `after_append` or `after_sync` stops
/// the process at that point of the next commit, for the crash test.
pub var fault_env: ?[]const u8 = null;
/// `ZIT_DAEMON_MAX_FILE_BYTES`, read in `main`: a lower ceiling on one put,
/// so a test can send an oversized file without sending a gibibyte.
pub var max_file_bytes_env: ?[]const u8 = null;
/// `ZIT_DAEMON_MAX_REQUEST_BYTES` and `ZIT_DAEMON_STALL_MS`, read in `main`:
/// the per-request content cap and the stalled-trailer timeout, lowered by
/// tests.
pub var max_request_bytes_env: ?[]const u8 = null;
pub var stall_ms_env: ?[]const u8 = null;
pub fn runDaemon(
gpa: std.mem.Allocator,
io: std.Io,
arena: std.mem.Allocator,
verb_args: []const []const u8,
streams: cli.Streams,
amb: cli.Ambient,
) cli.Exit {
const parsed = switch (cli.parseDaemonArgs(verb_args)) {
.ok => |p| p,
.unknown_option => |tok| return cli.usageError(streams, "daemon", .unknown_option, tok),
.extra_argument => |tok| return cli.usageError(streams, "daemon", .extra_argument, tok),
.bad_value => |tok| {
if (std.mem.eql(u8, tok, "--idle-exit")) {
streams.err.writeAll("zit daemon: --idle-exit wants a whole number of seconds above zero\n") catch {};
} else {
streams.err.writeAll("zit daemon: --socket wants a path\n") catch {};
}
streams.err.writeAll("try 'zit daemon --help'\n") catch {};
return .usage;
},
};
if (parsed.help) {
streams.out.writeAll(main.daemon_help) catch {};
return .ok;
}
if (amb.repo_override == null) {
streams.err.writeAll("zit daemon: needs -R STORE, the store to serve\n") catch {};
streams.err.writeAll("try 'zit daemon --help'\n") catch {};
return .usage;
}
const fault: server.Fault = if (fault_env) |v|
std.meta.stringToEnum(server.Fault, v) orelse .none
else
.none;
var limits: zit.daemon_protocol.Limits = .{};
if (max_file_bytes_env) |v| limits.max_file_bytes = std.fmt.parseInt(u64, v, 10) catch limits.max_file_bytes;
if (max_request_bytes_env) |v| limits.max_request_bytes = std.fmt.parseInt(u64, v, 10) catch limits.max_request_bytes;
const stall_ms = if (stall_ms_env) |v| std.fmt.parseInt(u64, v, 10) catch server.default_stall_ms else server.default_stall_ms;
const root = (cli.resolveRoot(arena, io, amb.repo_override) catch return main.stateOom(streams, "daemon")) orelse {
main.reportNoRoot(streams, io, "daemon", amb.repo_override);
return .failure;
};
var root_dir = root.dir;
defer root_dir.close(io);
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const socket_path = parsed.socket orelse server.defaultSocketPath(&path_buf, tmpdir_env orelse "/tmp", server.currentUid(), root.abs) orelse {
streams.err.print("zit daemon: the default socket path under {s} is longer than a Unix socket path can be ({d} bytes); name one with --socket\n", .{ tmpdir_env orelse "/tmp", server.socket_path_max }) catch {};
return .failure;
};
var policy_ctx: PolicyCtx = .{ .io = io, .root_dir = root_dir, .amb = amb };
var diag: server.Diag = .{};
server.run(gpa, io, root_dir, .{
// Kept as given: a relative path is the cwd's, as for every path
// argument, and the daemon never changes directory.
.socket_path = socket_path,
.store_abs = root.abs,
.zit_version = main.zit_version,
.exit_on_stdin_close = parsed.exit_on_stdin_close,
.idle_exit_ms = if (parsed.idle_exit_secs) |s| s * std.time.ms_per_s else null,
.limits = limits,
.fault = fault,
.policy_hook = .{ .ctx = &policy_ctx, .resolve = PolicyCtx.resolve },
.elided_hook = .{ .elided = elided },
.stall_ms = @max(stall_ms, 1),
.log = streams.err,
}, streams.out, &diag) catch |e| {
switch (e) {
error.OutOfMemory => return main.stateOom(streams, "daemon"),
error.NoConcurrency => streams.err.writeAll("zit daemon: this platform cannot wait on several things at once\n") catch {},
else => {},
}
if (diag.len != 0) streams.err.print("zit daemon: {s}\n", .{diag.text()}) catch {};
return .failure;
};
return .ok;
}
/// The settings tiers, read once when the daemon opens the store.
const PolicyCtx = struct {
io: std.Io,
root_dir: std.Io.Dir,
amb: cli.Ambient,
fn resolve(ctx: ?*anyopaque, arena: std.mem.Allocator, cfg: ?*const zit.logstore.Config) server.PolicyAnswer {
const self: *PolicyCtx = @ptrCast(@alignCast(ctx.?));
const resolved = cmd_checkout.directCommitPolicy(self.io, arena, self.root_dir, self.amb, cfg);
// Committing unsigned under a setting that signs every check-in would
// quietly break the store's promise; refusing says so up front.
if (resolved.clearsign) return .{ .refused = "this store's clearsign setting signs every check-in, and the daemon does not sign; turn clearsign off for this store or commit with 'zit commit'" };
return .{ .policy = resolved.policy };
}
};
fn elided(ctx: ?*anyopaque, gpa: std.mem.Allocator, io: std.Io, store_dir: std.Io.Dir, name: []const u8) bool {
_ = ctx;
const e = cmd_sync.blobElision(gpa, io, store_dir, name) orelse return false;
return e.elided;
}