╭ serve_common.zig ─────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("./event.zig"); 3 const xit = @import("xit"); 4 const rp = xit.repo; 5 const ssh = @import("./serve_ssh_protocol.zig"); 6 7 // progress is optional and best effort: a failed report must not fail the work 8 pub fn reportProgress(comptime repo_opts: rp.RepoOpts(.xit), io: std.Io, progress_ctx_maybe: ?repo_opts.ProgressCtx, event: rp.ProgressEvent) void { 9 if (repo_opts.ProgressCtx != void) { 10 if (progress_ctx_maybe) |progress_ctx| progress_ctx.run(io, event) catch {}; 11 } 12 } 13 14 // progress for a push, sent on the pack stream's sideband 15 pub const PushProgress = struct { 16 response: *xit.net_server_receive_pack.Response, 17 writer: *std.Io.Writer, 18 // the client to probe, absent when nothing is listening 19 sess: ?*ssh.SessionCtx = null, 20 // set once it has hung up, so the phase in flight can stop too 21 gone: bool = false, 22 label: []const u8 = "Processing commits", 23 count: usize = 0, 24 total: usize = 0, 25 // the last reported step, so a line is sent only when it changes 26 step: usize = 0, 27 28 // how many items a phase with no total advances between reports 29 const report_every = 1000; 30 31 // the phases a push reports: the objects unpacked from the pack it 32 // received, then everything counted after it 33 fn reported(kind: rp.ProgressKind) bool { 34 return kind == .writing_object_from_pack or kind == .writing_patch; 35 } 36 37 pub fn run(self: *PushProgress, _: std.Io, event: rp.ProgressEvent) !void { 38 switch (event) { 39 .start => |start| { 40 if (!reported(start.kind)) return; 41 // the unpack phase sends no label of its own 42 if (start.kind == .writing_object_from_pack) self.label = "Unpacking objects"; 43 self.total = start.estimated_total_items; 44 self.count = 0; 45 self.step = 0; 46 }, 47 .complete_one => |kind| { 48 if (!reported(kind)) return; 49 self.count += 1; 50 }, 51 .end => |kind| { 52 if (!reported(kind)) return; 53 }, 54 // each phase names itself before reporting its count 55 .text => |text| { 56 self.label = text; 57 return; 58 }, 59 else => return, 60 } 61 // a phase that knows no total counts instead, and stays quiet until it 62 // has something to count 63 if (self.total == 0 and self.count == 0) return; 64 65 // every report flushes, so one is sent per percent, or per block of 66 // items when there is no total to turn into one 67 const step: usize = if (self.total > 0) @intCast(@as(u128, self.count) * 100 / self.total) else self.count / report_every; 68 if (event == .complete_one and step == self.step) return; 69 self.step = step; 70 71 // reporting is the only moment a transaction looks up from its work 72 if (self.sess) |sess| if (sess.peerGone()) { 73 self.gone = true; 74 return error.ClientGone; 75 }; 76 77 var buffer: [128]u8 = undefined; 78 const suffix = if (event == .end) "\n" else "\r"; 79 const text = if (self.total > 0) 80 try std.fmt.bufPrint(&buffer, "{s}: {d}% ({d}/{d}){s}", .{ self.label, step, self.count, self.total, suffix }) 81 else 82 try std.fmt.bufPrint(&buffer, "{s}: {d}{s}", .{ self.label, self.count, suffix }); 83 try self.response.progress(self.writer, text); 84 } 85 }; 86 87 // whether the client the work is reporting to has hung up. only a sideband 88 // reporter probes, so any other context never cancels. 89 pub fn progressCancelled(comptime repo_opts: rp.RepoOpts(.xit), progress_ctx_maybe: ?repo_opts.ProgressCtx) bool { 90 if (repo_opts.ProgressCtx != *SidebandProgress) return false; 91 const progress_ctx = progress_ctx_maybe orelse return false; 92 return progress_ctx.gone; 93 } 94 95 // the outcome of resolving a requested repo path to its on-disk directory. 96 // the http and ssh paths each map the cases to their own error responses. 97 pub const RepoPath = union(enum) { 98 ok: []const u8, // resolved on-disk path; the caller owns and frees it 99 invalid, // a <owner>/<repo> path was expected but not given 100 not_found, // unknown owner, or the repo doesn't exist and we aren't creating 101 }; 102 103 // resolve a requested repo path to its on-disk directory, parsed as 104 // <owner>/<repo> through the event store, minting a repo event for a fresh push. 105 pub fn resolveRepoPath( 106 io: std.Io, 107 allocator: std.mem.Allocator, 108 repo_root_path: []const u8, 109 admin_repo_path: []const u8, 110 requested: []const u8, 111 create_if_missing: bool, 112 ) !RepoPath { 113 const owner_repo = evt.parseOwnerRepoPath(requested) orelse return .invalid; 114 const event_id_hex = (try evt.resolveOrCreateRepo( 115 io, 116 allocator, 117 admin_repo_path, 118 owner_repo.owner, 119 owner_repo.name, 120 if (create_if_missing) .{} else null, 121 )) orelse return .not_found; 122 return .{ .ok = try std.fs.path.join(allocator, &.{ repo_root_path, &event_id_hex }) }; 123 } 124 125 // every connection task shares one error writer, so writing to it takes a 126 // lock. uncancelable, since a log line shouldn't be a cancelation point. 127 var log_mutex: std.Io.Mutex = .init; 128 129 pub fn logError(io: std.Io, err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void { 130 log_mutex.lockUncancelable(io); 131 defer log_mutex.unlock(io); 132 err.print(fmt, args) catch return; 133 err.flush() catch {}; 134 } 135 136 // accept connections forever, spawning `handleConn(context, stream)` as a task 137 // for each. the handler owns the stream (it closes it). `name` labels accept 138 // errors in the log. shared by the http, ssh, and web ui listeners. 139 pub fn runListener( 140 io: std.Io, 141 net_server: *std.Io.net.Server, 142 tasks: *std.Io.Group, 143 err: *std.Io.Writer, 144 name: []const u8, 145 context: anytype, 146 comptime handleConn: fn (@TypeOf(context), std.Io.net.Stream) void, 147 ) void { 148 const Context = @TypeOf(context); 149 150 const Conn = struct { 151 context: Context, 152 stream: std.Io.net.Stream, 153 154 fn run(c: @This()) void { 155 handleConn(c.context, c.stream); 156 } 157 }; 158 159 const Listener = struct { 160 io: std.Io, 161 net_server: *std.Io.net.Server, 162 tasks: *std.Io.Group, 163 err: *std.Io.Writer, 164 name: []const u8, 165 context: Context, 166 167 fn run(self: @This()) void { 168 while (true) { 169 const stream = self.net_server.accept(self.io) catch |accept_err| { 170 if (accept_err == error.Canceled) return; 171 logError(self.io, self.err, "{s} accept failed: {s}\n", .{ self.name, @errorName(accept_err) }); 172 continue; 173 }; 174 self.tasks.async(self.io, Conn.run, .{Conn{ .context = self.context, .stream = stream }}); 175 } 176 } 177 }; 178 179 tasks.async(io, Listener.run, .{Listener{ 180 .io = io, 181 .net_server = net_server, 182 .tasks = tasks, 183 .err = err, 184 .name = name, 185 .context = context, 186 }}); 187 } 188