Skip to content
zit Artifact [b157dbc4bd]
Login

Artifact [b157dbc4bd]

Artifact b157dbc4bd519706613a90885a09107e9ae78828d722877cc9a9743bdd5d4e64:


//! `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;
}