╭ changed ────╮ │src/serve.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────╮ │1 const std = @import("std"); │ │2 -const builtin = @import("builtin");│ │2 const xit = @import("xit"); │ │3 const rp = xit.repo; │ │4 const ui = @import("./ui.zig"); │ ╰──────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │68 │ │69 const admin_repo_path = try std.fs.path.resolve(allocator, &.{ data_dir_path, "admin" });│ │70 defer allocator.free(admin_repo_path); │ │72 - │ │73 - // a debug build is a dev run, so a taken port falls back to a random one; │ │74 - // a release build fails, since whatever points at the configured port │ │75 - // would silently break. │ │76 - const fallback = builtin.mode == .Debug; │ │71 │ │72 // create http listener │ │73 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │74 var http_maybe: ?BoundListener = if (options.http_listen) |value| blk: { │ │75 const address = try parseListenAddress(value); │ │82 - break :blk .{ .address = address, .server = try listenWithFallback(io, address, fallback) };│ │76 + break :blk .{ .address = address, .server = try listen(io, address, .reuse) }; │ │77 } else null; │ │78 defer if (http_maybe) |*http| http.deinit(io); │ │79 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │84 // create wui listener │ │85 │ │86 const wui_listen_address = try parseListenAddress(options.wui_listen); │ │93 - var wui_server = try listenWithFallback(io, wui_listen_address, fallback);│ │87 + var wui_server = try listen(io, wui_listen_address, .reuse); │ │88 defer wui_server.deinit(io); │ │89 │ │90 // create ssh listener and its session state │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 92 const clone_http_port: ?u16 = if (http_maybe) |*http| http.port() else null; │ │ 93 var ssh_maybe: ?SshService = if (options.ssh_listen) |value| blk: { │ │ 94 const address = try parseListenAddress(value); │ │101 - var listener = BoundListener{ .address = address, .server = try listenWithFallback(io, address, fallback) };│ │ 95 + var listener = BoundListener{ .address = address, .server = try listen(io, address, .reuse) }; │ │ 96 errdefer listener.deinit(io); │ │ 97 const ssh_port = listener.port(); │ │ 98 break :blk .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │162 // always fall back, so another instance viewing a second repo just binds │ │163 // its own port │ │164 const wui_listen_address = try parseListenAddress((Options{}).wui_listen);│ │171 - var wui_server = try listenWithFallback(io, wui_listen_address, true); │ │165 + var wui_server = try listen(io, wui_listen_address, .fallback); │ │166 defer wui_server.deinit(io); │ │167 │ │168 var tasks: std.Io.Group = .init; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │205 }, handle); │ │206 } │ │207 │ │214 -// bind the address. when it's taken, fall back to an OS-assigned port, or │ │215 -// fail without `fallback`. no reuse_address: on linux it also sets │ │216 -// SO_REUSEPORT, which would let this bind silently share a port an │ │217 -// already-running server holds rather than seeing it as taken. │ │218 -fn listenWithFallback(io: std.Io, listen_address: ListenAddress, fallback: bool) !std.Io.net.Server {│ │208 +// what a listener does with a port that is already taken │ │209 +const PortPolicy = enum { │ │210 + // bind it anyway, so a restart isn't blocked by the last run's connections │ │211 + // sitting in TIME_WAIT. on linux this also sets SO_REUSEPORT, so a server │ │212 + // already holding the port ends up sharing it. │ │213 + reuse, │ │214 + // leave it to whoever holds it and take an OS-assigned port instead │ │215 + fallback, │ │216 +}; │ │217 + │ │218 +fn listen(io: std.Io, listen_address: ListenAddress, policy: PortPolicy) !std.Io.net.Server { │ │219 const address = try std.Io.net.IpAddress.parseIp4(listen_address.host, listen_address.port); │ │220 - return address.listen(io, .{}) catch |listen_err| switch (listen_err) { │ │221 - error.AddressInUse => blk: { │ │222 - if (!fallback) return listen_err; │ │223 - const any_port = try std.Io.net.IpAddress.parseIp4(listen_address.host, 0); │ │224 - break :blk try any_port.listen(io, .{}); │ │220 + return switch (policy) { │ │221 + .reuse => try address.listen(io, .{ .reuse_address = true }), │ │222 + .fallback => address.listen(io, .{}) catch |listen_err| switch (listen_err) { │ │223 + error.AddressInUse => blk: { │ │224 + const any_port = try std.Io.net.IpAddress.parseIp4(listen_address.host, 0); │ │225 + break :blk try any_port.listen(io, .{}); │ │226 + }, │ │227 + else => |e| return e, │ │228 }, │ │226 - else => |e| return e, │ │229 }; │ │230 } │ │231 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯