╭ serve.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const xit = @import("xit"); 3 const rp = xit.repo; 4 const ui = @import("./ui.zig"); 5 const web = @import("./web.zig"); 6 const serve_common = @import("./serve_common.zig"); 7 const serve_ssh_protocol = @import("./serve_ssh_protocol.zig"); 8 const serve_ssh = @import("./serve_ssh.zig"); 9 const serve_http = @import("./serve_http.zig"); 10 11 pub const Options = struct { 12 http_listen: ?[]const u8 = "127.0.0.1:8080", 13 ssh_listen: ?[]const u8 = "127.0.0.1:8022", 14 wui_listen: []const u8 = "127.0.0.1:8000", 15 data_dir: []const u8 = ".", 16 }; 17 18 const ListenAddress = struct { 19 host: []const u8, 20 port: u16, 21 }; 22 23 const BoundListener = struct { 24 address: ListenAddress, 25 server: std.Io.net.Server, 26 27 fn deinit(self: *BoundListener, io: std.Io) void { 28 self.server.deinit(io); 29 } 30 31 fn port(self: *const BoundListener) u16 { 32 return self.server.socket.address.getPort(); 33 } 34 }; 35 36 const SshService = struct { 37 listener: BoundListener, 38 host_key: serve_ssh_protocol.HostKey, 39 session_handler: serve_ssh.SessionHandler, 40 }; 41 42 pub fn run( 43 comptime repo_kind: rp.RepoKind, 44 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 45 io: std.Io, 46 allocator: std.mem.Allocator, 47 cwd_path: []const u8, 48 options: Options, 49 err: *std.Io.Writer, 50 runnable: anytype, 51 ) !void { 52 // create the data dir 53 54 const data_dir_path = try std.fs.path.resolve(allocator, &.{ cwd_path, options.data_dir }); 55 defer allocator.free(data_dir_path); 56 57 const data_dir = try std.Io.Dir.cwd().createDirPathOpen(io, data_dir_path, .{}); 58 defer data_dir.close(io); 59 60 // create the repos dir 61 62 const repo_root_path = try std.fs.path.resolve(allocator, &.{ data_dir_path, "repos" }); 63 defer allocator.free(repo_root_path); 64 65 try std.Io.Dir.cwd().createDirPath(io, repo_root_path); 66 67 // the admin repo path is where the user/repo metadata events live 68 69 const admin_repo_path = try std.fs.path.resolve(allocator, &.{ data_dir_path, "admin" }); 70 defer allocator.free(admin_repo_path); 71 72 // create http listener 73 74 var http_maybe: ?BoundListener = if (options.http_listen) |value| blk: { 75 const address = try parseListenAddress(value); 76 break :blk .{ .address = address, .server = try listen(io, address, .reuse) }; 77 } else null; 78 defer if (http_maybe) |*http| http.deinit(io); 79 80 // disk-backed store for web login sessions 81 const session_store = try web.SessionStore.init(io, data_dir); 82 defer session_store.deinit(); 83 84 // create wui listener 85 86 const wui_listen_address = try parseListenAddress(options.wui_listen); 87 var wui_server = try listen(io, wui_listen_address, .reuse); 88 defer wui_server.deinit(io); 89 90 // create ssh listener and its session state 91 92 const git_http_port: ?u16 = if (http_maybe) |*http| http.port() else null; 93 var ssh_maybe: ?SshService = if (options.ssh_listen) |value| blk: { 94 const address = try parseListenAddress(value); 95 var listener = BoundListener{ .address = address, .server = try listen(io, address, .reuse) }; 96 errdefer listener.deinit(io); 97 const ssh_port = listener.port(); 98 break :blk .{ 99 .listener = listener, 100 .host_key = try serve_ssh_protocol.HostKey.loadOrGenerate(io, allocator, data_dir_path), 101 .session_handler = .{ 102 .admin_repo_path = admin_repo_path, 103 .repo_root_path = repo_root_path, 104 .wui_port = wui_server.socket.address.getPort(), 105 .git_http_port = git_http_port, 106 .git_ssh_port = ssh_port, 107 .err = err, 108 }, 109 }; 110 } else null; 111 defer if (ssh_maybe) |*ssh| ssh.listener.deinit(io); 112 113 // start task group 114 115 var tasks: std.Io.Group = .init; 116 defer tasks.cancel(io); 117 118 // run listeners, printing the ports actually bound 119 120 const git_ssh_port: ?u16 = if (ssh_maybe) |*ssh| ssh.listener.port() else null; 121 122 if (http_maybe) |*http| { 123 try err.print("serving HTTP on {s}:{d}, repo root {s}\n", .{ http.address.host, http.port(), repo_root_path }); 124 try err.flush(); 125 serve_http.runListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, admin_repo_path, &http.server, &tasks, err); 126 } 127 128 if (ssh_maybe) |*ssh| { 129 try err.print("serving SSH on {s}:{d}\n", .{ ssh.listener.address.host, ssh.listener.port() }); 130 try err.flush(); 131 serve_ssh.runListener(io, allocator, &ssh.host_key, &ssh.session_handler, &ssh.listener.server, &tasks, err); 132 } 133 134 try err.print("serving web UI on http://{s}:{d}/\n", .{ wui_listen_address.host, wui_server.socket.address.getPort() }); 135 try err.flush(); 136 137 runWebListener(io, allocator, &wui_server, &tasks, .{ .remote = .{ 138 .admin_repo_path = admin_repo_path, 139 .session_store = session_store, 140 .git_http_port = git_http_port, 141 .git_ssh_port = git_ssh_port, 142 } }, err); 143 144 if (@TypeOf(runnable) != void) { 145 try runnable.run(wui_server.socket.address.getPort(), git_http_port, git_ssh_port); 146 } else { 147 try tasks.await(io); 148 } 149 } 150 151 // serve just the web UI for a single local repo, running `runnable` (the 152 // local TUI) in the foreground while the listener runs in the background. 153 // when the default port is taken, a random ephemeral port is used instead; 154 // the bound port is passed to `runnable.run`. 155 pub fn runLocal( 156 io: std.Io, 157 allocator: std.mem.Allocator, 158 local: ui.RepoSource, 159 err: *std.Io.Writer, 160 runnable: anytype, 161 ) !void { 162 // always fall back, so another instance viewing a second repo just binds 163 // its own port 164 const wui_listen_address = try parseListenAddress((Options{}).wui_listen); 165 var wui_server = try listen(io, wui_listen_address, .fallback); 166 defer wui_server.deinit(io); 167 168 var tasks: std.Io.Group = .init; 169 defer tasks.cancel(io); 170 171 runWebListener(io, allocator, &wui_server, &tasks, .{ .local = local }, err); 172 173 try runnable.run(wui_server.socket.address.getPort()); 174 } 175 176 fn runWebListener( 177 io: std.Io, 178 allocator: std.mem.Allocator, 179 net_server: *std.Io.net.Server, 180 tasks: *std.Io.Group, 181 host: web.Host, 182 err: *std.Io.Writer, 183 ) void { 184 const Context = struct { 185 io: std.Io, 186 allocator: std.mem.Allocator, 187 host: web.Host, 188 err: *std.Io.Writer, 189 }; 190 191 const handle = struct { 192 fn h(ctx: Context, stream: std.Io.net.Stream) void { 193 defer stream.close(ctx.io); 194 web.handleConnection(ctx.io, ctx.allocator, stream, ctx.host, ctx.err) catch |request_err| { 195 serve_common.logError(ctx.io, ctx.err, "web ui request failed: {s}\n", .{@errorName(request_err)}); 196 }; 197 } 198 }.h; 199 200 serve_common.runListener(io, net_server, tasks, err, "web ui", Context{ 201 .io = io, 202 .allocator = allocator, 203 .host = host, 204 .err = err, 205 }, handle); 206 } 207 208 // what a listener does with a port that is already taken 209 const PortPolicy = enum { 210 // bind it anyway, so a restart isn't blocked by the last run's connections 211 // sitting in TIME_WAIT. on linux this also sets SO_REUSEPORT, so a server 212 // already holding the port ends up sharing it. 213 reuse, 214 // leave it to whoever holds it and take an OS-assigned port instead 215 fallback, 216 }; 217 218 fn listen(io: std.Io, listen_address: ListenAddress, policy: PortPolicy) !std.Io.net.Server { 219 const address = try std.Io.net.IpAddress.parseIp4(listen_address.host, listen_address.port); 220 return switch (policy) { 221 .reuse => try address.listen(io, .{ .reuse_address = true }), 222 .fallback => address.listen(io, .{}) catch |listen_err| switch (listen_err) { 223 error.AddressInUse => blk: { 224 const any_port = try std.Io.net.IpAddress.parseIp4(listen_address.host, 0); 225 break :blk try any_port.listen(io, .{}); 226 }, 227 else => |e| return e, 228 }, 229 }; 230 } 231 232 fn parseListenAddress(value: []const u8) !ListenAddress { 233 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; 234 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; 235 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 236 return .{ .host = value[0..colon], .port = port }; 237 } 238