╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ serve.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
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 // create http listener 74 75 var http_maybe: ?BoundListener = if (options.http_listen) |value| blk: { 76 const address = try parseListenAddress(value); 77 break :blk .{ .address = address, .server = try listen(io, address, .reuse) }; 78 } else null; 79 defer if (http_maybe) |*http| http.deinit(io); 80 81 // disk-backed store for web login sessions 82 const session_store = try web.SessionStore.init(io, data_dir); 83 defer session_store.deinit(); 84 85 // create wui listener 86 87 const wui_listen_address = try parseListenAddress(options.wui_listen); 88 var wui_server = try listen(io, wui_listen_address, .reuse); 89 defer wui_server.deinit(io); 90 91 // create ssh listener and its session state 92 93 const git_http_port: ?u16 = if (http_maybe) |*http| http.port() else null; 94 var ssh_maybe: ?SshService = if (options.ssh_listen) |value| blk: { 95 const address = try parseListenAddress(value); 96 var listener = BoundListener{ .address = address, .server = try listen(io, address, .reuse) }; 97 errdefer listener.deinit(io); 98 const ssh_port = listener.port(); 99 break :blk .{ 100 .listener = listener, 101 .host_key = try serve_ssh_protocol.HostKey.loadOrGenerate(io, allocator, data_dir_path), 102 .session_handler = .{ 103 .admin_repo_path = admin_repo_path, 104 .repo_root_path = repo_root_path, 105 .wui_port = wui_server.socket.address.getPort(), 106 .git_http_port = git_http_port, 107 .git_ssh_port = ssh_port, 108 .git_ssh_prefix = options.git_ssh_prefix, 109 .err = err, 110 }, 111 }; 112 } else null; 113 defer if (ssh_maybe) |*ssh| ssh.listener.deinit(io); 114 115 // start task group 116 117 var tasks: std.Io.Group = .init; 118 defer tasks.cancel(io); 119 120 // run listeners, printing the ports actually bound 121 122 const git_ssh_port: ?u16 = if (ssh_maybe) |*ssh| ssh.listener.port() else null; 123 124 if (http_maybe) |*http| { 125 try err.print("serving HTTP on {s}:{d}, repo root {s}\n", .{ http.address.host, http.port(), repo_root_path }); 126 try err.flush(); 127 serve_http.runListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, admin_repo_path, &http.server, &tasks, err); 128 } 129 130 if (ssh_maybe) |*ssh| { 131 try err.print("serving SSH on {s}:{d}\n", .{ ssh.listener.address.host, ssh.listener.port() }); 132 try err.flush(); 133 serve_ssh.runListener(io, allocator, &ssh.host_key, &ssh.session_handler, &ssh.listener.server, &tasks, err); 134 } 135 136 try err.print("serving web UI on http://{s}:{d}/\n", .{ wui_listen_address.host, wui_server.socket.address.getPort() }); 137 try err.flush(); 138 139 runWebListener(io, allocator, &wui_server, &tasks, .{ .remote = .{ 140 .admin_repo_path = admin_repo_path, 141 .session_store = session_store, 142 .git_http_port = git_http_port, 143 .git_ssh_port = git_ssh_port, 144 .git_ssh_prefix = options.git_ssh_prefix, 145 } }, err); 146 147 if (@TypeOf(runnable) != void) { 148 try runnable.run(wui_server.socket.address.getPort(), git_http_port, git_ssh_port); 149 } else { 150 try tasks.await(io); 151 } 152 } 153 154 // serve just the web UI for a single local repo, running `runnable` (the 155 // local TUI) in the foreground while the listener runs in the background. 156 // when the default port is taken, a random ephemeral port is used instead; 157 // the bound port is passed to `runnable.run`. 158 pub fn runLocal( 159 io: std.Io, 160 allocator: std.mem.Allocator, 161 local: ui.RepoSource, 162 err: *std.Io.Writer, 163 runnable: anytype, 164 ) !void { 165 // always fall back, so another instance viewing a second repo just binds 166 // its own port 167 const wui_listen_address = try parseListenAddress((Options{}).wui_listen); 168 var wui_server = try listen(io, wui_listen_address, .fallback); 169 defer wui_server.deinit(io); 170 171 var tasks: std.Io.Group = .init; 172 defer tasks.cancel(io); 173 174 runWebListener(io, allocator, &wui_server, &tasks, .{ .local = local }, err); 175 176 try runnable.run(wui_server.socket.address.getPort()); 177 } 178 179 fn runWebListener( 180 io: std.Io, 181 allocator: std.mem.Allocator, 182 net_server: *std.Io.net.Server, 183 tasks: *std.Io.Group, 184 host: web.Host, 185 err: *std.Io.Writer, 186 ) void { 187 const Context = struct { 188 io: std.Io, 189 allocator: std.mem.Allocator, 190 host: web.Host, 191 err: *std.Io.Writer, 192 }; 193 194 const handle = struct { 195 fn h(ctx: Context, stream: std.Io.net.Stream) void { 196 defer stream.close(ctx.io); 197 web.handleConnection(ctx.io, ctx.allocator, stream, ctx.host, ctx.err) catch |request_err| { 198 serve_common.logError(ctx.io, ctx.err, "web ui request failed: {s}\n", .{@errorName(request_err)}); 199 }; 200 } 201 }.h; 202 203 serve_common.runListener(io, net_server, tasks, err, "web ui", Context{ 204 .io = io, 205 .allocator = allocator, 206 .host = host, 207 .err = err, 208 }, handle); 209 } 210 211 // what a listener does with a port that is already taken 212 const PortPolicy = enum { 213 // bind it anyway, so a restart isn't blocked by the last run's connections 214 // sitting in TIME_WAIT. on linux this also sets SO_REUSEPORT, so a server 215 // already holding the port ends up sharing it. 216 reuse, 217 // leave it to whoever holds it and take an OS-assigned port instead 218 fallback, 219 }; 220 221 fn listen(io: std.Io, listen_address: ListenAddress, policy: PortPolicy) !std.Io.net.Server { 222 const address = try std.Io.net.IpAddress.parseIp4(listen_address.host, listen_address.port); 223 return switch (policy) { 224 .reuse => try address.listen(io, .{ .reuse_address = true }), 225 .fallback => address.listen(io, .{}) catch |listen_err| switch (listen_err) { 226 error.AddressInUse => blk: { 227 const any_port = try std.Io.net.IpAddress.parseIp4(listen_address.host, 0); 228 break :blk try any_port.listen(io, .{}); 229 }, 230 else => |e| return e, 231 }, 232 }; 233 } 234 235 fn parseListenAddress(value: []const u8) !ListenAddress { 236 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; 237 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; 238 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 239 return .{ .host = value[0..colon], .port = port }; 240 } 241