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