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.zig143
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