╭ changed ────╮ │src/serve.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │113 } }, err); │ │114 │ │115 if (@TypeOf(runnable) != void) { │ │118 - try runnable.run(); │ │116 + try runnable.run(wui_server.socket.address.getPort(), ssh_server.socket.address.getPort());│ │117 } else { │ │118 try tasks.await(io); │ │119 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │130 err: *std.Io.Writer, │ │131 runnable: anytype, │ │132 ) !void { │ │135 - // no reuse_address here: on linux it also sets SO_REUSEPORT, which would │ │136 - // let this bind silently share a port an already-running server holds │ │137 - // instead of failing over to a random one. │ │133 + // always fall back, so another instance viewing a second repo just binds │ │134 + // its own port │ │135 const wui_listen_address = try parseListenAddress((Options{}).wui_listen); │ │139 - const wui_address = try std.Io.net.IpAddress.parseIp4(wui_listen_address.host, wui_listen_address.port);│ │140 - var wui_server = wui_address.listen(io, .{}) catch |listen_err| switch (listen_err) { │ │141 - // the default port is taken; bind port 0 so the OS assigns a free one │ │142 - error.AddressInUse => blk: { │ │143 - const any_port = try std.Io.net.IpAddress.parseIp4(wui_listen_address.host, 0); │ │144 - break :blk try any_port.listen(io, .{}); │ │145 - }, │ │146 - else => |e| return e, │ │147 - }; │ │136 + var wui_server = try listenWithFallback(io, wui_listen_address, true); │ │137 defer wui_server.deinit(io); │ │138 │ │139 var tasks: std.Io.Group = .init; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │176 }, handle); │ │177 } │ │178 │ │179 +// bind the address. when it's taken, fall back to an OS-assigned port, or │ │180 +// fail without `fallback`. no reuse_address: on linux it also sets │ │181 +// SO_REUSEPORT, which would let this bind silently share a port an │ │182 +// already-running server holds rather than seeing it as taken. │ │183 +fn listenWithFallback(io: std.Io, listen_address: ListenAddress, fallback: bool) !std.Io.net.Server {│ │184 + const address = try std.Io.net.IpAddress.parseIp4(listen_address.host, listen_address.port); │ │185 + return address.listen(io, .{}) catch |listen_err| switch (listen_err) { │ │186 + error.AddressInUse => blk: { │ │187 + if (!fallback) return listen_err; │ │188 + const any_port = try std.Io.net.IpAddress.parseIp4(listen_address.host, 0); │ │189 + break :blk try any_port.listen(io, .{}); │ │190 + }, │ │191 + else => |e| return e, │ │192 + }; │ │193 +} │ │194 + │ │195 fn parseListenAddress(value: []const u8) !ListenAddress { │ │196 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress;│ │197 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯