diff options
Diffstat (limited to 'src/detached/client.zig')
| -rw-r--r-- | src/detached/client.zig | 143 |
1 files changed, 88 insertions, 55 deletions
diff --git a/src/detached/client.zig b/src/detached/client.zig index f317b2e2..0350ecf7 100644 --- a/src/detached/client.zig +++ b/src/detached/client.zig @@ -40,7 +40,7 @@ //! DISPLAY: a daemon nobody is looking at has no clipboard and no browser. //! The answer to `read_clipboard` is not a reply message: it is an ordinary //! `Event.paste` sent back through `send`, which is the same asynchronous -//! shape `pull_read_clipboard` already has in-process. +//! shape `read_clipboard` already has in-process. //! * `refuse` is followed by the session closing the connection, and `quit` //! means the session itself has ended. //! The switch in `next` is exhaustive over that set on purpose: putting a @@ -481,26 +481,7 @@ pub fn resolve(buf: *[server.path_max]u8, requested: []const u8) Resolved { return .{ .name = buf[0..len] }; } -/// How long a frontend's attached loop waits on the socket before it goes back -/// to whatever else it owns. It lives HERE, beside the `wait` it parameterises, -/// because both frontends need it and both had defined it for themselves — -/// which is how a measured number drifts from the thing it was measured -/// against. -/// -/// A frontend cannot hand `poll(2)` one descriptor for the session and one for -/// its own input: vaxis delivers the terminal's events on a reader thread into -/// a mutex/condvar queue, and SDL has its own pump, so neither has a -/// descriptor. `wait` takes a timeout for exactly that reason. -/// -/// 8 ms is half a 60 Hz frame: a keystroke waits at most one of those before it -/// is on the wire (4 ms on average), and the frame it causes needs no wait at -/// all — it lands in the poll the moment the session writes it. The price is -/// 125 poll rounds a second on a frontend nobody is touching, measured below -/// the noise of what an idle pardes already costs: on an i7-11700 at 100 Hz -/// jiffies an idle attached frontend used 0.16% of one core over 60 s and 0.18% -/// over 120 s, against 0.11% and 0.31% for an idle in-process session on the -/// same screen over the same windows. Reach for an eventfd and a waker thread — -/// fuse.zig's `pollLoop` is the pattern — only if that stops being true. +// Input arrives through frontend queues, so attached clients bound socket waits to half a frame. pub const poll_ms: u32 = 8; /// One round of waiting for the greeting, and how many of them. The COUNT is @@ -541,22 +522,8 @@ pub const Attempt = union(enum) { lost: anyerror, }; -/// Resolve a name, connect to it, and WAIT FOR THE WELCOME. On every failure -/// path this closes whatever it opened, so a caller that gets anything but -/// `.greeted` has nothing to clean up. -/// -/// The waiting is the point, and it is why this function exists rather than -/// each frontend calling `resolve` and `open` in turn. `open` is not a -/// handshake — it connects and writes the hello, and the `welcome` or the -/// `refuse` arrives later through this loop. A frontend that treats a -/// successful `connect(2)` as proof of attachment will tear its local session -/// down — reap its pane shells, unmount its control filesystem, free every -/// undo history — and only then discover `refuse .version`, which is the -/// routine case: `zig build` replaces the binary under a running session, so -/// two protocol versions on one machine is expected rather than exotic. The -/// contract the `Attach` word owes is that a failed attach changes NOTHING, and -/// that contract can only be kept by a caller that has the welcome in hand -/// before it starts destroying things. +/// Wait for welcome before the caller replaces its session; connect alone can +/// still lead to a version refusal. Every non-greeted result owns no resources. pub fn attempt(gpa: std.mem.Allocator, requested: []const u8, cols: u16, rows: u16) Attempt { var buf: [server.path_max]u8 = undefined; const name = switch (resolve(&buf, requested)) { @@ -648,7 +615,16 @@ const Harness = struct { errdefer h.arena.deinit(); // `io` is not optional on `Session`: the daemon does the file watching // and the theme scan itself now, and both of those take a `std.Io`. - h.session = .{ .gpa = testing.allocator, .io = std.testing.io, .core = h.core, .cols = cols, .rows = rows }; + h.session = .{ + .gpa = testing.allocator, + .worker_gpa = testing.allocator, + .io = std.testing.io, + .core = h.core, + .cols = cols, + .rows = rows, + }; + try h.session.initAsync(); + errdefer h.session.deinit(); h.name = "s"; try testing.expect(h.session.listen(h.name)); // The pre-loop drain tty.zig has, for its reason: the startup spawns are @@ -683,12 +659,12 @@ const Harness = struct { /// for hosts whose worker threads post from off the loop. fn pump(h: *Harness) !void { const host = h.session.host(); - host.vtable.pull_wait_input.?(host.ctx, 20); + host.vtable.wait_input.?(host.ctx, 20); while (h.core.nextEffect()) |e| h.core.perform(e); if (h.core.quit) return; _ = h.arena.reset(.retain_capacity); const surface = try h.core.render(h.arena.allocator()); - host.vtable.push_present.?(host.ctx, surface); + host.vtable.present.?(host.ctx, surface); } /// The round budget every `pumpUntil*` below shares. A round moves at most @@ -833,6 +809,71 @@ test "detached session: input from a frontend reaches the core and comes back as try h.pumpUntilShowsCore(&c); } +test "detached Restore keeps attached frontends and follows queued frames with a full replacement" { + var h: Harness = undefined; + try h.init(60, 16); + defer h.deinit(); + _ = try h.core.setTestFile("saved body\n"); + var c = try h.attach(60, 16); + defer c.deinit(); + _ = try h.pumpUntil(&c, .frame); + try h.pumpUntilShowsCore(&c); + + const before = h.core; + const fd = h.session.clients[c.slot].fd; + try testing.expectError(error.BadDumpMagic, h.session.restore( + ".{ .magic = \"not-a-pardes-dump\", .theme = \"dark\", .screen = .{ .cols = 60, .rows = 16 } }", + )); + try testing.expectEqual(before, h.session.core); + try testing.expectEqual(fd, h.session.clients[c.slot].fd); + try testing.expect(h.session.clients[c.slot].attached); + + try h.core.dumpState(); + const saved = try testing.allocator.dupe(u8, h.core.dump_out.?); + defer testing.allocator.free(saved); + while (h.core.nextEffect()) |_| {} + _ = try h.core.setTestFile("changed after dump\n"); + try h.session.restore(saved); + h.core = h.session.core; + try testing.expect(h.core != before); + try testing.expectEqual(fd, h.session.clients[c.slot].fd); + try testing.expect(h.session.clients[c.slot].attached); + try testing.expectEqualStrings("saved body\n", h.core.panes[0].?.file.?.content); + try testing.expect(h.session.clients[c.slot].need_full); + var wake = [_]libc.pollfd{.{ .fd = h.session.mailbox.wake[0], .events = poll_in, .revents = 0 }}; + try testing.expectEqual(@as(c_int, 1), libc.poll(&wake, wake.len, 0)); + for (0..Harness.rounds) |_| { + if ((try h.pumpUntil(&c, .frame)).frame.kind == .full) break; + } else return error.NoFullReplacement; + try h.pumpUntilShowsCore(&c); +} + +test "detached selection pipe runs off the loop and returns through the attached frontend" { + var h: Harness = undefined; + try h.init(60, 16); + defer h.deinit(); + const pane = try h.core.setTestFile("one\ntwo\n"); + pane.cur_row = 0; + pane.cur_col = 2; + pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + var client = try h.attach(60, 16); + defer client.deinit(); + _ = try h.pumpUntil(&client, .frame); + + try client.send(.{ .event = .{ .key = .{ .cp = '|' } } }); + try client.send(.{ .event = .{ .key = .{ .cp = 't', .text = "tr a-z A-Z" } } }); + try client.send(.{ .event = .{ .key = .{ .cp = pardes.Key.enter } } }); + for (0..Harness.rounds) |_| { + try h.pump(); + try client.wait(5); + while (try client.next()) |_| {} + if (std.mem.eql(u8, pane.file.?.content, "ONE\ntwo\n")) break; + } else return error.PipeDidNotComplete; + try testing.expect(h.core.pipe_wait == null); + try testing.expectEqual(@as(usize, 0), h.session.pipe_tasks.len); + try h.pumpUntilShowsCore(&client); +} + test "detached session: two frontends share one screen at the smallest common grid" { var h: Harness = undefined; try h.init(80, 24); @@ -1031,26 +1072,21 @@ test "detached session: the seam's own routing rules, per surviving effect" { const host = h.session.host(); // BROADCAST: the yank register is a fact about the session, so every // display it is being watched on gets it. - host.vtable.push_set_clipboard.?(host.ctx, "yank"); + host.vtable.set_clipboard.?(host.ctx, "yank"); try expectBoth(&h, &a, &b, .set_clipboard); - // ORIGIN, ELSE PRIMARY. This is the "only one frontend is asked" rule that - // the shell-forking and file-writing messages used to demonstrate; the - // daemon does that work itself now, so the same claim is made about the two - // effects that still travel. `read_clipboard` is asked ONCE — two frontends - // answering would paste twice for one Ctrl-V, which is the rule host.zig - // states. + // Ask one frontend: two clipboard answers would paste twice. h.session.origin = 1; - host.vtable.pull_read_clipboard.?(host.ctx); + host.vtable.read_clipboard.?(host.ctx); try expectOnly(&h, &b, &a, .read_clipboard); // `open_link` follows the same origin: the browser that opens is the one on // the display of the human who clicked, not the oldest attachment's. - host.vtable.push_open_link.?(host.ctx, "https://x"); + host.vtable.open_link.?(host.ctx, "https://x"); try expectOnly(&h, &b, &a, .open_link); // ...and with no origin it falls back to the primary, which is what a link // opened by something other than a keystroke gets. h.session.origin = 0; - host.vtable.push_open_link.?(host.ctx, "https://y"); + host.vtable.open_link.?(host.ctx, "https://y"); try expectOnly(&h, &a, &b, .open_link); } @@ -1185,17 +1221,14 @@ test "detached session: a frontend that stops reading is dropped, not waited for } try testing.expect(h.session.clients[1].attached); - // Broadcast enough control traffic to pass `out_backlog`. A clipboard - // mirror is the honest vehicle: it is a real `push_` that reaches every - // frontend and carries the yank register, so this is a session yanking a - // lot rather than a synthetic poke. + // Clipboard updates broadcast to every frontend. const text = try testing.allocator.alloc(u8, 256 * 1024); defer testing.allocator.free(text); @memset(text, 'y'); const host = h.session.host(); for (0..24) |_| { if (!h.session.clients[1].attached) break; - host.vtable.push_set_clipboard.?(host.ctx, text); + host.vtable.set_clipboard.?(host.ctx, text); // Read `good` back to EMPTY before the next mirror, rather than // pumping once and taking whatever one write fitted. One pump moves at // most one socket buffer, and that buffer is 8 KiB here |
