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