a73x

58a409f7

refactor: one read-only poll slot, pollIn

a73x   2026-08-29 10:01

Commit message
refactor: one read-only poll slot, pollIn

Six pollfd literals in the daemon's poll spelled the same three fields,
four of them across five lines. pollIn is the one owner, so the -1 idiom
for a dead slot is stated once and a new table gets a call rather than a
sixth copy. The client arm keeps its literal: it is the only slot that
ever asks for POLLOUT.

Pinned by every pump test — a slot built wrong stops delivering — and by
the agent e2e legs, which poll a session listener and a live channel.

src/server/server.zig
Old New
@@ -141,6 +141,11 @@ pub fn countLive(slots: anytype) usize {
141 return n; 141 return n;
142 } 142 }
143 143
144 /// One read-only poll slot. A dead one polls -1, which poll(2) ignores.
145 fn pollIn(fd: std.posix.fd_t) std.posix.pollfd {
146 return .{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 };
147 }
148
144 pub var shutdown_flag = std.atomic.Value(bool).init(false); 149 pub var shutdown_flag = std.atomic.Value(bool).init(false);
145 150
146 fn onShutdownSignal(_: c_int) callconv(.c) void { 151 fn onShutdownSignal(_: c_int) callconv(.c) void {
@@ -996,16 +1001,10 @@ pub const Server = struct {
996 max_sessions + 1 + max_clients + max_observers + 1001 max_sessions + 1 + max_clients + max_observers +
997 max_sessions + max_agent_chans + 1 1002 max_sessions + max_agent_chans + 1
998 ]std.posix.pollfd = undefined; 1003 ]std.posix.pollfd = undefined;
999 // A dead session slot polls fd -1, the same idiom as a dead client
1000 // slot below: poll(2) ignores negative fds.
1001 for (&self.sessions.table, 0..) |*slot, si| { 1004 for (&self.sessions.table, 0..) |*slot, si| {
1002 fds[si] = .{ 1005 fds[si] = pollIn(if (slot.*) |*s| s.pty.master else -1);
1003 .fd = if (slot.*) |*s| s.pty.master else -1,
1004 .events = std.posix.POLL.IN,
1005 .revents = 0,
1006 };
1007 } 1006 }
1008 fds[listener_idx] = .{ .fd = self.listener.stream.handle, .events = std.posix.POLL.IN, .revents = 0 }; 1007 fds[listener_idx] = pollIn(self.listener.stream.handle);
1009 for (&self.clients, 0..) |*slot, i| { 1008 for (&self.clients, 0..) |*slot, i| {
1010 if (slot.*) |*c| { 1009 if (slot.*) |*c| {
1011 // POLLOUT only while something is owed: asking for it on an 1010 // POLLOUT only while something is owed: asking for it on an
@@ -1015,40 +1014,27 @@ pub const Server = struct {
1015 if (c.pending.items.len > 0) events |= std.posix.POLL.OUT; 1014 if (c.pending.items.len > 0) events |= std.posix.POLL.OUT;
1016 fds[client_base + i] = .{ .fd = c.sink.pollFd(), .events = events, .revents = 0 }; 1015 fds[client_base + i] = .{ .fd = c.sink.pollFd(), .events = events, .revents = 0 };
1017 } else { 1016 } else {
1018 fds[client_base + i] = .{ .fd = -1, .events = std.posix.POLL.IN, .revents = 0 }; 1017 fds[client_base + i] = pollIn(-1);
1019 } 1018 }
1020 } 1019 }
1021 for (&self.observers, 0..) |*slot, i| { 1020 for (&self.observers, 0..) |*slot, i| {
1022 fds[obs_base + i] = .{ .fd = if (slot.*) |*o| o.fd else -1, .events = std.posix.POLL.IN, .revents = 0 }; 1021 fds[obs_base + i] = pollIn(if (slot.*) |*o| o.fd else -1);
1023 } 1022 }
1024 // Each session's agent socket and every live channel on it, on the 1023 // Each session's agent socket and every live channel on it. In the
1025 // same -1 idiom as the dead slots above. In the main poll rather 1024 // main poll rather than a loop of their own: an agent exchange is
1026 // than a loop of their own: an agent exchange is several round trips 1025 // several round trips deep inside an ssh handshake, and a second
1027 // deep inside an ssh handshake, and a second loop would pay it a 1026 // loop would pay it a poll cycle per leg.
1028 // poll cycle per leg.
1029 for (&self.sessions.table, 0..) |*slot, si| { 1027 for (&self.sessions.table, 0..) |*slot, si| {
1030 fds[agent_listener_base + si] = .{ 1028 fds[agent_listener_base + si] = pollIn(if (slot.*) |*s| s.agent_listener else -1);
1031 .fd = if (slot.*) |*s| s.agent_listener else -1,
1032 .events = std.posix.POLL.IN,
1033 .revents = 0,
1034 };
1035 } 1029 }
1036 for (self.agents.chans, 0..) |slot, s| { 1030 for (self.agents.chans, 0..) |slot, s| {
1037 fds[agent_chan_base + s] = .{ 1031 fds[agent_chan_base + s] = pollIn(if (slot) |ch| ch.fd else -1);
1038 .fd = if (slot) |ch| ch.fd else -1,
1039 .events = std.posix.POLL.IN,
1040 .revents = 0,
1041 };
1042 } 1032 }
1043 // One extra descriptor for every QUIC client there will ever be: 1033 // One extra descriptor for every QUIC client there will ever be:
1044 // they share it, which is the whole reason a client slot cannot be 1034 // they share it, which is the whole reason a client slot cannot be
1045 // a descriptor. 1035 // a descriptor.
1046 const quic_idx = fds.len - 1; 1036 const quic_idx = fds.len - 1;
1047 fds[quic_idx] = .{ 1037 fds[quic_idx] = pollIn(if (self.quicListener()) |q| q.pollFd() else -1);
1048 .fd = if (self.quicListener()) |q| q.pollFd() else -1,
1049 .events = std.posix.POLL.IN,
1050 .revents = 0,
1051 };
1052 1038
1053 // ngtcp2's timers, folded in: the listener's earliest deadline 1039 // ngtcp2's timers, folded in: the listener's earliest deadline
1054 // shortens this poll, so retransmits and idle timeouts happen on 1040 // shortens this poll, so retransmits and idle timeouts happen on
@@ -2855,12 +2841,13 @@ pub const Server = struct {
2855 // What a failed encode gives up is this one event: a 2841 // What a failed encode gives up is this one event: a
2856 // clipboard copy the user asked for that silently does not 2842 // clipboard copy the user asked for that silently does not
2857 // happen. Silently because there is nowhere to say it — this 2843 // happen. Silently because there is nowhere to say it — this
2858 // file makes no log calls and Stats counts no errors, and a 2844 // file makes no log calls and `upgrade.Counters` counts no
2859 // mechanism invented for this one path would be the only one 2845 // errors, and a mechanism invented for this one path would
2860 // of its kind. `continue` rather than `return` because the 2846 // be the only one of its kind. `continue` rather than
2861 // failure is per-event and the sizes are wildly uneven: a 2847 // `return` because the failure is per-event and the sizes
2862 // 64 KiB clipboard can exhaust memory while the 1-byte bell 2848 // are wildly uneven: a 64 KiB clipboard can exhaust memory
2863 // queued behind it would have gone out fine. 2849 // while the 1-byte bell queued behind it would have gone
2850 // out fine.
2864 .clipboard => proto.encodeClipboardEvent( 2851 .clipboard => proto.encodeClipboardEvent(
2865 &payload, 2852 &payload,
2866 self.alloc, 2853 self.alloc,
@@ -3043,8 +3030,8 @@ pub const Server = struct {
3043 3030
3044 /// Every client of THIS session gets it, and the rebuild happens with 3031 /// Every client of THIS session gets it, and the rebuild happens with
3045 /// nobody attached too, so the tracker stays usable. No term_modes, 3032 /// nobody attached too, so the tracker stays usable. No term_modes,
3046 /// unlike sendResync: reaching here for a client that has never been 3033 /// unlike sendResync: every attached client was told the mode on attach
3047 /// through an attach breaks that, silently. 3034 /// and at each change; a resync that skips it breaks that, silently.
3048 pub fn resyncSnapshot(self: *Server, si: usize) void { 3035 pub fn resyncSnapshot(self: *Server, si: usize) void {
3049 if (!self.rebuildTracker(si)) return; 3036 if (!self.rebuildTracker(si)) return;
3050 if (!self.hasClientsIn(si)) return; 3037 if (!self.hasClientsIn(si)) return;
@@ -3591,6 +3578,8 @@ pub const Server = struct {
3591 self.stats.deltas, self.stats.delta_bytes, 3578 self.stats.deltas, self.stats.delta_bytes,
3592 self.stats.snapshot_equiv_bytes, self.liveClients(), 3579 self.stats.snapshot_equiv_bytes, self.liveClients(),
3593 self.stats.attaches, self.sessions.live(), 3580 self.stats.attaches, self.sessions.live(),
3581 // `agents.*`, not `stats.*`: the relay counts the refusals
3582 // live and `stats` only carries the adoption-time pair.
3594 self.agents.live(), self.agents.refused_no_offer, 3583 self.agents.live(), self.agents.refused_no_offer,
3595 self.agents.refused_full, 3584 self.agents.refused_full,
3596 }, 3585 },