summaryrefslogtreecommitdiff
path: root/src/detached/client.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/detached/client.zig')
-rw-r--r--src/detached/client.zig85
1 files changed, 76 insertions, 9 deletions
diff --git a/src/detached/client.zig b/src/detached/client.zig
index fc39164b..f317b2e2 100644
--- a/src/detached/client.zig
+++ b/src/detached/client.zig
@@ -156,7 +156,10 @@ pub const Client = struct {
server.setNonblock(fd);
var c: Client = .{ .gpa = gpa, .fd = fd };
errdefer c.deinit();
- try c.send(.{ .hello = .{ .cols = cols, .rows = rows } });
+ try c.send(.{ .hello = .{
+ .cols = @min(cols, wire.max_cols),
+ .rows = @min(rows, wire.max_rows),
+ } });
return c;
}
@@ -214,8 +217,33 @@ pub const Client = struct {
/// Tell the session this frontend's window changed. Not a promise about the
/// next frame: with other frontends attached the session grid is the
/// smallest common one.
+ ///
+ /// Clamped, like the hello in `open`: a window past `max_cols`/`max_rows`
+ /// is a geometry the protocol cannot carry, and the decoder on the far end
+ /// answers one with `BadValue` — which `apply` turns into `close(.protocol)`
+ /// with no `refuse` behind it, so the frontend was told only that the
+ /// session "hung up on the connect". A 4K display at a small font is
+ /// already past `max_rows`, which made an ordinary big screen unable to
+ /// attach at all. Asking for the largest grid the wire carries is what this
+ /// file's GEOMETRY note already promises a frontend gets: the session is
+ /// drawn at ITS size wherever the window is bigger, exactly as it is when
+ /// another frontend is the smaller one.
+ ///
+ /// A ZERO is dropped rather than clamped, and that asymmetry is the whole
+ /// point of `getCols` refusing zero in the first place: the session grid is
+ /// the smallest common one, so a frontend that reported 1 would collapse
+ /// every other frontend to a single cell. `TIOCGWINSZ` answers 0x0 while a
+ /// terminal is being torn down and tty.zig forwards a `winsize` verbatim
+ /// (its ATTACH path already refuses a zero one, its resize path did not),
+ /// so this is reachable without a hostile peer — and it reached the session
+ /// as the same mute `close(.protocol)` the oversize geometry did. Keeping
+ /// the last real window is what a momentary zero means.
pub fn resize(c: *Client, cols: u16, rows: u16) (Error || wire.Error)!void {
- return c.send(.{ .event = .{ .resize = .{ .cols = cols, .rows = rows } } });
+ if (cols == 0 or rows == 0) return;
+ return c.send(.{ .event = .{ .resize = .{
+ .cols = @min(cols, wire.max_cols),
+ .rows = @min(rows, wire.max_rows),
+ } } });
}
/// Wait up to `timeout_ms` for the session to say something, and push
@@ -663,10 +691,20 @@ const Harness = struct {
host.vtable.push_present.?(host.ctx, surface);
}
- /// Pump until this client has the message we are waiting for. Bounded, so a
- /// broken transport fails a test rather than hanging the suite.
+ /// The round budget every `pumpUntil*` below shares. A round moves at most
+ /// one socket buffer, because nothing here is concurrent: the session
+ /// flushes until `EAGAIN`, and only then does the client read. That buffer
+ /// is 8 KiB on darwin (`net.local.stream.sendspace`) against linux's 208
+ /// KiB, which is the same asymmetry the slow-frontend test at the bottom of
+ /// this file already had to say out loud — and a full frame of the largest
+ /// grid the protocol carries is near a megabyte, so 64 rounds is a linux-
+ /// only number. High enough for that frame on the smaller buffer, and still
+ /// a bound: a broken transport fails a test rather than hanging the suite.
+ const rounds = 256;
+
+ /// Pump until this client has the message we are waiting for.
fn pumpUntil(h: *Harness, c: *Client, comptime want: std.meta.Tag(wire.ServerMsg)) !wire.ServerMsg {
- for (0..64) |_| {
+ for (0..rounds) |_| {
try h.pump();
try c.wait(5);
while (try c.next()) |msg| if (std.meta.activeTag(msg) == want) return msg;
@@ -679,7 +717,7 @@ const Harness = struct {
/// input queued (a `Look` on a directory emits a spawn) lands a frame
/// later, and the frame in between legitimately says nothing.
fn pumpUntilChange(h: *Harness, c: *Client) !wire.Frame {
- for (0..64) |_| {
+ for (0..rounds) |_| {
const msg = try h.pumpUntil(c, .frame);
if (msg.frame.nruns > 0) return msg.frame;
}
@@ -691,7 +729,7 @@ const Harness = struct {
/// because a resize is announced when the session settles it, which may be
/// one empty frame after the pump that caused it.
fn pumpUntilGrid(h: *Harness, c: *Client, cols: u16, rows: u16) !void {
- for (0..64) |_| {
+ for (0..rounds) |_| {
try h.pump();
try c.wait(5);
while (try c.next()) |_| {}
@@ -706,7 +744,7 @@ const Harness = struct {
/// what a shared session promises is that they CONVERGE, which is what this
/// waits for.
fn pumpUntilSameScreen(h: *Harness, a: *Client, b: *Client) !void {
- for (0..64) |_| {
+ for (0..rounds) |_| {
try h.pump();
try a.wait(5);
try b.wait(5);
@@ -733,7 +771,7 @@ const Harness = struct {
/// a transport that really does drop one must fail as a wrong screen, not
/// as a timeout.
fn pumpUntilShowsCore(h: *Harness, c: *Client) !void {
- for (0..64) |_| {
+ for (0..rounds) |_| {
_ = h.arena.reset(.retain_capacity);
if (sameScreen((try h.core.render(h.arena.allocator())).cells, c.grid.items)) return;
try h.pump();
@@ -835,6 +873,35 @@ test "detached session: two frontends share one screen at the smallest common gr
try h.pumpUntilSameScreen(&a, &b);
}
+test "detached session: a window past the protocol attaches at the largest grid it carries" {
+ var h: Harness = undefined;
+ try h.init(80, 24);
+ defer h.deinit();
+
+ // A 4K display at a small font is already past `max_rows`, and until the
+ // clamp in `open` that hello was a geometry the session's decoder refused:
+ // `apply` answered `BadValue` with `close(.protocol)` and no `refuse`
+ // behind it, so a frontend on a big screen was told the session "hung up on
+ // the connect" — at a session with every slot free. It attaches now, at the
+ // biggest grid the wire has.
+ var c = try h.attach(wire.max_cols + 400, wire.max_rows + 70);
+ defer c.deinit();
+ try testing.expectEqual(wire.max_cols, c.cols);
+ try testing.expectEqual(wire.max_rows, c.rows);
+
+ // ...and the full frame that follows is the 65536-cell one whose single run
+ // does not fit a u16 count (wire.zig `run_max`), which is the half of this
+ // the clamp alone would have made universal rather than fixed.
+ const frame = (try h.pumpUntil(&c, .frame)).frame;
+ try testing.expectEqual(wire.FrameKind.full, frame.kind);
+ try testing.expectEqual(@as(usize, @as(usize, wire.max_cols) * wire.max_rows), c.grid.items.len);
+ // TWO runs and not one, which is the assertion that fails first if the
+ // encoder's bound goes: this grid's full frame is 65536 painted cells and
+ // a run counts to 65535.
+ try testing.expectEqual(@as(u32, 2), frame.nruns);
+ try h.pumpUntilShowsCore(&c);
+}
+
test "detached session: a frontend that dies takes nothing with it" {
var h: Harness = undefined;
try h.init(60, 16);