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