╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ serve.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
33 34 fn deinit(self: *BoundListener, io: std.Io) void { 35 self.server.deinit(io); 36 } 37 38 fn port(self: *const BoundListener) u16 { 39 return self.server.socket.address.getPort(); 40 } 41 }; 42 43 const SshService = struct { 44 listener: BoundListener, 45 host_key: serve_ssh_protocol.HostKey, 46 session_handler: serve_ssh.SessionHandler, 47 watchdog: serve_ssh.Watchdog = .{}, 48 }; 49 50 pub fn run( 51 comptime repo_kind: rp.RepoKind, 52 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 53 io: std.Io, 54 allocator: std.mem.Allocator, 55 cwd_path: []const u8, 56 options: Options, 57 err: *std.Io.Writer, 58 runnable: anytype, 59 ) !void { 60 // create the data dir 61 62 const data_dir_path = try std.Io.Dir.path.resolveAlloc(allocator, &.{ cwd_path, options.data_dir }); 63 defer allocator.free(data_dir_path); 64 65 const data_dir = try std.Io.Dir.cwd().createDirPathOpen(io, data_dir_path, .{}); 66 defer data_dir.close(io); 67 68 // create the users dir 69 70 const users_dir = try std.Io.Dir.path.resolveAlloc(allocator, &.{ data_dir_path, "users" }); 71 defer allocator.free(users_dir); 72 73 try std.Io.Dir.cwd().createDirPath(io, users_dir); 74 75 // the admin repo path is where the user/repo metadata events live 76 77 const admin_repo_path = try std.Io.Dir.path.resolveAlloc(allocator, &.{ data_dir_path, "admin" }); 78 defer allocator.free(admin_repo_path); 79 80 // create the admin repo on first run 81 { 82 var admin_repo = rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }) catch |open_err| switch (open_err) { 83 error.RepoNotFound, error.FileNotFound => try evt.initAdminRepo(io, allocator, admin_repo_path), 84 else => |e| return e, 85 }; 86 defer admin_repo.deinit(io, allocator); 87 } 88 89 const port_policy: PortPolicy = if (options.fallback_on_address_in_use) .fallback else .exact; 90 91 // disk-backed store for web login sessions 92 const session_store = try web.SessionStore.init(io, data_dir); 93 defer session_store.deinit(); 94 95 // create web listener 96 97 const web_listen_address = try parseListenAddress(options.web_listen); 98 var web_server = try listen(io, web_listen_address, port_policy); 99 defer web_server.deinit(io); 100 101 const web_url = options.web_url orelse try allocator.print("http://localhost:{d}", .{web_server.socket.address.getPort()}); 102 defer if (options.web_url == null) allocator.free(web_url); 103 104 // create ssh listener and its session state 105 106 var ssh_url_owned: ?[]const u8 = null; 107 defer if (ssh_url_owned) |url| allocator.free(url); 108 var ssh_maybe: ?SshService = if (options.ssh_listen) |value| blk: { 109 const address = try parseListenAddress(value); 110 var listener = BoundListener{ .address = address, .server = try listen(io, address, port_policy) }; 111 errdefer listener.deinit(io); 112 const ssh_url = options.ssh_url orelse url_blk: { 113 const url = try allocator.print("ssh://localhost:{d}", .{listener.port()}); 114 ssh_url_owned = url; 115 break :url_blk url; 116 }; 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, .admin_repo_path = admin_repo_path }, 123 .web_url = web_url, 124 .ssh_url = ssh_url, 125 .git_ssh_prefix = options.git_ssh_prefix, 126 .max_repo_size = options.max_repo_size, 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 // run listeners, printing the ports actually bound 139 140 const ssh_url: ?[]const u8 = if (ssh_maybe) |*ssh| ssh.session_handler.ssh_url else null; 141 142 if (ssh_maybe) |*ssh| { 143 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) }); 144 try err.flush(); 145 serve_ssh.runListener(io, allocator, &ssh.host_key, &ssh.session_handler, &ssh.watchdog, &ssh.listener.server, &tasks, err); 146 } 147 148 try err.print("serving web UI and git over HTTP on http://{s}:{d}/, users dir {s}\n", .{ web_listen_address.host, web_server.socket.address.getPort(), users_dir }); 149 try err.flush(); 150 151 runWebListener(io, allocator, &web_server, &tasks, .{ .server = .{ 152 .admin_repo_path = admin_repo_path, 153 .users_dir = users_dir, 154 .session_store = session_store, 155 .web_url = web_url, 156 .ssh_url = ssh_url, 157 .git_ssh_prefix = options.git_ssh_prefix, 158 .serve_git = serve_http.gitHandler(repo_kind, any_repo_opts), 159 } }, err); 160 161 if (@TypeOf(runnable) != void) { 162 try runnable.run(web_url, ssh_url); 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 web UI's url 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 web_listen_address = try parseListenAddress((Options{}).web_listen); 182 var web_server = try listen(io, web_listen_address, .fallback); 183 defer web_server.deinit(io); 184 185 var tasks: std.Io.Group = .init; 186 defer tasks.cancel(io); 187 188 runWebListener(io, allocator, &web_server, &tasks, .{ .local = local }, err); 189 190 const web_url = try allocator.print("http://localhost:{d}", .{web_server.socket.address.getPort()}); 191 defer allocator.free(web_url); 192 try runnable.run(web_url); 193 } 194 195 fn runWebListener( 196 io: std.Io, 197 allocator: std.mem.Allocator, 198 net_server: *std.Io.net.Server, 199 tasks: *std.Io.Group, 200 host: web.Host, 201 err: *std.Io.Writer, 202 ) void { 203 const Context = struct { 204 io: std.Io, 205 allocator: std.mem.Allocator, 206 host: web.Host, 207 err: *std.Io.Writer, 208 }; 209 210 const handle = struct { 211 fn h(ctx: Context, stream: std.Io.net.Stream) void { 212 defer stream.close(ctx.io); 213 web.handleConnection(ctx.io, ctx.allocator, stream, ctx.host, ctx.err) catch |request_err| { 214 serve_common.logError(ctx.io, ctx.err, "web ui request failed: {s}\n", .{@errorName(request_err)}); 215 }; 216 } 217 }.h; 218 219 serve_common.runListener(io, net_server, tasks, err, "web ui", Context{ 220 .io = io, 221 .allocator = allocator, 222 .host = host, 223 .err = err, 224 }, handle); 225 } 226 227 // what a listener does with a port that is already taken 228 const PortPolicy = enum { 229 exact, 230 // leave it to whoever holds it and take an OS-assigned port instead 231 fallback, 232 }; 233 234 fn listen(io: std.Io, listen_address: ListenAddress, policy: PortPolicy) !std.Io.net.Server { 235 const address = try std.Io.net.IpAddress.parseIp4(listen_address.host, listen_address.port); 236 return switch (policy) { 237 // so a restart can bind past the old process's connections in TIME_WAIT. 238 // not for fallback, where it would let a second instance share the port. 239 .exact => try address.listen(io, .{ .reuse_address = true }), 240 .fallback => address.listen(io, .{}) catch |listen_err| switch (listen_err) { 241 error.AddressInUse => blk: { 242 const any_port = try std.Io.net.IpAddress.parseIp4(listen_address.host, 0); 243 break :blk try any_port.listen(io, .{}); 244 }, 245 else => |e| return e, 246 }, 247 }; 248 } 249 250 fn parseListenAddress(value: []const u8) !ListenAddress { 251 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; 252 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; 253 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 254 return .{ .host = value[0..colon], .port = port }; 255 } 256