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