8fe125a8
refactor: Transport wraps the Link, policy stays and mechanics move
a73x 2026-08-31 19:15
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -215,7 +215,7 @@ const mod_table = [_]ModSpec{ | |||
| 215 | // sits here rather than in either front so the CLI wall and the browser | 215 | // sits here rather than in either front so the CLI wall and the browser |
| 216 | // hub resolve a host line the same way. Nothing here WRITES that file — | 216 | // hub resolve a host line the same way. Nothing here WRITES that file — |
| 217 | // `wall_host.recordHost` and `webhub_main` do. | 217 | // `wall_host.recordHost` and `webhub_main` do. |
| 218 | .{ .name = "client", .path = "src/client/client.zig", .link_libc = true, .imports = &.{ "term", "quic", "xdg", "sockpath", "dial" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 218 | .{ .name = "client", .path = "src/client/client.zig", .link_libc = true, .imports = &.{ "term", "quic", "xdg", "sockpath", "dial", "link" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 219 | // ---- the two fronts ---- | 219 | // ---- the two fronts ---- |
| 220 | // The browser hub's HTTP/WebSocket decisions: Origin gate, route table, | 220 | // The browser hub's HTTP/WebSocket decisions: Origin gate, route table, |
| 221 | // WS endpoint naming. Assets are injected (the exe root @embedFiles | 221 | // WS endpoint naming. Assets are injected (the exe root @embedFiles |
src/client/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -17,6 +17,10 @@ const sockpath = @import("sockpath"); | |||
| 17 | // already takes a `dial: ?*handoff.Dial` parameter, and a parameter that | 17 | // already takes a `dial: ?*handoff.Dial` parameter, and a parameter that |
| 18 | // shadows a file-scope declaration does not compile. | 18 | // shadows a file-scope declaration does not compile. |
| 19 | const dialer = @import("dial"); | 19 | const dialer = @import("dial"); |
| 20 | // The fd|pipe|quic union and its await loop, under a name that is not | ||
| 21 | // `link`: `Transport.link` is the field this module reads everywhere, and | ||
| 22 | // several local variables are called `link` too. | ||
| 23 | const link_mod = @import("link"); | ||
| 20 | 24 | ||
| 21 | // The client link's public seams: the wall, the hub and the mains reach | 25 | // The client link's public seams: the wall, the hub and the mains reach |
| 22 | // these as client.X — the table stays one row, the files stay children. | 26 | // these as client.X — the table stays one row, the files stay children. |
| @@ -135,22 +139,8 @@ pub const PendingSwitch = struct { | |||
| 135 | } | 139 | } |
| 136 | }; | 140 | }; |
| 137 | 141 | ||
| 138 | /// The client's transport: a read fd and a write fd. For a unix socket they | 142 | /// Callers spell this `client.Incoming`; the union itself is the link row's. |
| 139 | /// are one and the same; under `--via` they are the child command's stdout | 143 | pub const Incoming = link_mod.Incoming; |
| 140 | /// and stdin. Nothing below the transport setup knows which it is — that | ||
| 141 | /// blindness is the point: transport is a swap, not a redesign. | ||
| 142 | const Conn = struct { r: std.posix.fd_t, w: std.posix.fd_t }; | ||
| 143 | |||
| 144 | /// Three outcomes, not two: QUIC's socket goes readable for acks and half | ||
| 145 | /// frames, so `null` cannot keep the socket path's meaning of "peer gone" | ||
| 146 | /// without making every partial frame reconnect. | ||
| 147 | pub const Incoming = union(enum) { | ||
| 148 | frame: proto.Frame, | ||
| 149 | /// The transport is healthy; there is simply no whole frame yet. | ||
| 150 | incomplete, | ||
| 151 | /// Finished: EOF, a frame torn in half, a connection that died. | ||
| 152 | closed, | ||
| 153 | }; | ||
| 154 | 144 | ||
| 155 | /// A `--via` that died before the first frame carried no connection, and the | 145 | /// A `--via` that died before the first frame carried no connection, and the |
| 156 | /// cause is already on that command's stderr. `session_epoch` tells the two | 146 | /// cause is already on that command's stderr. `session_epoch` tells the two |
| @@ -322,32 +312,21 @@ pub const SpecError = error{ MissingKey, OutOfMemory }; | |||
| 322 | 312 | ||
| 323 | /// What the open produced — the live wire. Distinct from `Target` because a | 313 | /// What the open produced — the live wire. Distinct from `Target` because a |
| 324 | /// `hand` recipe yields either a quic or a pipe link, and which one is | 314 | /// `hand` recipe yields either a quic or a pipe link, and which one is |
| 325 | /// decided inside `openHandoff` at runtime. | 315 | /// decided inside `openHandoff` at runtime. The union is the link row's; |
| 326 | pub const Link = union(enum) { | 316 | /// this name is what the rest of the client spells, `std.meta.Tag(Link)` |
| 327 | /// A unix socket: `conn.r == conn.w`, and there is nothing else to own. | 317 | /// included. |
| 328 | fd, | 318 | pub const Link = link_mod.Link; |
| 329 | /// `--via`, and the ssh half of a handoff: the command whose stdio *is* | ||
| 330 | /// the transport. | ||
| 331 | pipe: std.process.Child, | ||
| 332 | /// The connection that IS the transport. `conn` holds the UDP socket in | ||
| 333 | /// `.r` so the poll path needs no special case, and `.w` is -1 because | ||
| 334 | /// there is nothing to write(2) to — bytes go through the stream layer. | ||
| 335 | quic: *quic.Client, | ||
| 336 | }; | ||
| 337 | 319 | ||
| 338 | /// One live connection to a daemon, however it was reached. The point of the | 320 | /// One live connection to a daemon, however it was reached. The point of the |
| 339 | /// struct is that it can be closed and opened again from the same `Target`, | 321 | /// struct is that it can be closed and opened again from the same `Target`, |
| 340 | /// which is what lets a session outlive its transport instead of exiting | 322 | /// which is what lets a session outlive its transport instead of exiting |
| 341 | /// with it. | 323 | /// with it. |
| 342 | pub const Transport = struct { | 324 | pub const Transport = struct { |
| 343 | conn: Conn, | 325 | /// The wire itself. Everything below the frame — send, read, wait, |
| 344 | link: Link = .fd, | 326 | /// close — is the link's. What stays on this struct is what the link has |
| 345 | /// Bytes the QUIC ring would not take yet. Frames are appended whole and | 327 | /// no business knowing: the second fd a handoff's ssh talks on, the last |
| 346 | /// handed over a prefix at a time, so a short accept can never split one | 328 | /// line it said, and whether that line is relayed onward. |
| 347 | /// on the wire — it just means the rest waits here. Keystrokes and | 329 | link: Link, |
| 348 | /// attach frames are all this ever holds. | ||
| 349 | qout: std.ArrayList(u8) = .empty, | ||
| 350 | alloc: std.mem.Allocator = undefined, | ||
| 351 | /// The handoff ssh's stderr for a `.pipe` link born of a handoff; -1 | 330 | /// The handoff ssh's stderr for a `.pipe` link born of a handoff; -1 |
| 352 | /// for every other link and for `--via`, whose stderr is the user's. | 331 | /// for every other link and for `--via`, whose stderr is the user's. |
| 353 | err_fd: std.posix.fd_t = -1, | 332 | err_fd: std.posix.fd_t = -1, |
| @@ -421,10 +400,11 @@ pub const Transport = struct { | |||
| 421 | /// The transport a piped child IS: its stdio is the wire, and the child | 400 | /// The transport a piped child IS: its stdio is the wire, and the child |
| 422 | /// itself is what has to be owned and reaped. | 401 | /// itself is what has to be owned and reaped. |
| 423 | fn pipeTransport(child: std.process.Child) Transport { | 402 | fn pipeTransport(child: std.process.Child) Transport { |
| 424 | return .{ | 403 | return .{ .link = .{ .pipe = .{ |
| 425 | .conn = .{ .r = child.stdout.?.handle, .w = child.stdin.?.handle }, | 404 | .child = child, |
| 426 | .link = .{ .pipe = child }, | 405 | .r = child.stdout.?.handle, |
| 427 | }; | 406 | .w = child.stdin.?.handle, |
| 407 | } } }; | ||
| 428 | } | 408 | } |
| 429 | 409 | ||
| 430 | /// `connect` only creates state, so the handshake wait belongs here: | 410 | /// `connect` only creates state, so the handshake wait belongs here: |
| @@ -442,11 +422,7 @@ pub const Transport = struct { | |||
| 442 | const cl = try quic.Client.connect(alloc, addr, key, idle_ms); | 422 | const cl = try quic.Client.connect(alloc, addr, key, idle_ms); |
| 443 | errdefer cl.deinit(); | 423 | errdefer cl.deinit(); |
| 444 | try waitReady(cl, budget_ms, alloc, carry, abort_fd); | 424 | try waitReady(cl, budget_ms, alloc, carry, abort_fd); |
| 445 | return .{ | 425 | return .{ .link = .{ .quic = .{ .cl = cl, .alloc = alloc } } }; |
| 446 | .conn = .{ .r = cl.pollFd(), .w = -1 }, | ||
| 447 | .link = .{ .quic = cl }, | ||
| 448 | .alloc = alloc, | ||
| 449 | }; | ||
| 450 | } | 426 | } |
| 451 | 427 | ||
| 452 | /// Errors are returned rather than reported here — the caller knows | 428 | /// Errors are returned rather than reported here — the caller knows |
| @@ -482,10 +458,7 @@ pub const Transport = struct { | |||
| 482 | }, | 458 | }, |
| 483 | .sock => |path| { | 459 | .sock => |path| { |
| 484 | const stream = try dialer.dial(path); | 460 | const stream = try dialer.dial(path); |
| 485 | return .{ | 461 | return .{ .link = .{ .fd = stream.handle } }; |
| 486 | .conn = .{ .r = stream.handle, .w = stream.handle }, | ||
| 487 | .link = .fd, | ||
| 488 | }; | ||
| 489 | }, | 462 | }, |
| 490 | } | 463 | } |
| 491 | } | 464 | } |
| @@ -661,7 +634,7 @@ pub const Transport = struct { | |||
| 661 | 634 | ||
| 662 | /// Not where frames come from: QUIC polls UDP, reads frames above it. | 635 | /// Not where frames come from: QUIC polls UDP, reads frames above it. |
| 663 | pub fn pollFd(self: *const Transport) std.posix.fd_t { | 636 | pub fn pollFd(self: *const Transport) std.posix.fd_t { |
| 664 | return self.conn.r; | 637 | return self.link.pollFd(); |
| 665 | } | 638 | } |
| 666 | 639 | ||
| 667 | /// The second fd a handoff's owner polls, or null when there is none. | 640 | /// The second fd a handoff's owner polls, or null when there is none. |
| @@ -684,125 +657,61 @@ pub const Transport = struct { | |||
| 684 | /// ssh may need the tty for a password. `qout` is what would otherwise | 657 | /// ssh may need the tty for a password. `qout` is what would otherwise |
| 685 | /// cross: two threads on one non-thread-safe arena. | 658 | /// cross: two threads on one non-thread-safe arena. |
| 686 | pub fn adopt(self: *Transport, alloc: std.mem.Allocator) void { | 659 | pub fn adopt(self: *Transport, alloc: std.mem.Allocator) void { |
| 687 | if (self.link != .quic) return; | 660 | const q = switch (self.link) { |
| 688 | std.debug.assert(self.qout.items.len == 0); | 661 | .quic => |*q| q, |
| 689 | self.qout.deinit(self.alloc); | 662 | .fd, .pipe => return, |
| 690 | self.qout = .empty; | 663 | }; |
| 691 | self.alloc = alloc; | 664 | std.debug.assert(q.qout.items.len == 0); |
| 665 | q.qout.deinit(q.alloc); | ||
| 666 | q.qout = .empty; | ||
| 667 | q.alloc = alloc; | ||
| 692 | } | 668 | } |
| 693 | 669 | ||
| 694 | pub fn writeFrame(self: *Transport, t: proto.MsgType, payload: []const u8) !void { | 670 | pub fn writeFrame(self: *Transport, t: proto.MsgType, payload: []const u8) !void { |
| 695 | switch (self.link) { | 671 | return self.link.sendFrame(t, payload); |
| 696 | .quic => { | ||
| 697 | // Appended whole, handed over a prefix at a time. A frame can | ||
| 698 | // therefore never be split across a refusal — the ring takes | ||
| 699 | // what it takes and the remainder is offered again next pass. | ||
| 700 | try proto.appendFrame(&self.qout, self.alloc, t, payload); | ||
| 701 | self.flushQuic(); | ||
| 702 | }, | ||
| 703 | .fd, .pipe => return proto.writeFrame(self.conn.w, t, payload), | ||
| 704 | } | ||
| 705 | } | 672 | } |
| 706 | 673 | ||
| 707 | /// Unconditional: a QUIC connection's timers are the only thing that | 674 | /// Unconditional: a QUIC connection's timers are the only thing that |
| 708 | /// notices a peer which stopped answering. | 675 | /// notices a peer which stopped answering. |
| 709 | pub fn service(self: *Transport) void { | 676 | pub fn service(self: *Transport) void { |
| 710 | switch (self.link) { | 677 | self.link.service(); |
| 711 | .quic => |cl| { | ||
| 712 | cl.pump(); | ||
| 713 | self.flushQuic(); | ||
| 714 | }, | ||
| 715 | .fd, .pipe => {}, | ||
| 716 | } | ||
| 717 | } | 678 | } |
| 718 | 679 | ||
| 719 | /// Folds ngtcp2's next deadline in, so retransmits and idle timeouts | 680 | /// Folds ngtcp2's next deadline in, so retransmits and idle timeouts |
| 720 | /// happen on time without a second timer. | 681 | /// happen on time without a second timer. |
| 721 | pub fn timeoutMs(self: *Transport, cap_ms: i32) i32 { | 682 | pub fn timeoutMs(self: *Transport, cap_ms: i32) i32 { |
| 722 | return switch (self.link) { | 683 | return self.link.timeoutMs(cap_ms); |
| 723 | .quic => |cl| cl.timeoutMs(cap_ms), | ||
| 724 | .fd, .pipe => cap_ms, | ||
| 725 | }; | ||
| 726 | } | 684 | } |
| 727 | 685 | ||
| 728 | /// Offer the outbound queue to the ring again. Called after every write | 686 | /// Offer the outbound queue to the ring again. Called after every write |
| 729 | /// and on every service pass, because the room to accept comes from | 687 | /// and on every service pass, because the room to accept comes from |
| 730 | /// acknowledgements, which arrive on their own schedule. | 688 | /// acknowledgements, which arrive on their own schedule. |
| 731 | pub fn flushQuic(self: *Transport) void { | 689 | pub fn flushQuic(self: *Transport) void { |
| 732 | const cl = switch (self.link) { | 690 | self.link.flushQuic(); |
| 733 | .quic => |cl| cl, | ||
| 734 | .fd, .pipe => return, | ||
| 735 | }; | ||
| 736 | if (self.qout.items.len == 0) return; | ||
| 737 | const n = cl.send(self.qout.items); | ||
| 738 | if (n == 0) return; | ||
| 739 | self.qout.replaceRangeAssumeCapacity(0, n, &.{}); | ||
| 740 | } | 691 | } |
| 741 | 692 | ||
| 742 | /// The next whole frame, if there is one. See `Incoming` for why a | 693 | /// The next whole frame, if there is one. See `Incoming` for why a |
| 743 | /// missing frame is not automatically a dead transport. | 694 | /// missing frame is not automatically a dead transport, and |
| 695 | /// `link.Link.readFrame` for the two faults that stay errors. | ||
| 744 | pub fn readFrame(self: *Transport, alloc: std.mem.Allocator) !Incoming { | 696 | pub fn readFrame(self: *Transport, alloc: std.mem.Allocator) !Incoming { |
| 745 | switch (self.link) { | 697 | return self.link.readFrame(alloc); |
| 746 | .quic => |cl| { | ||
| 747 | // Death is checked after the pump, so bytes that arrived in the | ||
| 748 | // same pass as the close are still delivered before the tear. | ||
| 749 | const got = proto.takeFrame(alloc, &cl.in) catch |err| switch (err) { | ||
| 750 | // Not a transport event, and it stays loud. | ||
| 751 | error.OutOfMemory => return err, | ||
| 752 | else => return .closed, | ||
| 753 | }; | ||
| 754 | if (got) |frame| return .{ .frame = frame }; | ||
| 755 | return if (cl.dead) .closed else .incomplete; | ||
| 756 | }, | ||
| 757 | .fd, .pipe => { | ||
| 758 | const frame = (proto.readFrame(alloc, self.conn.r) catch |err| switch (err) { | ||
| 759 | // Not a transport event, and it stays loud. | ||
| 760 | error.OutOfMemory => return err, | ||
| 761 | else => return .closed, | ||
| 762 | }) orelse return .closed; | ||
| 763 | return .{ .frame = frame }; | ||
| 764 | }, | ||
| 765 | } | ||
| 766 | } | 698 | } |
| 767 | 699 | ||
| 768 | /// Idempotent, and it has to be: a re-dial releases the dead transport on | 700 | /// Idempotent, and it has to be: a re-dial releases the dead transport on |
| 769 | /// entry, and an abort then closes the same value again through the pump's | 701 | /// entry, and an abort then closes the same value again through the pump's |
| 770 | /// `defer`. A second `close(2)` on a stale fd is EBADF, which `std.posix` | 702 | /// `defer`. A second `close(2)` on a stale fd is EBADF, which `std.posix` |
| 771 | /// maps to `unreachable` — a panic that `--sock` does not hide. | 703 | /// maps to `unreachable` — a panic that `--sock` does not hide. |
| 704 | /// | ||
| 705 | /// Only the stderr fd is closed here: it is the one thing the link never | ||
| 706 | /// knew about, and `openHandoff` took it off the Child precisely so the | ||
| 707 | /// kill inside `Link.close` would leave it alone. | ||
| 772 | pub fn close(self: *Transport) void { | 708 | pub fn close(self: *Transport) void { |
| 773 | if (self.conn.r == -1) return; // already released | 709 | if (self.link == .fd and self.link.fd == -1) return; // already released |
| 774 | defer self.conn = .{ .r = -1, .w = -1 }; | 710 | if (self.err_fd >= 0) { |
| 775 | // The link is reset to `.fd` with the fds, so nothing that outlives | 711 | std.posix.close(self.err_fd); |
| 776 | // this call holds a freed client or a reaped child behind a tag that | 712 | self.err_fd = -1; |
| 777 | // says otherwise. | ||
| 778 | defer self.link = .fd; | ||
| 779 | switch (self.link) { | ||
| 780 | .quic => |cl| { | ||
| 781 | self.qout.deinit(self.alloc); | ||
| 782 | self.qout = .empty; | ||
| 783 | // Closes the UDP socket with it, so the fd this returns | ||
| 784 | // through `conn.r` must not be closed again. | ||
| 785 | cl.deinit(); | ||
| 786 | }, | ||
| 787 | .pipe => |*c| { | ||
| 788 | // Close stdin first so the command sees EOF and can wind down | ||
| 789 | // its remote end cleanly; then TERM it. kill() waitpid()s | ||
| 790 | // internally, so this also reaps — no zombie is left behind. | ||
| 791 | if (c.stdin) |*in| { | ||
| 792 | in.close(); | ||
| 793 | c.stdin = null; | ||
| 794 | } | ||
| 795 | // Ours, not the child's: `openHandoff` took stderr off the | ||
| 796 | // Child so `kill` would leave it alone, so nothing else | ||
| 797 | // will close it. | ||
| 798 | if (self.err_fd >= 0) { | ||
| 799 | std.posix.close(self.err_fd); | ||
| 800 | self.err_fd = -1; | ||
| 801 | } | ||
| 802 | _ = c.kill() catch {}; | ||
| 803 | }, | ||
| 804 | .fd => std.posix.close(self.conn.r), | ||
| 805 | } | 713 | } |
| 714 | self.link.close(); | ||
| 806 | } | 715 | } |
| 807 | }; | 716 | }; |
| 808 | 717 | ||
| @@ -1173,32 +1082,53 @@ pub const birth_rows: u16 = 24; | |||
| 1173 | // the tile reads [refused], which is where it stood before. | 1082 | // the tile reads [refused], which is where it stood before. |
| 1174 | const birth_budget_ms: i64 = 3000; | 1083 | const birth_budget_ms: i64 = 3000; |
| 1175 | 1084 | ||
| 1176 | /// One bounded wait on a side connection: false means loop again without | 1085 | /// A caller that named more than one acceptable answer, waiting on a |
| 1177 | /// reading, `error.Timeout` means the budget is spent. No EINTR arm — | 1086 | /// primitive that takes exactly one. `Link.awaitFrame` waits for `want[0]` |
| 1178 | /// `std.posix.poll` retries INTR itself, and its error set holds only failures | 1087 | /// and hands every other frame to this sink, which recognises the rest and |
| 1179 | /// the fd never recovers from. | 1088 | /// ends the wait by erroring — the escape the sink contract exists for. |
| 1180 | fn awaitFrames(tr: *Transport, deadline: i64) !bool { | 1089 | /// The frame is COPIED because awaitFrame frees what it lends the sink. |
| 1181 | const left = deadline - std.time.milliTimestamp(); | 1090 | const AltWant = struct { |
| 1182 | if (left <= 0) return error.Timeout; | 1091 | alloc: std.mem.Allocator, |
| 1183 | var fds = [_]std.posix.pollfd{ | 1092 | /// `want[1..]`: the types awaitFrame is not itself watching for. |
| 1184 | .{ .fd = tr.pollFd(), .events = std.posix.POLL.IN, .revents = 0 }, | 1093 | rest: []const proto.MsgType, |
| 1185 | }; | 1094 | got: ?proto.Frame = null, |
| 1186 | _ = std.posix.poll(&fds, tr.timeoutMs(@intCast(@min(left, 100)))) catch | 1095 | |
| 1187 | return error.Transport; | 1096 | /// Never leaves `roundTrip`, which turns it back into the frame. |
| 1188 | tr.service(); | 1097 | const Answer = error.AltAnswer; |
| 1189 | // Over QUIC a frame can arrive with the socket never going readable. | 1098 | |
| 1190 | return fds[0].revents != 0 or tr.link == .quic; | 1099 | fn on(ctx: ?*anyopaque, frame: proto.Frame) anyerror!void { |
| 1191 | } | 1100 | const self: *AltWant = @ptrCast(@alignCast(ctx.?)); |
| 1101 | for (self.rest) |w| { | ||
| 1102 | if (frame.type != w) continue; | ||
| 1103 | self.got = .{ | ||
| 1104 | .type = frame.type, | ||
| 1105 | .payload = try self.alloc.dupe(u8, frame.payload), | ||
| 1106 | }; | ||
| 1107 | return Answer; | ||
| 1108 | } | ||
| 1109 | // Narration the caller never asked for; awaitFrame frees it. | ||
| 1110 | } | ||
| 1111 | }; | ||
| 1192 | 1112 | ||
| 1193 | /// One question on a side connection: send `req`, then hand back the first | 1113 | /// One question on a side connection: send `req`, then hand back the first |
| 1194 | /// frame whose type is one the caller named, deinit-ing every other frame | 1114 | /// frame whose type is one the caller named, deinit-ing every other frame |
| 1195 | /// the daemon says on the way. The frame returned is the caller's to deinit. | 1115 | /// the daemon says on the way. The frame returned is the caller's to deinit. |
| 1196 | /// | 1116 | /// |
| 1197 | /// The errors are thin on purpose — `Timeout` for a budget spent, `Closed` | 1117 | /// The errors are thin on purpose — `Timeout` for a budget spent, `Closed` |
| 1198 | /// for a peer that hung up before answering, `Transport` for a poll that | 1118 | /// for a wait that ended without an answer, `FrameTooLarge` for a peer that |
| 1199 | /// failed, `OutOfMemory` for this machine's allocator. What one MEANS is | 1119 | /// spoke and got the framing wrong, `OutOfMemory` for this machine's |
| 1200 | /// the caller's to say: the same close is a refusal to a birth and a box | 1120 | /// allocator. What one MEANS is the caller's to say: the same close is a |
| 1201 | /// that is down to a poll. | 1121 | /// refusal to a birth and a box that is down to a poll. `FrameTooLarge` is |
| 1122 | /// deliberately NOT folded into `Closed` — a daemon whose reply cannot be | ||
| 1123 | /// framed is a different fact from one that is not there, and a birth | ||
| 1124 | /// reports the second as a refusal. | ||
| 1125 | /// | ||
| 1126 | /// `Closed` is also what a failed `poll(2)` inside the wait becomes, since | ||
| 1127 | /// `Link.awaitFrame` owns the poll and has one word for "the wait cannot go | ||
| 1128 | /// on". That is a widening: this used to answer `Transport` there. The two | ||
| 1129 | /// failures poll can report are out of kernel memory and a bug, so the | ||
| 1130 | /// mislabel costs a "refused" instead of a "transport error" in a case | ||
| 1131 | /// where nothing on the box is working anyway. | ||
| 1202 | fn roundTrip( | 1132 | fn roundTrip( |
| 1203 | tr: *Transport, | 1133 | tr: *Transport, |
| 1204 | alloc: std.mem.Allocator, | 1134 | alloc: std.mem.Allocator, |
| @@ -1207,25 +1137,30 @@ fn roundTrip( | |||
| 1207 | want: []const proto.MsgType, | 1137 | want: []const proto.MsgType, |
| 1208 | deadline: i64, | 1138 | deadline: i64, |
| 1209 | ) !proto.Frame { | 1139 | ) !proto.Frame { |
| 1140 | std.debug.assert(want.len != 0); // a question with no acceptable answer | ||
| 1210 | try tr.writeFrame(req, payload); | 1141 | try tr.writeFrame(req, payload); |
| 1211 | while (true) { | 1142 | const left = deadline - std.time.milliTimestamp(); |
| 1212 | if (!try awaitFrames(tr, deadline)) continue; | 1143 | if (left <= 0) return error.Timeout; |
| 1213 | while (true) { | 1144 | var alt: AltWant = .{ .alloc = alloc, .rest = want[1..] }; |
| 1214 | // `readFrame` turns every wire fault into `.closed`, so the one | 1145 | // The wait takes milliseconds as a u32. Every budget in the tree is |
| 1215 | // error it can still raise is this machine's own allocator. | 1146 | // seconds at most, and clamping is what a caller that passed more would |
| 1216 | switch (try tr.readFrame(alloc)) { | 1147 | // have meant anyway — a cast would panic on it. |
| 1217 | .incomplete => break, | 1148 | const ms: u32 = @intCast(@min(left, std.math.maxInt(u32))); |
| 1218 | .closed => return error.Closed, | 1149 | const f = tr.link.awaitFrame(alloc, want[0], ms, .{ |
| 1219 | .frame => |f| { | 1150 | .ctx = &alt, |
| 1220 | for (want) |w| if (f.type == w) return f; | 1151 | .on = AltWant.on, |
| 1221 | f.deinit(alloc); | 1152 | }) catch |e| switch (e) { |
| 1222 | }, | 1153 | AltWant.Answer => return alt.got.?, |
| 1223 | } | 1154 | error.Closed => return error.Closed, |
| 1224 | // One readable event is one frame on a socket link, and a | 1155 | error.OutOfMemory => return error.OutOfMemory, |
| 1225 | // second read would block until the daemon spoke again. | 1156 | error.FrameTooLarge => return error.FrameTooLarge, |
| 1226 | if (tr.link != .quic) break; | 1157 | // Nothing reaches here today — awaitFrame raises only the three |
| 1227 | } | 1158 | // above and whatever the sink returns — but the arm keeps this |
| 1228 | } | 1159 | // function's error set the thin one its callers switch on rather |
| 1160 | // than letting the sink's `anyerror` widen it. | ||
| 1161 | else => return error.Transport, | ||
| 1162 | }; | ||
| 1163 | return f orelse error.Timeout; | ||
| 1229 | } | 1164 | } |
| 1230 | 1165 | ||
| 1231 | /// Creates `name` on `target` over a connection of its own, then leaves. | 1166 | /// Creates `name` on `target` over a connection of its own, then leaves. |
| @@ -1587,7 +1522,8 @@ test "Transport.close is idempotent: the abort path closes what a re-dial alread | |||
| 1587 | transport.close(); | 1522 | transport.close(); |
| 1588 | transport.close(); | 1523 | transport.close(); |
| 1589 | transport.close(); | 1524 | transport.close(); |
| 1590 | try std.testing.expectEqual(@as(std.posix.fd_t, -1), transport.conn.r); | 1525 | try std.testing.expect(transport.link == .fd); |
| 1526 | try std.testing.expectEqual(@as(std.posix.fd_t, -1), transport.link.fd); | ||
| 1591 | } | 1527 | } |
| 1592 | 1528 | ||
| 1593 | test "connectAgent: a live socket connects, a dead path returns null" { | 1529 | test "connectAgent: a live socket connects, a dead path returns null" { |
| @@ -1647,7 +1583,7 @@ test "Transport.open: a --via target yields a pipe, a --sock target an fd" { | |||
| 1647 | 1583 | ||
| 1648 | // Read before the close, not after: kill() reaps, and the child value | 1584 | // Read before the close, not after: kill() reaps, and the child value |
| 1649 | // goes with the link when close resets it. | 1585 | // goes with the link when close resets it. |
| 1650 | const pid = v.link.pipe.id; | 1586 | const pid = v.link.pipe.child.id; |
| 1651 | v.close(); | 1587 | v.close(); |
| 1652 | // Observed, not assumed: close() owns the reaping (Child.kill waitpid()s | 1588 | // Observed, not assumed: close() owns the reaping (Child.kill waitpid()s |
| 1653 | // internally), so the pid must be gone rather than a zombie — signal 0 | 1589 | // internally), so the pid must be gone rather than a zombie — signal 0 |
| @@ -1717,7 +1653,7 @@ test "--via: the words reach the program verbatim — no shell splits, expands o | |||
| 1717 | var buf: [512]u8 = undefined; | 1653 | var buf: [512]u8 = undefined; |
| 1718 | var got: usize = 0; | 1654 | var got: usize = 0; |
| 1719 | while (true) { | 1655 | while (true) { |
| 1720 | const n = std.posix.read(v.conn.r, buf[got..]) catch 0; | 1656 | const n = std.posix.read(v.link.pipe.r, buf[got..]) catch 0; |
| 1721 | if (n == 0) break; | 1657 | if (n == 0) break; |
| 1722 | got += n; | 1658 | got += n; |
| 1723 | } | 1659 | } |
src/link.zig
| Old | New | ||
|---|---|---|---|
| @@ -107,7 +107,7 @@ pub const Link = union(enum) { | |||
| 107 | /// Offer the outbound queue to the ring again. After every write and on | 107 | /// Offer the outbound queue to the ring again. After every write and on |
| 108 | /// every service pass, because the room to accept comes from | 108 | /// every service pass, because the room to accept comes from |
| 109 | /// acknowledgements, which arrive on their own schedule. | 109 | /// acknowledgements, which arrive on their own schedule. |
| 110 | fn flushQuic(self: *Link) void { | 110 | pub fn flushQuic(self: *Link) void { |
| 111 | const q = switch (self.*) { | 111 | const q = switch (self.*) { |
| 112 | .quic => |*q| q, | 112 | .quic => |*q| q, |
| 113 | .fd, .pipe => return, | 113 | .fd, .pipe => return, |
src/tui/wall_test_pump.zig
| Old | New | ||
|---|---|---|---|
| @@ -304,13 +304,14 @@ test "agent channels: slots fill in order, a full table refuses, and a close is | |||
| 304 | try std.testing.expectEqual(@as(?usize, 1), wall_pump.findLocal(&locals, 11)); | 304 | try std.testing.expectEqual(@as(?usize, 1), wall_pump.findLocal(&locals, 11)); |
| 305 | try std.testing.expectEqual(@as(?usize, null), wall_pump.findLocal(&locals, 99)); | 305 | try std.testing.expectEqual(@as(?usize, null), wall_pump.findLocal(&locals, 99)); |
| 306 | 306 | ||
| 307 | // A pipe for the link, which is what a `.fd` transport writes to. The | 307 | // A pipe for the link: the transport holds the WRITE end, since these |
| 308 | // frame that comes back out of it is the daemon's only notice that this | 308 | // tests only ever send, and reads the pipe themselves. The frame that |
| 309 | // end hung up on the channel. | 309 | // comes back out of it is the daemon's only notice that this end hung |
| 310 | // up on the channel. | ||
| 310 | const link = try std.posix.pipe2(.{ .NONBLOCK = true }); | 311 | const link = try std.posix.pipe2(.{ .NONBLOCK = true }); |
| 311 | defer std.posix.close(link[0]); | 312 | defer std.posix.close(link[0]); |
| 312 | defer std.posix.close(link[1]); | 313 | defer std.posix.close(link[1]); |
| 313 | var transport: client.Transport = .{ .conn = .{ .r = link[0], .w = link[1] }, .link = .fd }; | 314 | var transport: client.Transport = .{ .link = .{ .fd = link[1] } }; |
| 314 | 315 | ||
| 315 | wall_pump.closeLocal(&locals, 1, &transport); | 316 | wall_pump.closeLocal(&locals, 1, &transport); |
| 316 | try std.testing.expectEqual(@as(?AgentLocal, null), locals[1]); | 317 | try std.testing.expectEqual(@as(?AgentLocal, null), locals[1]); |
| @@ -339,7 +340,7 @@ test "agent channels: an oversize frame hangs the channel up, a full one lands" | |||
| 339 | const link = try std.posix.pipe2(.{ .NONBLOCK = true }); | 340 | const link = try std.posix.pipe2(.{ .NONBLOCK = true }); |
| 340 | defer std.posix.close(link[0]); | 341 | defer std.posix.close(link[0]); |
| 341 | defer std.posix.close(link[1]); | 342 | defer std.posix.close(link[1]); |
| 342 | var transport: client.Transport = .{ .conn = .{ .r = link[0], .w = link[1] }, .link = .fd }; | 343 | var transport: client.Transport = .{ .link = .{ .fd = link[1] } }; |
| 343 | 344 | ||
| 344 | // The ordinary case first, so the refusal below is a refusal and not a | 345 | // The ordinary case first, so the refusal below is a refusal and not a |
| 345 | // delivery path that never worked. | 346 | // delivery path that never worked. |
| @@ -412,7 +413,7 @@ test "agent forwarding is per tile: no -A offers nothing and opens nothing" { | |||
| 412 | const link = try std.posix.pipe2(.{ .NONBLOCK = true }); | 413 | const link = try std.posix.pipe2(.{ .NONBLOCK = true }); |
| 413 | defer std.posix.close(link[0]); | 414 | defer std.posix.close(link[0]); |
| 414 | defer std.posix.close(link[1]); | 415 | defer std.posix.close(link[1]); |
| 415 | var transport: client.Transport = .{ .conn = .{ .r = link[0], .w = link[1] }, .link = .fd }; | 416 | var transport: client.Transport = .{ .link = .{ .fd = link[1] } }; |
| 416 | 417 | ||
| 417 | try wall_pump.sendAttach(&t, &transport, 0, 0); | 418 | try wall_pump.sendAttach(&t, &transport, 0, 0); |
| 418 | const first = (try proto.readFrame(std.testing.allocator, link[0])) orelse | 419 | const first = (try proto.readFrame(std.testing.allocator, link[0])) orelse |
| @@ -457,7 +458,7 @@ test "a view tile's attach makes no size claim; a tile the user asked for does" | |||
| 457 | const link = try std.posix.pipe2(.{ .NONBLOCK = true }); | 458 | const link = try std.posix.pipe2(.{ .NONBLOCK = true }); |
| 458 | defer std.posix.close(link[0]); | 459 | defer std.posix.close(link[0]); |
| 459 | defer std.posix.close(link[1]); | 460 | defer std.posix.close(link[1]); |
| 460 | var transport: client.Transport = .{ .conn = .{ .r = link[0], .w = link[1] }, .link = .fd }; | 461 | var transport: client.Transport = .{ .link = .{ .fd = link[1] } }; |
| 461 | 462 | ||
| 462 | // A view tile: 0x0 on the wire, and the rect owed to the doorbell. | 463 | // A view tile: 0x0 on the wire, and the rect owed to the doorbell. |
| 463 | t.creates = false; | 464 | t.creates = false; |
| @@ -509,7 +510,7 @@ test "a newborn tile's first claim is its real stripe, not the placeholder" { | |||
| 509 | const link = try std.posix.pipe2(.{ .NONBLOCK = true }); | 510 | const link = try std.posix.pipe2(.{ .NONBLOCK = true }); |
| 510 | defer std.posix.close(link[0]); | 511 | defer std.posix.close(link[0]); |
| 511 | defer std.posix.close(link[1]); | 512 | defer std.posix.close(link[1]); |
| 512 | var transport: client.Transport = .{ .conn = .{ .r = link[0], .w = link[1] }, .link = .fd }; | 513 | var transport: client.Transport = .{ .link = .{ .fd = link[1] } }; |
| 513 | try wall_pump.sendAttach(&t, &transport, 0, 0); | 514 | try wall_pump.sendAttach(&t, &transport, 0, 0); |
| 514 | const fr = (try proto.readFrame(std.testing.allocator, link[0])) orelse | 515 | const fr = (try proto.readFrame(std.testing.allocator, link[0])) orelse |
| 515 | return error.NoAttach; | 516 | return error.NoAttach; |