a73x

c2004563

The harness owns a running daemon and a threaded await

a73x   2026-08-31 16:32

Commit message
The harness owns a running daemon and a threaded await

Two lifecycle primitives the daemon's tests were re-deriving by hand.

`TestDaemon` is the tmpdir + socket path + `Server` block that opened 142
of these tests, with the teardown order stated once instead of assembled
per test: the socket path and the shell string outlive `srv.deinit`
because `Server` keeps both slices, and a threaded daemon raises its stop
flag BEFORE the join. That last one was two `defer`s whose declaration
order was load-bearing and documented nowhere — reverse them and the test
hangs until the runner is killed, which is how it was watched failing
here. `open`/`start` is the two-phase form for the tests whose session
shell is a script they must first write into that directory; `shutdown`
is the daemon alone, for the tests that ask what the teardown left on
disk. 132 sites converted.

`awaitFrameOn` is `awaitFrame` for the 17 tests whose daemon runs on
`serverThread` and so cannot pump it a second time. It also fixes what
those loops spelled: `deadline_ms -|= 100` charges a full 100 ms per
`poll` even when the poll returned instantly because a frame was already
queued, so a "5000 ms" budget was spent in fifty near-instant turns. Every
such budget was a frame count wearing a millisecond's name, and a slow box
shortened it rather than giving it more time. The new budget is real
elapsed `milliTimestamp`. `server_test_session.zig`'s `awaitFrameThreaded`
was this primitive already and is gone.

Deliberately not converted, each for a reason:
  - the loops that COLLECT (sizes, modes, titles, payload bytes), COUNT
    occurrences over a settling window, wait on a replica-text condition,
    or assert on what arrives BEFORE the wanted frame — a generic awaiter
    discards those frames and would destroy the claim. This is most of
    them, `awaitMarkerWithoutSnapshot` and `drainHeld` included.
  - `readQueued` (await): a zero-ready poll returns null by design, so it
    reads only what is ALREADY queued rather than waiting.
  - `anyReturnWithin` (await), `awaitSnapshotSize` (attach): they match a
    decoded payload predicate, not a frame type.
  - the four `server_test_upgrade.zig` daemons whose fds and child pass to
    an `initFromManifest` successor: that Server is deliberately never
    deinit'd, which is the subject of those tests.
  - `server_test_attach.zig`'s daemon-restart test: two Servers share ONE
    socket path, and a TestDaemon names its own.
  - two `server_test_quic.zig` tests: they declare the listener's teardown
    BEFORE the server's so the listener outlives `srv.deinit`, and a
    fixture whose defer is first would flip that order.
  - `server_test_session.zig`'s two bind-failure tests: no daemon survives
    the call being tested.
  - `IntegratedSession` (await): heap-allocated for a stable address, with
    a planted HOME entangled with its `extra_env`, and its teardown is
    already one `deinit` — nothing re-derived to remove.

No assertion, timeout, session name or comment claim changed: 1056/1056
tests before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

src/server/server_test_agent.zig
Old New
@@ -2,7 +2,6 @@ const std = @import("std");
2 const proto = @import("term").protocol; 2 const proto = @import("term").protocol;
3 const quic = @import("quic"); 3 const quic = @import("quic");
4 const quic_server = @import("quic_server.zig"); 4 const quic_server = @import("quic_server.zig");
5 const TmpDir = @import("testtmp").TmpDir;
6 const h = @import("server_test_harness.zig"); 5 const h = @import("server_test_harness.zig");
7 const dial = h.dial; 6 const dial = h.dial;
8 const srv_mod = @import("server.zig"); 7 const srv_mod = @import("server.zig");
@@ -26,16 +25,11 @@ fn isSocketAt(path: [:0]const u8) bool {
26 test "Server: a session shell is born with a live SSH_AUTH_SOCK" { 25 test "Server: a session shell is born with a live SSH_AUTH_SOCK" {
27 const alloc = std.testing.allocator; 26 const alloc = std.testing.allocator;
28 27
29 var tmp = try TmpDir.make(); 28 var td = try h.TestDaemon.init(alloc, "agentenv", .{ .shell = "/bin/sh" });
30 defer tmp.cleanup(); 29 defer td.deinit();
31 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentenv.sock", .{tmp.path()});
32 defer alloc.free(sock_path);
33 30
34 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 31 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
35 defer srv.deinit(); 32 try std.testing.expect(td.srv.ses(0).agent_listener >= 0);
36
37 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket;
38 try std.testing.expect(srv.ses(0).agent_listener >= 0);
39 try std.testing.expect(isSocketAt(path)); 33 try std.testing.expect(isSocketAt(path));
40 34
41 // And the shell was TOLD, which is the half no field can answer: the 35 // And the shell was TOLD, which is the half no field can answer: the
@@ -50,41 +44,38 @@ test "Server: a session shell is born with a live SSH_AUTH_SOCK" {
50 .{std.fs.path.basename(path)}, 44 .{std.fs.path.basename(path)},
51 ); 45 );
52 defer alloc.free(cmd); 46 defer alloc.free(cmd);
53 try proto.writeAllFd(srv.ses(0).pty.master, cmd); 47 try proto.writeAllFd(td.srv.ses(0).pty.master, cmd);
54 // The typed line the pty echoes back carries AGENT""OK, so only the 48 // The typed line the pty echoes back carries AGENT""OK, so only the
55 // command's own output can satisfy this needle. 49 // command's own output can satisfy this needle.
56 try std.testing.expect(try awaitGridText(alloc, &srv, "AGENTOK", 5000)); 50 try std.testing.expect(try awaitGridText(alloc, &td.srv, "AGENTOK", 5000));
57 } 51 }
58 52
59 test "Server: a reaped session takes its agent socket with it" { 53 test "Server: a reaped session takes its agent socket with it" {
60 const alloc = std.testing.allocator; 54 const alloc = std.testing.allocator;
61 55
62 var tmp = try TmpDir.make(); 56 var td = try h.TestDaemon.open(alloc, "agentreap");
63 defer tmp.cleanup(); 57 defer td.deinit();
64 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentreap.sock", .{tmp.path()});
65 defer alloc.free(sock_path);
66 58
67 const script = try writeDyingGapShell(alloc, &tmp); 59 const script = try writeDyingGapShell(alloc, &td.tmp);
68 defer alloc.free(script); 60 defer alloc.free(script);
69 61
70 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 62 try td.start(.{ .shell = script });
71 defer srv.deinit();
72 63
73 // Copied, because the reap frees the session's own: the whole point is 64 // Copied, because the reap frees the session's own: the whole point is
74 // to ask about the path after the session that owned it is gone. 65 // to ask about the path after the session that owned it is gone.
75 const path = try alloc.dupeZ(u8, srv.ses(0).agent_path orelse return error.NoAgentSocket); 66 const path = try alloc.dupeZ(u8, td.srv.ses(0).agent_path orelse return error.NoAgentSocket);
76 defer alloc.free(path); 67 defer alloc.free(path);
77 try std.testing.expect(isSocketAt(path)); 68 try std.testing.expect(isSocketAt(path));
78 69
79 // The script reads twice before it exits; the first line is what makes 70 // The script reads twice before it exits; the first line is what makes
80 // the shell live and pumping before the second one kills it. 71 // the shell live and pumping before the second one kills it.
81 try proto.writeAllFd(srv.ses(0).pty.master, "go\n"); 72 try proto.writeAllFd(td.srv.ses(0).pty.master, "go\n");
82 try std.testing.expect(try awaitGridText(alloc, &srv, "after-osc", 3000)); 73 try std.testing.expect(try awaitGridText(alloc, &td.srv, "after-osc", 3000));
83 try proto.writeAllFd(srv.ses(0).pty.master, "die\n"); 74 try proto.writeAllFd(td.srv.ses(0).pty.master, "die\n");
84 var reaped = false; 75 var reaped = false;
85 for (0..600) |_| { 76 for (0..600) |_| {
86 srv.pumpOnce(5) catch break; 77 td.srv.pumpOnce(5) catch break;
87 if (srv.sessions.table[0] == null) { 78 if (td.srv.sessions.table[0] == null) {
88 reaped = true; 79 reaped = true;
89 break; 80 break;
90 } 81 }
@@ -99,27 +90,22 @@ test "Server: a reaped session takes its agent socket with it" {
99 test "Server: agent_offer flags the slot, and an unknown type leaves the client answering" { 90 test "Server: agent_offer flags the slot, and an unknown type leaves the client answering" {
100 const alloc = std.testing.allocator; 91 const alloc = std.testing.allocator;
101 92
102 var tmp = try TmpDir.make(); 93 var td = try h.TestDaemon.init(alloc, "offer", .{ .shell = "/bin/cat" });
103 defer tmp.cleanup(); 94 defer td.deinit();
104 const sock_path = try std.fmt.allocPrint(alloc, "{s}/offer.sock", .{tmp.path()});
105 defer alloc.free(sock_path);
106
107 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
108 defer srv.deinit();
109 95
110 const c = try dial.dialAttachNamed(sock_path, 80, 24, ""); 96 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "");
111 defer c.close(); 97 defer c.close();
112 var spun: usize = 0; 98 var spun: usize = 0;
113 while (spun < 200 and srv.clients[0] == null) : (spun += 1) try srv.pumpOnce(5); 99 while (spun < 200 and td.srv.clients[0] == null) : (spun += 1) try td.srv.pumpOnce(5);
114 try std.testing.expect(srv.clients[0] != null); 100 try std.testing.expect(td.srv.clients[0] != null);
115 // The offer is opt-in, so the flag must start false or every client 101 // The offer is opt-in, so the flag must start false or every client
116 // would look like it had offered. 102 // would look like it had offered.
117 try std.testing.expect(!srv.clients[0].?.agent_offer); 103 try std.testing.expect(!td.srv.clients[0].?.agent_offer);
118 104
119 try proto.writeFrame(c.handle, .agent_offer, ""); 105 try proto.writeFrame(c.handle, .agent_offer, "");
120 spun = 0; 106 spun = 0;
121 while (spun < 200 and !srv.clients[0].?.agent_offer) : (spun += 1) try srv.pumpOnce(5); 107 while (spun < 200 and !td.srv.clients[0].?.agent_offer) : (spun += 1) try td.srv.pumpOnce(5);
122 try std.testing.expect(srv.clients[0].?.agent_offer); 108 try std.testing.expect(td.srv.clients[0].?.agent_offer);
123 109
124 // 0x7e is unmapped in MsgType. Skipping it rather than dropping the 110 // 0x7e is unmapped in MsgType. Skipping it rather than dropping the
125 // client is what lets a client offering agent forwarding talk to a 111 // client is what lets a client offering agent forwarding talk to a
@@ -127,10 +113,10 @@ test "Server: agent_offer flags the slot, and an unknown type leaves the client
127 // the point: the client is still seated AND still answering. 113 // the point: the client is still seated AND still answering.
128 try proto.writeFrame(c.handle, @enumFromInt(0x7e), ""); 114 try proto.writeFrame(c.handle, @enumFromInt(0x7e), "");
129 try proto.writeFrame(c.handle, .stats_req, ""); 115 try proto.writeFrame(c.handle, .stats_req, "");
130 const reply = try awaitFrame(alloc, &srv, c.handle, .stats_reply, 400); 116 const reply = try awaitFrame(alloc, &td.srv, c.handle, .stats_reply, 400);
131 defer if (reply) |f| f.deinit(alloc); 117 defer if (reply) |f| f.deinit(alloc);
132 try std.testing.expect(reply != null); 118 try std.testing.expect(reply != null);
133 try std.testing.expect(srv.clients[0] != null); 119 try std.testing.expect(td.srv.clients[0] != null);
134 } 120 }
135 121
136 // --------------------------------------------------------------------------- 122 // ---------------------------------------------------------------------------
@@ -176,15 +162,13 @@ fn attachOffering(srv: *Server, fd: std.posix.fd_t, slot: usize, name: []const u
176 test "Server: a session with no agent socket does not inherit the daemon's" { 162 test "Server: a session with no agent socket does not inherit the daemon's" {
177 const alloc = std.testing.allocator; 163 const alloc = std.testing.allocator;
178 164
179 var tmp = try TmpDir.make(); 165 var td = try h.TestDaemon.open(alloc, "agentless");
180 defer tmp.cleanup(); 166 defer td.deinit();
181 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentless.sock", .{tmp.path()});
182 defer alloc.free(sock_path);
183 167
184 // The daemon is given an agent of its own, which is the whole hazard: 168 // The daemon is given an agent of its own, which is the whole hazard:
185 // the daemon is often started from a desktop session that has one, and every 169 // the daemon is often started from a desktop session that has one, and every
186 // session it spawns inherits whatever the daemon's own environment holds. 170 // session it spawns inherits whatever the daemon's own environment holds.
187 const inherited = try std.fmt.allocPrintSentinel(alloc, "{s}/daemons-own.sock", .{tmp.path()}, 0); 171 const inherited = try std.fmt.allocPrintSentinel(alloc, "{s}/daemons-own.sock", .{td.tmp.path()}, 0);
188 defer alloc.free(inherited); 172 defer alloc.free(inherited);
189 const libc = @cImport({ 173 const libc = @cImport({
190 @cInclude("stdlib.h"); 174 @cInclude("stdlib.h");
@@ -199,20 +183,19 @@ test "Server: a session with no agent socket does not inherit the daemon's" {
199 _ = libc.unsetenv(proto.agent_sock_env); 183 _ = libc.unsetenv(proto.agent_sock_env);
200 }; 184 };
201 185
202 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 186 try td.start(.{ .shell = "/bin/sh" });
203 defer srv.deinit();
204 187
205 // Every later failure of makeAgentDir looks exactly like this: no 188 // Every later failure of makeAgentDir looks exactly like this: no
206 // directory, so no session bound after it gets a socket. Reached by 189 // directory, so no session bound after it gets a socket. Reached by
207 // nulling the field the failure nulls rather than by a stand-in — and 190 // nulling the field the failure nulls rather than by a stand-in — and
208 // freeing what deinit can no longer see, which the real failure never 191 // freeing what deinit can no longer see, which the real failure never
209 // allocated in the first place. 192 // allocated in the first place.
210 if (srv.agents.dir) |d| alloc.free(d); 193 if (td.srv.agents.dir) |d| alloc.free(d);
211 srv.agents.dir = null; 194 td.srv.agents.dir = null;
212 195
213 const probe = try std.fmt.allocPrint(alloc, "{s}/probe", .{tmp.path()}); 196 const probe = try std.fmt.allocPrint(alloc, "{s}/probe", .{td.tmp.path()});
214 defer alloc.free(probe); 197 defer alloc.free(probe);
215 const done = try std.fmt.allocPrint(alloc, "{s}/probe.done", .{tmp.path()}); 198 const done = try std.fmt.allocPrint(alloc, "{s}/probe.done", .{td.tmp.path()});
216 defer alloc.free(done); 199 defer alloc.free(done);
217 const cmd = try std.fmt.allocPrint( 200 const cmd = try std.fmt.allocPrint(
218 alloc, 201 alloc,
@@ -221,17 +204,17 @@ test "Server: a session with no agent socket does not inherit the daemon's" {
221 ); 204 );
222 defer alloc.free(cmd); 205 defer alloc.free(cmd);
223 206
224 const c = try dial.dialAttachNamed(sock_path, 80, 24, "nosock"); 207 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "nosock");
225 defer c.close(); 208 defer c.close();
226 var spun: usize = 0; 209 var spun: usize = 0;
227 while (spun < 200 and srv.clients[0] == null) : (spun += 1) try srv.pumpOnce(5); 210 while (spun < 200 and td.srv.clients[0] == null) : (spun += 1) try td.srv.pumpOnce(5);
228 const si = srv.clients[0].?.session orelse return error.ClientNeverSeated; 211 const si = td.srv.clients[0].?.session orelse return error.ClientNeverSeated;
229 try std.testing.expect(srv.sessions.table[si].?.agent_path == null); 212 try std.testing.expect(td.srv.sessions.table[si].?.agent_path == null);
230 213
231 try proto.writeFrame(c.handle, .input, cmd); 214 try proto.writeFrame(c.handle, .input, cmd);
232 spun = 0; 215 spun = 0;
233 while (spun < 600) : (spun += 1) { 216 while (spun < 600) : (spun += 1) {
234 try srv.pumpOnce(5); 217 try td.srv.pumpOnce(5);
235 std.fs.accessAbsolute(done, .{}) catch continue; 218 std.fs.accessAbsolute(done, .{}) catch continue;
236 break; 219 break;
237 } 220 }
@@ -247,18 +230,13 @@ test "Server: a session with no agent socket does not inherit the daemon's" {
247 test "Server: a full channel table refuses the newest dial and says so once" { 230 test "Server: a full channel table refuses the newest dial and says so once" {
248 const alloc = std.testing.allocator; 231 const alloc = std.testing.allocator;
249 232
250 var tmp = try TmpDir.make(); 233 var td = try h.TestDaemon.init(alloc, "agentfull", .{ .shell = "/bin/cat" });
251 defer tmp.cleanup(); 234 defer td.deinit();
252 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentfull.sock", .{tmp.path()});
253 defer alloc.free(sock_path);
254 235
255 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 236 const c = try dial.dial(td.sock_path);
256 defer srv.deinit();
257
258 const c = try dial.dial(sock_path);
259 defer c.close(); 237 defer c.close();
260 try attachOffering(&srv, c.handle, 0, ""); 238 try attachOffering(&td.srv, c.handle, 0, "");
261 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 239 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
262 240
263 // Exactly the table, then one more. ssh opens a channel per auth attempt, 241 // Exactly the table, then one more. ssh opens a channel per auth attempt,
264 // so a long-lived agent connection holding a slot is the field failure 242 // so a long-lived agent connection holding a slot is the field failure
@@ -268,70 +246,65 @@ test "Server: a full channel table refuses the newest dial and says so once" {
268 defer for (dials) |d| d.close(); 246 defer for (dials) |d| d.close();
269 247
270 var spun: usize = 0; 248 var spun: usize = 0;
271 while (spun < 400 and srv.agents.refused_full == 0) : (spun += 1) try srv.pumpOnce(5); 249 while (spun < 400 and td.srv.agents.refused_full == 0) : (spun += 1) try td.srv.pumpOnce(5);
272 for (srv.agents.chans) |slot| try std.testing.expect(slot != null); 250 for (td.srv.agents.chans) |slot| try std.testing.expect(slot != null);
273 try std.testing.expectEqual(@as(u32, 1), srv.agents.refused_full); 251 try std.testing.expectEqual(@as(u32, 1), td.srv.agents.refused_full);
274 try std.testing.expectEqual(@as(u32, 0), srv.agents.refused_no_offer); 252 try std.testing.expectEqual(@as(u32, 0), td.srv.agents.refused_no_offer);
275 253
276 // Refused means closed, not accepted and left silent: ssh reads the 254 // Refused means closed, not accepted and left silent: ssh reads the
277 // hangup as "agent refused" and moves on, where silence costs it a 255 // hangup as "agent refused" and moves on, where silence costs it a
278 // timeout on every dial. 256 // timeout on every dial.
279 var buf: [8]u8 = undefined; 257 var buf: [8]u8 = undefined;
280 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&srv, dials[max_agent_chans].handle, &buf, 60)); 258 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, dials[max_agent_chans].handle, &buf, 60));
281 259
282 // Ids are unique across the live table, which is the only property both 260 // Ids are unique across the live table, which is the only property both
283 // ends rely on — and the counter's wrap must not break it. 261 // ends rely on — and the counter's wrap must not break it.
284 var seen: [max_agent_chans]u32 = undefined; 262 var seen: [max_agent_chans]u32 = undefined;
285 for (srv.agents.chans, 0..) |slot, i| seen[i] = slot.?.id; 263 for (td.srv.agents.chans, 0..) |slot, i| seen[i] = slot.?.id;
286 for (seen, 0..) |id, i| for (seen[i + 1 ..]) |other| try std.testing.expect(id != other); 264 for (seen, 0..) |id, i| for (seen[i + 1 ..]) |other| try std.testing.expect(id != other);
287 srv.agents.next_id = seen[3]; 265 td.srv.agents.next_id = seen[3];
288 try std.testing.expect(srv.agents.nextId() != seen[3]); 266 try std.testing.expect(td.srv.agents.nextId() != seen[3]);
289 267
290 // "Once", which is the half the name claims and nothing asserted: a 268 // "Once", which is the half the name claims and nothing asserted: a
291 // second dial against the same full table counts again and says nothing, 269 // second dial against the same full table counts again and says nothing,
292 // and only a freed slot re-arms the line. ssh retries, so a log that 270 // and only a freed slot re-arms the line. ssh retries, so a log that
293 // scrolled would be its own outage. 271 // scrolled would be its own outage.
294 try std.testing.expect(srv.agents.full_said); 272 try std.testing.expect(td.srv.agents.full_said);
295 const again = try dial.dial(path); 273 const again = try dial.dial(path);
296 defer again.close(); 274 defer again.close();
297 spun = 0; 275 spun = 0;
298 while (spun < 400 and srv.agents.refused_full < 2) : (spun += 1) try srv.pumpOnce(5); 276 while (spun < 400 and td.srv.agents.refused_full < 2) : (spun += 1) try td.srv.pumpOnce(5);
299 try std.testing.expectEqual(@as(u32, 2), srv.agents.refused_full); 277 try std.testing.expectEqual(@as(u32, 2), td.srv.agents.refused_full);
300 try std.testing.expect(srv.agents.full_said); 278 try std.testing.expect(td.srv.agents.full_said);
301 srv.agents.closeChan(&srv, 0, .notify); 279 td.srv.agents.closeChan(&td.srv, 0, .notify);
302 try std.testing.expect(!srv.agents.full_said); 280 try std.testing.expect(!td.srv.agents.full_said);
303 } 281 }
304 282
305 test "Server: an agent connection with nobody offering is refused fast" { 283 test "Server: an agent connection with nobody offering is refused fast" {
306 const alloc = std.testing.allocator; 284 const alloc = std.testing.allocator;
307 285
308 var tmp = try TmpDir.make(); 286 var td = try h.TestDaemon.init(alloc, "agentrefuse", .{ .shell = "/bin/cat" });
309 defer tmp.cleanup(); 287 defer td.deinit();
310 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentrefuse.sock", .{tmp.path()});
311 defer alloc.free(sock_path);
312
313 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
314 defer srv.deinit();
315 288
316 const c = try dial.dialAttachNamed(sock_path, 80, 24, ""); 289 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "");
317 defer c.close(); 290 defer c.close();
318 var spun: usize = 0; 291 var spun: usize = 0;
319 while (spun < 200 and srv.clients[0] == null) : (spun += 1) try srv.pumpOnce(5); 292 while (spun < 200 and td.srv.clients[0] == null) : (spun += 1) try td.srv.pumpOnce(5);
320 try std.testing.expect(srv.clients[0] != null); 293 try std.testing.expect(td.srv.clients[0] != null);
321 // Attached but never offered — the case this test is about. The socket 294 // Attached but never offered — the case this test is about. The socket
322 // exists for as long as the session does; only the answer comes and goes. 295 // exists for as long as the session does; only the answer comes and goes.
323 try std.testing.expect(!srv.clients[0].?.agent_offer); 296 try std.testing.expect(!td.srv.clients[0].?.agent_offer);
324 297
325 // An offerer, but watching a DIFFERENT session. A socket per session 298 // An offerer, but watching a DIFFERENT session. A socket per session
326 // exists so a dial can be attributed to one shell; an answerer taken 299 // exists so a dial can be attributed to one shell; an answerer taken
327 // from the daemon at large would hand this session's ssh the key of 300 // from the daemon at large would hand this session's ssh the key of
328 // somebody sitting in front of another one. 301 // somebody sitting in front of another one.
329 const other = try dial.dial(sock_path); 302 const other = try dial.dial(td.sock_path);
330 defer other.close(); 303 defer other.close();
331 try attachOffering(&srv, other.handle, 1, "b"); 304 try attachOffering(&td.srv, other.handle, 1, "b");
332 try std.testing.expect(srv.clients[1].?.session != srv.clients[0].?.session); 305 try std.testing.expect(td.srv.clients[1].?.session != td.srv.clients[0].?.session);
333 306
334 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 307 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
335 const agent = try dial.dial(path); 308 const agent = try dial.dial(path);
336 defer agent.close(); 309 defer agent.close();
337 310
@@ -340,33 +313,28 @@ test "Server: an agent connection with nobody offering is refused fast" {
340 // where a connection left open and silent would make it wait out a 313 // where a connection left open and silent would make it wait out a
341 // timeout on every dial instead. 314 // timeout on every dial instead.
342 var buf: [16]u8 = undefined; 315 var buf: [16]u8 = undefined;
343 const n = try pumpUntilReadable(&srv, agent.handle, &buf, 200); 316 const n = try pumpUntilReadable(&td.srv, agent.handle, &buf, 200);
344 try std.testing.expectEqual(@as(?usize, 0), n); 317 try std.testing.expectEqual(@as(?usize, 0), n);
345 } 318 }
346 319
347 test "Server: agent bytes pump both ways through a channel" { 320 test "Server: agent bytes pump both ways through a channel" {
348 const alloc = std.testing.allocator; 321 const alloc = std.testing.allocator;
349 322
350 var tmp = try TmpDir.make(); 323 var td = try h.TestDaemon.init(alloc, "agentpump", .{ .shell = "/bin/cat" });
351 defer tmp.cleanup(); 324 defer td.deinit();
352 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentpump.sock", .{tmp.path()});
353 defer alloc.free(sock_path);
354 325
355 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 326 const c = try dial.dial(td.sock_path);
356 defer srv.deinit();
357
358 const c = try dial.dial(sock_path);
359 defer c.close(); 327 defer c.close();
360 try attachOffering(&srv, c.handle, 0, ""); 328 try attachOffering(&td.srv, c.handle, 0, "");
361 329
362 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 330 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
363 const agent = try dial.dial(path); 331 const agent = try dial.dial(path);
364 defer agent.close(); 332 defer agent.close();
365 333
366 // The daemon allocates the id and announces the channel; the client 334 // The daemon allocates the id and announces the channel; the client
367 // never asks for one, because only the daemon knows a connection 335 // never asks for one, because only the daemon knows a connection
368 // happened. 336 // happened.
369 const open = (try awaitFrame(alloc, &srv, c.handle, .agent_open, 200)) orelse 337 const open = (try awaitFrame(alloc, &td.srv, c.handle, .agent_open, 200)) orelse
370 return error.NoAgentOpen; 338 return error.NoAgentOpen;
371 defer open.deinit(alloc); 339 defer open.deinit(alloc);
372 const id = try proto.decodeAgentId(open.payload); 340 const id = try proto.decodeAgentId(open.payload);
@@ -379,13 +347,13 @@ test "Server: agent bytes pump both ways through a channel" {
379 @memcpy(req[proto.agent_id_len..], "req"); 347 @memcpy(req[proto.agent_id_len..], "req");
380 try proto.writeFrame(c.handle, .agent_data, &req); 348 try proto.writeFrame(c.handle, .agent_data, &req);
381 var buf: [64]u8 = undefined; 349 var buf: [64]u8 = undefined;
382 const n = (try pumpUntilReadable(&srv, agent.handle, &buf, 200)) orelse 350 const n = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 200)) orelse
383 return error.NoAgentRequest; 351 return error.NoAgentRequest;
384 try std.testing.expectEqualSlices(u8, "req", buf[0..n]); 352 try std.testing.expectEqualSlices(u8, "req", buf[0..n]);
385 353
386 // Agent to client, on the same id. 354 // Agent to client, on the same id.
387 try proto.writeAllFd(agent.handle, "resp"); 355 try proto.writeAllFd(agent.handle, "resp");
388 const data = (try awaitFrame(alloc, &srv, c.handle, .agent_data, 200)) orelse 356 const data = (try awaitFrame(alloc, &td.srv, c.handle, .agent_data, 200)) orelse
389 return error.NoAgentReply; 357 return error.NoAgentReply;
390 defer data.deinit(alloc); 358 defer data.deinit(alloc);
391 try std.testing.expectEqual(id, try proto.decodeAgentId(data.payload)); 359 try std.testing.expectEqual(id, try proto.decodeAgentId(data.payload));
@@ -395,7 +363,7 @@ test "Server: agent bytes pump both ways through a channel" {
395 // only close of this descriptor. The daemon sees the same read of 0 it 363 // only close of this descriptor. The daemon sees the same read of 0 it
396 // would from an ssh that finished and exited. 364 // would from an ssh that finished and exited.
397 try std.posix.shutdown(agent.handle, .send); 365 try std.posix.shutdown(agent.handle, .send);
398 const closed = (try awaitFrame(alloc, &srv, c.handle, .agent_close, 200)) orelse 366 const closed = (try awaitFrame(alloc, &td.srv, c.handle, .agent_close, 200)) orelse
399 return error.NoAgentClose; 367 return error.NoAgentClose;
400 defer closed.deinit(alloc); 368 defer closed.deinit(alloc);
401 try std.testing.expectEqual(id, try proto.decodeAgentId(closed.payload)); 369 try std.testing.expectEqual(id, try proto.decodeAgentId(closed.payload));
@@ -404,22 +372,17 @@ test "Server: agent bytes pump both ways through a channel" {
404 test "Server: an agent connection is routed to the latest-active offerer" { 372 test "Server: an agent connection is routed to the latest-active offerer" {
405 const alloc = std.testing.allocator; 373 const alloc = std.testing.allocator;
406 374
407 var tmp = try TmpDir.make(); 375 var td = try h.TestDaemon.init(alloc, "agentroute", .{ .shell = "/bin/cat" });
408 defer tmp.cleanup(); 376 defer td.deinit();
409 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentroute.sock", .{tmp.path()});
410 defer alloc.free(sock_path);
411
412 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
413 defer srv.deinit();
414 377
415 const ca = try dial.dial(sock_path); 378 const ca = try dial.dial(td.sock_path);
416 defer ca.close(); 379 defer ca.close();
417 try attachOffering(&srv, ca.handle, 0, ""); 380 try attachOffering(&td.srv, ca.handle, 0, "");
418 const cb = try dial.dial(sock_path); 381 const cb = try dial.dial(td.sock_path);
419 defer cb.close(); 382 defer cb.close();
420 try attachOffering(&srv, cb.handle, 1, ""); 383 try attachOffering(&td.srv, cb.handle, 1, "");
421 384
422 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 385 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
423 386
424 // Two dials, with the lead changing hands in between, because ONE dial 387 // Two dials, with the lead changing hands in between, because ONE dial
425 // cannot tell the rule apart from the wrong ones: whoever offered first, 388 // cannot tell the rule apart from the wrong ones: whoever offered first,
@@ -428,21 +391,21 @@ test "Server: an agent connection is routed to the latest-active offerer" {
428 // pair of answers disagrees. 391 // pair of answers disagrees.
429 try proto.writeFrame(cb.handle, .input, "x"); 392 try proto.writeFrame(cb.handle, .input, "x");
430 var spun: usize = 0; 393 var spun: usize = 0;
431 while (spun < 200 and srv.clients[1].?.activity < srv.clients[0].?.activity) : (spun += 1) { 394 while (spun < 200 and td.srv.clients[1].?.activity < td.srv.clients[0].?.activity) : (spun += 1) {
432 try srv.pumpOnce(5); 395 try td.srv.pumpOnce(5);
433 } 396 }
434 try std.testing.expect(srv.clients[1].?.activity > srv.clients[0].?.activity); 397 try std.testing.expect(td.srv.clients[1].?.activity > td.srv.clients[0].?.activity);
435 398
436 const first = try dial.dial(path); 399 const first = try dial.dial(path);
437 defer first.close(); 400 defer first.close();
438 const to_b = (try awaitFrame(alloc, &srv, cb.handle, .agent_open, 200)) orelse 401 const to_b = (try awaitFrame(alloc, &td.srv, cb.handle, .agent_open, 200)) orelse
439 return error.NoAgentOpenForB; 402 return error.NoAgentOpenForB;
440 defer to_b.deinit(alloc); 403 defer to_b.deinit(alloc);
441 // And A is not told, which is the half that matters for keys: an 404 // And A is not told, which is the half that matters for keys: an
442 // announcement to every offerer would be every offerer's agent asked to 405 // announcement to every offerer would be every offerer's agent asked to
443 // sign. With the positive already asserted, a bounded null here means 406 // sign. With the positive already asserted, a bounded null here means
444 // the frame was routed, not merely slow. 407 // the frame was routed, not merely slow.
445 const stray_a = try awaitFrame(alloc, &srv, ca.handle, .agent_open, 60); 408 const stray_a = try awaitFrame(alloc, &td.srv, ca.handle, .agent_open, 60);
446 defer if (stray_a) |f| f.deinit(alloc); 409 defer if (stray_a) |f| f.deinit(alloc);
447 try std.testing.expect(stray_a == null); 410 try std.testing.expect(stray_a == null);
448 411
@@ -451,20 +414,20 @@ test "Server: an agent connection is routed to the latest-active offerer" {
451 // that would fail the signature rather than change identity. 414 // that would fail the signature rather than change identity.
452 try proto.writeFrame(ca.handle, .input, "y"); 415 try proto.writeFrame(ca.handle, .input, "y");
453 spun = 0; 416 spun = 0;
454 while (spun < 200 and srv.clients[0].?.activity < srv.clients[1].?.activity) : (spun += 1) { 417 while (spun < 200 and td.srv.clients[0].?.activity < td.srv.clients[1].?.activity) : (spun += 1) {
455 try srv.pumpOnce(5); 418 try td.srv.pumpOnce(5);
456 } 419 }
457 try std.testing.expect(srv.clients[0].?.activity > srv.clients[1].?.activity); 420 try std.testing.expect(td.srv.clients[0].?.activity > td.srv.clients[1].?.activity);
458 421
459 const second = try dial.dial(path); 422 const second = try dial.dial(path);
460 defer second.close(); 423 defer second.close();
461 const to_a = (try awaitFrame(alloc, &srv, ca.handle, .agent_open, 200)) orelse 424 const to_a = (try awaitFrame(alloc, &td.srv, ca.handle, .agent_open, 200)) orelse
462 return error.NoAgentOpenForA; 425 return error.NoAgentOpenForA;
463 defer to_a.deinit(alloc); 426 defer to_a.deinit(alloc);
464 try std.testing.expect( 427 try std.testing.expect(
465 (try proto.decodeAgentId(to_a.payload)) != (try proto.decodeAgentId(to_b.payload)), 428 (try proto.decodeAgentId(to_a.payload)) != (try proto.decodeAgentId(to_b.payload)),
466 ); 429 );
467 const stray_b = try awaitFrame(alloc, &srv, cb.handle, .agent_open, 60); 430 const stray_b = try awaitFrame(alloc, &td.srv, cb.handle, .agent_open, 60);
468 defer if (stray_b) |f| f.deinit(alloc); 431 defer if (stray_b) |f| f.deinit(alloc);
469 try std.testing.expect(stray_b == null); 432 try std.testing.expect(stray_b == null);
470 } 433 }
@@ -472,26 +435,21 @@ test "Server: an agent connection is routed to the latest-active offerer" {
472 test "Server: agent_data for an unknown or another client's channel is dropped" { 435 test "Server: agent_data for an unknown or another client's channel is dropped" {
473 const alloc = std.testing.allocator; 436 const alloc = std.testing.allocator;
474 437
475 var tmp = try TmpDir.make(); 438 var td = try h.TestDaemon.init(alloc, "agentforeign", .{ .shell = "/bin/cat" });
476 defer tmp.cleanup(); 439 defer td.deinit();
477 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentforeign.sock", .{tmp.path()});
478 defer alloc.free(sock_path);
479
480 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
481 defer srv.deinit();
482 440
483 const ca = try dial.dial(sock_path); 441 const ca = try dial.dial(td.sock_path);
484 defer ca.close(); 442 defer ca.close();
485 try attachOffering(&srv, ca.handle, 0, ""); 443 try attachOffering(&td.srv, ca.handle, 0, "");
486 const cb = try dial.dial(sock_path); 444 const cb = try dial.dial(td.sock_path);
487 defer cb.close(); 445 defer cb.close();
488 try attachOffering(&srv, cb.handle, 1, ""); 446 try attachOffering(&td.srv, cb.handle, 1, "");
489 447
490 // B attached last, so the channel is B's. 448 // B attached last, so the channel is B's.
491 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 449 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
492 const agent = try dial.dial(path); 450 const agent = try dial.dial(path);
493 defer agent.close(); 451 defer agent.close();
494 const open = (try awaitFrame(alloc, &srv, cb.handle, .agent_open, 200)) orelse 452 const open = (try awaitFrame(alloc, &td.srv, cb.handle, .agent_open, 200)) orelse
495 return error.NoAgentOpen; 453 return error.NoAgentOpen;
496 defer open.deinit(alloc); 454 defer open.deinit(alloc);
497 const id = try proto.decodeAgentId(open.payload); 455 const id = try proto.decodeAgentId(open.payload);
@@ -508,10 +466,10 @@ test "Server: agent_data for an unknown or another client's channel is dropped"
508 unknown[proto.agent_id_len] = 'x'; 466 unknown[proto.agent_id_len] = 'x';
509 try proto.writeFrame(cb.handle, .agent_data, &unknown); 467 try proto.writeFrame(cb.handle, .agent_data, &unknown);
510 var buf: [64]u8 = undefined; 468 var buf: [64]u8 = undefined;
511 try std.testing.expect((try pumpUntilReadable(&srv, agent.handle, &buf, 60)) == null); 469 try std.testing.expect((try pumpUntilReadable(&td.srv, agent.handle, &buf, 60)) == null);
512 // Dropped, not filed: an id nobody holds must not be an id anybody can 470 // Dropped, not filed: an id nobody holds must not be an id anybody can
513 // conjure a channel with. 471 // conjure a channel with.
514 try std.testing.expect(srv.agents.chans[1] == null); 472 try std.testing.expect(td.srv.agents.chans[1] == null);
515 473
516 // The positive control, so the silence above is a refusal and not a 474 // The positive control, so the silence above is a refusal and not a
517 // pump that was never working: the owner's bytes still land. 475 // pump that was never working: the owner's bytes still land.
@@ -519,16 +477,16 @@ test "Server: agent_data for an unknown or another client's channel is dropped"
519 @memcpy(mine[0..proto.agent_id_len], &proto.encodeAgentId(id)); 477 @memcpy(mine[0..proto.agent_id_len], &proto.encodeAgentId(id));
520 @memcpy(mine[proto.agent_id_len..], "ok"); 478 @memcpy(mine[proto.agent_id_len..], "ok");
521 try proto.writeFrame(cb.handle, .agent_data, &mine); 479 try proto.writeFrame(cb.handle, .agent_data, &mine);
522 const n = (try pumpUntilReadable(&srv, agent.handle, &buf, 200)) orelse 480 const n = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 200)) orelse
523 return error.NoAgentRequest; 481 return error.NoAgentRequest;
524 try std.testing.expectEqualSlices(u8, "ok", buf[0..n]); 482 try std.testing.expectEqualSlices(u8, "ok", buf[0..n]);
525 483
526 // And neither sender was punished for asking: an id that names nothing 484 // And neither sender was punished for asking: an id that names nothing
527 // is dropped like an unknown frame type, not answered with a hangup. 485 // is dropped like an unknown frame type, not answered with a hangup.
528 try std.testing.expect(srv.clients[0] != null); 486 try std.testing.expect(td.srv.clients[0] != null);
529 try std.testing.expect(srv.clients[1] != null); 487 try std.testing.expect(td.srv.clients[1] != null);
530 try proto.writeFrame(ca.handle, .stats_req, ""); 488 try proto.writeFrame(ca.handle, .stats_req, "");
531 const reply = try awaitFrame(alloc, &srv, ca.handle, .stats_reply, 400); 489 const reply = try awaitFrame(alloc, &td.srv, ca.handle, .stats_reply, 400);
532 defer if (reply) |f| f.deinit(alloc); 490 defer if (reply) |f| f.deinit(alloc);
533 try std.testing.expect(reply != null); 491 try std.testing.expect(reply != null);
534 } 492 }
@@ -536,22 +494,17 @@ test "Server: agent_data for an unknown or another client's channel is dropped"
536 test "Server: an agent_data frame past the cap hangs the channel up" { 494 test "Server: an agent_data frame past the cap hangs the channel up" {
537 const alloc = std.testing.allocator; 495 const alloc = std.testing.allocator;
538 496
539 var tmp = try TmpDir.make(); 497 var td = try h.TestDaemon.init(alloc, "agentbig", .{ .shell = "/bin/cat" });
540 defer tmp.cleanup(); 498 defer td.deinit();
541 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentbig.sock", .{tmp.path()});
542 defer alloc.free(sock_path);
543 499
544 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 500 const c = try dial.dial(td.sock_path);
545 defer srv.deinit();
546
547 const c = try dial.dial(sock_path);
548 defer c.close(); 501 defer c.close();
549 try attachOffering(&srv, c.handle, 0, ""); 502 try attachOffering(&td.srv, c.handle, 0, "");
550 503
551 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 504 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
552 const agent = try dial.dial(path); 505 const agent = try dial.dial(path);
553 defer agent.close(); 506 defer agent.close();
554 const open = (try awaitFrame(alloc, &srv, c.handle, .agent_open, 200)) orelse 507 const open = (try awaitFrame(alloc, &td.srv, c.handle, .agent_open, 200)) orelse
555 return error.NoAgentOpen; 508 return error.NoAgentOpen;
556 defer open.deinit(alloc); 509 defer open.deinit(alloc);
557 const id = try proto.decodeAgentId(open.payload); 510 const id = try proto.decodeAgentId(open.payload);
@@ -567,20 +520,20 @@ test "Server: an agent_data frame past the cap hangs the channel up" {
567 520
568 // The channel goes, and the client hears so: it is holding an fd to the 521 // The channel goes, and the client hears so: it is holding an fd to the
569 // local agent that nothing will answer on again. 522 // local agent that nothing will answer on again.
570 const closed = (try awaitFrame(alloc, &srv, c.handle, .agent_close, 200)) orelse 523 const closed = (try awaitFrame(alloc, &td.srv, c.handle, .agent_close, 200)) orelse
571 return error.NoAgentClose; 524 return error.NoAgentClose;
572 defer closed.deinit(alloc); 525 defer closed.deinit(alloc);
573 try std.testing.expectEqual(id, try proto.decodeAgentId(closed.payload)); 526 try std.testing.expectEqual(id, try proto.decodeAgentId(closed.payload));
574 try std.testing.expect(srv.agents.find(id, 0) == null); 527 try std.testing.expect(td.srv.agents.find(id, 0) == null);
575 528
576 // Not one byte of it reached the agent: a truncated request is worse 529 // Not one byte of it reached the agent: a truncated request is worse
577 // than none, and EOF here is the hangup rather than an idle socket. 530 // than none, and EOF here is the hangup rather than an idle socket.
578 var buf: [64]u8 = undefined; 531 var buf: [64]u8 = undefined;
579 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&srv, agent.handle, &buf, 60)); 532 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, agent.handle, &buf, 60));
580 533
581 // And the client itself survives — the frame was refused, not the peer. 534 // And the client itself survives — the frame was refused, not the peer.
582 try proto.writeFrame(c.handle, .stats_req, ""); 535 try proto.writeFrame(c.handle, .stats_req, "");
583 const reply = try awaitFrame(alloc, &srv, c.handle, .stats_reply, 400); 536 const reply = try awaitFrame(alloc, &td.srv, c.handle, .stats_reply, 400);
584 defer if (reply) |f| f.deinit(alloc); 537 defer if (reply) |f| f.deinit(alloc);
585 try std.testing.expect(reply != null); 538 try std.testing.expect(reply != null);
586 } 539 }
@@ -588,22 +541,17 @@ test "Server: an agent_data frame past the cap hangs the channel up" {
588 test "Server: a client closing a channel hangs up the agent connection without an echo" { 541 test "Server: a client closing a channel hangs up the agent connection without an echo" {
589 const alloc = std.testing.allocator; 542 const alloc = std.testing.allocator;
590 543
591 var tmp = try TmpDir.make(); 544 var td = try h.TestDaemon.init(alloc, "agentcliclose", .{ .shell = "/bin/cat" });
592 defer tmp.cleanup(); 545 defer td.deinit();
593 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentcliclose.sock", .{tmp.path()});
594 defer alloc.free(sock_path);
595
596 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
597 defer srv.deinit();
598 546
599 const c = try dial.dial(sock_path); 547 const c = try dial.dial(td.sock_path);
600 defer c.close(); 548 defer c.close();
601 try attachOffering(&srv, c.handle, 0, ""); 549 try attachOffering(&td.srv, c.handle, 0, "");
602 550
603 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 551 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
604 const agent = try dial.dial(path); 552 const agent = try dial.dial(path);
605 defer agent.close(); 553 defer agent.close();
606 const open = (try awaitFrame(alloc, &srv, c.handle, .agent_open, 200)) orelse 554 const open = (try awaitFrame(alloc, &td.srv, c.handle, .agent_open, 200)) orelse
607 return error.NoAgentOpen; 555 return error.NoAgentOpen;
608 defer open.deinit(alloc); 556 defer open.deinit(alloc);
609 const id = try proto.decodeAgentId(open.payload); 557 const id = try proto.decodeAgentId(open.payload);
@@ -615,14 +563,14 @@ test "Server: a client closing a channel hangs up the agent connection without a
615 var buf: [16]u8 = undefined; 563 var buf: [16]u8 = undefined;
616 try std.testing.expectEqual( 564 try std.testing.expectEqual(
617 @as(?usize, 0), 565 @as(?usize, 0),
618 try pumpUntilReadable(&srv, agent.handle, &buf, 200), 566 try pumpUntilReadable(&td.srv, agent.handle, &buf, 200),
619 ); 567 );
620 try std.testing.expect(srv.agents.chans[0] == null); 568 try std.testing.expect(td.srv.agents.chans[0] == null);
621 569
622 // And no agent_close comes back: the client asked for this, so an echo 570 // And no agent_close comes back: the client asked for this, so an echo
623 // is an event it would have to learn to ignore — and one it could not 571 // is an event it would have to learn to ignore — and one it could not
624 // tell from the far end closing a channel it had reopened. 572 // tell from the far end closing a channel it had reopened.
625 const echo = try awaitFrame(alloc, &srv, c.handle, .agent_close, 60); 573 const echo = try awaitFrame(alloc, &td.srv, c.handle, .agent_close, 60);
626 defer if (echo) |f| f.deinit(alloc); 574 defer if (echo) |f| f.deinit(alloc);
627 try std.testing.expect(echo == null); 575 try std.testing.expect(echo == null);
628 } 576 }
@@ -630,22 +578,17 @@ test "Server: a client closing a channel hangs up the agent connection without a
630 test "Server: an agent listener and the connections it accepts are close-on-exec" { 578 test "Server: an agent listener and the connections it accepts are close-on-exec" {
631 const alloc = std.testing.allocator; 579 const alloc = std.testing.allocator;
632 580
633 var tmp = try TmpDir.make(); 581 var td = try h.TestDaemon.init(alloc, "agentcloexec", .{ .shell = "/bin/cat" });
634 defer tmp.cleanup(); 582 defer td.deinit();
635 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentcloexec.sock", .{tmp.path()});
636 defer alloc.free(sock_path);
637
638 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
639 defer srv.deinit();
640 583
641 const c = try dial.dial(sock_path); 584 const c = try dial.dial(td.sock_path);
642 defer c.close(); 585 defer c.close();
643 try attachOffering(&srv, c.handle, 0, ""); 586 try attachOffering(&td.srv, c.handle, 0, "");
644 587
645 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 588 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
646 const agent = try dial.dial(path); 589 const agent = try dial.dial(path);
647 defer agent.close(); 590 defer agent.close();
648 const open = (try awaitFrame(alloc, &srv, c.handle, .agent_open, 200)) orelse 591 const open = (try awaitFrame(alloc, &td.srv, c.handle, .agent_open, 200)) orelse
649 return error.NoAgentOpen; 592 return error.NoAgentOpen;
650 defer open.deinit(alloc); 593 defer open.deinit(alloc);
651 594
@@ -656,33 +599,28 @@ test "Server: an agent listener and the connections it accepts are close-on-exec
656 // channel that no close of ours could ever finish closing. 599 // channel that no close of ours could ever finish closing.
657 const flags = std.posix.FD_CLOEXEC; 600 const flags = std.posix.FD_CLOEXEC;
658 try std.testing.expect( 601 try std.testing.expect(
659 try std.posix.fcntl(srv.ses(0).agent_listener, std.posix.F.GETFD, 0) & flags != 0, 602 try std.posix.fcntl(td.srv.ses(0).agent_listener, std.posix.F.GETFD, 0) & flags != 0,
660 ); 603 );
661 const ch = srv.agents.chans[0] orelse return error.NoChannel; 604 const ch = td.srv.agents.chans[0] orelse return error.NoChannel;
662 try std.testing.expect(try std.posix.fcntl(ch.fd, std.posix.F.GETFD, 0) & flags != 0); 605 try std.testing.expect(try std.posix.fcntl(ch.fd, std.posix.F.GETFD, 0) & flags != 0);
663 } 606 }
664 607
665 test "Server: a client's agent channels die with the client" { 608 test "Server: a client's agent channels die with the client" {
666 const alloc = std.testing.allocator; 609 const alloc = std.testing.allocator;
667 610
668 var tmp = try TmpDir.make(); 611 var td = try h.TestDaemon.init(alloc, "agentorphan", .{ .shell = "/bin/cat" });
669 defer tmp.cleanup(); 612 defer td.deinit();
670 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentorphan.sock", .{tmp.path()});
671 defer alloc.free(sock_path);
672 613
673 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 614 const c = try dial.dial(td.sock_path);
674 defer srv.deinit(); 615 try attachOffering(&td.srv, c.handle, 0, "");
675 616
676 const c = try dial.dial(sock_path); 617 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
677 try attachOffering(&srv, c.handle, 0, "");
678
679 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket;
680 const agent = try dial.dial(path); 618 const agent = try dial.dial(path);
681 defer agent.close(); 619 defer agent.close();
682 const open = (try awaitFrame(alloc, &srv, c.handle, .agent_open, 200)) orelse 620 const open = (try awaitFrame(alloc, &td.srv, c.handle, .agent_open, 200)) orelse
683 return error.NoAgentOpen; 621 return error.NoAgentOpen;
684 defer open.deinit(alloc); 622 defer open.deinit(alloc);
685 try std.testing.expect(srv.agents.chans[0] != null); 623 try std.testing.expect(td.srv.agents.chans[0] != null);
686 624
687 // The client vanishes mid-exchange — a lid closed, a WAN link dropped. 625 // The client vanishes mid-exchange — a lid closed, a WAN link dropped.
688 // The ssh at the far end is waiting on a signature that can no longer 626 // The ssh at the far end is waiting on a signature that can no longer
@@ -690,23 +628,18 @@ test "Server: a client's agent channels die with the client" {
690 // EOF now, not a channel held open against a client that is gone. 628 // EOF now, not a channel held open against a client that is gone.
691 c.close(); 629 c.close();
692 var buf: [16]u8 = undefined; 630 var buf: [16]u8 = undefined;
693 const n = try pumpUntilReadable(&srv, agent.handle, &buf, 200); 631 const n = try pumpUntilReadable(&td.srv, agent.handle, &buf, 200);
694 try std.testing.expectEqual(@as(?usize, 0), n); 632 try std.testing.expectEqual(@as(?usize, 0), n);
695 try std.testing.expect(srv.clients[0] == null); 633 try std.testing.expect(td.srv.clients[0] == null);
696 try std.testing.expect(srv.agents.chans[0] == null); 634 try std.testing.expect(td.srv.agents.chans[0] == null);
697 } 635 }
698 636
699 test "Server: a QUIC client's agent channels die with the client" { 637 test "Server: a QUIC client's agent channels die with the client" {
700 const alloc = std.testing.allocator; 638 const alloc = std.testing.allocator;
701 var tmp = try TmpDir.make(); 639 var td = try h.TestDaemon.init(alloc, "qagentgone", .{ .shell = "/bin/sh" });
702 defer tmp.cleanup(); 640 defer td.deinit();
703 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qagentgone.sock", .{tmp.path()});
704 defer alloc.free(sock_path);
705
706 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
707 defer srv.deinit();
708 const key: quic.Key = .{ .bytes = [_]u8{0x7E} ** quic.key_len }; 641 const key: quic.Key = .{ .bytes = [_]u8{0x7E} ** quic.key_len };
709 const q = try quicTestServer(&srv, key); 642 const q = try quicTestServer(&td.srv, key);
710 defer q.l.deinit(); 643 defer q.l.deinit();
711 644
712 var cl = try quic_server.TestPeer.init(q.addr, key); 645 var cl = try quic_server.TestPeer.init(q.addr, key);
@@ -722,40 +655,40 @@ test "Server: a QUIC client's agent channels die with the client" {
722 cl.drain(); 655 cl.drain();
723 656
724 var only = [_]*quic_server.TestPeer{&cl}; 657 var only = [_]*quic_server.TestPeer{&cl};
725 try quicPump(&srv, &only, 10000, &srv, struct { 658 try quicPump(&td.srv, &only, 10000, &td.srv, struct {
726 fn f(s: *Server) bool { 659 fn f(s: *Server) bool {
727 return s.clients[0] != null and s.clients[0].?.agent_offer; 660 return s.clients[0] != null and s.clients[0].?.agent_offer;
728 } 661 }
729 }.f); 662 }.f);
730 if (srv.clients[0] == null or !srv.clients[0].?.agent_offer) return error.ClientNeverOffered; 663 if (td.srv.clients[0] == null or !td.srv.clients[0].?.agent_offer) return error.ClientNeverOffered;
731 664
732 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 665 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
733 const agent = try dial.dial(path); 666 const agent = try dial.dial(path);
734 defer agent.close(); 667 defer agent.close();
735 try quicPump(&srv, &only, 10000, &cl, struct { 668 try quicPump(&td.srv, &only, 10000, &cl, struct {
736 fn f(t: *quic_server.TestPeer) bool { 669 fn f(t: *quic_server.TestPeer) bool {
737 return findFrame(t.cl.in.items, .agent_open) != null; 670 return findFrame(t.cl.in.items, .agent_open) != null;
738 } 671 }
739 }.f); 672 }.f);
740 if (srv.agents.chans[0] == null) return error.NoAgentOpen; 673 if (td.srv.agents.chans[0] == null) return error.NoAgentOpen;
741 674
742 // quic_server.Listener.kill is private; closeConn plus a hand-called 675 // quic_server.Listener.kill is private; closeConn plus a hand-called
743 // quicOnClose is faithful to it because it repeats kill's order — the 676 // quicOnClose is faithful to it because it repeats kill's order — the
744 // conn is freed, then the callback runs. The socket twin above carries 677 // conn is freed, then the callback runs. The socket twin above carries
745 // the scenario. 678 // the scenario.
746 const qid = srv.clients[0].?.sink.quic.id; 679 const qid = td.srv.clients[0].?.sink.quic.id;
747 q.l.closeConn(qid); 680 q.l.closeConn(qid);
748 Server.quicOnClose(@ptrCast(&srv), qid); 681 Server.quicOnClose(@ptrCast(&td.srv), qid);
749 try std.testing.expect(srv.clients[0] == null); 682 try std.testing.expect(td.srv.clients[0] == null);
750 // Same verdict the socket path hands down in dropClient: the ssh at 683 // Same verdict the socket path hands down in dropClient: the ssh at
751 // the far end waits on a signature nobody can produce any more, and a 684 // the far end waits on a signature nobody can produce any more, and a
752 // channel left behind would route into whoever next takes the slot. 685 // channel left behind would route into whoever next takes the slot.
753 var abuf: [16]u8 = undefined; 686 var abuf: [16]u8 = undefined;
754 try std.testing.expectEqual( 687 try std.testing.expectEqual(
755 @as(?usize, 0), 688 @as(?usize, 0),
756 try pumpUntilReadable(&srv, agent.handle, &abuf, 200), 689 try pumpUntilReadable(&td.srv, agent.handle, &abuf, 200),
757 ); 690 );
758 try std.testing.expect(srv.agents.chans[0] == null); 691 try std.testing.expect(td.srv.agents.chans[0] == null);
759 } 692 }
760 693
761 /// Test helper: a channel brought to the moment the answer clock starts. 694 /// Test helper: a channel brought to the moment the answer clock starts.
@@ -779,66 +712,56 @@ fn openAndAsk(
779 test "Server: an offerer that never answers its first request is hung up on and stops offering" { 712 test "Server: an offerer that never answers its first request is hung up on and stops offering" {
780 const alloc = std.testing.allocator; 713 const alloc = std.testing.allocator;
781 714
782 var tmp = try TmpDir.make(); 715 var td = try h.TestDaemon.init(alloc, "agentmute", .{ .shell = "/bin/cat" });
783 defer tmp.cleanup(); 716 defer td.deinit();
784 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentmute.sock", .{tmp.path()}); 717 td.srv.agents.answer_ms = 150;
785 defer alloc.free(sock_path);
786 718
787 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 719 const c = try dial.dial(td.sock_path);
788 defer srv.deinit();
789 srv.agents.answer_ms = 150;
790
791 const c = try dial.dial(sock_path);
792 defer c.close(); 720 defer c.close();
793 try attachOffering(&srv, c.handle, 0, ""); 721 try attachOffering(&td.srv, c.handle, 0, "");
794 722
795 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 723 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
796 const agent = try dial.dial(path); 724 const agent = try dial.dial(path);
797 defer agent.close(); 725 defer agent.close();
798 const id = try openAndAsk(alloc, &srv, c.handle, agent.handle); 726 const id = try openAndAsk(alloc, &td.srv, c.handle, agent.handle);
799 727
800 // The client says nothing. Measured (decisions.md): ssh on a socket 728 // The client says nothing. Measured (decisions.md): ssh on a socket
801 // that accepts and never replies blocks past 8s; on one that accepts 729 // that accepts and never replies blocks past 8s; on one that accepts
802 // and closes it falls through to its next auth method in 2ms. So the 730 // and closes it falls through to its next auth method in 2ms. So the
803 // daemon's answer to silence is the close the client should have sent. 731 // daemon's answer to silence is the close the client should have sent.
804 var buf: [16]u8 = undefined; 732 var buf: [16]u8 = undefined;
805 const n = try pumpUntilReadable(&srv, agent.handle, &buf, 200); 733 const n = try pumpUntilReadable(&td.srv, agent.handle, &buf, 200);
806 try std.testing.expectEqual(@as(?usize, 0), n); 734 try std.testing.expectEqual(@as(?usize, 0), n);
807 try std.testing.expect(srv.agents.chans[0] == null); 735 try std.testing.expect(td.srv.agents.chans[0] == null);
808 // The client is told, like any other far-end close it did not ask for. 736 // The client is told, like any other far-end close it did not ask for.
809 const closed = (try awaitFrame(alloc, &srv, c.handle, .agent_close, 200)) orelse 737 const closed = (try awaitFrame(alloc, &td.srv, c.handle, .agent_close, 200)) orelse
810 return error.NoAgentClose; 738 return error.NoAgentClose;
811 defer closed.deinit(alloc); 739 defer closed.deinit(alloc);
812 try std.testing.expectEqual(id, try proto.decodeAgentId(closed.payload)); 740 try std.testing.expectEqual(id, try proto.decodeAgentId(closed.payload));
813 // And it is no longer an offerer: the next dial must not route here 741 // And it is no longer an offerer: the next dial must not route here
814 // again, or every ssh pays the bound before falling through. 742 // again, or every ssh pays the bound before falling through.
815 try std.testing.expect(!srv.clients[0].?.agent_offer); 743 try std.testing.expect(!td.srv.clients[0].?.agent_offer);
816 const again = try dial.dial(path); 744 const again = try dial.dial(path);
817 defer again.close(); 745 defer again.close();
818 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&srv, again.handle, &buf, 60)); 746 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, again.handle, &buf, 60));
819 try std.testing.expectEqual(@as(u32, 1), srv.agents.refused_no_offer); 747 try std.testing.expectEqual(@as(u32, 1), td.srv.agents.refused_no_offer);
820 } 748 }
821 749
822 test "Server: a channel that has answered once is never timed out" { 750 test "Server: a channel that has answered once is never timed out" {
823 const alloc = std.testing.allocator; 751 const alloc = std.testing.allocator;
824 752
825 var tmp = try TmpDir.make(); 753 var td = try h.TestDaemon.init(alloc, "agentslow", .{ .shell = "/bin/cat" });
826 defer tmp.cleanup(); 754 defer td.deinit();
827 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentslow.sock", .{tmp.path()}); 755 td.srv.agents.answer_ms = 150;
828 defer alloc.free(sock_path);
829
830 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
831 defer srv.deinit();
832 srv.agents.answer_ms = 150;
833 756
834 const c = try dial.dial(sock_path); 757 const c = try dial.dial(td.sock_path);
835 defer c.close(); 758 defer c.close();
836 try attachOffering(&srv, c.handle, 0, ""); 759 try attachOffering(&td.srv, c.handle, 0, "");
837 760
838 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 761 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
839 const agent = try dial.dial(path); 762 const agent = try dial.dial(path);
840 defer agent.close(); 763 defer agent.close();
841 const id = try openAndAsk(alloc, &srv, c.handle, agent.handle); 764 const id = try openAndAsk(alloc, &td.srv, c.handle, agent.handle);
842 765
843 // One reply proves the client speaks for an agent. 766 // One reply proves the client speaks for an agent.
844 var resp: [proto.agent_id_len + 4]u8 = undefined; 767 var resp: [proto.agent_id_len + 4]u8 = undefined;
@@ -846,7 +769,7 @@ test "Server: a channel that has answered once is never timed out" {
846 @memcpy(resp[proto.agent_id_len..], "resp"); 769 @memcpy(resp[proto.agent_id_len..], "resp");
847 try proto.writeFrame(c.handle, .agent_data, &resp); 770 try proto.writeFrame(c.handle, .agent_data, &resp);
848 var buf: [64]u8 = undefined; 771 var buf: [64]u8 = undefined;
849 _ = (try pumpUntilReadable(&srv, agent.handle, &buf, 200)) orelse return error.NoReply; 772 _ = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 200)) orelse return error.NoReply;
850 773
851 // A second request the client takes its time over — a SIGN against a 774 // A second request the client takes its time over — a SIGN against a
852 // token waiting for a touch. The bound is for a peer that cannot 775 // token waiting for a touch. The bound is for a peer that cannot
@@ -854,34 +777,29 @@ test "Server: a channel that has answered once is never timed out" {
854 // rule, decisions.md): this is the arm that would pass silently if the 777 // rule, decisions.md): this is the arm that would pass silently if the
855 // clock were anchored on the open or re-armed per request. 778 // clock were anchored on the open or re-armed per request.
856 try proto.writeAllFd(agent.handle, "req2"); 779 try proto.writeAllFd(agent.handle, "req2");
857 const fwd = (try awaitFrame(alloc, &srv, c.handle, .agent_data, 200)) orelse 780 const fwd = (try awaitFrame(alloc, &td.srv, c.handle, .agent_data, 200)) orelse
858 return error.RequestNeverForwarded; 781 return error.RequestNeverForwarded;
859 fwd.deinit(alloc); 782 fwd.deinit(alloc);
860 try std.testing.expectEqual(@as(?usize, null), try pumpUntilReadable(&srv, agent.handle, &buf, 100)); 783 try std.testing.expectEqual(@as(?usize, null), try pumpUntilReadable(&td.srv, agent.handle, &buf, 100));
861 try std.testing.expect(srv.agents.chans[0] != null); 784 try std.testing.expect(td.srv.agents.chans[0] != null);
862 try std.testing.expect(srv.clients[0].?.agent_offer); 785 try std.testing.expect(td.srv.clients[0].?.agent_offer);
863 } 786 }
864 787
865 test "Server: bytes a client sends before it was asked prove nothing" { 788 test "Server: bytes a client sends before it was asked prove nothing" {
866 const alloc = std.testing.allocator; 789 const alloc = std.testing.allocator;
867 790
868 var tmp = try TmpDir.make(); 791 var td = try h.TestDaemon.init(alloc, "agenteager", .{ .shell = "/bin/cat" });
869 defer tmp.cleanup(); 792 defer td.deinit();
870 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agenteager.sock", .{tmp.path()}); 793 td.srv.agents.answer_ms = 150;
871 defer alloc.free(sock_path);
872 794
873 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 795 const c = try dial.dial(td.sock_path);
874 defer srv.deinit();
875 srv.agents.answer_ms = 150;
876
877 const c = try dial.dial(sock_path);
878 defer c.close(); 796 defer c.close();
879 try attachOffering(&srv, c.handle, 0, ""); 797 try attachOffering(&td.srv, c.handle, 0, "");
880 798
881 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 799 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
882 const agent = try dial.dial(path); 800 const agent = try dial.dial(path);
883 defer agent.close(); 801 defer agent.close();
884 const open = (try awaitFrame(alloc, &srv, c.handle, .agent_open, 200)) orelse 802 const open = (try awaitFrame(alloc, &td.srv, c.handle, .agent_open, 200)) orelse
885 return error.NoAgentOpen; 803 return error.NoAgentOpen;
886 defer open.deinit(alloc); 804 defer open.deinit(alloc);
887 const id = try proto.decodeAgentId(open.payload); 805 const id = try proto.decodeAgentId(open.payload);
@@ -894,37 +812,32 @@ test "Server: bytes a client sends before it was asked prove nothing" {
894 @memcpy(eager[proto.agent_id_len..], "hi"); 812 @memcpy(eager[proto.agent_id_len..], "hi");
895 try proto.writeFrame(c.handle, .agent_data, &eager); 813 try proto.writeFrame(c.handle, .agent_data, &eager);
896 var buf: [64]u8 = undefined; 814 var buf: [64]u8 = undefined;
897 _ = (try pumpUntilReadable(&srv, agent.handle, &buf, 200)) orelse return error.NotForwarded; 815 _ = (try pumpUntilReadable(&td.srv, agent.handle, &buf, 200)) orelse return error.NotForwarded;
898 816
899 // Then ssh asks and the client goes quiet: the clock must still run. 817 // Then ssh asks and the client goes quiet: the clock must still run.
900 try proto.writeAllFd(agent.handle, "req"); 818 try proto.writeAllFd(agent.handle, "req");
901 const fwd = (try awaitFrame(alloc, &srv, c.handle, .agent_data, 200)) orelse 819 const fwd = (try awaitFrame(alloc, &td.srv, c.handle, .agent_data, 200)) orelse
902 return error.RequestNeverForwarded; 820 return error.RequestNeverForwarded;
903 fwd.deinit(alloc); 821 fwd.deinit(alloc);
904 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&srv, agent.handle, &buf, 200)); 822 try std.testing.expectEqual(@as(?usize, 0), try pumpUntilReadable(&td.srv, agent.handle, &buf, 200));
905 try std.testing.expect(!srv.clients[0].?.agent_offer); 823 try std.testing.expect(!td.srv.clients[0].?.agent_offer);
906 } 824 }
907 825
908 test "Server: a channel nobody has asked anything on is not timed out" { 826 test "Server: a channel nobody has asked anything on is not timed out" {
909 const alloc = std.testing.allocator; 827 const alloc = std.testing.allocator;
910 828
911 var tmp = try TmpDir.make(); 829 var td = try h.TestDaemon.init(alloc, "agentidle", .{ .shell = "/bin/cat" });
912 defer tmp.cleanup(); 830 defer td.deinit();
913 const sock_path = try std.fmt.allocPrint(alloc, "{s}/agentidle.sock", .{tmp.path()}); 831 td.srv.agents.answer_ms = 150;
914 defer alloc.free(sock_path);
915
916 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
917 defer srv.deinit();
918 srv.agents.answer_ms = 150;
919 832
920 const c = try dial.dial(sock_path); 833 const c = try dial.dial(td.sock_path);
921 defer c.close(); 834 defer c.close();
922 try attachOffering(&srv, c.handle, 0, ""); 835 try attachOffering(&td.srv, c.handle, 0, "");
923 836
924 const path = srv.ses(0).agent_path orelse return error.NoAgentSocket; 837 const path = td.srv.ses(0).agent_path orelse return error.NoAgentSocket;
925 const agent = try dial.dial(path); 838 const agent = try dial.dial(path);
926 defer agent.close(); 839 defer agent.close();
927 const open = (try awaitFrame(alloc, &srv, c.handle, .agent_open, 200)) orelse 840 const open = (try awaitFrame(alloc, &td.srv, c.handle, .agent_open, 200)) orelse
928 return error.NoAgentOpen; 841 return error.NoAgentOpen;
929 open.deinit(alloc); 842 open.deinit(alloc);
930 843
@@ -932,7 +845,7 @@ test "Server: a channel nobody has asked anything on is not timed out" {
932 // has been handed a request, so a clock anchored on the open would 845 // has been handed a request, so a clock anchored on the open would
933 // hang up on a working client for the peer's pause. 846 // hang up on a working client for the peer's pause.
934 var buf: [16]u8 = undefined; 847 var buf: [16]u8 = undefined;
935 try std.testing.expectEqual(@as(?usize, null), try pumpUntilReadable(&srv, agent.handle, &buf, 100)); 848 try std.testing.expectEqual(@as(?usize, null), try pumpUntilReadable(&td.srv, agent.handle, &buf, 100));
936 try std.testing.expect(srv.agents.chans[0] != null); 849 try std.testing.expect(td.srv.agents.chans[0] != null);
937 try std.testing.expect(srv.clients[0].?.agent_offer); 850 try std.testing.expect(td.srv.clients[0].?.agent_offer);
938 } 851 }
src/server/server_test_attach.zig
Old New
@@ -17,23 +17,14 @@ const serverThread = h.serverThread;
17 test "Server: survives a client that dies without detaching; next attach works" { 17 test "Server: survives a client that dies without detaching; next attach works" {
18 const alloc = std.testing.allocator; 18 const alloc = std.testing.allocator;
19 19
20 var tmp = try TmpDir.make(); 20 var td = try h.TestDaemon.init(alloc, "kill", .{ .shell = "/bin/sh" });
21 defer tmp.cleanup(); 21 defer td.deinit();
22 const dir_path = tmp.path();
23 const sock_path = try std.fmt.allocPrint(alloc, "{s}/kill.sock", .{dir_path});
24 defer alloc.free(sock_path);
25
26 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
27 defer srv.deinit();
28 installSignalHandlers(); // includes SIGPIPE ignore 22 installSignalHandlers(); // includes SIGPIPE ignore
29 23
30 var stop = std.atomic.Value(bool).init(false); 24 try td.threaded();
31 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
32 defer th.join();
33 defer stop.store(true, .release);
34 25
35 // Client 1 attaches, provokes output, then vanishes without detach. 26 // Client 1 attaches, provokes output, then vanishes without detach.
36 const a = try dial.dialAttach(sock_path, 80, 24); 27 const a = try dial.dialAttach(td.sock_path, 80, 24);
37 try proto.writeFrame(a.handle, .input, "echo pre-kill\n"); 28 try proto.writeFrame(a.handle, .input, "echo pre-kill\n");
38 std.Thread.sleep(300 * std.time.ns_per_ms); 29 std.Thread.sleep(300 * std.time.ns_per_ms);
39 a.close(); // abrupt: no detach frame 30 a.close(); // abrupt: no detach frame
@@ -42,20 +33,12 @@ test "Server: survives a client that dies without detaching; next attach works"
42 std.Thread.sleep(300 * std.time.ns_per_ms); 33 std.Thread.sleep(300 * std.time.ns_per_ms);
43 34
44 // Daemon must still be serving: a fresh attach gets a snapshot. 35 // Daemon must still be serving: a fresh attach gets a snapshot.
45 const b = try dial.dialAttach(sock_path, 80, 24); 36 const b = try dial.dialAttach(td.sock_path, 80, 24);
46 defer b.close(); 37 defer b.close();
47 var got_snapshot = false; 38 var got_snapshot = false;
48 var deadline_ms: u64 = 5000; 39 if (try h.awaitFrameOn(alloc, b.handle, .snapshot, 5000)) |frame| {
49 while (deadline_ms > 0 and !got_snapshot) { 40 frame.deinit(alloc);
50 var pfd = [_]std.posix.pollfd{ 41 got_snapshot = true;
51 .{ .fd = b.handle, .events = std.posix.POLL.IN, .revents = 0 },
52 };
53 const ready = try std.posix.poll(&pfd, 100);
54 deadline_ms -|= 100;
55 if (ready == 0) continue;
56 const frame = (try proto.readFrame(alloc, b.handle)) orelse break;
57 defer frame.deinit(alloc);
58 if (frame.type == .snapshot) got_snapshot = true;
59 } 42 }
60 try std.testing.expect(got_snapshot); 43 try std.testing.expect(got_snapshot);
61 } 44 }
@@ -63,21 +46,12 @@ test "Server: survives a client that dies without detaching; next attach works"
63 test "Server: serves scrollback chunks on request" { 46 test "Server: serves scrollback chunks on request" {
64 const alloc = std.testing.allocator; 47 const alloc = std.testing.allocator;
65 48
66 var tmp = try TmpDir.make(); 49 var td = try h.TestDaemon.init(alloc, "sb", .{ .shell = "/bin/sh" });
67 defer tmp.cleanup(); 50 defer td.deinit();
68 const dir_path = tmp.path();
69 const sock_path = try std.fmt.allocPrint(alloc, "{s}/sb.sock", .{dir_path});
70 defer alloc.free(sock_path);
71 51
72 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 52 try td.threaded();
73 defer srv.deinit();
74 53
75 var stop = std.atomic.Value(bool).init(false); 54 const c = try dial.dialAttach(td.sock_path, 80, 24);
76 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
77 defer th.join();
78 defer stop.store(true, .release);
79
80 const c = try dial.dialAttach(sock_path, 80, 24);
81 defer c.close(); 55 defer c.close();
82 try proto.writeFrame(c.handle, .input, "seq 1 100\n"); 56 try proto.writeFrame(c.handle, .input, "seq 1 100\n");
83 57
@@ -106,18 +80,8 @@ test "Server: serves scrollback chunks on request" {
106 try proto.writeFrame(c.handle, .fetch_scrollback, &proto.encodeScrollbackReq(0, 24)); 80 try proto.writeFrame(c.handle, .fetch_scrollback, &proto.encodeScrollbackReq(0, 24));
107 var chunk: ?[]u8 = null; 81 var chunk: ?[]u8 = null;
108 defer if (chunk) |ch| alloc.free(ch); 82 defer if (chunk) |ch| alloc.free(ch);
109 deadline_ms = 5000; 83 if (try h.awaitFrameOn(alloc, c.handle, .scrollback_chunk, 5000)) |frame| {
110 while (deadline_ms > 0 and chunk == null) { 84 chunk = frame.payload; // ownership taken
111 var pfd = [_]std.posix.pollfd{
112 .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 },
113 };
114 const ready = try std.posix.poll(&pfd, 100);
115 deadline_ms -|= 100;
116 if (ready == 0) continue;
117 const frame = (try proto.readFrame(alloc, c.handle)) orelse break;
118 if (frame.type == .scrollback_chunk) {
119 chunk = frame.payload;
120 } else frame.deinit(alloc);
121 } 85 }
122 try std.testing.expect(chunk != null); 86 try std.testing.expect(chunk != null);
123 try std.testing.expect(chunk.?.len > 6); 87 try std.testing.expect(chunk.?.len > 6);
@@ -130,19 +94,10 @@ test "Server: serves scrollback chunks on request" {
130 test "Server: a full session refuses the next attach instead of displacing anyone" { 94 test "Server: a full session refuses the next attach instead of displacing anyone" {
131 const alloc = std.testing.allocator; 95 const alloc = std.testing.allocator;
132 96
133 var tmp = try TmpDir.make(); 97 var td = try h.TestDaemon.init(alloc, "full", .{ .shell = "/bin/sh" });
134 defer tmp.cleanup(); 98 defer td.deinit();
135 const dir_path = tmp.path();
136 const sock_path = try std.fmt.allocPrint(alloc, "{s}/full.sock", .{dir_path});
137 defer alloc.free(sock_path);
138
139 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
140 defer srv.deinit();
141 99
142 var stop = std.atomic.Value(bool).init(false); 100 try td.threaded();
143 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
144 defer th.join();
145 defer stop.store(true, .release);
146 101
147 // Fill every client slot. Each attach is confirmed before the next 102 // Fill every client slot. Each attach is confirmed before the next
148 // connects, both to keep observer slots free (attach vacates one) and 103 // connects, both to keep observer slots free (attach vacates one) and
@@ -151,30 +106,20 @@ test "Server: a full session refuses the next attach instead of displacing anyon
151 var opened: usize = 0; 106 var opened: usize = 0;
152 defer for (streams[0..opened]) |s| s.close(); 107 defer for (streams[0..opened]) |s| s.close();
153 while (opened < max_clients) : (opened += 1) { 108 while (opened < max_clients) : (opened += 1) {
154 streams[opened] = try dial.dialAttach(sock_path, 80, 24); 109 streams[opened] = try dial.dialAttach(td.sock_path, 80, 24);
155 const fd = streams[opened].handle; 110 const fd = streams[opened].handle;
156 const first = try firstStateFrame(alloc, fd, 10_000); 111 const first = try firstStateFrame(alloc, fd, 10_000);
157 try std.testing.expect(first != null); 112 try std.testing.expect(first != null);
158 } 113 }
159 114
160 // The ninth attach is refused; nobody already attached is evicted. 115 // The ninth attach is refused; nobody already attached is evicted.
161 const extra = try dial.dialAttach(sock_path, 80, 24); 116 const extra = try dial.dialAttach(td.sock_path, 80, 24);
162 defer extra.close(); 117 defer extra.close();
163 var refused = false; 118 var refused = false;
164 var deadline_ms: u64 = 5000; 119 if (try h.awaitFrameOn(alloc, extra.handle, .exit_status, 5000)) |frame| {
165 while (deadline_ms > 0 and !refused) {
166 var pfd = [_]std.posix.pollfd{
167 .{ .fd = extra.handle, .events = std.posix.POLL.IN, .revents = 0 },
168 };
169 const ready = try std.posix.poll(&pfd, 100);
170 deadline_ms -|= 100;
171 if (ready == 0) continue;
172 const frame = (try proto.readFrame(alloc, extra.handle)) orelse break;
173 defer frame.deinit(alloc); 120 defer frame.deinit(alloc);
174 if (frame.type == .exit_status) { 121 try std.testing.expect(frame.payload.len == 1 and frame.payload[0] == 1);
175 try std.testing.expect(frame.payload.len == 1 and frame.payload[0] == 1); 122 refused = true;
176 refused = true;
177 }
178 } 123 }
179 try std.testing.expect(refused); 124 try std.testing.expect(refused);
180 125
@@ -183,7 +128,7 @@ test "Server: a full session refuses the next attach instead of displacing anyon
183 var replica = try Engine.init(alloc, .{ .cols = 80, .rows = 24 }); 128 var replica = try Engine.init(alloc, .{ .cols = 80, .rows = 24 });
184 defer replica.deinit(); 129 defer replica.deinit();
185 var alive = false; 130 var alive = false;
186 deadline_ms = 10_000; 131 var deadline_ms: u64 = 10_000;
187 while (deadline_ms > 0 and !alive) { 132 while (deadline_ms > 0 and !alive) {
188 var pfd = [_]std.posix.pollfd{ 133 var pfd = [_]std.posix.pollfd{
189 .{ .fd = streams[0].handle, .events = std.posix.POLL.IN, .revents = 0 }, 134 .{ .fd = streams[0].handle, .events = std.posix.POLL.IN, .revents = 0 },
@@ -205,23 +150,14 @@ test "Server: a full session refuses the next attach instead of displacing anyon
205 test "Server: two clients converge on one session" { 150 test "Server: two clients converge on one session" {
206 const alloc = std.testing.allocator; 151 const alloc = std.testing.allocator;
207 152
208 var tmp = try TmpDir.make(); 153 var td = try h.TestDaemon.init(alloc, "two", .{ .shell = "/bin/sh" });
209 defer tmp.cleanup(); 154 defer td.deinit();
210 const dir_path = tmp.path();
211 const sock_path = try std.fmt.allocPrint(alloc, "{s}/two.sock", .{dir_path});
212 defer alloc.free(sock_path);
213 155
214 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 156 try td.threaded();
215 defer srv.deinit();
216
217 var stop = std.atomic.Value(bool).init(false);
218 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
219 defer th.join();
220 defer stop.store(true, .release);
221 157
222 const a = try dial.dialAttach(sock_path, 80, 24); 158 const a = try dial.dialAttach(td.sock_path, 80, 24);
223 defer a.close(); 159 defer a.close();
224 const b = try dial.dialAttach(sock_path, 80, 24); 160 const b = try dial.dialAttach(td.sock_path, 80, 24);
225 defer b.close(); 161 defer b.close();
226 162
227 // Input through A must reach both replicas. 163 // Input through A must reach both replicas.
@@ -309,21 +245,12 @@ test "Server: two clients converge on one session" {
309 test "Server: a same-size join snapshots the joiner only" { 245 test "Server: a same-size join snapshots the joiner only" {
310 const alloc = std.testing.allocator; 246 const alloc = std.testing.allocator;
311 247
312 var tmp = try TmpDir.make(); 248 var td = try h.TestDaemon.init(alloc, "join", .{ .shell = "/bin/sh" });
313 defer tmp.cleanup(); 249 defer td.deinit();
314 const dir_path = tmp.path();
315 const sock_path = try std.fmt.allocPrint(alloc, "{s}/join.sock", .{dir_path});
316 defer alloc.free(sock_path);
317 250
318 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 251 try td.threaded();
319 defer srv.deinit();
320
321 var stop = std.atomic.Value(bool).init(false);
322 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
323 defer th.join();
324 defer stop.store(true, .release);
325 252
326 const a = try dial.dialAttach(sock_path, 80, 24); 253 const a = try dial.dialAttach(td.sock_path, 80, 24);
327 defer a.close(); 254 defer a.close();
328 255
329 var replica_a = try Engine.init(alloc, .{ .cols = 80, .rows = 24 }); 256 var replica_a = try Engine.init(alloc, .{ .cols = 80, .rows = 24 });
@@ -354,21 +281,13 @@ test "Server: a same-size join snapshots the joiner only" {
354 281
355 // B joins at the same size: nothing about A's grid changed, so A must 282 // B joins at the same size: nothing about A's grid changed, so A must
356 // not be repainted. B, which has nothing, must be. 283 // not be repainted. B, which has nothing, must be.
357 const b = try dial.dialAttach(sock_path, 80, 24); 284 const b = try dial.dialAttach(td.sock_path, 80, 24);
358 defer b.close(); 285 defer b.close();
359 286
360 var b_snapshot = false; 287 var b_snapshot = false;
361 deadline_ms = 5000; 288 if (try h.awaitFrameOn(alloc, b.handle, .snapshot, 5000)) |frame| {
362 while (deadline_ms > 0 and !b_snapshot) { 289 frame.deinit(alloc);
363 var pfd = [_]std.posix.pollfd{ 290 b_snapshot = true;
364 .{ .fd = b.handle, .events = std.posix.POLL.IN, .revents = 0 },
365 };
366 const ready = try std.posix.poll(&pfd, 100);
367 deadline_ms -|= 100;
368 if (ready == 0) continue;
369 const frame = (try proto.readFrame(alloc, b.handle)) orelse break;
370 defer frame.deinit(alloc);
371 if (frame.type == .snapshot) b_snapshot = true;
372 } 291 }
373 try std.testing.expect(b_snapshot); 292 try std.testing.expect(b_snapshot);
374 293
@@ -421,17 +340,11 @@ test "Server: a same-size join snapshots the joiner only" {
421 test "Server: latest attacher's size wins; earlier client is resnapshotted at the new size" { 340 test "Server: latest attacher's size wins; earlier client is resnapshotted at the new size" {
422 const alloc = std.testing.allocator; 341 const alloc = std.testing.allocator;
423 342
424 var tmp = try TmpDir.make(); 343 var td = try h.TestDaemon.init(alloc, "latest", .{ .shell = "/bin/sh" });
425 defer tmp.cleanup(); 344 defer td.deinit();
426 const dir_path = tmp.path();
427 const sock_path = try std.fmt.allocPrint(alloc, "{s}/latest.sock", .{dir_path});
428 defer alloc.free(sock_path);
429
430 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
431 defer srv.deinit();
432 345
433 var stop = std.atomic.Value(bool).init(false); 346 var stop = std.atomic.Value(bool).init(false);
434 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop }); 347 const th = try std.Thread.spawn(.{}, serverThread, .{ &td.srv, &stop });
435 // Stopped and joined explicitly below so the final assertion can read 348 // Stopped and joined explicitly below so the final assertion can read
436 // server state without racing the daemon thread. 349 // server state without racing the daemon thread.
437 var joined = false; 350 var joined = false;
@@ -440,36 +353,26 @@ test "Server: latest attacher's size wins; earlier client is resnapshotted at th
440 th.join(); 353 th.join();
441 }; 354 };
442 355
443 const a = try dial.dialAttach(sock_path, 80, 24); 356 const a = try dial.dialAttach(td.sock_path, 80, 24);
444 defer a.close(); 357 defer a.close();
445 358
446 // A is attached once it has been answered; only then can B's attach be 359 // A is attached once it has been answered; only then can B's attach be
447 // the *later* event this test is about. 360 // the *later* event this test is about.
448 var a_attached = false; 361 var a_attached = false;
449 var deadline_ms: u64 = 10_000; 362 if (try h.awaitFrameOn(alloc, a.handle, .snapshot, 10_000)) |frame| {
450 while (deadline_ms > 0 and !a_attached) {
451 var pfd = [_]std.posix.pollfd{
452 .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 },
453 };
454 const ready = try std.posix.poll(&pfd, 100);
455 deadline_ms -|= 100;
456 if (ready == 0) continue;
457 const frame = (try proto.readFrame(alloc, a.handle)) orelse break;
458 defer frame.deinit(alloc); 363 defer frame.deinit(alloc);
459 if (frame.type == .snapshot) { 364 const p = try proto.readSnapshotPrefix(frame.payload);
460 const p = try proto.readSnapshotPrefix(frame.payload); 365 try std.testing.expectEqual(@as(u16, 80), p.cols);
461 try std.testing.expectEqual(@as(u16, 80), p.cols); 366 a_attached = true;
462 a_attached = true;
463 }
464 } 367 }
465 try std.testing.expect(a_attached); 368 try std.testing.expect(a_attached);
466 369
467 const b = try dial.dialAttach(sock_path, 100, 30); 370 const b = try dial.dialAttach(td.sock_path, 100, 30);
468 defer b.close(); 371 defer b.close();
469 372
470 // The grid follows the newest attacher, and A is told about it. 373 // The grid follows the newest attacher, and A is told about it.
471 var a_resized = false; 374 var a_resized = false;
472 deadline_ms = 5000; 375 var deadline_ms: u64 = 5000;
473 while (deadline_ms > 0 and !a_resized) { 376 while (deadline_ms > 0 and !a_resized) {
474 var pfd = [_]std.posix.pollfd{ 377 var pfd = [_]std.posix.pollfd{
475 .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 }, 378 .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 },
@@ -488,8 +391,8 @@ test "Server: latest attacher's size wins; earlier client is resnapshotted at th
488 stop.store(true, .release); 391 stop.store(true, .release);
489 th.join(); 392 th.join();
490 joined = true; 393 joined = true;
491 try std.testing.expectEqual(@as(u16, 100), @as(u16, @intCast(srv.sessions.table[0].?.eng.term.cols))); 394 try std.testing.expectEqual(@as(u16, 100), @as(u16, @intCast(td.srv.sessions.table[0].?.eng.term.cols)));
492 try std.testing.expectEqual(@as(u16, 30), @as(u16, @intCast(srv.sessions.table[0].?.eng.term.rows))); 395 try std.testing.expectEqual(@as(u16, 30), @as(u16, @intCast(td.srv.sessions.table[0].?.eng.term.rows)));
493 } 396 }
494 397
495 /// Lenient about what precedes it: the join snapshot and the shell's deltas 398 /// Lenient about what precedes it: the join snapshot and the shell's deltas
@@ -521,28 +424,19 @@ fn awaitSnapshotSize(
521 test "Server: typing claims the grid for the typist (latest-wins on input)" { 424 test "Server: typing claims the grid for the typist (latest-wins on input)" {
522 const alloc = std.testing.allocator; 425 const alloc = std.testing.allocator;
523 426
524 var tmp = try TmpDir.make(); 427 var td = try h.TestDaemon.init(alloc, "typing", .{ .shell = "/bin/sh" });
525 defer tmp.cleanup(); 428 defer td.deinit();
526 const dir_path = tmp.path();
527 const sock_path = try std.fmt.allocPrint(alloc, "{s}/typing.sock", .{dir_path});
528 defer alloc.free(sock_path);
529 429
530 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 430 try td.threaded();
531 defer srv.deinit();
532
533 var stop = std.atomic.Value(bool).init(false);
534 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
535 defer th.join();
536 defer stop.store(true, .release);
537 431
538 // A joins first and, being the only client, sets the grid: attach-wins 432 // A joins first and, being the only client, sets the grid: attach-wins
539 // is unchanged by any of this. 433 // is unchanged by any of this.
540 const a = try dial.dialAttach(sock_path, 100, 30); 434 const a = try dial.dialAttach(td.sock_path, 100, 30);
541 defer a.close(); 435 defer a.close();
542 try std.testing.expect(try awaitSnapshotSize(alloc, a.handle, 100, 30, 10_000)); 436 try std.testing.expect(try awaitSnapshotSize(alloc, a.handle, 100, 30, 10_000));
543 437
544 // B joins at a different size and takes the grid, which A is told about. 438 // B joins at a different size and takes the grid, which A is told about.
545 const b = try dial.dialAttach(sock_path, 80, 24); 439 const b = try dial.dialAttach(td.sock_path, 80, 24);
546 defer b.close(); 440 defer b.close();
547 try std.testing.expect(try awaitSnapshotSize(alloc, a.handle, 80, 24, 10_000)); 441 try std.testing.expect(try awaitSnapshotSize(alloc, a.handle, 80, 24, 10_000));
548 try std.testing.expect(try awaitSnapshotSize(alloc, b.handle, 80, 24, 10_000)); 442 try std.testing.expect(try awaitSnapshotSize(alloc, b.handle, 80, 24, 10_000));
@@ -614,17 +508,11 @@ fn awaitMarkerWithoutSnapshot(
614 test "Server: a size the grid refuses never becomes a claim" { 508 test "Server: a size the grid refuses never becomes a claim" {
615 const alloc = std.testing.allocator; 509 const alloc = std.testing.allocator;
616 510
617 var tmp = try TmpDir.make(); 511 var td = try h.TestDaemon.init(alloc, "refused", .{ .shell = "/bin/sh" });
618 defer tmp.cleanup(); 512 defer td.deinit();
619 const dir_path = tmp.path();
620 const sock_path = try std.fmt.allocPrint(alloc, "{s}/refused.sock", .{dir_path});
621 defer alloc.free(sock_path);
622
623 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
624 defer srv.deinit();
625 513
626 var stop = std.atomic.Value(bool).init(false); 514 var stop = std.atomic.Value(bool).init(false);
627 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop }); 515 const th = try std.Thread.spawn(.{}, serverThread, .{ &td.srv, &stop });
628 // Stopped and joined explicitly below so the last assertion can read 516 // Stopped and joined explicitly below so the last assertion can read
629 // server state without racing the daemon thread. 517 // server state without racing the daemon thread.
630 var joined = false; 518 var joined = false;
@@ -633,7 +521,7 @@ test "Server: a size the grid refuses never becomes a claim" {
633 th.join(); 521 th.join();
634 }; 522 };
635 523
636 const a = try dial.dialAttach(sock_path, 80, 24); 524 const a = try dial.dialAttach(td.sock_path, 80, 24);
637 defer a.close(); 525 defer a.close();
638 try std.testing.expect(try awaitSnapshotSize(alloc, a.handle, 80, 24, 10_000)); 526 try std.testing.expect(try awaitSnapshotSize(alloc, a.handle, 80, 24, 10_000));
639 _ = try drainHeld(alloc, a.handle, 10_000); 527 _ = try drainHeld(alloc, a.handle, 10_000);
@@ -642,7 +530,7 @@ test "Server: a size the grid refuses never becomes a claim" {
642 // It still joins and is still served — it is only its *size* that is 530 // It still joins and is still served — it is only its *size* that is
643 // refused — but the grid must not move, and A must not be repainted for 531 // refused — but the grid must not move, and A must not be repainted for
644 // a resize that never happened. 532 // a resize that never happened.
645 const d = try dial.dialAttach(sock_path, 1, 1); 533 const d = try dial.dialAttach(td.sock_path, 1, 1);
646 defer d.close(); 534 defer d.close();
647 try std.testing.expect(try awaitSnapshotSize(alloc, d.handle, 80, 24, 10_000)); 535 try std.testing.expect(try awaitSnapshotSize(alloc, d.handle, 80, 24, 10_000));
648 536
@@ -683,36 +571,26 @@ test "Server: a size the grid refuses never becomes a claim" {
683 th.join(); 571 th.join();
684 joined = true; 572 joined = true;
685 // The grid is where the only client with an accepted size put it. 573 // The grid is where the only client with an accepted size put it.
686 try std.testing.expectEqual(@as(u16, 100), @as(u16, @intCast(srv.sessions.table[0].?.eng.term.cols))); 574 try std.testing.expectEqual(@as(u16, 100), @as(u16, @intCast(td.srv.sessions.table[0].?.eng.term.cols)));
687 try std.testing.expectEqual(@as(u16, 30), @as(u16, @intCast(srv.sessions.table[0].?.eng.term.rows))); 575 try std.testing.expectEqual(@as(u16, 30), @as(u16, @intCast(td.srv.sessions.table[0].?.eng.term.rows)));
688 } 576 }
689 577
690 test "Server: scrollback fetch is per-client and independent" { 578 test "Server: scrollback fetch is per-client and independent" {
691 const alloc = std.testing.allocator; 579 const alloc = std.testing.allocator;
692 580
693 var tmp = try TmpDir.make(); 581 var td = try h.TestDaemon.init(alloc, "percli", .{
694 defer tmp.cleanup();
695 const dir_path = tmp.path();
696 const sock_path = try std.fmt.allocPrint(alloc, "{s}/percli.sock", .{dir_path});
697 defer alloc.free(sock_path);
698
699 var srv = try Server.init(alloc, .{
700 .sock_path = sock_path,
701 .shell = "/bin/sh", 582 .shell = "/bin/sh",
702 // Pin the prompt width so the command's screen-space columns do 583 // Pin the prompt width so the command's screen-space columns do
703 // not depend on which implementation /bin/sh names on the host. 584 // not depend on which implementation /bin/sh names on the host.
704 .extra_env = &.{.{ .key = "PS1", .value = ">>" }}, 585 .extra_env = &.{.{ .key = "PS1", .value = ">>" }},
705 }); 586 });
706 defer srv.deinit(); 587 defer td.deinit();
707 588
708 var stop = std.atomic.Value(bool).init(false); 589 try td.threaded();
709 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
710 defer th.join();
711 defer stop.store(true, .release);
712 590
713 const a = try dial.dialAttach(sock_path, 80, 24); 591 const a = try dial.dialAttach(td.sock_path, 80, 24);
714 defer a.close(); 592 defer a.close();
715 const b = try dial.dialAttach(sock_path, 100, 30); 593 const b = try dial.dialAttach(td.sock_path, 100, 30);
716 defer b.close(); 594 defer b.close();
717 595
718 // Typed by B; both clients see the history it produces. 596 // Typed by B; both clients see the history it produces.
@@ -836,18 +714,8 @@ test "Server: scrollback fetch is per-client and independent" {
836 try proto.writeFrame(a.handle, .fetch_scrollback, &proto.encodeScrollbackReq(0, 24)); 714 try proto.writeFrame(a.handle, .fetch_scrollback, &proto.encodeScrollbackReq(0, 24));
837 var chunk: ?[]u8 = null; 715 var chunk: ?[]u8 = null;
838 defer if (chunk) |ch| alloc.free(ch); 716 defer if (chunk) |ch| alloc.free(ch);
839 deadline_ms = 5000; 717 if (try h.awaitFrameOn(alloc, a.handle, .scrollback_chunk, 5000)) |frame| {
840 while (deadline_ms > 0 and chunk == null) { 718 chunk = frame.payload; // ownership taken
841 var pfd = [_]std.posix.pollfd{
842 .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 },
843 };
844 const ready = try std.posix.poll(&pfd, 100);
845 deadline_ms -|= 100;
846 if (ready == 0) continue;
847 const frame = (try proto.readFrame(alloc, a.handle)) orelse break;
848 if (frame.type == .scrollback_chunk) {
849 chunk = frame.payload; // ownership taken
850 } else frame.deinit(alloc);
851 } 719 }
852 try std.testing.expect(chunk != null); 720 try std.testing.expect(chunk != null);
853 try std.testing.expect(std.mem.indexOf(u8, chunk.?[6..], "seq 1 100") != null); 721 try std.testing.expect(std.mem.indexOf(u8, chunk.?[6..], "seq 1 100") != null);
@@ -884,21 +752,12 @@ test "Server: scrollback fetch is per-client and independent" {
884 test "Server: replica rebuilt from snapshots matches the authoritative grid" { 752 test "Server: replica rebuilt from snapshots matches the authoritative grid" {
885 const alloc = std.testing.allocator; 753 const alloc = std.testing.allocator;
886 754
887 var tmp = try TmpDir.make(); 755 var td = try h.TestDaemon.init(alloc, "m2", .{ .shell = "/bin/sh" });
888 defer tmp.cleanup(); 756 defer td.deinit();
889 const dir_path = tmp.path();
890 const sock_path = try std.fmt.allocPrint(alloc, "{s}/m2.sock", .{dir_path});
891 defer alloc.free(sock_path);
892 757
893 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 758 try td.threaded();
894 defer srv.deinit();
895
896 var stop = std.atomic.Value(bool).init(false);
897 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
898 defer th.join();
899 defer stop.store(true, .release);
900 759
901 const stream = try dial.dialAttach(sock_path, 100, 30); 760 const stream = try dial.dialAttach(td.sock_path, 100, 30);
902 defer stream.close(); 761 defer stream.close();
903 const fd = stream.handle; 762 const fd = stream.handle;
904 763
@@ -959,38 +818,19 @@ test "Server: replica rebuilt from snapshots matches the authoritative grid" {
959 test "Server: typing produces deltas, not snapshots; stats track both" { 818 test "Server: typing produces deltas, not snapshots; stats track both" {
960 const alloc = std.testing.allocator; 819 const alloc = std.testing.allocator;
961 820
962 var tmp = try TmpDir.make(); 821 var td = try h.TestDaemon.init(alloc, "delta", .{ .shell = "/bin/sh" });
963 defer tmp.cleanup(); 822 defer td.deinit();
964 const dir_path = tmp.path();
965 const sock_path = try std.fmt.allocPrint(alloc, "{s}/delta.sock", .{dir_path});
966 defer alloc.free(sock_path);
967 823
968 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 824 try td.threaded();
969 defer srv.deinit();
970
971 var stop = std.atomic.Value(bool).init(false);
972 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
973 defer th.join();
974 defer stop.store(true, .release);
975 825
976 const c = try dial.dialAttach(sock_path, 80, 24); 826 const c = try dial.dialAttach(td.sock_path, 80, 24);
977 defer c.close(); 827 defer c.close();
978 828
979 // The attach is answered with a full snapshot carrying the tracker seq. 829 // The attach is answered with a full snapshot carrying the tracker seq.
980 var attach_seq: u64 = 0; 830 var attach_seq: u64 = 0;
981 var deadline_ms: u64 = 10_000; 831 if (try h.awaitFrameOn(alloc, c.handle, .snapshot, 10_000)) |frame| {
982 while (deadline_ms > 0 and attach_seq == 0) {
983 var pfd = [_]std.posix.pollfd{
984 .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 },
985 };
986 const ready = try std.posix.poll(&pfd, 100);
987 deadline_ms -|= 100;
988 if (ready == 0) continue;
989 const frame = (try proto.readFrame(alloc, c.handle)) orelse break;
990 defer frame.deinit(alloc); 832 defer frame.deinit(alloc);
991 if (frame.type == .snapshot) { 833 attach_seq = (try proto.readSnapshotPrefix(frame.payload)).seq;
992 attach_seq = (try proto.readSnapshotPrefix(frame.payload)).seq;
993 }
994 } 834 }
995 try std.testing.expect(attach_seq > 0); 835 try std.testing.expect(attach_seq > 0);
996 836
@@ -998,7 +838,7 @@ test "Server: typing produces deltas, not snapshots; stats track both" {
998 // tracker isn't diffing, which is the regression this test guards. 838 // tracker isn't diffing, which is the regression this test guards.
999 try proto.writeFrame(c.handle, .input, "x"); 839 try proto.writeFrame(c.handle, .input, "x");
1000 var hdr: ?proto.DeltaHeader = null; 840 var hdr: ?proto.DeltaHeader = null;
1001 deadline_ms = 10_000; 841 var deadline_ms: u64 = 10_000;
1002 while (deadline_ms > 0 and hdr == null) { 842 while (deadline_ms > 0 and hdr == null) {
1003 var pfd = [_]std.posix.pollfd{ 843 var pfd = [_]std.posix.pollfd{
1004 .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 }, 844 .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 },
@@ -1032,18 +872,8 @@ test "Server: typing produces deltas, not snapshots; stats track both" {
1032 try proto.writeFrame(c.handle, .stats_req, ""); 872 try proto.writeFrame(c.handle, .stats_req, "");
1033 var stats_text: ?[]u8 = null; 873 var stats_text: ?[]u8 = null;
1034 defer if (stats_text) |s| alloc.free(s); 874 defer if (stats_text) |s| alloc.free(s);
1035 deadline_ms = 5000; 875 if (try h.awaitFrameOn(alloc, c.handle, .stats_reply, 5000)) |frame| {
1036 while (deadline_ms > 0 and stats_text == null) { 876 stats_text = frame.payload; // ownership taken
1037 var pfd = [_]std.posix.pollfd{
1038 .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 },
1039 };
1040 const ready = try std.posix.poll(&pfd, 100);
1041 deadline_ms -|= 100;
1042 if (ready == 0) continue;
1043 const frame = (try proto.readFrame(alloc, c.handle)) orelse break;
1044 if (frame.type == .stats_reply) {
1045 stats_text = frame.payload; // ownership taken
1046 } else frame.deinit(alloc);
1047 } 877 }
1048 try std.testing.expect(stats_text != null); 878 try std.testing.expect(stats_text != null);
1049 try std.testing.expect(std.mem.indexOf(u8, stats_text.?, "deltas=") != null); 879 try std.testing.expect(std.mem.indexOf(u8, stats_text.?, "deltas=") != null);
@@ -1053,26 +883,17 @@ test "Server: typing produces deltas, not snapshots; stats track both" {
1053 test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a delta" { 883 test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a delta" {
1054 const alloc = std.testing.allocator; 884 const alloc = std.testing.allocator;
1055 885
1056 var tmp = try TmpDir.make(); 886 var td = try h.TestDaemon.init(alloc, "resync", .{ .shell = "/bin/sh" });
1057 defer tmp.cleanup(); 887 defer td.deinit();
1058 const dir_path = tmp.path();
1059 const sock_path = try std.fmt.allocPrint(alloc, "{s}/resync.sock", .{dir_path});
1060 defer alloc.free(sock_path);
1061 888
1062 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 889 try td.threaded();
1063 defer srv.deinit();
1064
1065 var stop = std.atomic.Value(bool).init(false);
1066 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
1067 defer th.join();
1068 defer stop.store(true, .release);
1069 890
1070 // Session 1: attach cold, produce output, note the newest seq we hold 891 // Session 1: attach cold, produce output, note the newest seq we hold
1071 // and the session epoch the daemon stamps into its snapshots. 892 // and the session epoch the daemon stamps into its snapshots.
1072 var last_seq: u64 = 0; 893 var last_seq: u64 = 0;
1073 var epoch: u64 = 0; 894 var epoch: u64 = 0;
1074 { 895 {
1075 const c1 = try dial.dialAttach(sock_path, 80, 24); 896 const c1 = try dial.dialAttach(td.sock_path, 80, 24);
1076 defer c1.close(); 897 defer c1.close();
1077 // A resize is a discontinuity, so this pins reset_seq above 1 no 898 // A resize is a discontinuity, so this pins reset_seq above 1 no
1078 // matter how the shell's first output raced the attach — session 3 899 // matter how the shell's first output raced the attach — session 3
@@ -1093,7 +914,7 @@ test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a
1093 // instance. Those seqs describe a different history; honouring one would 914 // instance. Those seqs describe a different history; honouring one would
1094 // paint this session's screen with another's rows. Snapshot. 915 // paint this session's screen with another's rows. Snapshot.
1095 { 916 {
1096 const c2 = try dial.dial(sock_path); 917 const c2 = try dial.dial(td.sock_path);
1097 defer c2.close(); 918 defer c2.close();
1098 try proto.writeFrame(c2.handle, .attach, &proto.encodeAttach(80, 24, last_seq, epoch ^ 1)); 919 try proto.writeFrame(c2.handle, .attach, &proto.encodeAttach(80, 24, last_seq, epoch ^ 1));
1099 const first = try firstStateFrame(alloc, c2.handle, 10_000); 920 const first = try firstStateFrame(alloc, c2.handle, 10_000);
@@ -1110,7 +931,7 @@ test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a
1110 // Session 3: a real seq with have_epoch = 0 — a pre-epoch client, or one 931 // Session 3: a real seq with have_epoch = 0 — a pre-epoch client, or one
1111 // hoping 0 means "any". It means "none", and none is never serviceable. 932 // hoping 0 means "any". It means "none", and none is never serviceable.
1112 { 933 {
1113 const c3 = try dial.dial(sock_path); 934 const c3 = try dial.dial(td.sock_path);
1114 defer c3.close(); 935 defer c3.close();
1115 try proto.writeFrame(c3.handle, .attach, &proto.encodeAttach(80, 24, last_seq, 0)); 936 try proto.writeFrame(c3.handle, .attach, &proto.encodeAttach(80, 24, last_seq, 0));
1116 const first = try firstStateFrame(alloc, c3.handle, 10_000); 937 const first = try firstStateFrame(alloc, c3.handle, 10_000);
@@ -1123,7 +944,7 @@ test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a
1123 // Session 4: right seq, right epoch — we really are up to date, so the 944 // Session 4: right seq, right epoch — we really are up to date, so the
1124 // daemon owes us a delta (possibly empty), never a full repaint. 945 // daemon owes us a delta (possibly empty), never a full repaint.
1125 { 946 {
1126 const c4 = try dial.dial(sock_path); 947 const c4 = try dial.dial(td.sock_path);
1127 defer c4.close(); 948 defer c4.close();
1128 try proto.writeFrame(c4.handle, .attach, &proto.encodeAttach(80, 24, last_seq, epoch)); 949 try proto.writeFrame(c4.handle, .attach, &proto.encodeAttach(80, 24, last_seq, epoch));
1129 const first = try firstStateFrame(alloc, c4.handle, 10_000); 950 const first = try firstStateFrame(alloc, c4.handle, 10_000);
@@ -1135,7 +956,7 @@ test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a
1135 // Session 5: right epoch, but have_seq predates the last discontinuity, 956 // Session 5: right epoch, but have_seq predates the last discontinuity,
1136 // so no delta can reconstruct our state — full snapshot. 957 // so no delta can reconstruct our state — full snapshot.
1137 { 958 {
1138 const c5 = try dial.dial(sock_path); 959 const c5 = try dial.dial(td.sock_path);
1139 defer c5.close(); 960 defer c5.close();
1140 try proto.writeFrame(c5.handle, .attach, &proto.encodeAttach(80, 24, 1, epoch)); 961 try proto.writeFrame(c5.handle, .attach, &proto.encodeAttach(80, 24, 1, epoch));
1141 const first = try firstStateFrame(alloc, c5.handle, 10_000); 962 const first = try firstStateFrame(alloc, c5.handle, 10_000);
@@ -1230,20 +1051,14 @@ test "Server: a daemon restart invalidates have_seq even with the old epoch pres
1230 test "Server: injected bytes reach frame handling, split anywhere" { 1051 test "Server: injected bytes reach frame handling, split anywhere" {
1231 const alloc = std.testing.allocator; 1052 const alloc = std.testing.allocator;
1232 1053
1233 var tmp = try TmpDir.make(); 1054 var td = try h.TestDaemon.init(alloc, "inject", .{ .shell = "/bin/sh" });
1234 defer tmp.cleanup(); 1055 defer td.deinit();
1235 const dir_path = tmp.path();
1236 const sock_path = try std.fmt.allocPrint(alloc, "{s}/inject.sock", .{dir_path});
1237 defer alloc.free(sock_path);
1238
1239 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
1240 defer srv.deinit();
1241 1056
1242 // A real socket pair so the slot behaves like any other client: the 1057 // A real socket pair so the slot behaves like any other client: the
1243 // point of this test is the INBOUND route, not the outbound one. 1058 // point of this test is the INBOUND route, not the outbound one.
1244 const c = try connectedPair(); 1059 const c = try connectedPair();
1245 defer std.posix.close(c.peer); 1060 defer std.posix.close(c.peer);
1246 srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 }; 1061 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 };
1247 1062
1248 // A resize frame, built exactly as a client would send it, then fed in 1063 // A resize frame, built exactly as a client would send it, then fed in
1249 // as if it had arrived over some transport that is not this fd. The 1064 // as if it had arrived over some transport that is not this fd. The
@@ -1257,22 +1072,22 @@ test "Server: injected bytes reach frame handling, split anywhere" {
1257 // mid-payload, which is the shape stream transports actually deliver 1072 // mid-payload, which is the shape stream transports actually deliver
1258 // and the reason the slot carries an inbound buffer at all. 1073 // and the reason the slot carries an inbound buffer at all.
1259 for (frame.items) |b| { 1074 for (frame.items) |b| {
1260 try std.testing.expectEqual(@as(u16, 80), srv.colsNow(0)); // nothing yet 1075 try std.testing.expectEqual(@as(u16, 80), td.srv.colsNow(0)); // nothing yet
1261 srv.pushInbound(0, &.{b}); 1076 td.srv.pushInbound(0, &.{b});
1262 } 1077 }
1263 try std.testing.expectEqual(@as(u16, 100), srv.colsNow(0)); 1078 try std.testing.expectEqual(@as(u16, 100), td.srv.colsNow(0));
1264 try std.testing.expectEqual(@as(u16, 30), srv.rowsNow(0)); 1079 try std.testing.expectEqual(@as(u16, 30), td.srv.rowsNow(0));
1265 // Fully consumed: a frame that was handled must not sit in the buffer. 1080 // Fully consumed: a frame that was handled must not sit in the buffer.
1266 try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.inbound.items.len); 1081 try std.testing.expectEqual(@as(usize, 0), td.srv.clients[0].?.inbound.items.len);
1267 1082
1268 // Two frames in one delivery, the other shape a stream produces. 1083 // Two frames in one delivery, the other shape a stream produces.
1269 var pair: std.ArrayList(u8) = .empty; 1084 var pair: std.ArrayList(u8) = .empty;
1270 defer pair.deinit(alloc); 1085 defer pair.deinit(alloc);
1271 try proto.appendFrame(&pair, alloc, .resize, &proto.encodeSize(90, 20)); 1086 try proto.appendFrame(&pair, alloc, .resize, &proto.encodeSize(90, 20));
1272 try proto.appendFrame(&pair, alloc, .resize, &proto.encodeSize(120, 40)); 1087 try proto.appendFrame(&pair, alloc, .resize, &proto.encodeSize(120, 40));
1273 srv.pushInbound(0, pair.items); 1088 td.srv.pushInbound(0, pair.items);
1274 try std.testing.expectEqual(@as(u16, 120), srv.colsNow(0)); 1089 try std.testing.expectEqual(@as(u16, 120), td.srv.colsNow(0));
1275 try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.inbound.items.len); 1090 try std.testing.expectEqual(@as(usize, 0), td.srv.clients[0].?.inbound.items.len);
1276 } 1091 }
1277 1092
1278 /// The rescue thread completes the frame after a deadline, so a pump that 1093 /// The rescue thread completes the frame after a deadline, so a pump that
@@ -1298,18 +1113,12 @@ const HalfFrameRescue = struct {
1298 test "Server: a client that sends half a frame does not stall the pump" { 1113 test "Server: a client that sends half a frame does not stall the pump" {
1299 const alloc = std.testing.allocator; 1114 const alloc = std.testing.allocator;
1300 1115
1301 var tmp = try TmpDir.make(); 1116 var td = try h.TestDaemon.init(alloc, "half", .{ .shell = "/bin/sh" });
1302 defer tmp.cleanup(); 1117 defer td.deinit();
1303 const dir_path = tmp.path();
1304 const sock_path = try std.fmt.allocPrint(alloc, "{s}/half.sock", .{dir_path});
1305 defer alloc.free(sock_path);
1306
1307 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
1308 defer srv.deinit();
1309 1118
1310 const c = try connectedPair(); 1119 const c = try connectedPair();
1311 defer std.posix.close(c.peer); 1120 defer std.posix.close(c.peer);
1312 srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 }; 1121 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 };
1313 1122
1314 var frame: std.ArrayList(u8) = .empty; 1123 var frame: std.ArrayList(u8) = .empty;
1315 defer frame.deinit(alloc); 1124 defer frame.deinit(alloc);
@@ -1325,21 +1134,21 @@ test "Server: a client that sends half a frame does not stall the pump" {
1325 1134
1326 // One pump with a 20ms poll must come back on its own — the readable 1135 // One pump with a 20ms poll must come back on its own — the readable
1327 // socket is serviced, the partial frame is held, and the loop returns. 1136 // socket is serviced, the partial frame is held, and the loop returns.
1328 try srv.pumpOnce(20); 1137 try td.srv.pumpOnce(20);
1329 rescue.stop.store(true, .release); 1138 rescue.stop.store(true, .release);
1330 if (rescue.fired.load(.acquire)) { 1139 if (rescue.fired.load(.acquire)) {
1331 std.debug.print("pumpOnce blocked on a half frame; the rescue thread had to complete it\n", .{}); 1140 std.debug.print("pumpOnce blocked on a half frame; the rescue thread had to complete it\n", .{});
1332 return error.PumpBlockedOnHalfFrame; 1141 return error.PumpBlockedOnHalfFrame;
1333 } 1142 }
1334 try std.testing.expectEqual(@as(u16, 80), srv.colsNow(0)); // nothing applied yet 1143 try std.testing.expectEqual(@as(u16, 80), td.srv.colsNow(0)); // nothing applied yet
1335 try std.testing.expect(srv.clients[0] != null); // and nobody was dropped 1144 try std.testing.expect(td.srv.clients[0] != null); // and nobody was dropped
1336 1145
1337 // The rest arrives; the frame completes on the next pump. 1146 // The rest arrives; the frame completes on the next pump.
1338 try proto.writeAllFd(c.peer, frame.items[3..]); 1147 try proto.writeAllFd(c.peer, frame.items[3..]);
1339 try srv.pumpOnce(20); 1148 try td.srv.pumpOnce(20);
1340 try std.testing.expectEqual(@as(u16, 100), srv.colsNow(0)); 1149 try std.testing.expectEqual(@as(u16, 100), td.srv.colsNow(0));
1341 try std.testing.expectEqual(@as(u16, 30), srv.rowsNow(0)); 1150 try std.testing.expectEqual(@as(u16, 30), td.srv.rowsNow(0));
1342 try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.inbound.items.len); 1151 try std.testing.expectEqual(@as(usize, 0), td.srv.clients[0].?.inbound.items.len);
1343 } 1152 }
1344 1153
1345 /// What a client would be holding if it detached now: the newest seq it has 1154 /// What a client would be holding if it detached now: the newest seq it has
@@ -1382,70 +1191,60 @@ fn drainHeld(alloc: std.mem.Allocator, fd: std.posix.fd_t, timeout_ms: u64) !Hel
1382 test "Server: a session is created at the attacher's size" { 1191 test "Server: a session is created at the attacher's size" {
1383 const alloc = std.testing.allocator; 1192 const alloc = std.testing.allocator;
1384 1193
1385 var tmp = try TmpDir.make(); 1194 var td = try h.TestDaemon.init(alloc, "bigsize", .{ .shell = "/bin/cat" });
1386 defer tmp.cleanup(); 1195 defer td.deinit();
1387 const sock_path = try std.fmt.allocPrint(alloc, "{s}/bigsize.sock", .{tmp.path()});
1388 defer alloc.free(sock_path);
1389
1390 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
1391 defer srv.deinit();
1392 1196
1393 const c = try dial.dialAttachNamed(sock_path, 100, 30, "big"); 1197 const c = try dial.dialAttachNamed(td.sock_path, 100, 30, "big");
1394 defer c.close(); 1198 defer c.close();
1395 const f = (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400)) orelse 1199 const f = (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400)) orelse
1396 return error.NoSnapshot; 1200 return error.NoSnapshot;
1397 defer f.deinit(alloc); 1201 defer f.deinit(alloc);
1398 const p = try proto.readSnapshotPrefix(f.payload); 1202 const p = try proto.readSnapshotPrefix(f.payload);
1399 try std.testing.expectEqual(@as(u16, 100), p.cols); 1203 try std.testing.expectEqual(@as(u16, 100), p.cols);
1400 try std.testing.expectEqual(@as(u16, 30), p.rows); 1204 try std.testing.expectEqual(@as(u16, 30), p.rows);
1401 1205
1402 const si = srv.sessions.find("big") orelse return error.NotCreated; 1206 const si = td.srv.sessions.find("big") orelse return error.NotCreated;
1403 try std.testing.expectEqual(@as(u16, 100), srv.colsNow(si)); 1207 try std.testing.expectEqual(@as(u16, 100), td.srv.colsNow(si));
1404 try std.testing.expectEqual(@as(u16, 30), srv.rowsNow(si)); 1208 try std.testing.expectEqual(@as(u16, 30), td.srv.rowsNow(si));
1405 // The default session was not dragged to the new attacher's size. 1209 // The default session was not dragged to the new attacher's size.
1406 try std.testing.expectEqual(@as(u16, 80), srv.colsNow(0)); 1210 try std.testing.expectEqual(@as(u16, 80), td.srv.colsNow(0));
1407 } 1211 }
1408 1212
1409 test "Server: a 0x0 attach joins but never creates" { 1213 test "Server: a 0x0 attach joins but never creates" {
1410 const alloc = std.testing.allocator; 1214 const alloc = std.testing.allocator;
1411 1215
1412 var tmp = try TmpDir.make(); 1216 var td = try h.TestDaemon.init(alloc, "zerosize", .{ .shell = "/bin/cat" });
1413 defer tmp.cleanup(); 1217 defer td.deinit();
1414 const sock_path = try std.fmt.allocPrint(alloc, "{s}/zerosize.sock", .{tmp.path()});
1415 defer alloc.free(sock_path);
1416
1417 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
1418 defer srv.deinit();
1419 1218
1420 // mux a send attaches at 0x0 — it makes no size claim. Against a name 1219 // mux a send attaches at 0x0 — it makes no size claim. Against a name
1421 // that does not exist, that must be a refusal, never a shell spawned 1220 // that does not exist, that must be a refusal, never a shell spawned
1422 // at a size nobody has. 1221 // at a size nobody has.
1423 const c1 = try dial.dialAttachNamed(sock_path, 0, 0, "b"); 1222 const c1 = try dial.dialAttachNamed(td.sock_path, 0, 0, "b");
1424 defer c1.close(); 1223 defer c1.close();
1425 const f1 = (try awaitFrame(alloc, &srv, c1.handle, .exit_status, 400)) orelse 1224 const f1 = (try awaitFrame(alloc, &td.srv, c1.handle, .exit_status, 400)) orelse
1426 return error.NoRefusal; 1225 return error.NoRefusal;
1427 defer f1.deinit(alloc); 1226 defer f1.deinit(alloc);
1428 try std.testing.expect(f1.payload.len == 1 and f1.payload[0] == 1); 1227 try std.testing.expect(f1.payload.len == 1 and f1.payload[0] == 1);
1429 try std.testing.expect(srv.sessions.table[1] == null); 1228 try std.testing.expect(td.srv.sessions.table[1] == null);
1430 1229
1431 // Create it properly, at a real size... 1230 // Create it properly, at a real size...
1432 const c2 = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 1231 const c2 = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
1433 defer c2.close(); 1232 defer c2.close();
1434 const f2 = (try awaitFrame(alloc, &srv, c2.handle, .snapshot, 400)) orelse 1233 const f2 = (try awaitFrame(alloc, &td.srv, c2.handle, .snapshot, 400)) orelse
1435 return error.NoSnapshotOnCreate; 1234 return error.NoSnapshotOnCreate;
1436 f2.deinit(alloc); 1235 f2.deinit(alloc);
1437 const si_b = srv.sessions.find("b") orelse return error.NotCreated; 1236 const si_b = td.srv.sessions.find("b") orelse return error.NotCreated;
1438 1237
1439 // ...and the very same 0x0 attach now joins it. 1238 // ...and the very same 0x0 attach now joins it.
1440 const c3 = try dial.dialAttachNamed(sock_path, 0, 0, "b"); 1239 const c3 = try dial.dialAttachNamed(td.sock_path, 0, 0, "b");
1441 defer c3.close(); 1240 defer c3.close();
1442 const f3 = (try awaitFrame(alloc, &srv, c3.handle, .snapshot, 400)) orelse 1241 const f3 = (try awaitFrame(alloc, &td.srv, c3.handle, .snapshot, 400)) orelse
1443 return error.ZeroSizeJoinRefused; 1242 return error.ZeroSizeJoinRefused;
1444 f3.deinit(alloc); 1243 f3.deinit(alloc);
1445 1244
1446 // Joined, not spawned: both clients point at the one "b". 1245 // Joined, not spawned: both clients point at the one "b".
1447 var joined: usize = 0; 1246 var joined: usize = 0;
1448 for (srv.clients) |slot| { 1247 for (td.srv.clients) |slot| {
1449 const cs = slot orelse continue; 1248 const cs = slot orelse continue;
1450 if ((cs.session orelse continue) == si_b) joined += 1; 1249 if ((cs.session orelse continue) == si_b) joined += 1;
1451 } 1250 }
@@ -1455,13 +1254,8 @@ test "Server: a 0x0 attach joins but never creates" {
1455 test "Server: a 1x1 attach joins but never creates" { 1254 test "Server: a 1x1 attach joins but never creates" {
1456 const alloc = std.testing.allocator; 1255 const alloc = std.testing.allocator;
1457 1256
1458 var tmp = try TmpDir.make(); 1257 var td = try h.TestDaemon.init(alloc, "tinysize", .{ .shell = "/bin/cat" });
1459 defer tmp.cleanup(); 1258 defer td.deinit();
1460 const sock_path = try std.fmt.allocPrint(alloc, "{s}/tinysize.sock", .{tmp.path()});
1461 defer alloc.free(sock_path);
1462
1463 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
1464 defer srv.deinit();
1465 1259
1466 // Unlike 0x0, nothing spells passivity as 1x1 — which is exactly why 1260 // Unlike 0x0, nothing spells passivity as 1x1 — which is exactly why
1467 // this case needs its own pin. 0x0 is a contract and gets refused by 1261 // this case needs its own pin. 0x0 is a contract and gets refused by
@@ -1473,33 +1267,33 @@ test "Server: a 1x1 attach joins but never creates" {
1473 // grid to 1x1, so a session created at 1x1 could never be resized by 1267 // grid to 1x1, so a session created at 1x1 could never be resized by
1474 // the client that caused it — a shell nobody can use, spawned by 1268 // the client that caused it — a shell nobody can use, spawned by
1475 // attaching with a name nobody had created yet. 1269 // attaching with a name nobody had created yet.
1476 const c1 = try dial.dialAttachNamed(sock_path, 1, 1, "tile"); 1270 const c1 = try dial.dialAttachNamed(td.sock_path, 1, 1, "tile");
1477 defer c1.close(); 1271 defer c1.close();
1478 const f1 = (try awaitFrame(alloc, &srv, c1.handle, .exit_status, 400)) orelse 1272 const f1 = (try awaitFrame(alloc, &td.srv, c1.handle, .exit_status, 400)) orelse
1479 return error.NoRefusal; 1273 return error.NoRefusal;
1480 defer f1.deinit(alloc); 1274 defer f1.deinit(alloc);
1481 try std.testing.expect(f1.payload.len == 1 and f1.payload[0] == 1); 1275 try std.testing.expect(f1.payload.len == 1 and f1.payload[0] == 1);
1482 try std.testing.expect(srv.sessions.find("tile") == null); 1276 try std.testing.expect(td.srv.sessions.find("tile") == null);
1483 1277
1484 // Created at a real size by someone who has one... 1278 // Created at a real size by someone who has one...
1485 const c2 = try dial.dialAttachNamed(sock_path, 80, 24, "tile"); 1279 const c2 = try dial.dialAttachNamed(td.sock_path, 80, 24, "tile");
1486 defer c2.close(); 1280 defer c2.close();
1487 const f2 = (try awaitFrame(alloc, &srv, c2.handle, .snapshot, 400)) orelse 1281 const f2 = (try awaitFrame(alloc, &td.srv, c2.handle, .snapshot, 400)) orelse
1488 return error.NoSnapshotOnCreate; 1282 return error.NoSnapshotOnCreate;
1489 f2.deinit(alloc); 1283 f2.deinit(alloc);
1490 const si = srv.sessions.find("tile") orelse return error.NotCreated; 1284 const si = td.srv.sessions.find("tile") orelse return error.NotCreated;
1491 1285
1492 // ...and the same 1x1 attach now JOINS it, leaving the grid alone. 1286 // ...and the same 1x1 attach now JOINS it, leaving the grid alone.
1493 // Refusing the creation must not cost the client its view: a terminal 1287 // Refusing the creation must not cost the client its view: a terminal
1494 // too small to spawn a session is still allowed to watch one, and may 1288 // too small to spawn a session is still allowed to watch one, and may
1495 // never be the reason one exists or the reason one resizes. 1289 // never be the reason one exists or the reason one resizes.
1496 const c3 = try dial.dialAttachNamed(sock_path, 1, 1, "tile"); 1290 const c3 = try dial.dialAttachNamed(td.sock_path, 1, 1, "tile");
1497 defer c3.close(); 1291 defer c3.close();
1498 const f3 = (try awaitFrame(alloc, &srv, c3.handle, .snapshot, 400)) orelse 1292 const f3 = (try awaitFrame(alloc, &td.srv, c3.handle, .snapshot, 400)) orelse
1499 return error.TinyJoinRefused; 1293 return error.TinyJoinRefused;
1500 f3.deinit(alloc); 1294 f3.deinit(alloc);
1501 try std.testing.expectEqual(@as(u16, 80), srv.colsNow(si)); 1295 try std.testing.expectEqual(@as(u16, 80), td.srv.colsNow(si));
1502 try std.testing.expectEqual(@as(u16, 24), srv.rowsNow(si)); 1296 try std.testing.expectEqual(@as(u16, 24), td.srv.rowsNow(si));
1503 } 1297 }
1504 1298
1505 // --------------------------------------------------------------------------- 1299 // ---------------------------------------------------------------------------
@@ -1512,63 +1306,54 @@ test "Server: a 1x1 attach joins but never creates" {
1512 test "Server: attaching seats a client in activity order, and typing or resizing reorders it" { 1306 test "Server: attaching seats a client in activity order, and typing or resizing reorders it" {
1513 const alloc = std.testing.allocator; 1307 const alloc = std.testing.allocator;
1514 1308
1515 var tmp = try TmpDir.make(); 1309 var td = try h.TestDaemon.open(alloc, "activity");
1516 defer tmp.cleanup(); 1310 defer td.deinit();
1517 const sock_path = try std.fmt.allocPrint(alloc, "{s}/activity.sock", .{tmp.path()});
1518 defer alloc.free(sock_path);
1519 1311
1520 // /bin/cat never prints unprompted, so every bump below is one this test 1312 // /bin/cat never prints unprompted, so every bump below is one this test
1521 // caused rather than a prompt redraw's. 1313 // caused rather than a prompt redraw's.
1522 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 1314 try td.start(.{ .shell = "/bin/cat" });
1523 defer srv.deinit();
1524 1315
1525 const ca = try dial.dialAttachNamed(sock_path, 80, 24, ""); 1316 const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "");
1526 defer ca.close(); 1317 defer ca.close();
1527 var spun: usize = 0; 1318 var spun: usize = 0;
1528 while (spun < 200 and srv.clients[0] == null) : (spun += 1) try srv.pumpOnce(5); 1319 while (spun < 200 and td.srv.clients[0] == null) : (spun += 1) try td.srv.pumpOnce(5);
1529 1320
1530 // Seated one at a time so the slot indices below are the attach order: 1321 // Seated one at a time so the slot indices below are the attach order:
1531 // freeClientSlot hands out the lowest free slot. 1322 // freeClientSlot hands out the lowest free slot.
1532 const cb = try dial.dialAttachNamed(sock_path, 80, 24, ""); 1323 const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "");
1533 defer cb.close(); 1324 defer cb.close();
1534 spun = 0; 1325 spun = 0;
1535 while (spun < 200 and srv.clients[1] == null) : (spun += 1) try srv.pumpOnce(5); 1326 while (spun < 200 and td.srv.clients[1] == null) : (spun += 1) try td.srv.pumpOnce(5);
1536 1327
1537 const a_attached = (srv.clients[0] orelse return error.ClientANeverSeated).activity; 1328 const a_attached = (td.srv.clients[0] orelse return error.ClientANeverSeated).activity;
1538 const b_attached = (srv.clients[1] orelse return error.ClientBNeverSeated).activity; 1329 const b_attached = (td.srv.clients[1] orelse return error.ClientBNeverSeated).activity;
1539 // Strict, not >=: the clock is a counter, so two attaches can never tie. 1330 // Strict, not >=: the clock is a counter, so two attaches can never tie.
1540 try std.testing.expect(b_attached > a_attached); 1331 try std.testing.expect(b_attached > a_attached);
1541 1332
1542 // Typing is activity too, and it takes the lead back. 1333 // Typing is activity too, and it takes the lead back.
1543 try proto.writeFrame(ca.handle, .input, "x"); 1334 try proto.writeFrame(ca.handle, .input, "x");
1544 spun = 0; 1335 spun = 0;
1545 while (spun < 200 and srv.clients[0].?.activity == a_attached) : (spun += 1) { 1336 while (spun < 200 and td.srv.clients[0].?.activity == a_attached) : (spun += 1) {
1546 try srv.pumpOnce(5); 1337 try td.srv.pumpOnce(5);
1547 } 1338 }
1548 try std.testing.expect(srv.clients[0].?.activity > srv.clients[1].?.activity); 1339 try std.testing.expect(td.srv.clients[0].?.activity > td.srv.clients[1].?.activity);
1549 1340
1550 // Resizing is the third activity verb, and B dragging its window back is 1341 // Resizing is the third activity verb, and B dragging its window back is
1551 // enough to take the lead without B typing a character. 1342 // enough to take the lead without B typing a character.
1552 const a_typed = srv.clients[0].?.activity; 1343 const a_typed = td.srv.clients[0].?.activity;
1553 try proto.writeFrame(cb.handle, .resize, &proto.encodeSize(100, 30)); 1344 try proto.writeFrame(cb.handle, .resize, &proto.encodeSize(100, 30));
1554 spun = 0; 1345 spun = 0;
1555 while (spun < 200 and srv.clients[1].?.activity < a_typed) : (spun += 1) { 1346 while (spun < 200 and td.srv.clients[1].?.activity < a_typed) : (spun += 1) {
1556 try srv.pumpOnce(5); 1347 try td.srv.pumpOnce(5);
1557 } 1348 }
1558 try std.testing.expect(srv.clients[1].?.activity > srv.clients[0].?.activity); 1349 try std.testing.expect(td.srv.clients[1].?.activity > td.srv.clients[0].?.activity);
1559 } 1350 }
1560 1351
1561 test "Server: a slot promoted before it attached enters the activity order at its own attach" { 1352 test "Server: a slot promoted before it attached enters the activity order at its own attach" {
1562 const alloc = std.testing.allocator; 1353 const alloc = std.testing.allocator;
1563 1354
1564 var tmp = try TmpDir.make(); 1355 var td = try h.TestDaemon.init(alloc, "quicorder", .{ .shell = "/bin/cat" });
1565 defer tmp.cleanup(); 1356 defer td.deinit();
1566 const dir_path = tmp.path();
1567 const sock_path = try std.fmt.allocPrint(alloc, "{s}/quicorder.sock", .{dir_path});
1568 defer alloc.free(sock_path);
1569
1570 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
1571 defer srv.deinit();
1572 1357
1573 const c = try connectedPair(); 1358 const c = try connectedPair();
1574 defer std.posix.close(c.peer); 1359 defer std.posix.close(c.peer);
@@ -1578,8 +1363,8 @@ test "Server: a slot promoted before it attached enters the activity order at it
1578 // path's bump cannot cover this one, and the rest of the suite attaches 1363 // path's bump cannot cover this one, and the rest of the suite attaches
1579 // over sockets — so without this test, deleting handleFrame's bump stays 1364 // over sockets — so without this test, deleting handleFrame's bump stays
1580 // green (the hazard the reattach test above records hitting already). 1365 // green (the hazard the reattach test above records hitting already).
1581 srv.clients[0] = .{ .sink = .{ .socket = c.daemon } }; 1366 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon } };
1582 try std.testing.expectEqual(@as(u64, 0), srv.clients[0].?.activity); 1367 try std.testing.expectEqual(@as(u64, 0), td.srv.clients[0].?.activity);
1583 1368
1584 var frame: std.ArrayList(u8) = .empty; 1369 var frame: std.ArrayList(u8) = .empty;
1585 defer frame.deinit(alloc); 1370 defer frame.deinit(alloc);
@@ -1587,36 +1372,30 @@ test "Server: a slot promoted before it attached enters the activity order at it
1587 1372
1588 // pushInbound dispatches the frame inline, so no pump is needed to see 1373 // pushInbound dispatches the frame inline, so no pump is needed to see
1589 // the arm's effect. 1374 // the arm's effect.
1590 srv.pushInbound(0, frame.items); 1375 td.srv.pushInbound(0, frame.items);
1591 // Off 0 is the claim: 0 means seated but never attached, which is 1376 // Off 0 is the claim: 0 means seated but never attached, which is
1592 // precisely the slot an activity ranking must not pick. 1377 // precisely the slot an activity ranking must not pick.
1593 const first = srv.clients[0].?.activity; 1378 const first = td.srv.clients[0].?.activity;
1594 try std.testing.expect(first > 0); 1379 try std.testing.expect(first > 0);
1595 1380
1596 // The same arm serves a reconnect over a live connection, and that is 1381 // The same arm serves a reconnect over a live connection, and that is
1597 // activity again rather than a no-op. 1382 // activity again rather than a no-op.
1598 srv.pushInbound(0, frame.items); 1383 td.srv.pushInbound(0, frame.items);
1599 try std.testing.expect(srv.clients[0].?.activity > first); 1384 try std.testing.expect(td.srv.clients[0].?.activity > first);
1600 } 1385 }
1601 1386
1602 test "Server: an observer that sends one byte does not stall the pump, and finishes its frame later" { 1387 test "Server: an observer that sends one byte does not stall the pump, and finishes its frame later" {
1603 const alloc = std.testing.allocator; 1388 const alloc = std.testing.allocator;
1604 1389
1605 var tmp = try TmpDir.make(); 1390 var td = try h.TestDaemon.init(alloc, "obs", .{ .shell = "/bin/sh" });
1606 defer tmp.cleanup(); 1391 defer td.deinit();
1607 const dir_path = tmp.path();
1608 const sock_path = try std.fmt.allocPrint(alloc, "{s}/obs.sock", .{dir_path});
1609 defer alloc.free(sock_path);
1610
1611 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
1612 defer srv.deinit();
1613 1392
1614 // Through the real listener, so the accept path seats it as the daemon 1393 // Through the real listener, so the accept path seats it as the daemon
1615 // would seat `mux d stats`. 1394 // would seat `mux d stats`.
1616 const obs = try dial.dial(sock_path); 1395 const obs = try dial.dial(td.sock_path);
1617 defer obs.close(); 1396 defer obs.close();
1618 try srv.pumpOnce(20); // accept 1397 try td.srv.pumpOnce(20); // accept
1619 try std.testing.expect(srv.observers[0] != null); 1398 try std.testing.expect(td.srv.observers[0] != null);
1620 1399
1621 var frame: std.ArrayList(u8) = .empty; 1400 var frame: std.ArrayList(u8) = .empty;
1622 defer frame.deinit(alloc); 1401 defer frame.deinit(alloc);
@@ -1626,25 +1405,25 @@ test "Server: an observer that sends one byte does not stall the pump, and finis
1626 var rescue: HalfFrameRescue = .{ .peer = obs.handle, .rest = frame.items[1..] }; 1405 var rescue: HalfFrameRescue = .{ .peer = obs.handle, .rest = frame.items[1..] };
1627 const th = try std.Thread.spawn(.{}, HalfFrameRescue.run, .{&rescue}); 1406 const th = try std.Thread.spawn(.{}, HalfFrameRescue.run, .{&rescue});
1628 defer th.join(); 1407 defer th.join();
1629 try srv.pumpOnce(20); 1408 try td.srv.pumpOnce(20);
1630 rescue.stop.store(true, .release); 1409 rescue.stop.store(true, .release);
1631 if (rescue.fired.load(.acquire)) { 1410 if (rescue.fired.load(.acquire)) {
1632 std.debug.print("pumpOnce blocked on a one-byte observer; the rescue thread had to complete it\n", .{}); 1411 std.debug.print("pumpOnce blocked on a one-byte observer; the rescue thread had to complete it\n", .{});
1633 return error.PumpBlockedOnHalfFrame; 1412 return error.PumpBlockedOnHalfFrame;
1634 } 1413 }
1635 try std.testing.expect(srv.observers[0] != null); // held, not dropped 1414 try std.testing.expect(td.srv.observers[0] != null); // held, not dropped
1636 1415
1637 // A second observer is served while the first is still mid-frame: the 1416 // A second observer is served while the first is still mid-frame: the
1638 // daemon is answering everyone, which is the whole claim. 1417 // daemon is answering everyone, which is the whole claim.
1639 const obs2 = try dial.dial(sock_path); 1418 const obs2 = try dial.dial(td.sock_path);
1640 defer obs2.close(); 1419 defer obs2.close();
1641 try proto.writeFrame(obs2.handle, .stats_req, ""); 1420 try proto.writeFrame(obs2.handle, .stats_req, "");
1642 const r2 = (try awaitFrame(alloc, &srv, obs2.handle, .stats_reply, 100)) orelse 1421 const r2 = (try awaitFrame(alloc, &td.srv, obs2.handle, .stats_reply, 100)) orelse
1643 return error.NoStatsReplyWhileAnotherObserverStalls; 1422 return error.NoStatsReplyWhileAnotherObserverStalls;
1644 r2.deinit(alloc); 1423 r2.deinit(alloc);
1645 1424
1646 try proto.writeAllFd(obs.handle, frame.items[1..]); 1425 try proto.writeAllFd(obs.handle, frame.items[1..]);
1647 const r1 = (try awaitFrame(alloc, &srv, obs.handle, .stats_reply, 100)) orelse 1426 const r1 = (try awaitFrame(alloc, &td.srv, obs.handle, .stats_reply, 100)) orelse
1648 return error.NoStatsReplyAfterCompletion; 1427 return error.NoStatsReplyAfterCompletion;
1649 r1.deinit(alloc); 1428 r1.deinit(alloc);
1650 } 1429 }
@@ -1652,44 +1431,38 @@ test "Server: an observer that sends one byte does not stall the pump, and finis
1652 test "Server: stalled observers are dropped on the idle deadline, and the table is free again" { 1431 test "Server: stalled observers are dropped on the idle deadline, and the table is free again" {
1653 const alloc = std.testing.allocator; 1432 const alloc = std.testing.allocator;
1654 1433
1655 var tmp = try TmpDir.make(); 1434 var td = try h.TestDaemon.init(alloc, "idle", .{ .shell = "/bin/sh" });
1656 defer tmp.cleanup(); 1435 defer td.deinit();
1657 const dir_path = tmp.path();
1658 const sock_path = try std.fmt.allocPrint(alloc, "{s}/idle.sock", .{dir_path});
1659 defer alloc.free(sock_path);
1660
1661 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
1662 defer srv.deinit();
1663 1436
1664 // Every slot, not one: the failure this pins is a table full of peers 1437 // Every slot, not one: the failure this pins is a table full of peers
1665 // that each wrote one byte and went quiet, so the next `mux` is closed 1438 // that each wrote one byte and went quiet, so the next `mux` is closed
1666 // at accept with nothing said. 1439 // at accept with nothing said.
1667 var peers: [srv_mod.max_observers]std.net.Stream = undefined; 1440 var peers: [srv_mod.max_observers]std.net.Stream = undefined;
1668 for (&peers) |*p| { 1441 for (&peers) |*p| {
1669 p.* = try dial.dial(sock_path); 1442 p.* = try dial.dial(td.sock_path);
1670 try proto.writeAllFd(p.handle, &[_]u8{@intFromEnum(proto.MsgType.stats_req)}); 1443 try proto.writeAllFd(p.handle, &[_]u8{@intFromEnum(proto.MsgType.stats_req)});
1671 try srv.pumpOnce(20); 1444 try td.srv.pumpOnce(20);
1672 } 1445 }
1673 defer for (peers) |p| p.close(); 1446 defer for (peers) |p| p.close();
1674 for (srv.observers) |o| try std.testing.expect(o != null); 1447 for (td.srv.observers) |o| try std.testing.expect(o != null);
1675 1448
1676 // Armed only once every peer is seated: peer 0's clock starts at ITS 1449 // Armed only once every peer is seated: peer 0's clock starts at ITS
1677 // accept, three seating rounds before this assertion, and a Debug 1450 // accept, three seating rounds before this assertion, and a Debug
1678 // ghostty on a loaded box takes longer than the deadline to get here. 1451 // ghostty on a loaded box takes longer than the deadline to get here.
1679 // A gate that fails for its own setup's timing pins nothing. 1452 // A gate that fails for its own setup's timing pins nothing.
1680 srv.observer_idle_ms = 50; 1453 td.srv.observer_idle_ms = 50;
1681 1454
1682 std.Thread.sleep(60 * std.time.ns_per_ms); 1455 std.Thread.sleep(60 * std.time.ns_per_ms);
1683 try srv.pumpOnce(20); 1456 try td.srv.pumpOnce(20);
1684 for (srv.observers) |o| try std.testing.expect(o == null); 1457 for (td.srv.observers) |o| try std.testing.expect(o == null);
1685 // Dropped means closed: the peer reads EOF, not a hang. 1458 // Dropped means closed: the peer reads EOF, not a hang.
1686 var b: [1]u8 = undefined; 1459 var b: [1]u8 = undefined;
1687 try std.testing.expectEqual(@as(usize, 0), try std.posix.read(peers[0].handle, &b)); 1460 try std.testing.expectEqual(@as(usize, 0), try std.posix.read(peers[0].handle, &b));
1688 1461
1689 const fresh = try dial.dial(sock_path); 1462 const fresh = try dial.dial(td.sock_path);
1690 defer fresh.close(); 1463 defer fresh.close();
1691 try proto.writeFrame(fresh.handle, .stats_req, ""); 1464 try proto.writeFrame(fresh.handle, .stats_req, "");
1692 const r = (try awaitFrame(alloc, &srv, fresh.handle, .stats_reply, 100)) orelse 1465 const r = (try awaitFrame(alloc, &td.srv, fresh.handle, .stats_reply, 100)) orelse
1693 return error.NoStatsReplyAfterIdleReap; 1466 return error.NoStatsReplyAfterIdleReap;
1694 r.deinit(alloc); 1467 r.deinit(alloc);
1695 } 1468 }
@@ -1697,16 +1470,10 @@ test "Server: stalled observers are dropped on the idle deadline, and the table
1697 test "Server: bytes after an attach in the same write reach the promoted client" { 1470 test "Server: bytes after an attach in the same write reach the promoted client" {
1698 const alloc = std.testing.allocator; 1471 const alloc = std.testing.allocator;
1699 1472
1700 var tmp = try TmpDir.make(); 1473 var td = try h.TestDaemon.init(alloc, "promote", .{ .shell = "/bin/sh" });
1701 defer tmp.cleanup(); 1474 defer td.deinit();
1702 const dir_path = tmp.path();
1703 const sock_path = try std.fmt.allocPrint(alloc, "{s}/promote.sock", .{dir_path});
1704 defer alloc.free(sock_path);
1705 1475
1706 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 1476 const c = try dial.dial(td.sock_path);
1707 defer srv.deinit();
1708
1709 const c = try dial.dial(sock_path);
1710 defer c.close(); 1477 defer c.close();
1711 1478
1712 // attach + resize in ONE write: the resize lands in the observer's 1479 // attach + resize in ONE write: the resize lands in the observer's
@@ -1718,42 +1485,37 @@ test "Server: bytes after an attach in the same write reach the promoted client"
1718 try proto.writeAllFd(c.handle, both.items); 1485 try proto.writeAllFd(c.handle, both.items);
1719 1486
1720 // awaitFrame, not firstStateFrame: this test owns the pump itself (it 1487 // awaitFrame, not firstStateFrame: this test owns the pump itself (it
1721 // reads srv state below, which a server thread would race), and 1488 // reads td.srv state below, which a server thread would race), and
1722 // firstStateFrame only polls the peer. 1489 // firstStateFrame only polls the peer.
1723 const first = (try awaitFrame(alloc, &srv, c.handle, .snapshot, 200)) orelse 1490 const first = (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 200)) orelse
1724 return error.NoSnapshotAfterAttach; 1491 return error.NoSnapshotAfterAttach;
1725 first.deinit(alloc); 1492 first.deinit(alloc);
1726 var i: usize = 0; 1493 var i: usize = 0;
1727 while (i < 100 and srv.colsNow(0) != 132) : (i += 1) try srv.pumpOnce(5); 1494 while (i < 100 and td.srv.colsNow(0) != 132) : (i += 1) try td.srv.pumpOnce(5);
1728 try std.testing.expectEqual(@as(u16, 132), srv.colsNow(0)); 1495 try std.testing.expectEqual(@as(u16, 132), td.srv.colsNow(0));
1729 try std.testing.expectEqual(@as(u16, 50), srv.rowsNow(0)); 1496 try std.testing.expectEqual(@as(u16, 50), td.srv.rowsNow(0));
1730 // Nothing left behind on either side of the promotion. 1497 // Nothing left behind on either side of the promotion.
1731 for (srv.observers) |o| try std.testing.expect(o == null); 1498 for (td.srv.observers) |o| try std.testing.expect(o == null);
1732 try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.inbound.items.len); 1499 try std.testing.expectEqual(@as(usize, 0), td.srv.clients[0].?.inbound.items.len);
1733 } 1500 }
1734 1501
1735 test "Server: an observer that never reads its replies is dropped, not allowed to stop the pump" { 1502 test "Server: an observer that never reads its replies is dropped, not allowed to stop the pump" {
1736 const alloc = std.testing.allocator; 1503 const alloc = std.testing.allocator;
1737 1504
1738 var tmp = try TmpDir.make(); 1505 var td = try h.TestDaemon.init(alloc, "nagger", .{ .shell = "/bin/sh" });
1739 defer tmp.cleanup(); 1506 defer td.deinit();
1740 const sock_path = try std.fmt.allocPrint(alloc, "{s}/nagger.sock", .{tmp.path()});
1741 defer alloc.free(sock_path);
1742
1743 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
1744 defer srv.deinit();
1745 // Long, so the drop under test is the write's answer and not the idle 1507 // Long, so the drop under test is the write's answer and not the idle
1746 // reaper's — a nagging peer is never idle anyway. 1508 // reaper's — a nagging peer is never idle anyway.
1747 srv.observer_idle_ms = 60_000; 1509 td.srv.observer_idle_ms = 60_000;
1748 1510
1749 const peer = try dial.dial(sock_path); 1511 const peer = try dial.dial(td.sock_path);
1750 defer peer.close(); 1512 defer peer.close();
1751 try srv.pumpOnce(20); 1513 try td.srv.pumpOnce(20);
1752 // Every reply is up to sessions_text_len bytes and the peer never reads 1514 // Every reply is up to sessions_text_len bytes and the peer never reads
1753 // one, so a small send buffer is what an unread socket looks like a few 1515 // one, so a small send buffer is what an unread socket looks like a few
1754 // frames sooner. Set on the DAEMON's end: for AF_UNIX it is the sender's 1516 // frames sooner. Set on the DAEMON's end: for AF_UNIX it is the sender's
1755 // buffer that fills. 1517 // buffer that fills.
1756 const fd = srv.observers[0].?.fd; 1518 const fd = td.srv.observers[0].?.fd;
1757 const sndbuf: c_int = 4096; 1519 const sndbuf: c_int = 4096;
1758 try std.posix.setsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.SNDBUF, std.mem.asBytes(&sndbuf)); 1520 try std.posix.setsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.SNDBUF, std.mem.asBytes(&sndbuf));
1759 1521
@@ -1765,29 +1527,24 @@ test "Server: an observer that never reads its replies is dropped, not allowed t
1765 try proto.writeAllFd(peer.handle, burst.items); 1527 try proto.writeAllFd(peer.handle, burst.items);
1766 1528
1767 const t0 = std.time.milliTimestamp(); 1529 const t0 = std.time.milliTimestamp();
1768 try srv.pumpOnce(20); 1530 try td.srv.pumpOnce(20);
1769 const spent = std.time.milliTimestamp() - t0; 1531 const spent = std.time.milliTimestamp() - t0;
1770 // The claim: the pump came back. A blocking write here never returns 1532 // The claim: the pump came back. A blocking write here never returns
1771 // until the peer reads, and no session on the box is served meanwhile. 1533 // until the peer reads, and no session on the box is served meanwhile.
1772 try std.testing.expect(spent < 5_000); 1534 try std.testing.expect(spent < 5_000);
1773 try std.testing.expect(srv.observers[0] == null); 1535 try std.testing.expect(td.srv.observers[0] == null);
1774 } 1536 }
1775 1537
1776 test "Server: a burst of observer frames is answered across pumps, never all in one" { 1538 test "Server: a burst of observer frames is answered across pumps, never all in one" {
1777 const alloc = std.testing.allocator; 1539 const alloc = std.testing.allocator;
1778 1540
1779 var tmp = try TmpDir.make(); 1541 var td = try h.TestDaemon.init(alloc, "burst", .{ .shell = "/bin/sh" });
1780 defer tmp.cleanup(); 1542 defer td.deinit();
1781 const sock_path = try std.fmt.allocPrint(alloc, "{s}/burst.sock", .{tmp.path()}); 1543 td.srv.observer_idle_ms = 60_000;
1782 defer alloc.free(sock_path);
1783
1784 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
1785 defer srv.deinit();
1786 srv.observer_idle_ms = 60_000;
1787 1544
1788 const peer = try dial.dial(sock_path); 1545 const peer = try dial.dial(td.sock_path);
1789 defer peer.close(); 1546 defer peer.close();
1790 try srv.pumpOnce(20); 1547 try td.srv.pumpOnce(20);
1791 1548
1792 const over = srv_mod.max_observer_frames_per_pump + 3; 1549 const over = srv_mod.max_observer_frames_per_pump + 3;
1793 var burst: std.ArrayList(u8) = .empty; 1550 var burst: std.ArrayList(u8) = .empty;
@@ -1795,23 +1552,23 @@ test "Server: a burst of observer frames is answered across pumps, never all in
1795 for (0..over) |_| try proto.appendFrame(&burst, alloc, .sessions_req, ""); 1552 for (0..over) |_| try proto.appendFrame(&burst, alloc, .sessions_req, "");
1796 try proto.writeAllFd(peer.handle, burst.items); 1553 try proto.writeAllFd(peer.handle, burst.items);
1797 1554
1798 try srv.pumpOnce(20); 1555 try td.srv.pumpOnce(20);
1799 try std.testing.expect(srv.observers[0] != null); 1556 try std.testing.expect(td.srv.observers[0] != null);
1800 // The cap is what bounds one pump's work; the leftovers are still owed. 1557 // The cap is what bounds one pump's work; the leftovers are still owed.
1801 try std.testing.expect(srv.observers[0].?.inbound.items.len > 0); 1558 try std.testing.expect(td.srv.observers[0].?.inbound.items.len > 0);
1802 1559
1803 // And owed means answered without another byte from the peer: a burst 1560 // And owed means answered without another byte from the peer: a burst
1804 // bigger than the cap must not sit in the buffer until the peer 1561 // bigger than the cap must not sit in the buffer until the peer
1805 // happens to write again. 1562 // happens to write again.
1806 var pumps: usize = 0; 1563 var pumps: usize = 0;
1807 while (srv.observers[0] != null and srv.observers[0].?.inbound.items.len > 0 and pumps < 20) : (pumps += 1) 1564 while (td.srv.observers[0] != null and td.srv.observers[0].?.inbound.items.len > 0 and pumps < 20) : (pumps += 1)
1808 try srv.pumpOnce(20); 1565 try td.srv.pumpOnce(20);
1809 try std.testing.expect(srv.observers[0] != null); 1566 try std.testing.expect(td.srv.observers[0] != null);
1810 try std.testing.expectEqual(@as(usize, 0), srv.observers[0].?.inbound.items.len); 1567 try std.testing.expectEqual(@as(usize, 0), td.srv.observers[0].?.inbound.items.len);
1811 1568
1812 // Every one of them was answered, in order, with the whole list. 1569 // Every one of them was answered, in order, with the whole list.
1813 for (0..over) |_| { 1570 for (0..over) |_| {
1814 const r = (try awaitFrame(alloc, &srv, peer.handle, .sessions_reply, 200)) orelse 1571 const r = (try awaitFrame(alloc, &td.srv, peer.handle, .sessions_reply, 200)) orelse
1815 return error.MissingReply; 1572 return error.MissingReply;
1816 defer r.deinit(alloc); 1573 defer r.deinit(alloc);
1817 try std.testing.expectEqualStrings("0", r.payload); 1574 try std.testing.expectEqualStrings("0", r.payload);
@@ -1821,15 +1578,12 @@ test "Server: a burst of observer frames is answered across pumps, never all in
1821 test "Server: the two bounded deadlines read a monotonic clock, not the calendar" { 1578 test "Server: the two bounded deadlines read a monotonic clock, not the calendar" {
1822 const alloc = std.testing.allocator; 1579 const alloc = std.testing.allocator;
1823 1580
1824 var tmp = try TmpDir.make(); 1581 var td = try h.TestDaemon.open(alloc, "mono");
1825 defer tmp.cleanup(); 1582 defer td.deinit();
1826 const sock_path = try std.fmt.allocPrint(alloc, "{s}/mono.sock", .{tmp.path()}); 1583 const script = try h.writeStubbornShell(alloc, &td.tmp);
1827 defer alloc.free(sock_path);
1828 const script = try h.writeStubbornShell(alloc, &tmp);
1829 defer alloc.free(script); 1584 defer alloc.free(script);
1830 1585
1831 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 1586 try td.start(.{ .shell = script });
1832 defer srv.deinit();
1833 1587
1834 // CLOCK_MONOTONIC counts from boot and CLOCK_REALTIME from 1970, so any 1588 // CLOCK_MONOTONIC counts from boot and CLOCK_REALTIME from 1970, so any
1835 // machine that has been up less than a decade separates them by a 1589 // machine that has been up less than a decade separates them by a
@@ -1840,19 +1594,19 @@ test "Server: the two bounded deadlines read a monotonic clock, not the calendar
1840 // observer on a step forwards. 1594 // observer on a step forwards.
1841 const wall_now = std.time.milliTimestamp(); 1595 const wall_now = std.time.milliTimestamp();
1842 1596
1843 const obs = try dial.dial(sock_path); 1597 const obs = try dial.dial(td.sock_path);
1844 defer obs.close(); 1598 defer obs.close();
1845 try srv.pumpOnce(20); 1599 try td.srv.pumpOnce(20);
1846 try std.testing.expect(srv.observers[0] != null); 1600 try std.testing.expect(td.srv.observers[0] != null);
1847 try std.testing.expect(srv.observers[0].?.since_ms < wall_now - 1_000_000); 1601 try std.testing.expect(td.srv.observers[0].?.since_ms < wall_now - 1_000_000);
1848 1602
1849 var rq: [proto.end_req_max_len]u8 = undefined; 1603 var rq: [proto.end_req_max_len]u8 = undefined;
1850 try proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, true, "")); 1604 try proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, true, ""));
1851 const r = (try awaitFrame(alloc, &srv, obs.handle, .end_reply, 400)) orelse 1605 const r = (try awaitFrame(alloc, &td.srv, obs.handle, .end_reply, 400)) orelse
1852 return error.NoEndReply; 1606 return error.NoEndReply;
1853 defer r.deinit(alloc); 1607 defer r.deinit(alloc);
1854 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted); 1608 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);
1855 const by = srv.sessions.table[0].?.end_by_ms orelse return error.NoDeadline; 1609 const by = td.srv.sessions.table[0].?.end_by_ms orelse return error.NoDeadline;
1856 try std.testing.expect(by < wall_now - 1_000_000); 1610 try std.testing.expect(by < wall_now - 1_000_000);
1857 try std.testing.expect(by > srv_mod.monoMs()); 1611 try std.testing.expect(by > srv_mod.monoMs());
1858 } 1612 }
@@ -1860,18 +1614,13 @@ test "Server: the two bounded deadlines read a monotonic clock, not the calendar
1860 test "Server: an observer that dribbles a huge frame is dropped at the cap, not held to the idle deadline" { 1614 test "Server: an observer that dribbles a huge frame is dropped at the cap, not held to the idle deadline" {
1861 const alloc = std.testing.allocator; 1615 const alloc = std.testing.allocator;
1862 1616
1863 var tmp = try TmpDir.make(); 1617 var td = try h.TestDaemon.init(alloc, "fat", .{ .shell = "/bin/sh" });
1864 defer tmp.cleanup(); 1618 defer td.deinit();
1865 const sock_path = try std.fmt.allocPrint(alloc, "{s}/fat.sock", .{tmp.path()});
1866 defer alloc.free(sock_path);
1867
1868 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
1869 defer srv.deinit();
1870 1619
1871 const peer = try dial.dial(sock_path); 1620 const peer = try dial.dial(td.sock_path);
1872 defer peer.close(); 1621 defer peer.close();
1873 try srv.pumpOnce(20); 1622 try td.srv.pumpOnce(20);
1874 try std.testing.expect(srv.observers[0] != null); 1623 try std.testing.expect(td.srv.observers[0] != null);
1875 1624
1876 // A header claiming a payload the protocol admits (16 MB is the CLIENT 1625 // A header claiming a payload the protocol admits (16 MB is the CLIENT
1877 // path's paste ceiling), then a dribble. Nothing an observer asks for 1626 // path's paste ceiling), then a dribble. Nothing an observer asks for
@@ -1887,8 +1636,8 @@ test "Server: an observer that dribbles a huge frame is dropped at the cap, not
1887 @memset(dribble, 'x'); 1636 @memset(dribble, 'x');
1888 try proto.writeAllFd(peer.handle, dribble); 1637 try proto.writeAllFd(peer.handle, dribble);
1889 1638
1890 try srv.pumpOnce(20); 1639 try td.srv.pumpOnce(20);
1891 try std.testing.expect(srv.observers[0] == null); 1640 try std.testing.expect(td.srv.observers[0] == null);
1892 1641
1893 // And the cap still clears a real upgrade_req: the longest thing an 1642 // And the cap still clears a real upgrade_req: the longest thing an
1894 // observer legitimately sends is a path. 1643 // observer legitimately sends is a path.
src/server/server_test_await.zig
Old New
@@ -14,16 +14,13 @@ const connectedPair = h.connectedPair;
14 test "Server: OSC 133 marks reach attached clients as cmd_state pushes" { 14 test "Server: OSC 133 marks reach attached clients as cmd_state pushes" {
15 const alloc = std.testing.allocator; 15 const alloc = std.testing.allocator;
16 16
17 var tmp = try TmpDir.make(); 17 var td = try h.TestDaemon.open(alloc, "marks");
18 defer tmp.cleanup(); 18 defer td.deinit();
19 const dir_path = tmp.path();
20 const sock_path = try std.fmt.allocPrint(alloc, "{s}/marks.sock", .{dir_path});
21 defer alloc.free(sock_path);
22 19
23 // A SCRIPT rather than an interactive shell: an integrated shell emits marks 20 // A SCRIPT rather than an interactive shell: an integrated shell emits marks
24 // continuously, so "which push came from which mark" would be a guess. It 21 // continuously, so "which push came from which mark" would be a guess. It
25 // ends on a blocking read, so `deinit`'s SIGTERM lands on the shell itself. 22 // ends on a blocking read, so `deinit`'s SIGTERM lands on the shell itself.
26 try tmp.dir.writeFile(.{ 23 try td.tmp.dir.writeFile(.{
27 .sub_path = "marks.sh", 24 .sub_path = "marks.sh",
28 .data = 25 .data =
29 \\#!/bin/sh 26 \\#!/bin/sh
@@ -36,13 +33,12 @@ test "Server: OSC 133 marks reach attached clients as cmd_state pushes" {
36 , 33 ,
37 .flags = .{ .mode = 0o755 }, 34 .flags = .{ .mode = 0o755 },
38 }); 35 });
39 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/marks.sh", .{dir_path}, 0); 36 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/marks.sh", .{td.tmp.path()}, 0);
40 defer alloc.free(script); 37 defer alloc.free(script);
41 38
42 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 39 try td.start(.{ .shell = script });
43 defer srv.deinit();
44 40
45 const c = try dial.dialAttach(sock_path, 80, 24); 41 const c = try dial.dialAttach(td.sock_path, 80, 24);
46 defer c.close(); 42 defer c.close();
47 43
48 // Release the script into its C. The keystroke is echoed by the line 44 // Release the script into its C. The keystroke is echoed by the line
@@ -50,7 +46,7 @@ test "Server: OSC 133 marks reach attached clients as cmd_state pushes" {
50 // mark — which is the point: the push has to survive sharing a read 46 // mark — which is the point: the push has to survive sharing a read
51 // with ordinary output. 47 // with ordinary output.
52 try proto.writeFrame(c.handle, .input, "go\n"); 48 try proto.writeFrame(c.handle, .input, "go\n");
53 const f1 = (try awaitFrame(alloc, &srv, c.handle, .cmd_state, 500)) orelse 49 const f1 = (try awaitFrame(alloc, &td.srv, c.handle, .cmd_state, 500)) orelse
54 return error.NoRunningPush; 50 return error.NoRunningPush;
55 defer f1.deinit(alloc); 51 defer f1.deinit(alloc);
56 const running = try proto.decodeCmdState(f1.payload); 52 const running = try proto.decodeCmdState(f1.payload);
@@ -61,7 +57,7 @@ test "Server: OSC 133 marks reach attached clients as cmd_state pushes" {
61 57
62 // And into its D, which closes the command with a code. 58 // And into its D, which closes the command with a code.
63 try proto.writeFrame(c.handle, .input, "go\n"); 59 try proto.writeFrame(c.handle, .input, "go\n");
64 const f2 = (try awaitFrame(alloc, &srv, c.handle, .cmd_state, 500)) orelse 60 const f2 = (try awaitFrame(alloc, &td.srv, c.handle, .cmd_state, 500)) orelse
65 return error.NoReturnedPush; 61 return error.NoReturnedPush;
66 defer f2.deinit(alloc); 62 defer f2.deinit(alloc);
67 const returned = try proto.decodeCmdState(f2.payload); 63 const returned = try proto.decodeCmdState(f2.payload);
@@ -77,7 +73,7 @@ test "Server: OSC 133 marks reach attached clients as cmd_state pushes" {
77 // for its own chunk, so it covers the command's output. Ordering only: a C 73 // for its own chunk, so it covers the command's output. Ordering only: a C
78 // and a D in one read legitimately share a seq, hence `>=`. 74 // and a D in one read legitimately share a seq, hence `>=`.
79 try std.testing.expect(returned.seq >= running.seq); 75 try std.testing.expect(returned.seq >= running.seq);
80 try std.testing.expect(returned.seq <= srv.sessions.table[0].?.tracker.seq); 76 try std.testing.expect(returned.seq <= td.srv.sessions.table[0].?.tracker.seq);
81 try std.testing.expect(returned.seq > 0); 77 try std.testing.expect(returned.seq > 0);
82 } 78 }
83 79
@@ -195,18 +191,13 @@ test "Server: the injection is off unless the caller asks for it" {
195 // would pass this whichever way the default points, and prove nothing. 191 // would pass this whichever way the default points, and prove nothing.
196 std.fs.cwd().access("/bin/bash", .{}) catch return error.SkipZigTest; 192 std.fs.cwd().access("/bin/bash", .{}) catch return error.SkipZigTest;
197 193
198 var tmp = try TmpDir.make(); 194 var td = try h.TestDaemon.init(alloc, "nomarks", .{ .shell = "/bin/bash" });
199 defer tmp.cleanup(); 195 defer td.deinit();
200 const sock_path = try std.fmt.allocPrint(alloc, "{s}/nomarks.sock", .{tmp.path()});
201 defer alloc.free(sock_path);
202
203 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/bash" });
204 defer srv.deinit();
205 196
206 // No shim directory at all — the user's ZDOTDIR and DEBUG trap are their 197 // No shim directory at all — the user's ZDOTDIR and DEBUG trap are their
207 // own. `IntegratedSession` is the other side of this pin: it asks for the 198 // own. `IntegratedSession` is the other side of this pin: it asks for the
208 // injection and asserts the directory exists. 199 // injection and asserts the directory exists.
209 try std.testing.expect(srv.shellint_dir == null); 200 try std.testing.expect(td.srv.shellint_dir == null);
210 } 201 }
211 202
212 test "Server: bash emits one mark pair per command, and none at an idle prompt" { 203 test "Server: bash emits one mark pair per command, and none at an idle prompt" {
@@ -302,28 +293,25 @@ test "Server: the shim directory is private, and teardown takes it with it" {
302 const alloc = std.testing.allocator; 293 const alloc = std.testing.allocator;
303 std.fs.cwd().access("/bin/bash", .{}) catch return error.SkipZigTest; 294 std.fs.cwd().access("/bin/bash", .{}) catch return error.SkipZigTest;
304 295
305 var tmp = try TmpDir.make(); 296 var td = try h.TestDaemon.open(alloc, "shim");
306 defer tmp.cleanup(); 297 defer td.deinit();
307 const sock_path = try std.fmt.allocPrint(alloc, "{s}/shim.sock", .{tmp.path()});
308 defer alloc.free(sock_path);
309 298
310 // Copied out of the server's arena before deinit frees it: the whole 299 // Copied out of the server's arena before deinit frees it: the whole
311 // point of this test is to ask a question after the server is gone. 300 // point of this test is to ask a question after the server is gone.
312 var dir_buf: [256]u8 = undefined; 301 var dir_buf: [256]u8 = undefined;
313 var dir: []const u8 = undefined; 302 var dir: []const u8 = undefined;
314 { 303 {
315 var srv = try Server.init(alloc, .{ 304 try td.start(.{
316 .sock_path = sock_path,
317 .shell = "/bin/bash", 305 .shell = "/bin/bash",
318 .shell_integration = true, 306 .shell_integration = true,
319 }); 307 });
320 defer srv.deinit(); 308 defer td.shutdown();
321 dir = try std.fmt.bufPrint(&dir_buf, "{s}", .{srv.shellint_dir.?}); 309 dir = try std.fmt.bufPrint(&dir_buf, "{s}", .{td.srv.shellint_dir.?});
322 310
323 // Beside the socket, not somewhere world-readable: the file the 311 // Beside the socket, not somewhere world-readable: the file the
324 // session shell is about to source is a file that runs code as this 312 // session shell is about to source is a file that runs code as this
325 // user, so 0700 on the directory is part of the contract. 313 // user, so 0700 on the directory is part of the contract.
326 try std.testing.expectEqualStrings(std.fs.path.dirname(sock_path).?, std.fs.path.dirname(dir).?); 314 try std.testing.expectEqualStrings(std.fs.path.dirname(td.sock_path).?, std.fs.path.dirname(dir).?);
327 var d = try std.fs.cwd().openDir(dir, .{ .iterate = true }); 315 var d = try std.fs.cwd().openDir(dir, .{ .iterate = true });
328 defer d.close(); 316 defer d.close();
329 const st = try d.stat(); 317 const st = try d.stat();
@@ -339,10 +327,8 @@ test "Server: an entry already at the daemon's pid name costs neither the marks
339 const alloc = std.testing.allocator; 327 const alloc = std.testing.allocator;
340 std.fs.cwd().access("/bin/bash", .{}) catch return error.SkipZigTest; 328 std.fs.cwd().access("/bin/bash", .{}) catch return error.SkipZigTest;
341 329
342 var tmp = try TmpDir.make(); 330 var td = try h.TestDaemon.open(alloc, "degrade");
343 defer tmp.cleanup(); 331 defer td.deinit();
344 const sock_path = try std.fmt.allocPrint(alloc, "{s}/degrade.sock", .{tmp.path()});
345 defer alloc.free(sock_path);
346 332
347 // Plant a regular FILE at the pid-derived name, which is what a daemon 333 // Plant a regular FILE at the pid-derived name, which is what a daemon
348 // SIGKILLed before teardown leaves behind and what a stranger with write 334 // SIGKILLed before teardown leaves behind and what a stranger with write
@@ -351,35 +337,34 @@ test "Server: an entry already at the daemon's pid name costs neither the marks
351 const planted = try std.fmt.allocPrint( 337 const planted = try std.fmt.allocPrint(
352 alloc, 338 alloc,
353 "{s}/mux-shellint-{d}", 339 "{s}/mux-shellint-{d}",
354 .{ tmp.path(), std.os.linux.getpid() }, 340 .{ td.tmp.path(), std.os.linux.getpid() },
355 ); 341 );
356 defer alloc.free(planted); 342 defer alloc.free(planted);
357 try std.fs.cwd().writeFile(.{ .sub_path = planted, .data = "not a directory" }); 343 try std.fs.cwd().writeFile(.{ .sub_path = planted, .data = "not a directory" });
358 344
359 // Scoped so deinit runs before the survival check below. 345 // Scoped so deinit runs before the survival check below.
360 { 346 {
361 var srv = try Server.init(alloc, .{ 347 try td.start(.{
362 .sock_path = sock_path,
363 .shell = "/bin/bash", 348 .shell = "/bin/bash",
364 .shell_integration = true, 349 .shell_integration = true,
365 }); 350 });
366 defer srv.deinit(); 351 defer td.shutdown();
367 352
368 // The shim name carries a random suffix past the pid, so the planted 353 // The shim name carries a random suffix past the pid, so the planted
369 // entry is not in the way of anything: this session gets its marks. 354 // entry is not in the way of anything: this session gets its marks.
370 // Before that suffix existed, a collision here cost the session its 355 // Before that suffix existed, a collision here cost the session its
371 // marks for the lifetime of the daemon. 356 // marks for the lifetime of the daemon.
372 const dir = srv.shellint_dir orelse return error.NoShimDirectory; 357 const dir = td.srv.shellint_dir orelse return error.NoShimDirectory;
373 try std.testing.expect(std.mem.startsWith(u8, dir, planted)); 358 try std.testing.expect(std.mem.startsWith(u8, dir, planted));
374 try std.testing.expect(dir.len > planted.len); 359 try std.testing.expect(dir.len > planted.len);
375 try std.fs.cwd().access(dir, .{}); 360 try std.fs.cwd().access(dir, .{});
376 361
377 // ...and the session actually works. A daemon that starts and then 362 // ...and the session actually works. A daemon that starts and then
378 // cannot answer would satisfy every assertion above. 363 // cannot answer would satisfy every assertion above.
379 const c = try dial.dialAttach(sock_path, 80, 24); 364 const c = try dial.dialAttach(td.sock_path, 80, 24);
380 defer c.close(); 365 defer c.close();
381 try proto.writeFrame(c.handle, .status_req, ""); 366 try proto.writeFrame(c.handle, .status_req, "");
382 const f = (try awaitFrame(alloc, &srv, c.handle, .status_reply, 400)) orelse 367 const f = (try awaitFrame(alloc, &td.srv, c.handle, .status_reply, 400)) orelse
383 return error.NoStatusReply; 368 return error.NoStatusReply;
384 defer f.deinit(alloc); 369 defer f.deinit(alloc);
385 const st = try proto.decodeStatusReply(f.payload); 370 const st = try proto.decodeStatusReply(f.payload);
@@ -397,20 +382,17 @@ test "Server: an entry already at the daemon's pid name costs neither the marks
397 test "Server: an unknown shell is not injected into at all — no directory, no shim" { 382 test "Server: an unknown shell is not injected into at all — no directory, no shim" {
398 const alloc = std.testing.allocator; 383 const alloc = std.testing.allocator;
399 384
400 var tmp = try TmpDir.make(); 385 var td = try h.TestDaemon.open(alloc, "plain");
401 defer tmp.cleanup(); 386 defer td.deinit();
402 const sock_path = try std.fmt.allocPrint(alloc, "{s}/plain.sock", .{tmp.path()});
403 defer alloc.free(sock_path);
404 387
405 // Integration is ON, and /bin/sh still gets nothing: this is what keeps 388 // Integration is ON, and /bin/sh still gets nothing: this is what keeps
406 // every other test in this file describing the session it always did. 389 // every other test in this file describing the session it always did.
407 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 390 try td.start(.{ .shell = "/bin/sh" });
408 defer srv.deinit(); 391 try std.testing.expectEqual(@as(?[]const u8, null), td.srv.shellint_dir);
409 try std.testing.expectEqual(@as(?[]const u8, null), srv.shellint_dir);
410 392
411 // Nothing was written beside the socket either — the absence is on disk, 393 // Nothing was written beside the socket either — the absence is on disk,
412 // not merely in a field the teardown consults. 394 // not merely in a field the teardown consults.
413 var d = try std.fs.cwd().openDir(tmp.path(), .{ .iterate = true }); 395 var d = try std.fs.cwd().openDir(td.tmp.path(), .{ .iterate = true });
414 defer d.close(); 396 defer d.close();
415 var it = d.iterate(); 397 var it = d.iterate();
416 while (try it.next()) |entry| { 398 while (try it.next()) |entry| {
@@ -447,11 +429,8 @@ fn readQueued(alloc: std.mem.Allocator, fd: std.posix.fd_t, want: proto.MsgType)
447 test "Server: an await is held open, answered by a mark, and re-answered immediately after" { 429 test "Server: an await is held open, answered by a mark, and re-answered immediately after" {
448 const alloc = std.testing.allocator; 430 const alloc = std.testing.allocator;
449 431
450 var tmp = try TmpDir.make(); 432 var td = try h.TestDaemon.open(alloc, "awaitmark");
451 defer tmp.cleanup(); 433 defer td.deinit();
452 const dir_path = tmp.path();
453 const sock_path = try std.fmt.allocPrint(alloc, "{s}/awaitmark.sock", .{dir_path});
454 defer alloc.free(sock_path);
455 434
456 // Script-gated, so there is a moment at which "no reply yet" is a claim 435 // Script-gated, so there is a moment at which "no reply yet" is a claim
457 // worth making. The BURST is the point of its shape: real integration writes 436 // worth making. The BURST is the point of its shape: real integration writes
@@ -459,7 +438,7 @@ test "Server: an await is held open, answered by a mark, and re-answered immedia
459 // inside one pty read and the phase is back at `at_prompt` before any await 438 // inside one pty read and the phase is back at `at_prompt` before any await
460 // is examined. Stopping at the D passes an implementation that could only 439 // is examined. Stopping at the D passes an implementation that could only
461 // answer in the sliver between the two. 440 // answer in the sliver between the two.
462 try tmp.dir.writeFile(.{ 441 try td.tmp.dir.writeFile(.{
463 .sub_path = "await.sh", 442 .sub_path = "await.sh",
464 .data = 443 .data =
465 \\#!/bin/sh 444 \\#!/bin/sh
@@ -470,19 +449,18 @@ test "Server: an await is held open, answered by a mark, and re-answered immedia
470 , 449 ,
471 .flags = .{ .mode = 0o755 }, 450 .flags = .{ .mode = 0o755 },
472 }); 451 });
473 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/await.sh", .{dir_path}, 0); 452 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/await.sh", .{td.tmp.path()}, 0);
474 defer alloc.free(script); 453 defer alloc.free(script);
475 454
476 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 455 try td.start(.{ .shell = script });
477 defer srv.deinit();
478 456
479 const c = try dial.dialAttach(sock_path, 80, 24); 457 const c = try dial.dialAttach(td.sock_path, 80, 24);
480 defer c.close(); 458 defer c.close();
481 459
482 // since_seq is "what I already know about": only a return NEWER than this 460 // since_seq is "what I already know about": only a return NEWER than this
483 // may answer. Nothing has returned on this session at all. 461 // may answer. Nothing has returned on this session at all.
484 try proto.writeFrame(c.handle, .await_req, &proto.encodeAwaitReq(.{ 462 try proto.writeFrame(c.handle, .await_req, &proto.encodeAwaitReq(.{
485 .since_seq = srv.sessions.table[0].?.tracker.seq, 463 .since_seq = td.srv.sessions.table[0].?.tracker.seq,
486 .settle_ms = 0, 464 .settle_ms = 0,
487 .timeout_ms = 5000, 465 .timeout_ms = 5000,
488 })); 466 }));
@@ -490,7 +468,7 @@ test "Server: an await is held open, answered by a mark, and re-answered immedia
490 // Held open: no mark has landed, no settle floor was asked for, the 468 // Held open: no mark has landed, no settle floor was asked for, the
491 // timeout is seconds away, and this shell never moves its fg pgid. There 469 // timeout is seconds away, and this shell never moves its fg pgid. There
492 // is nothing that may honestly answer yet, so nothing must. 470 // is nothing that may honestly answer yet, so nothing must.
493 if (try awaitFrame(alloc, &srv, c.handle, .await_reply, 40)) |early| { 471 if (try awaitFrame(alloc, &td.srv, c.handle, .await_reply, 40)) |early| {
494 early.deinit(alloc); 472 early.deinit(alloc);
495 return error.AwaitAnsweredBeforeAnythingHappened; 473 return error.AwaitAnsweredBeforeAnythingHappened;
496 } 474 }
@@ -498,7 +476,7 @@ test "Server: an await is held open, answered by a mark, and re-answered immedia
498 // Release the script into its C-output-D. All three land in one pty read, 476 // Release the script into its C-output-D. All three land in one pty read,
499 // so the await resolves in the same pump that pushed the transitions. 477 // so the await resolves in the same pump that pushed the transitions.
500 try proto.writeFrame(c.handle, .input, "go\n"); 478 try proto.writeFrame(c.handle, .input, "go\n");
501 const f = (try awaitFrame(alloc, &srv, c.handle, .await_reply, 500)) orelse 479 const f = (try awaitFrame(alloc, &td.srv, c.handle, .await_reply, 500)) orelse
502 return error.NoAwaitReplyFromMark; 480 return error.NoAwaitReplyFromMark;
503 defer f.deinit(alloc); 481 defer f.deinit(alloc);
504 const rep = try proto.decodeAwaitReply(f.payload); 482 const rep = try proto.decodeAwaitReply(f.payload);
@@ -517,7 +495,7 @@ test "Server: an await is held open, answered by a mark, and re-answered immedia
517 .settle_ms = 0, 495 .settle_ms = 0,
518 .timeout_ms = 5000, 496 .timeout_ms = 5000,
519 })); 497 }));
520 try srv.pumpOnce(5); 498 try td.srv.pumpOnce(5);
521 const f2 = (try readQueued(alloc, c.handle, .await_reply)) orelse 499 const f2 = (try readQueued(alloc, c.handle, .await_reply)) orelse
522 return error.AwaitNotAnsweredOnTheSamePump; 500 return error.AwaitNotAnsweredOnTheSamePump;
523 defer f2.deinit(alloc); 501 defer f2.deinit(alloc);
@@ -530,9 +508,9 @@ test "Server: an await is held open, answered by a mark, and re-answered immedia
530 // rather than learning nothing until the next transition. The EXIT CODE 508 // rather than learning nothing until the next transition. The EXIT CODE
531 // carries the news: an untouched tracker reports null, so 3 means this push 509 // carries the news: an untouched tracker reports null, so 3 means this push
532 // came from the command that ran. 510 // came from the command that ran.
533 const late = try dial.dialAttach(sock_path, 80, 24); 511 const late = try dial.dialAttach(td.sock_path, 80, 24);
534 defer late.close(); 512 defer late.close();
535 const f3 = (try awaitFrame(alloc, &srv, late.handle, .cmd_state, 200)) orelse 513 const f3 = (try awaitFrame(alloc, &td.srv, late.handle, .cmd_state, 200)) orelse
536 return error.NoCmdStateOnAttach; 514 return error.NoCmdStateOnAttach;
537 defer f3.deinit(alloc); 515 defer f3.deinit(alloc);
538 const st3 = try proto.decodeCmdState(f3.payload); 516 const st3 = try proto.decodeCmdState(f3.payload);
@@ -543,16 +521,13 @@ test "Server: an await is held open, answered by a mark, and re-answered immedia
543 test "Server: a return is still answerable once the next command is running" { 521 test "Server: a return is still answerable once the next command is running" {
544 const alloc = std.testing.allocator; 522 const alloc = std.testing.allocator;
545 523
546 var tmp = try TmpDir.make(); 524 var td = try h.TestDaemon.open(alloc, "awaitnext");
547 defer tmp.cleanup(); 525 defer td.deinit();
548 const dir_path = tmp.path();
549 const sock_path = try std.fmt.allocPrint(alloc, "{s}/awaitnext.sock", .{dir_path});
550 defer alloc.free(sock_path);
551 526
552 // One command that finishes with code 3, then a second that stays running. 527 // One command that finishes with code 3, then a second that stays running.
553 // The LIVE tracker is describing the second by the time the await is asked, 528 // The LIVE tracker is describing the second by the time the await is asked,
554 // so anything answered out of it answers about the wrong command. 529 // so anything answered out of it answers about the wrong command.
555 try tmp.dir.writeFile(.{ 530 try td.tmp.dir.writeFile(.{
556 .sub_path = "next.sh", 531 .sub_path = "next.sh",
557 .data = 532 .data =
558 \\#!/bin/sh 533 \\#!/bin/sh
@@ -565,13 +540,12 @@ test "Server: a return is still answerable once the next command is running" {
565 , 540 ,
566 .flags = .{ .mode = 0o755 }, 541 .flags = .{ .mode = 0o755 },
567 }); 542 });
568 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/next.sh", .{dir_path}, 0); 543 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/next.sh", .{td.tmp.path()}, 0);
569 defer alloc.free(script); 544 defer alloc.free(script);
570 545
571 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 546 try td.start(.{ .shell = script });
572 defer srv.deinit();
573 547
574 const c = try dial.dialAttach(sock_path, 80, 24); 548 const c = try dial.dialAttach(td.sock_path, 80, 24);
575 defer c.close(); 549 defer c.close();
576 550
577 // Run the first command to completion, then start the second and wait 551 // Run the first command to completion, then start the second and wait
@@ -579,10 +553,10 @@ test "Server: a return is still answerable once the next command is running" {
579 try proto.writeFrame(c.handle, .input, "go\n"); 553 try proto.writeFrame(c.handle, .input, "go\n");
580 try proto.writeFrame(c.handle, .input, "go\n"); 554 try proto.writeFrame(c.handle, .input, "go\n");
581 var spun: usize = 0; 555 var spun: usize = 0;
582 while (spun < 500 and srv.sessions.table[0].?.cmd.phase != .running) : (spun += 1) { 556 while (spun < 500 and td.srv.sessions.table[0].?.cmd.phase != .running) : (spun += 1) {
583 try srv.pumpOnce(5); 557 try td.srv.pumpOnce(5);
584 } 558 }
585 try std.testing.expectEqual(proto.CmdPhase.running, srv.sessions.table[0].?.cmd.phase); 559 try std.testing.expectEqual(proto.CmdPhase.running, td.srv.sessions.table[0].?.cmd.phase);
586 560
587 // Now ask about everything since the beginning of time. The honest answer 561 // Now ask about everything since the beginning of time. The honest answer
588 // is the FIRST command's return — the client asked what had returned 562 // is the FIRST command's return — the client asked what had returned
@@ -592,7 +566,7 @@ test "Server: a return is still answerable once the next command is running" {
592 .settle_ms = 0, 566 .settle_ms = 0,
593 .timeout_ms = 5000, 567 .timeout_ms = 5000,
594 })); 568 }));
595 const f = (try awaitFrame(alloc, &srv, c.handle, .await_reply, 200)) orelse 569 const f = (try awaitFrame(alloc, &td.srv, c.handle, .await_reply, 200)) orelse
596 return error.NoAwaitReplyWhileNextCommandRuns; 570 return error.NoAwaitReplyWhileNextCommandRuns;
597 defer f.deinit(alloc); 571 defer f.deinit(alloc);
598 const rep = try proto.decodeAwaitReply(f.payload); 572 const rep = try proto.decodeAwaitReply(f.payload);
@@ -603,29 +577,26 @@ test "Server: a return is still answerable once the next command is running" {
603 try std.testing.expectEqual(@as(?u8, 3), rep.state.exit_code); 577 try std.testing.expectEqual(@as(?u8, 3), rep.state.exit_code);
604 // And the live tracker really has moved on, so the assertions above came 578 // And the live tracker really has moved on, so the assertions above came
605 // from the snapshot and could not have come from reading it. 579 // from the snapshot and could not have come from reading it.
606 try std.testing.expectEqual(proto.CmdPhase.running, srv.sessions.table[0].?.cmd.phase); 580 try std.testing.expectEqual(proto.CmdPhase.running, td.srv.sessions.table[0].?.cmd.phase);
607 try std.testing.expectEqual(@as(?u8, null), srv.sessions.table[0].?.cmd.exit_code); 581 try std.testing.expectEqual(@as(?u8, null), td.srv.sessions.table[0].?.cmd.exit_code);
608 } 582 }
609 583
610 test "Server: an await with a settle floor is answered by output going quiet" { 584 test "Server: an await with a settle floor is answered by output going quiet" {
611 const alloc = std.testing.allocator; 585 const alloc = std.testing.allocator;
612 586
613 var tmp = try TmpDir.make(); 587 var td = try h.TestDaemon.open(alloc, "settle");
614 defer tmp.cleanup(); 588 defer td.deinit();
615 const sock_path = try std.fmt.allocPrint(alloc, "{s}/settle.sock", .{tmp.path()});
616 defer alloc.free(sock_path);
617 589
618 // /bin/cat echoes once and then says nothing, which is the exact shape 590 // /bin/cat echoes once and then says nothing, which is the exact shape
619 // settle exists for: no shell integration, no job leaving the shell's 591 // settle exists for: no shell integration, no job leaving the shell's
620 // process group, and so no evidence available beyond "it stopped 592 // process group, and so no evidence available beyond "it stopped
621 // talking". 593 // talking".
622 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 594 try td.start(.{ .shell = "/bin/cat" });
623 defer srv.deinit();
624 595
625 const c = try dial.dialAttach(sock_path, 80, 24); 596 const c = try dial.dialAttach(td.sock_path, 80, 24);
626 defer c.close(); 597 defer c.close();
627 try proto.writeFrame(c.handle, .await_req, &proto.encodeAwaitReq(.{ 598 try proto.writeFrame(c.handle, .await_req, &proto.encodeAwaitReq(.{
628 .since_seq = srv.sessions.table[0].?.tracker.seq, 599 .since_seq = td.srv.sessions.table[0].?.tracker.seq,
629 .settle_ms = 200, 600 .settle_ms = 200,
630 .timeout_ms = 5000, 601 .timeout_ms = 5000,
631 })); 602 }));
@@ -633,7 +604,7 @@ test "Server: an await with a settle floor is answered by output going quiet" {
633 604
634 // Budget ~1.2s of wall clock against a 200ms floor and a 5s timeout, so 605 // Budget ~1.2s of wall clock against a 200ms floor and a 5s timeout, so
635 // the only reason that can legally arrive in the window is the settle. 606 // the only reason that can legally arrive in the window is the settle.
636 const f = (try awaitFrame(alloc, &srv, c.handle, .await_reply, 200)) orelse 607 const f = (try awaitFrame(alloc, &td.srv, c.handle, .await_reply, 200)) orelse
637 return error.NoSettleReply; 608 return error.NoSettleReply;
638 defer f.deinit(alloc); 609 defer f.deinit(alloc);
639 const rep = try proto.decodeAwaitReply(f.payload); 610 const rep = try proto.decodeAwaitReply(f.payload);
@@ -651,26 +622,21 @@ test "Server: an await with a settle floor is answered by output going quiet" {
651 test "Server: an await with nothing to answer it ends at the bound the caller set" { 622 test "Server: an await with nothing to answer it ends at the bound the caller set" {
652 const alloc = std.testing.allocator; 623 const alloc = std.testing.allocator;
653 624
654 var tmp = try TmpDir.make(); 625 var td = try h.TestDaemon.init(alloc, "timeout", .{ .shell = "/bin/cat" });
655 defer tmp.cleanup(); 626 defer td.deinit();
656 const sock_path = try std.fmt.allocPrint(alloc, "{s}/timeout.sock", .{tmp.path()});
657 defer alloc.free(sock_path);
658 627
659 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 628 const c = try dial.dialAttach(td.sock_path, 80, 24);
660 defer srv.deinit();
661
662 const c = try dial.dialAttach(sock_path, 80, 24);
663 defer c.close(); 629 defer c.close();
664 // Nothing is typed at this session, no settle floor is asked for and no 630 // Nothing is typed at this session, no settle floor is asked for and no
665 // mark will ever come: the timeout is the only thing left that can end 631 // mark will ever come: the timeout is the only thing left that can end
666 // this wait, which is the point. 632 // this wait, which is the point.
667 try proto.writeFrame(c.handle, .await_req, &proto.encodeAwaitReq(.{ 633 try proto.writeFrame(c.handle, .await_req, &proto.encodeAwaitReq(.{
668 .since_seq = srv.sessions.table[0].?.tracker.seq, 634 .since_seq = td.srv.sessions.table[0].?.tracker.seq,
669 .settle_ms = 0, 635 .settle_ms = 0,
670 .timeout_ms = 150, 636 .timeout_ms = 150,
671 })); 637 }));
672 638
673 const f = (try awaitFrame(alloc, &srv, c.handle, .await_reply, 120)) orelse 639 const f = (try awaitFrame(alloc, &td.srv, c.handle, .await_reply, 120)) orelse
674 return error.NoTimeoutReply; 640 return error.NoTimeoutReply;
675 defer f.deinit(alloc); 641 defer f.deinit(alloc);
676 const rep = try proto.decodeAwaitReply(f.payload); 642 const rep = try proto.decodeAwaitReply(f.payload);
@@ -684,17 +650,14 @@ test "Server: an await with nothing to answer it ends at the bound the caller se
684 test "Server: a timed-out await carries no exit code, not the last command's" { 650 test "Server: a timed-out await carries no exit code, not the last command's" {
685 const alloc = std.testing.allocator; 651 const alloc = std.testing.allocator;
686 652
687 var tmp = try TmpDir.make(); 653 var td = try h.TestDaemon.open(alloc, "timeoutmark");
688 defer tmp.cleanup(); 654 defer td.deinit();
689 const dir_path = tmp.path();
690 const sock_path = try std.fmt.allocPrint(alloc, "{s}/timeoutmark.sock", .{dir_path});
691 defer alloc.free(sock_path);
692 655
693 // The same burst the marks test uses, and for the same reason: `D;5` and 656 // The same burst the marks test uses, and for the same reason: `D;5` and
694 // the next prompt's `A` arrive in one pty read, which is what leaves the 657 // the next prompt's `A` arrive in one pty read, which is what leaves the
695 // session at a prompt with a code still standing in the tracker. That 658 // session at a prompt with a code still standing in the tracker. That
696 // standing code is the thing this test is about. 659 // standing code is the thing this test is about.
697 try tmp.dir.writeFile(.{ 660 try td.tmp.dir.writeFile(.{
698 .sub_path = "stale.sh", 661 .sub_path = "stale.sh",
699 .data = 662 .data =
700 \\#!/bin/sh 663 \\#!/bin/sh
@@ -705,23 +668,22 @@ test "Server: a timed-out await carries no exit code, not the last command's" {
705 , 668 ,
706 .flags = .{ .mode = 0o755 }, 669 .flags = .{ .mode = 0o755 },
707 }); 670 });
708 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/stale.sh", .{dir_path}, 0); 671 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/stale.sh", .{td.tmp.path()}, 0);
709 defer alloc.free(script); 672 defer alloc.free(script);
710 673
711 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 674 try td.start(.{ .shell = script });
712 defer srv.deinit();
713 675
714 const c = try dial.dialAttach(sock_path, 80, 24); 676 const c = try dial.dialAttach(td.sock_path, 80, 24);
715 defer c.close(); 677 defer c.close();
716 678
717 // First, collect the return honestly, exactly as an agent would. 679 // First, collect the return honestly, exactly as an agent would.
718 try proto.writeFrame(c.handle, .await_req, &proto.encodeAwaitReq(.{ 680 try proto.writeFrame(c.handle, .await_req, &proto.encodeAwaitReq(.{
719 .since_seq = srv.sessions.table[0].?.tracker.seq, 681 .since_seq = td.srv.sessions.table[0].?.tracker.seq,
720 .settle_ms = 0, 682 .settle_ms = 0,
721 .timeout_ms = 5000, 683 .timeout_ms = 5000,
722 })); 684 }));
723 try proto.writeFrame(c.handle, .input, "go\n"); 685 try proto.writeFrame(c.handle, .input, "go\n");
724 const f = (try awaitFrame(alloc, &srv, c.handle, .await_reply, 500)) orelse 686 const f = (try awaitFrame(alloc, &td.srv, c.handle, .await_reply, 500)) orelse
725 return error.NoAwaitReplyFromMark; 687 return error.NoAwaitReplyFromMark;
726 defer f.deinit(alloc); 688 defer f.deinit(alloc);
727 const rep = try proto.decodeAwaitReply(f.payload); 689 const rep = try proto.decodeAwaitReply(f.payload);
@@ -736,7 +698,7 @@ test "Server: a timed-out await carries no exit code, not the last command's" {
736 .settle_ms = 0, 698 .settle_ms = 0,
737 .timeout_ms = 150, 699 .timeout_ms = 150,
738 })); 700 }));
739 const f2 = (try awaitFrame(alloc, &srv, c.handle, .await_reply, 300)) orelse 701 const f2 = (try awaitFrame(alloc, &td.srv, c.handle, .await_reply, 300)) orelse
740 return error.NoTimeoutReply; 702 return error.NoTimeoutReply;
741 defer f2.deinit(alloc); 703 defer f2.deinit(alloc);
742 const rep2 = try proto.decodeAwaitReply(f2.payload); 704 const rep2 = try proto.decodeAwaitReply(f2.payload);
@@ -750,24 +712,21 @@ test "Server: a timed-out await carries no exit code, not the last command's" {
750 try std.testing.expectEqual(@as(?u8, null), rep2.state.exit_code); 712 try std.testing.expectEqual(@as(?u8, null), rep2.state.exit_code);
751 // And the live tracker really does still hold that 5, so the null above 713 // And the live tracker really does still hold that 5, so the null above
752 // was cleared by the timeout arm rather than there being nothing to clear. 714 // was cleared by the timeout arm rather than there being nothing to clear.
753 try std.testing.expectEqual(@as(?u8, 5), srv.sessions.table[0].?.cmd.exit_code); 715 try std.testing.expectEqual(@as(?u8, 5), td.srv.sessions.table[0].?.cmd.exit_code);
754 } 716 }
755 717
756 test "Server: without shell integration a foreground job's end is caught by the pgid edge" { 718 test "Server: without shell integration a foreground job's end is caught by the pgid edge" {
757 const alloc = std.testing.allocator; 719 const alloc = std.testing.allocator;
758 720
759 var tmp = try TmpDir.make(); 721 var td = try h.TestDaemon.open(alloc, "pgid");
760 defer tmp.cleanup(); 722 defer td.deinit();
761 const sock_path = try std.fmt.allocPrint(alloc, "{s}/pgid.sock", .{tmp.path()});
762 defer alloc.free(sock_path);
763 723
764 // A real interactive /bin/sh with NO integration: no mark will ever arrive, 724 // A real interactive /bin/sh with NO integration: no mark will ever arrive,
765 // so the only evidence a command ran is the foreground process group leaving 725 // so the only evidence a command ran is the foreground process group leaving
766 // the shell and coming back. 726 // the shell and coming back.
767 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 727 try td.start(.{ .shell = "/bin/sh" });
768 defer srv.deinit();
769 728
770 const c = try dial.dialAttach(sock_path, 80, 24); 729 const c = try dial.dialAttach(td.sock_path, 80, 24);
771 defer c.close(); 730 defer c.close();
772 // sleep 2, not sleep 1: the edge has to SEE the pgid off the shell before 731 // sleep 2, not sleep 1: the edge has to SEE the pgid off the shell before
773 // "back on the shell" can mean anything, so the busy window must be wide 732 // "back on the shell" can mean anything, so the busy window must be wide
@@ -775,14 +734,14 @@ test "Server: without shell integration a foreground job's end is caught by the
775 // for, not this. 734 // for, not this.
776 try proto.writeFrame(c.handle, .input, "sleep 2\n"); 735 try proto.writeFrame(c.handle, .input, "sleep 2\n");
777 try proto.writeFrame(c.handle, .await_req, &proto.encodeAwaitReq(.{ 736 try proto.writeFrame(c.handle, .await_req, &proto.encodeAwaitReq(.{
778 .since_seq = srv.sessions.table[0].?.tracker.seq, 737 .since_seq = td.srv.sessions.table[0].?.tracker.seq,
779 .settle_ms = 0, 738 .settle_ms = 0,
780 .timeout_ms = 10_000, 739 .timeout_ms = 10_000,
781 })); 740 }));
782 741
783 // ~9s of budget for a 2s job: bounded, and slack enough that a loaded box 742 // ~9s of budget for a 2s job: bounded, and slack enough that a loaded box
784 // does not read as a regression. 743 // does not read as a regression.
785 const f = (try awaitFrame(alloc, &srv, c.handle, .await_reply, 1500)) orelse 744 const f = (try awaitFrame(alloc, &td.srv, c.handle, .await_reply, 1500)) orelse
786 return error.NoPgidReply; 745 return error.NoPgidReply;
787 defer f.deinit(alloc); 746 defer f.deinit(alloc);
788 const rep = try proto.decodeAwaitReply(f.payload); 747 const rep = try proto.decodeAwaitReply(f.payload);
@@ -798,19 +757,14 @@ test "Server: without shell integration a foreground job's end is caught by the
798 test "Server: re-attaching to another session drops the await it left behind" { 757 test "Server: re-attaching to another session drops the await it left behind" {
799 const alloc = std.testing.allocator; 758 const alloc = std.testing.allocator;
800 759
801 var tmp = try TmpDir.make(); 760 var td = try h.TestDaemon.init(alloc, "awaithop", .{ .shell = "/bin/cat" });
802 defer tmp.cleanup(); 761 defer td.deinit();
803 const sock_path = try std.fmt.allocPrint(alloc, "{s}/awaithop.sock", .{tmp.path()});
804 defer alloc.free(sock_path);
805
806 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
807 defer srv.deinit();
808 762
809 const c = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 763 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
810 defer c.close(); 764 defer c.close();
811 var spun: usize = 0; 765 var spun: usize = 0;
812 while (spun < 200 and srv.sessions.find("a") == null) : (spun += 1) try srv.pumpOnce(5); 766 while (spun < 200 and td.srv.sessions.find("a") == null) : (spun += 1) try td.srv.pumpOnce(5);
813 const si_a = srv.sessions.find("a") orelse return error.NoSessionA; 767 const si_a = td.srv.sessions.find("a") orelse return error.NoSessionA;
814 768
815 // An await is a question about ONE session's seq series: since_seq is a 769 // An await is a question about ONE session's seq series: since_seq is a
816 // watermark in that session's tracker. 770 // watermark in that session's tracker.
@@ -821,22 +775,22 @@ test "Server: re-attaching to another session drops the await it left behind" {
821 })); 775 }));
822 spun = 0; 776 spun = 0;
823 while (spun < 200) : (spun += 1) { 777 while (spun < 200) : (spun += 1) {
824 try srv.pumpOnce(5); 778 try td.srv.pumpOnce(5);
825 if (slotAwaiting(&srv, si_a)) break; 779 if (slotAwaiting(&td.srv, si_a)) break;
826 } 780 }
827 try std.testing.expect(slotAwaiting(&srv, si_a)); 781 try std.testing.expect(slotAwaiting(&td.srv, si_a));
828 782
829 // Re-attach the SAME connection to a different session. Carrying the 783 // Re-attach the SAME connection to a different session. Carrying the
830 // watermark across would compare session a's seq against session b's 784 // watermark across would compare session a's seq against session b's
831 // last_return — an await that answers instantly or never, arbitrarily. 785 // last_return — an await that answers instantly or never, arbitrarily.
832 try attachNamed(c.handle, 80, 24, "b"); 786 try attachNamed(c.handle, 80, 24, "b");
833 spun = 0; 787 spun = 0;
834 while (spun < 200 and srv.sessions.find("b") == null) : (spun += 1) try srv.pumpOnce(5); 788 while (spun < 200 and td.srv.sessions.find("b") == null) : (spun += 1) try td.srv.pumpOnce(5);
835 const si_b = srv.sessions.find("b") orelse return error.NoSessionB; 789 const si_b = td.srv.sessions.find("b") orelse return error.NoSessionB;
836 try std.testing.expect(si_a != si_b); 790 try std.testing.expect(si_a != si_b);
837 791
838 var moved = false; 792 var moved = false;
839 for (srv.clients) |slot| { 793 for (td.srv.clients) |slot| {
840 const cs = slot orelse continue; 794 const cs = slot orelse continue;
841 if ((cs.session orelse continue) != si_b) continue; 795 if ((cs.session orelse continue) != si_b) continue;
842 moved = true; 796 moved = true;
@@ -860,16 +814,13 @@ fn slotAwaiting(srv: *Server, si: usize) bool {
860 test "Server: cmd_state pushes stay inside their session" { 814 test "Server: cmd_state pushes stay inside their session" {
861 const alloc = std.testing.allocator; 815 const alloc = std.testing.allocator;
862 816
863 var tmp = try TmpDir.make(); 817 var td = try h.TestDaemon.open(alloc, "markshome");
864 defer tmp.cleanup(); 818 defer td.deinit();
865 const dir_path = tmp.path();
866 const sock_path = try std.fmt.allocPrint(alloc, "{s}/markshome.sock", .{dir_path});
867 defer alloc.free(sock_path);
868 819
869 // The mark-emitting scripted shell the cmd_state push test uses, for 820 // The mark-emitting scripted shell the cmd_state push test uses, for
870 // the same reason: it emits exactly one mark when told to, so any push 821 // the same reason: it emits exactly one mark when told to, so any push
871 // arriving at the OTHER session's client is unambiguously a leak. 822 // arriving at the OTHER session's client is unambiguously a leak.
872 try tmp.dir.writeFile(.{ 823 try td.tmp.dir.writeFile(.{
873 .sub_path = "marks.sh", 824 .sub_path = "marks.sh",
874 .data = 825 .data =
875 \\#!/bin/sh 826 \\#!/bin/sh
@@ -880,26 +831,25 @@ test "Server: cmd_state pushes stay inside their session" {
880 , 831 ,
881 .flags = .{ .mode = 0o755 }, 832 .flags = .{ .mode = 0o755 },
882 }); 833 });
883 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/marks.sh", .{dir_path}, 0); 834 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/marks.sh", .{td.tmp.path()}, 0);
884 defer alloc.free(script); 835 defer alloc.free(script);
885 836
886 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 837 try td.start(.{ .shell = script });
887 defer srv.deinit();
888 838
889 const a = try dial.dialAttach(sock_path, 80, 24); 839 const a = try dial.dialAttach(td.sock_path, 80, 24);
890 defer a.close(); 840 defer a.close();
891 const b = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 841 const b = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
892 defer b.close(); 842 defer b.close();
893 const fa = (try awaitFrame(alloc, &srv, a.handle, .snapshot, 400)) orelse 843 const fa = (try awaitFrame(alloc, &td.srv, a.handle, .snapshot, 400)) orelse
894 return error.NoSnapshotA; 844 return error.NoSnapshotA;
895 fa.deinit(alloc); 845 fa.deinit(alloc);
896 const fb = (try awaitFrame(alloc, &srv, b.handle, .snapshot, 400)) orelse 846 const fb = (try awaitFrame(alloc, &td.srv, b.handle, .snapshot, 400)) orelse
897 return error.NoSnapshotB; 847 return error.NoSnapshotB;
898 fb.deinit(alloc); 848 fb.deinit(alloc);
899 849
900 // Drive a's session into its C: a push for a's clients alone. 850 // Drive a's session into its C: a push for a's clients alone.
901 try proto.writeFrame(a.handle, .input, "go\n"); 851 try proto.writeFrame(a.handle, .input, "go\n");
902 const f1 = (try awaitFrame(alloc, &srv, a.handle, .cmd_state, 500)) orelse 852 const f1 = (try awaitFrame(alloc, &td.srv, a.handle, .cmd_state, 500)) orelse
903 return error.NoRunningPush; 853 return error.NoRunningPush;
904 defer f1.deinit(alloc); 854 defer f1.deinit(alloc);
905 const running = try proto.decodeCmdState(f1.payload); 855 const running = try proto.decodeCmdState(f1.payload);
@@ -909,7 +859,7 @@ test "Server: cmd_state pushes stay inside their session" {
909 // b hears nothing: its own shell emitted no mark, and a's must not 859 // b hears nothing: its own shell emitted no mark, and a's must not
910 // cross. The budget keeps pumping so a misdirected push, which would 860 // cross. The budget keeps pumping so a misdirected push, which would
911 // have been queued in the same pump a's was, has amply arrived. 861 // have been queued in the same pump a's was, has amply arrived.
912 if (try awaitFrame(alloc, &srv, b.handle, .cmd_state, 60)) |leak| { 862 if (try awaitFrame(alloc, &td.srv, b.handle, .cmd_state, 60)) |leak| {
913 leak.deinit(alloc); 863 leak.deinit(alloc);
914 return error.CmdStateCrossedSessions; 864 return error.CmdStateCrossedSessions;
915 } 865 }
@@ -918,16 +868,13 @@ test "Server: cmd_state pushes stay inside their session" {
918 test "Server: an await resolves against the awaiting client's session" { 868 test "Server: an await resolves against the awaiting client's session" {
919 const alloc = std.testing.allocator; 869 const alloc = std.testing.allocator;
920 870
921 var tmp = try TmpDir.make(); 871 var td = try h.TestDaemon.open(alloc, "awaithome");
922 defer tmp.cleanup(); 872 defer td.deinit();
923 const dir_path = tmp.path();
924 const sock_path = try std.fmt.allocPrint(alloc, "{s}/awaithome.sock", .{dir_path});
925 defer alloc.free(sock_path);
926 873
927 // The await test's burst, one more session in the room: `D;3` and the 874 // The await test's burst, one more session in the room: `D;3` and the
928 // next prompt's `A` in one write, so the return is complete the moment 875 // next prompt's `A` in one write, so the return is complete the moment
929 // it lands. 876 // it lands.
930 try tmp.dir.writeFile(.{ 877 try td.tmp.dir.writeFile(.{
931 .sub_path = "await.sh", 878 .sub_path = "await.sh",
932 .data = 879 .data =
933 \\#!/bin/sh 880 \\#!/bin/sh
@@ -938,48 +885,47 @@ test "Server: an await resolves against the awaiting client's session" {
938 , 885 ,
939 .flags = .{ .mode = 0o755 }, 886 .flags = .{ .mode = 0o755 },
940 }); 887 });
941 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/await.sh", .{dir_path}, 0); 888 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/await.sh", .{td.tmp.path()}, 0);
942 defer alloc.free(script); 889 defer alloc.free(script);
943 890
944 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 891 try td.start(.{ .shell = script });
945 defer srv.deinit();
946 892
947 const a = try dial.dialAttach(sock_path, 80, 24); 893 const a = try dial.dialAttach(td.sock_path, 80, 24);
948 defer a.close(); 894 defer a.close();
949 const b = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 895 const b = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
950 defer b.close(); 896 defer b.close();
951 const fa = (try awaitFrame(alloc, &srv, a.handle, .snapshot, 400)) orelse 897 const fa = (try awaitFrame(alloc, &td.srv, a.handle, .snapshot, 400)) orelse
952 return error.NoSnapshotA; 898 return error.NoSnapshotA;
953 fa.deinit(alloc); 899 fa.deinit(alloc);
954 const fb = (try awaitFrame(alloc, &srv, b.handle, .snapshot, 400)) orelse 900 const fb = (try awaitFrame(alloc, &td.srv, b.handle, .snapshot, 400)) orelse
955 return error.NoSnapshotB; 901 return error.NoSnapshotB;
956 fb.deinit(alloc); 902 fb.deinit(alloc);
957 903
958 // a asks about ITS session; nothing there may answer yet. 904 // a asks about ITS session; nothing there may answer yet.
959 try proto.writeFrame(a.handle, .await_req, &proto.encodeAwaitReq(.{ 905 try proto.writeFrame(a.handle, .await_req, &proto.encodeAwaitReq(.{
960 .since_seq = srv.sessions.table[0].?.tracker.seq, 906 .since_seq = td.srv.sessions.table[0].?.tracker.seq,
961 .settle_ms = 0, 907 .settle_ms = 0,
962 .timeout_ms = 10_000, 908 .timeout_ms = 10_000,
963 })); 909 }));
964 910
965 // b's shell returns a command, and the daemon has demonstrably seen it. 911 // b's shell returns a command, and the daemon has demonstrably seen it.
966 const si_b = srv.sessions.find("b") orelse return error.SessionBMissing; 912 const si_b = td.srv.sessions.find("b") orelse return error.SessionBMissing;
967 try proto.writeFrame(b.handle, .input, "go\n"); 913 try proto.writeFrame(b.handle, .input, "go\n");
968 var spun: usize = 0; 914 var spun: usize = 0;
969 while (spun < 500 and srv.sessions.table[si_b].?.last_return == null) : (spun += 1) { 915 while (spun < 500 and td.srv.sessions.table[si_b].?.last_return == null) : (spun += 1) {
970 try srv.pumpOnce(5); 916 try td.srv.pumpOnce(5);
971 } 917 }
972 try std.testing.expect(srv.sessions.table[si_b].?.last_return != null); 918 try std.testing.expect(td.srv.sessions.table[si_b].?.last_return != null);
973 919
974 // That return is another session's and must not resolve a's await. 920 // That return is another session's and must not resolve a's await.
975 if (try awaitFrame(alloc, &srv, a.handle, .await_reply, 40)) |early| { 921 if (try awaitFrame(alloc, &td.srv, a.handle, .await_reply, 40)) |early| {
976 early.deinit(alloc); 922 early.deinit(alloc);
977 return error.AwaitAnsweredByAnotherSession; 923 return error.AwaitAnsweredByAnotherSession;
978 } 924 }
979 925
980 // a's own shell returning is what answers it, code and all. 926 // a's own shell returning is what answers it, code and all.
981 try proto.writeFrame(a.handle, .input, "go\n"); 927 try proto.writeFrame(a.handle, .input, "go\n");
982 const f = (try awaitFrame(alloc, &srv, a.handle, .await_reply, 500)) orelse 928 const f = (try awaitFrame(alloc, &td.srv, a.handle, .await_reply, 500)) orelse
983 return error.NoAwaitReplyFromOwnSession; 929 return error.NoAwaitReplyFromOwnSession;
984 defer f.deinit(alloc); 930 defer f.deinit(alloc);
985 const rep = try proto.decodeAwaitReply(f.payload); 931 const rep = try proto.decodeAwaitReply(f.payload);
@@ -991,14 +937,8 @@ test "Server: an await resolves against the awaiting client's session" {
991 test "Server: a promoted-but-unattached slot receives nothing" { 937 test "Server: a promoted-but-unattached slot receives nothing" {
992 const alloc = std.testing.allocator; 938 const alloc = std.testing.allocator;
993 939
994 var tmp = try TmpDir.make(); 940 var td = try h.TestDaemon.init(alloc, "limbo", .{ .shell = "/bin/cat" });
995 defer tmp.cleanup(); 941 defer td.deinit();
996 const dir_path = tmp.path();
997 const sock_path = try std.fmt.allocPrint(alloc, "{s}/limbo.sock", .{dir_path});
998 defer alloc.free(sock_path);
999
1000 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
1001 defer srv.deinit();
1002 942
1003 // The shape a QUIC connection has between handshake and first attach: 943 // The shape a QUIC connection has between handshake and first attach:
1004 // a live client slot with no session. Connection = session means such 944 // a live client slot with no session. Connection = session means such
@@ -1006,23 +946,23 @@ test "Server: a promoted-but-unattached slot receives nothing" {
1006 // until an attach says which session it is asking about. 946 // until an attach says which session it is asking about.
1007 const c = try connectedPair(); 947 const c = try connectedPair();
1008 defer std.posix.close(c.peer); 948 defer std.posix.close(c.peer);
1009 srv.clients[0] = .{ .sink = .{ .socket = c.daemon } }; 949 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon } };
1010 950
1011 // Make the default session produce broadcasts of every kind a pump 951 // Make the default session produce broadcasts of every kind a pump
1012 // emits: grid updates from the echo, and the mode poll's first report. 952 // emits: grid updates from the echo, and the mode poll's first report.
1013 try proto.writeAllFd(srv.sessions.table[0].?.pty.master, "say-something\n"); 953 try proto.writeAllFd(td.srv.sessions.table[0].?.pty.master, "say-something\n");
1014 var i: usize = 0; 954 var i: usize = 0;
1015 while (i < 40) : (i += 1) try srv.pumpOnce(5); 955 while (i < 40) : (i += 1) try td.srv.pumpOnce(5);
1016 956
1017 // Liveness: the grid really moved, so silence below is the filter at 957 // Liveness: the grid really moved, so silence below is the filter at
1018 // work and not a session that never spoke. 958 // work and not a session that never spoke.
1019 const plain = try srv.sessions.table[0].?.eng.dumpPlain(alloc); 959 const plain = try td.srv.sessions.table[0].?.eng.dumpPlain(alloc);
1020 defer alloc.free(plain); 960 defer alloc.free(plain);
1021 try std.testing.expect(std.mem.indexOf(u8, plain, "say-something") != null); 961 try std.testing.expect(std.mem.indexOf(u8, plain, "say-something") != null);
1022 962
1023 // The slot was told nothing — nothing queued, nothing on the wire. 963 // The slot was told nothing — nothing queued, nothing on the wire.
1024 try std.testing.expect(srv.clients[0] != null); 964 try std.testing.expect(td.srv.clients[0] != null);
1025 try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.pending.items.len); 965 try std.testing.expectEqual(@as(usize, 0), td.srv.clients[0].?.pending.items.len);
1026 var pfd = [_]std.posix.pollfd{ 966 var pfd = [_]std.posix.pollfd{
1027 .{ .fd = c.peer, .events = std.posix.POLL.IN, .revents = 0 }, 967 .{ .fd = c.peer, .events = std.posix.POLL.IN, .revents = 0 },
1028 }; 968 };
src/server/server_test_clipboard.zig
Old New
@@ -38,21 +38,15 @@ fn awaitSelectionReply(
38 test "Server: a session-less slot still answers a well-formed selection request" { 38 test "Server: a session-less slot still answers a well-formed selection request" {
39 const alloc = std.testing.allocator; 39 const alloc = std.testing.allocator;
40 40
41 var tmp = try TmpDir.make(); 41 var td = try h.TestDaemon.init(alloc, "selless", .{ .shell = "/bin/cat" });
42 defer tmp.cleanup(); 42 defer td.deinit();
43 const dir_path = tmp.path();
44 const sock_path = try std.fmt.allocPrint(alloc, "{s}/selless.sock", .{dir_path});
45 defer alloc.free(sock_path);
46
47 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
48 defer srv.deinit();
49 43
50 // The QUIC shape again: promoted by a completed handshake, never 44 // The QUIC shape again: promoted by a completed handshake, never
51 // attached. It has no grid to read, but its request carried an id, so 45 // attached. It has no grid to read, but its request carried an id, so
52 // the lane owes it exactly one correlated answer. 46 // the lane owes it exactly one correlated answer.
53 const c = try connectedPair(); 47 const c = try connectedPair();
54 defer std.posix.close(c.peer); 48 defer std.posix.close(c.peer);
55 srv.clients[0] = .{ .sink = .{ .socket = c.daemon } }; 49 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon } };
56 50
57 const req = proto.encodeSelectionReq(.{ 51 const req = proto.encodeSelectionReq(.{
58 .id = 4242, 52 .id = 4242,
@@ -61,7 +55,7 @@ test "Server: a session-less slot still answers a well-formed selection request"
61 }); 55 });
62 try proto.writeFrame(c.peer, .selection_req, &req); 56 try proto.writeFrame(c.peer, .selection_req, &req);
63 57
64 const reply = (try awaitSelectionReply(alloc, &srv, c.peer)) orelse 58 const reply = (try awaitSelectionReply(alloc, &td.srv, c.peer)) orelse
65 return error.NoSelectionReply; 59 return error.NoSelectionReply;
66 try std.testing.expectEqual(@as(u32, 4242), reply.id); 60 try std.testing.expectEqual(@as(u32, 4242), reply.id);
67 try std.testing.expectEqual(proto.SelectionStatus.unavailable, reply.status); 61 try std.testing.expectEqual(proto.SelectionStatus.unavailable, reply.status);
@@ -71,21 +65,15 @@ test "Server: a session-less slot still answers a well-formed selection request"
71 test "Server: an unencodable selection result still answers with unavailable" { 65 test "Server: an unencodable selection result still answers with unavailable" {
72 const alloc = std.testing.allocator; 66 const alloc = std.testing.allocator;
73 67
74 var tmp = try TmpDir.make(); 68 var td = try h.TestDaemon.init(alloc, "seloom", .{ .shell = "/bin/cat" });
75 defer tmp.cleanup(); 69 defer td.deinit();
76 const dir_path = tmp.path();
77 const sock_path = try std.fmt.allocPrint(alloc, "{s}/seloom.sock", .{dir_path});
78 defer alloc.free(sock_path);
79
80 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
81 defer srv.deinit();
82 70
83 const c = try connectedPair(); 71 const c = try connectedPair();
84 defer std.posix.close(c.peer); 72 defer std.posix.close(c.peer);
85 srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 }; 73 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 };
86 // The per-client queue must not be what fails: this test is about the 74 // The per-client queue must not be what fails: this test is about the
87 // scratch encode, and appendFrame draws on the same allocator. 75 // scratch encode, and appendFrame draws on the same allocator.
88 try srv.clients[0].?.pending.ensureTotalCapacity(alloc, 4096); 76 try td.srv.clients[0].?.pending.ensureTotalCapacity(alloc, 4096);
89 77
90 const text = try alloc.alloc(u8, 64 * 1024); 78 const text = try alloc.alloc(u8, 64 * 1024);
91 defer alloc.free(text); 79 defer alloc.free(text);
@@ -97,13 +85,13 @@ test "Server: an unencodable selection result still answers with unavailable" {
97 .fail_index = 1, 85 .fail_index = 1,
98 .resize_fail_index = 0, 86 .resize_fail_index = 0,
99 }); 87 });
100 const real = srv.alloc; 88 const real = td.srv.alloc;
101 srv.alloc = failing.allocator(); 89 td.srv.alloc = failing.allocator();
102 srv.queueSelectionReply(0, 99, .ok, 7, text); 90 td.srv.queueSelectionReply(0, 99, .ok, 7, text);
103 srv.alloc = real; 91 td.srv.alloc = real;
104 try std.testing.expect(failing.has_induced_failure); 92 try std.testing.expect(failing.has_induced_failure);
105 93
106 const reply = (try awaitSelectionReply(alloc, &srv, c.peer)) orelse 94 const reply = (try awaitSelectionReply(alloc, &td.srv, c.peer)) orelse
107 return error.NoSelectionReply; 95 return error.NoSelectionReply;
108 try std.testing.expectEqual(@as(u32, 99), reply.id); 96 try std.testing.expectEqual(@as(u32, 99), reply.id);
109 try std.testing.expectEqual(proto.SelectionStatus.unavailable, reply.status); 97 try std.testing.expectEqual(proto.SelectionStatus.unavailable, reply.status);
@@ -137,25 +125,22 @@ test "the engine's default clipboard cap is the wire's" {
137 test "Server: a clipboard event reaches this session's clients and no others" { 125 test "Server: a clipboard event reaches this session's clients and no others" {
138 const alloc = std.testing.allocator; 126 const alloc = std.testing.allocator;
139 127
140 var tmp = try TmpDir.make(); 128 var td = try h.TestDaemon.open(alloc, "clip");
141 defer tmp.cleanup(); 129 defer td.deinit();
142 const sock_path = try std.fmt.allocPrint(alloc, "{s}/clip.sock", .{tmp.path()});
143 defer alloc.free(sock_path);
144 130
145 // The OSC 52 arrives as INPUT rather than from a script: one `SpawnPlan` is 131 // The OSC 52 arrives as INPUT rather than from a script: one `SpawnPlan` is
146 // shared by every session, so a shell emitting the escape itself emits it in 132 // shared by every session, so a shell emitting the escape itself emits it in
147 // BOTH and the "no others" half could never be observed. 133 // BOTH and the "no others" half could never be observed.
148 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 134 try td.start(.{ .shell = "/bin/cat" });
149 defer srv.deinit();
150 135
151 const ca = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 136 const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
152 defer ca.close(); 137 defer ca.close();
153 (try awaitFrame(alloc, &srv, ca.handle, .snapshot, 400) orelse 138 (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400) orelse
154 return error.NoSnapshotA).deinit(alloc); 139 return error.NoSnapshotA).deinit(alloc);
155 140
156 const cb = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 141 const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
157 defer cb.close(); 142 defer cb.close();
158 (try awaitFrame(alloc, &srv, cb.handle, .snapshot, 400) orelse 143 (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400) orelse
159 return error.NoSnapshotB).deinit(alloc); 144 return error.NoSnapshotB).deinit(alloc);
160 145
161 // The newline is what flushes the line to cat in canonical mode; the 146 // The newline is what flushes the line to cat in canonical mode; the
@@ -163,7 +148,7 @@ test "Server: a clipboard event reaches this session's clients and no others" {
163 // ECHOCTL it is not an OSC at all, and unmangled it is the same event. 148 // ECHOCTL it is not an OSC at all, and unmangled it is the same event.
164 try proto.writeFrame(ca.handle, .input, "\x1b]52;c;aGk=\x07\n"); 149 try proto.writeFrame(ca.handle, .input, "\x1b]52;c;aGk=\x07\n");
165 150
166 const ev = (try awaitFrame(alloc, &srv, ca.handle, .term_event, 400)) orelse 151 const ev = (try awaitFrame(alloc, &td.srv, ca.handle, .term_event, 400)) orelse
167 return error.NoTermEventForEmittingSession; 152 return error.NoTermEventForEmittingSession;
168 defer ev.deinit(alloc); 153 defer ev.deinit(alloc);
169 switch (try proto.decodeTermEvent(ev.payload)) { 154 switch (try proto.decodeTermEvent(ev.payload)) {
@@ -177,7 +162,7 @@ test "Server: a clipboard event reaches this session's clients and no others" {
177 // The boundary, with the positive already in hand so this cannot pass 162 // The boundary, with the positive already in hand so this cannot pass
178 // vacuously: the budget keeps pumping, so a misdirected push would have 163 // vacuously: the budget keeps pumping, so a misdirected push would have
179 // arrived by now. A clipboard write belongs to the shell that produced it. 164 // arrived by now. A clipboard write belongs to the shell that produced it.
180 if (try awaitFrame(alloc, &srv, cb.handle, .term_event, 60)) |leaked| { 165 if (try awaitFrame(alloc, &td.srv, cb.handle, .term_event, 60)) |leaked| {
181 leaked.deinit(alloc); 166 leaked.deinit(alloc);
182 return error.TermEventCrossedSessions; 167 return error.TermEventCrossedSessions;
183 } 168 }
@@ -344,31 +329,28 @@ fn collectGapReplay(alloc: std.mem.Allocator, srv: *Server, fd: std.posix.fd_t)
344 test "Server: a delta reattach is replayed the gap's last clipboard set, then its bell" { 329 test "Server: a delta reattach is replayed the gap's last clipboard set, then its bell" {
345 const alloc = std.testing.allocator; 330 const alloc = std.testing.allocator;
346 331
347 var tmp = try TmpDir.make(); 332 var td = try h.TestDaemon.open(alloc, "gapdelta");
348 defer tmp.cleanup(); 333 defer td.deinit();
349 const sock_path = try std.fmt.allocPrint(alloc, "{s}/gapdelta.sock", .{tmp.path()});
350 defer alloc.free(sock_path);
351 334
352 const script = try writeGapShell(alloc, &tmp); 335 const script = try writeGapShell(alloc, &td.tmp);
353 defer alloc.free(script); 336 defer alloc.free(script);
354 337
355 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 338 try td.start(.{ .shell = script });
356 defer srv.deinit();
357 339
358 const wm = try clipboardIntoGap(alloc, &srv, sock_path); 340 const wm = try clipboardIntoGap(alloc, &td.srv, td.sock_path);
359 341
360 // The reattach that earns a delta: same size, and the watermark the 342 // The reattach that earns a delta: same size, and the watermark the
361 // departed client really held. Yank in vim, the link blinks, you are back 343 // departed client really held. Yank in vim, the link blinks, you are back
362 // two seconds later — losing the yank to that is how a copy feature 344 // two seconds later — losing the yank to that is how a copy feature
363 // becomes one you stop believing. 345 // becomes one you stop believing.
364 const b = try dial.dial(sock_path); 346 const b = try dial.dial(td.sock_path);
365 defer b.close(); 347 defer b.close();
366 try proto.writeFrame(b.handle, .attach, &proto.encodeAttach(80, 24, wm.seq, wm.epoch)); 348 try proto.writeFrame(b.handle, .attach, &proto.encodeAttach(80, 24, wm.seq, wm.epoch));
367 349
368 // Collected rather than asserted frame by frame, because four claims ride on 350 // Collected rather than asserted frame by frame, because four claims ride on
369 // the SEQUENCE: the delta arm answered, exactly one clipboard event came back, 351 // the SEQUENCE: the delta arm answered, exactly one clipboard event came back,
370 // it carries the second set, and the bell follows it. 352 // it carries the second set, and the bell follows it.
371 const got = try collectGapReplay(alloc, &srv, b.handle); 353 const got = try collectGapReplay(alloc, &td.srv, b.handle);
372 354
373 // The grid first, and by branch rather than merely "something arrived": 355 // The grid first, and by branch rather than merely "something arrived":
374 // the events are worthless if the repaint never landed, and a bell about 356 // the events are worthless if the repaint never landed, and a bell about
@@ -389,25 +371,22 @@ test "Server: a delta reattach is replayed the gap's last clipboard set, then it
389 test "Server: a reattach quoting the seq an event was stamped at is not replayed it" { 371 test "Server: a reattach quoting the seq an event was stamped at is not replayed it" {
390 const alloc = std.testing.allocator; 372 const alloc = std.testing.allocator;
391 373
392 var tmp = try TmpDir.make(); 374 var td = try h.TestDaemon.open(alloc, "gapsameseq");
393 defer tmp.cleanup(); 375 defer td.deinit();
394 const sock_path = try std.fmt.allocPrint(alloc, "{s}/gapsameseq.sock", .{tmp.path()});
395 defer alloc.free(sock_path);
396 376
397 const script = try writeGapShell(alloc, &tmp); 377 const script = try writeGapShell(alloc, &td.tmp);
398 defer alloc.free(script); 378 defer alloc.free(script);
399 379
400 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 380 try td.start(.{ .shell = script });
401 defer srv.deinit();
402 381
403 const wm = try clipboardIntoGap(alloc, &srv, sock_path); 382 const wm = try clipboardIntoGap(alloc, &td.srv, td.sock_path);
404 383
405 // The `>` in `replayPending`, at its exact boundary: a client quoting the seq 384 // The `>` in `replayPending`, at its exact boundary: a client quoting the seq
406 // an event was stamped at was THERE for it, and replaying overwrites whatever 385 // an event was stamped at was THERE for it, and replaying overwrites whatever
407 // the user has copied since. The stamps are read off the session and the 386 // the user has copied since. The stamps are read off the session and the
408 // HIGHEST is quoted, since the kinds need not share a seq. Walked via 387 // HIGHEST is quoted, since the kinds need not share a seq. Walked via
409 // `pendingSlots`, so a third kind cannot sit silently outside the `@max`. 388 // `pendingSlots`, so a third kind cannot sit silently outside the `@max`.
410 const s = &srv.sessions.table[0].?; 389 const s = &td.srv.sessions.table[0].?;
411 var at: u64 = 0; 390 var at: u64 = 0;
412 var lowest: u64 = std.math.maxInt(u64); 391 var lowest: u64 = std.math.maxInt(u64);
413 var recorded: usize = 0; 392 var recorded: usize = 0;
@@ -424,14 +403,14 @@ test "Server: a reattach quoting the seq an event was stamped at is not replayed
424 try std.testing.expectEqual(@as(usize, Session.kinds.len), recorded); 403 try std.testing.expectEqual(@as(usize, Session.kinds.len), recorded);
425 try std.testing.expect(lowest > wm.seq); 404 try std.testing.expect(lowest > wm.seq);
426 405
427 const b = try dial.dial(sock_path); 406 const b = try dial.dial(td.sock_path);
428 defer b.close(); 407 defer b.close();
429 try proto.writeFrame(b.handle, .attach, &proto.encodeAttach(80, 24, at, wm.epoch)); 408 try proto.writeFrame(b.handle, .attach, &proto.encodeAttach(80, 24, at, wm.epoch));
430 409
431 // A delta is what proves the branch: quoting a servable seq at the 410 // A delta is what proves the branch: quoting a servable seq at the
432 // current size takes the same arm the replay lives on, so an absence here 411 // current size takes the same arm the replay lives on, so an absence here
433 // is the comparison refusing rather than the branch never being reached. 412 // is the comparison refusing rather than the branch never being reached.
434 const got = try collectGapReplay(alloc, &srv, b.handle); 413 const got = try collectGapReplay(alloc, &td.srv, b.handle);
435 try std.testing.expectEqual(proto.MsgType.delta, got.content orelse 414 try std.testing.expectEqual(proto.MsgType.delta, got.content orelse
436 return error.AttachNeverAnswered); 415 return error.AttachNeverAnswered);
437 try std.testing.expectEqual(@as(usize, 0), got.clips); 416 try std.testing.expectEqual(@as(usize, 0), got.clips);
@@ -441,30 +420,27 @@ test "Server: a reattach quoting the seq an event was stamped at is not replayed
441 test "Server: a clipboard event in the gap is not replayed to a snapshot attach" { 420 test "Server: a clipboard event in the gap is not replayed to a snapshot attach" {
442 const alloc = std.testing.allocator; 421 const alloc = std.testing.allocator;
443 422
444 var tmp = try TmpDir.make(); 423 var td = try h.TestDaemon.open(alloc, "gapsnap");
445 defer tmp.cleanup(); 424 defer td.deinit();
446 const sock_path = try std.fmt.allocPrint(alloc, "{s}/gapsnap.sock", .{tmp.path()});
447 defer alloc.free(sock_path);
448 425
449 const script = try writeGapShell(alloc, &tmp); 426 const script = try writeGapShell(alloc, &td.tmp);
450 defer alloc.free(script); 427 defer alloc.free(script);
451 428
452 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 429 try td.start(.{ .shell = script });
453 defer srv.deinit();
454 430
455 _ = try clipboardIntoGap(alloc, &srv, sock_path); 431 _ = try clipboardIntoGap(alloc, &td.srv, td.sock_path);
456 432
457 // seq 0, epoch 0: what a fresh client sends, and what `canServe` refuses 433 // seq 0, epoch 0: what a fresh client sends, and what `canServe` refuses
458 // by construction. This client has never seen this session, so the 434 // by construction. This client has never seen this session, so the
459 // clipboard write it slept through is not addressed to it. 435 // clipboard write it slept through is not addressed to it.
460 const b = try dial.dialAttach(sock_path, 80, 24); 436 const b = try dial.dialAttach(td.sock_path, 80, 24);
461 defer b.close(); 437 defer b.close();
462 438
463 // `collectGapReplay` inspects EVERY frame: `awaitFrame` drops what it is not 439 // `collectGapReplay` inspects EVERY frame: `awaitFrame` drops what it is not
464 // looking for, so a replay queued ahead of the snapshot would be swallowed — 440 // looking for, so a replay queued ahead of the snapshot would be swallowed —
465 // and that is exactly where a hoisted replay lands. The snapshot is still 441 // and that is exactly where a hoisted replay lands. The snapshot is still
466 // required, so the absence cannot pass vacuously. 442 // required, so the absence cannot pass vacuously.
467 const got = try collectGapReplay(alloc, &srv, b.handle); 443 const got = try collectGapReplay(alloc, &td.srv, b.handle);
468 try std.testing.expectEqual(proto.MsgType.snapshot, got.content orelse 444 try std.testing.expectEqual(proto.MsgType.snapshot, got.content orelse
469 return error.AttachNeverAnswered); 445 return error.AttachNeverAnswered);
470 try std.testing.expectEqual(@as(usize, 0), got.clips); 446 try std.testing.expectEqual(@as(usize, 0), got.clips);
@@ -474,59 +450,53 @@ test "Server: a clipboard event in the gap is not replayed to a snapshot attach"
474 test "Server: a rebuild drops the pending clipboard it just put out of reach" { 450 test "Server: a rebuild drops the pending clipboard it just put out of reach" {
475 const alloc = std.testing.allocator; 451 const alloc = std.testing.allocator;
476 452
477 var tmp = try TmpDir.make(); 453 var td = try h.TestDaemon.open(alloc, "gapexpire");
478 defer tmp.cleanup(); 454 defer td.deinit();
479 const sock_path = try std.fmt.allocPrint(alloc, "{s}/gapexpire.sock", .{tmp.path()});
480 defer alloc.free(sock_path);
481 455
482 const script = try writeGapShell(alloc, &tmp); 456 const script = try writeGapShell(alloc, &td.tmp);
483 defer alloc.free(script); 457 defer alloc.free(script);
484 458
485 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 459 try td.start(.{ .shell = script });
486 defer srv.deinit();
487 460
488 _ = try clipboardIntoGap(alloc, &srv, sock_path); 461 _ = try clipboardIntoGap(alloc, &td.srv, td.sock_path);
489 462
490 // On daemon STATE rather than the wire, because an expired event and a 463 // On daemon STATE rather than the wire, because an expired event and a
491 // retained one are indistinguishable to every client. A rebuild moves 464 // retained one are indistinguishable to every client. A rebuild moves
492 // `reset_seq` past the recorded seq, so retaining leaves the user's copied 465 // `reset_seq` past the recorded seq, so retaining leaves the user's copied
493 // text in a daemon that can no longer give it to anyone. Every slot, so a 466 // text in a daemon that can no longer give it to anyone. Every slot, so a
494 // kind added later cannot go unchecked. 467 // kind added later cannot go unchecked.
495 for (srv.sessions.table[0].?.pendingSlots()) |slot| try std.testing.expect(slot.* != null); 468 for (td.srv.sessions.table[0].?.pendingSlots()) |slot| try std.testing.expect(slot.* != null);
496 469
497 // A joiner at a different size is the shortest route to a rebuild: it 470 // A joiner at a different size is the shortest route to a rebuild: it
498 // takes sendResync's size_changed arm straight into resyncSnapshot. 471 // takes sendResync's size_changed arm straight into resyncSnapshot.
499 const b = try dial.dialAttach(sock_path, 100, 30); 472 const b = try dial.dialAttach(td.sock_path, 100, 30);
500 defer b.close(); 473 defer b.close();
501 (try awaitFrame(alloc, &srv, b.handle, .snapshot, 400) orelse 474 (try awaitFrame(alloc, &td.srv, b.handle, .snapshot, 400) orelse
502 return error.NoSnapshotAfterResize).deinit(alloc); 475 return error.NoSnapshotAfterResize).deinit(alloc);
503 476
504 for (srv.sessions.table[0].?.pendingSlots()) |slot| try std.testing.expect(slot.* == null); 477 for (td.srv.sessions.table[0].?.pendingSlots()) |slot| try std.testing.expect(slot.* == null);
505 } 478 }
506 479
507 test "Server: the expiry keeps a servable event and drops one a stranded tracker cannot serve" { 480 test "Server: the expiry keeps a servable event and drops one a stranded tracker cannot serve" {
508 const alloc = std.testing.allocator; 481 const alloc = std.testing.allocator;
509 482
510 var tmp = try TmpDir.make(); 483 var td = try h.TestDaemon.open(alloc, "gappred");
511 defer tmp.cleanup(); 484 defer td.deinit();
512 const sock_path = try std.fmt.allocPrint(alloc, "{s}/gappred.sock", .{tmp.path()});
513 defer alloc.free(sock_path);
514 485
515 const script = try writeGapShell(alloc, &tmp); 486 const script = try writeGapShell(alloc, &td.tmp);
516 defer alloc.free(script); 487 defer alloc.free(script);
517 488
518 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 489 try td.start(.{ .shell = script });
519 defer srv.deinit();
520 490
521 _ = try clipboardIntoGap(alloc, &srv, sock_path); 491 _ = try clipboardIntoGap(alloc, &td.srv, td.sock_path);
522 const s = &srv.sessions.table[0].?; 492 const s = &td.srv.sessions.table[0].?;
523 for (s.pendingSlots()) |slot| try std.testing.expect(slot.* != null); 493 for (s.pendingSlots()) |slot| try std.testing.expect(slot.* != null);
524 494
525 // RETAIN: nothing has moved, so every stamp is still servable. This half has 495 // RETAIN: nothing has moved, so every stamp is still servable. This half has
526 // no other witness — a successful rebuild puts every stamp out of reach, so a 496 // no other witness — a successful rebuild puts every stamp out of reach, so a
527 // version that dropped unconditionally would behave identically. The 497 // version that dropped unconditionally would behave identically. The
528 // predicate only RETAINS where rebuild backs out, under allocation failure. 498 // predicate only RETAINS where rebuild backs out, under allocation failure.
529 s.dropUnservablePending(srv.alloc); 499 s.dropUnservablePending(td.srv.alloc);
530 for (s.pendingSlots()) |slot| try std.testing.expect(slot.* != null); 500 for (s.pendingSlots()) |slot| try std.testing.expect(slot.* != null);
531 501
532 // DROP. `rows == 0` is what a failure inside rebuild's dump loop leaves 502 // DROP. `rows == 0` is what a failure inside rebuild's dump loop leaves
@@ -534,19 +504,17 @@ test "Server: the expiry keeps a servable event and drops one a stranded tracker
534 // there is no failing-allocator seam into a live Server, so what stays 504 // there is no failing-allocator seam into a live Server, so what stays
535 // unfalsifiable is the WIRING and no longer the rule. 505 // unfalsifiable is the WIRING and no longer the rule.
536 s.tracker.rows = 0; 506 s.tracker.rows = 0;
537 s.dropUnservablePending(srv.alloc); 507 s.dropUnservablePending(td.srv.alloc);
538 for (s.pendingSlots()) |slot| try std.testing.expect(slot.* == null); 508 for (s.pendingSlots()) |slot| try std.testing.expect(slot.* == null);
539 } 509 }
540 510
541 test "Server: recordPending stores nothing against a tracker that can serve no seq" { 511 test "Server: recordPending stores nothing against a tracker that can serve no seq" {
542 const alloc = std.testing.allocator; 512 const alloc = std.testing.allocator;
543 513
544 var tmp = try TmpDir.make(); 514 var td = try h.TestDaemon.open(alloc, "gapunbuilt");
545 defer tmp.cleanup(); 515 defer td.deinit();
546 const sock_path = try std.fmt.allocPrint(alloc, "{s}/gapunbuilt.sock", .{tmp.path()});
547 defer alloc.free(sock_path);
548 516
549 const script = try writeGapShell(alloc, &tmp); 517 const script = try writeGapShell(alloc, &td.tmp);
550 defer alloc.free(script); 518 defer alloc.free(script);
551 519
552 // No client connects at all; the session exists because `Server.init` creates 520 // No client connects at all; the session exists because `Server.init` creates
@@ -554,67 +522,63 @@ test "Server: recordPending stores nothing against a tracker that can serve no s
554 // runs BEFORE the drain on every pty chunk, and a tracker with `rows == 0` 522 // runs BEFORE the drain on every pty chunk, and a tracker with `rows == 0`
555 // resyncs — which rebuilds even with nobody attached. So the FIRST chunk of 523 // resyncs — which rebuilds even with nobody attached. So the FIRST chunk of
556 // output builds the tracker whether or not anyone is watching. 524 // output builds the tracker whether or not anyone is watching.
557 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 525 try td.start(.{ .shell = script });
558 defer srv.deinit();
559 526
560 try proto.writeAllFd(srv.sessions.table[0].?.pty.master, "go\n"); 527 try proto.writeAllFd(td.srv.sessions.table[0].?.pty.master, "go\n");
561 try std.testing.expect(try awaitGridText(alloc, &srv, "after-osc", 3000)); 528 try std.testing.expect(try awaitGridText(alloc, &td.srv, "after-osc", 3000));
562 529
563 // The control, and it is what the paragraph above earns: with no client 530 // The control, and it is what the paragraph above earns: with no client
564 // ever attached the tracker IS servable, and the events ARE recorded. A 531 // ever attached the tracker IS servable, and the events ARE recorded. A
565 // refusal asserted without this would look like the guard working when it 532 // refusal asserted without this would look like the guard working when it
566 // was really the drain never reaching it. 533 // was really the drain never reaching it.
567 const s = &srv.sessions.table[0].?; 534 const s = &td.srv.sessions.table[0].?;
568 try std.testing.expect(s.tracker.canServe(s.tracker.seq)); 535 try std.testing.expect(s.tracker.canServe(s.tracker.seq));
569 for (s.pendingSlots()) |slot| try std.testing.expect(slot.* != null); 536 for (s.pendingSlots()) |slot| try std.testing.expect(slot.* != null);
570 s.freePending(srv.alloc); 537 s.freePending(td.srv.alloc);
571 538
572 // The guard itself, by direct call against a stranded tracker. Reachable in a 539 // The guard itself, by direct call against a stranded tracker. Reachable in a
573 // live daemon only under allocation failure: the rule is testable, the path 540 // live daemon only under allocation failure: the rule is testable, the path
574 // to it is not. 541 // to it is not.
575 s.tracker.rows = 0; 542 s.tracker.rows = 0;
576 s.recordPending(srv.alloc, .clipboard, gap_clip_first); 543 s.recordPending(td.srv.alloc, .clipboard, gap_clip_first);
577 s.recordPending(srv.alloc, .bell, ""); 544 s.recordPending(td.srv.alloc, .bell, "");
578 for (s.pendingSlots()) |slot| try std.testing.expect(slot.* == null); 545 for (s.pendingSlots()) |slot| try std.testing.expect(slot.* == null);
579 } 546 }
580 547
581 test "Server: a session that dies holding a pending event frees it" { 548 test "Server: a session that dies holding a pending event frees it" {
582 const alloc = std.testing.allocator; 549 const alloc = std.testing.allocator;
583 550
584 var tmp = try TmpDir.make(); 551 var td = try h.TestDaemon.open(alloc, "gapdeath");
585 defer tmp.cleanup(); 552 defer td.deinit();
586 const sock_path = try std.fmt.allocPrint(alloc, "{s}/gapdeath.sock", .{tmp.path()});
587 defer alloc.free(sock_path);
588 553
589 const script = try writeDyingGapShell(alloc, &tmp); 554 const script = try writeDyingGapShell(alloc, &td.tmp);
590 defer alloc.free(script); 555 defer alloc.free(script);
591 556
592 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 557 try td.start(.{ .shell = script });
593 defer srv.deinit();
594 558
595 // A client attaches, and NOT because the recording needs one. It is here 559 // A client attaches, and NOT because the recording needs one. It is here
596 // because `reap` does more for a session that HAS clients — queue 560 // because `reap` does more for a session that HAS clients — queue
597 // `exit_status`, drain, drop, then free — so attaching puts the free after 561 // `exit_status`, drain, drop, then free — so attaching puts the free after
598 // that sequence rather than after two no-op loops. 562 // that sequence rather than after two no-op loops.
599 const c = try dial.dialAttach(sock_path, 80, 24); 563 const c = try dial.dialAttach(td.sock_path, 80, 24);
600 defer c.close(); 564 defer c.close();
601 (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400) orelse 565 (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse
602 return error.NoSnapshot).deinit(alloc); 566 return error.NoSnapshot).deinit(alloc);
603 567
604 try proto.writeAllFd(srv.sessions.table[0].?.pty.master, "go\n"); 568 try proto.writeAllFd(td.srv.sessions.table[0].?.pty.master, "go\n");
605 try std.testing.expect(try awaitGridText(alloc, &srv, "after-osc", 3000)); 569 try std.testing.expect(try awaitGridText(alloc, &td.srv, "after-osc", 3000));
606 // Non-vacuity, and it is the whole test: with an empty slot the teardown 570 // Non-vacuity, and it is the whole test: with an empty slot the teardown
607 // below frees nothing and the leak this pins could not occur. 571 // below frees nothing and the leak this pins could not occur.
608 for (srv.sessions.table[0].?.pendingSlots()) |slot| try std.testing.expect(slot.* != null); 572 for (td.srv.sessions.table[0].?.pendingSlots()) |slot| try std.testing.expect(slot.* != null);
609 573
610 // Now let the shell exit: the `reap` teardown, a session dying while the 574 // Now let the shell exit: the `reap` teardown, a session dying while the
611 // daemon lives on. The other gap tests never reach it — they hold their 575 // daemon lives on. The other gap tests never reach it — they hold their
612 // shells open and so exercise only `Server.deinit`. 576 // shells open and so exercise only `Server.deinit`.
613 try proto.writeAllFd(srv.sessions.table[0].?.pty.master, "die\n"); 577 try proto.writeAllFd(td.srv.sessions.table[0].?.pty.master, "die\n");
614 var reaped = false; 578 var reaped = false;
615 for (0..600) |_| { 579 for (0..600) |_| {
616 srv.pumpOnce(5) catch break; 580 td.srv.pumpOnce(5) catch break;
617 if (srv.sessions.table[0] == null) { 581 if (td.srv.sessions.table[0] == null) {
618 reaped = true; 582 reaped = true;
619 break; 583 break;
620 } 584 }
src/server/server_test_deliver.zig
Old New
@@ -1,12 +1,8 @@
1 const std = @import("std"); 1 const std = @import("std");
2 const proto = @import("term").protocol; 2 const proto = @import("term").protocol;
3 const TmpDir = @import("testtmp").TmpDir;
4 const h = @import("server_test_harness.zig"); 3 const h = @import("server_test_harness.zig");
5 const dial = h.dial; 4 const dial = h.dial;
6 const srv_mod = @import("server.zig");
7 const Server = srv_mod.Server;
8 const connectedPair = h.connectedPair; 5 const connectedPair = h.connectedPair;
9 const serverThread = h.serverThread;
10 6
11 /// So a peer that never reads backs the socket up in a few KB rather than a 7 /// So a peer that never reads backs the socket up in a few KB rather than a
12 /// few hundred. Linux doubles and clamps the request: a floor, not a promise. 8 /// few hundred. Linux doubles and clamps the request: a floor, not a promise.
@@ -23,46 +19,40 @@ fn shrinkSendBuf(fd: std.posix.fd_t) !void {
23 test "Server: a stalled client does not block delivery to others" { 19 test "Server: a stalled client does not block delivery to others" {
24 const alloc = std.testing.allocator; 20 const alloc = std.testing.allocator;
25 21
26 var tmp = try TmpDir.make(); 22 var td = try h.TestDaemon.init(alloc, "stall", .{ .shell = "/bin/sh" });
27 defer tmp.cleanup(); 23 defer td.deinit();
28 const dir_path = tmp.path();
29 const sock_path = try std.fmt.allocPrint(alloc, "{s}/stall.sock", .{dir_path});
30 defer alloc.free(sock_path);
31
32 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
33 defer srv.deinit();
34 24
35 // A never reads a byte, so its send buffer fills and stays full. B 25 // A never reads a byte, so its send buffer fills and stays full. B
36 // drains normally. srv.deinit() closes both daemon-side fds. 26 // drains normally. td.srv.deinit() closes both daemon-side fds.
37 const a = try connectedPair(); 27 const a = try connectedPair();
38 defer std.posix.close(a.peer); 28 defer std.posix.close(a.peer);
39 try shrinkSendBuf(a.daemon); 29 try shrinkSendBuf(a.daemon);
40 const b = try connectedPair(); 30 const b = try connectedPair();
41 defer std.posix.close(b.peer); 31 defer std.posix.close(b.peer);
42 32
43 srv.clients[0] = .{ .sink = .{ .socket = a.daemon }, .session = 0 }; 33 td.srv.clients[0] = .{ .sink = .{ .socket = a.daemon }, .session = 0 };
44 srv.clients[1] = .{ .sink = .{ .socket = b.daemon }, .session = 0 }; 34 td.srv.clients[1] = .{ .sink = .{ .socket = b.daemon }, .session = 0 };
45 35
46 // Snapshot everyone until A backs up. A blocking write into A's full 36 // Snapshot everyone until A backs up. A blocking write into A's full
47 // buffer would hang here rather than fail, so reaching the assertions 37 // buffer would hang here rather than fail, so reaching the assertions
48 // at all is itself the "never stalls" half of this test. 38 // at all is itself the "never stalls" half of this test.
49 var rounds: usize = 0; 39 var rounds: usize = 0;
50 while (rounds < 64) : (rounds += 1) { 40 while (rounds < 64) : (rounds += 1) {
51 if (srv.clients[0]) |slot| { 41 if (td.srv.clients[0]) |slot| {
52 if (slot.pending.items.len > 0) break; 42 if (slot.pending.items.len > 0) break;
53 } else break; 43 } else break;
54 srv.sessions.table[0].?.eng.feed("stalled-client-test output\r\n"); 44 td.srv.sessions.table[0].?.eng.feed("stalled-client-test output\r\n");
55 srv.resyncSnapshot(0); 45 td.srv.resyncSnapshot(0);
56 } 46 }
57 47
58 // A is backed up but still attached: this is nowhere near the 8 MiB cap, 48 // A is backed up but still attached: this is nowhere near the 8 MiB cap,
59 // so a slow peer is queued, not dropped and not waited on. 49 // so a slow peer is queued, not dropped and not waited on.
60 try std.testing.expect(srv.clients[0] != null); 50 try std.testing.expect(td.srv.clients[0] != null);
61 try std.testing.expect(srv.clients[0].?.pending.items.len > 0); 51 try std.testing.expect(td.srv.clients[0].?.pending.items.len > 0);
62 52
63 // B is unaffected: its queue drained completely into the kernel... 53 // B is unaffected: its queue drained completely into the kernel...
64 try std.testing.expect(srv.clients[1] != null); 54 try std.testing.expect(td.srv.clients[1] != null);
65 try std.testing.expectEqual(@as(usize, 0), srv.clients[1].?.pending.items.len); 55 try std.testing.expectEqual(@as(usize, 0), td.srv.clients[1].?.pending.items.len);
66 56
67 // ...and what it received is whole parseable frames, one snapshot per 57 // ...and what it received is whole parseable frames, one snapshot per
68 // round, not a stream truncated or interleaved by A's backpressure. 58 // round, not a stream truncated or interleaved by A's backpressure.
@@ -80,37 +70,31 @@ test "Server: a stalled client does not block delivery to others" {
80 test "Server: a client exceeding the pending cap is dropped" { 70 test "Server: a client exceeding the pending cap is dropped" {
81 const alloc = std.testing.allocator; 71 const alloc = std.testing.allocator;
82 72
83 var tmp = try TmpDir.make(); 73 var td = try h.TestDaemon.init(alloc, "cap", .{ .shell = "/bin/sh" });
84 defer tmp.cleanup(); 74 defer td.deinit();
85 const dir_path = tmp.path();
86 const sock_path = try std.fmt.allocPrint(alloc, "{s}/cap.sock", .{dir_path});
87 defer alloc.free(sock_path);
88
89 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
90 defer srv.deinit();
91 75
92 const c = try connectedPair(); 76 const c = try connectedPair();
93 defer std.posix.close(c.peer); 77 defer std.posix.close(c.peer);
94 try shrinkSendBuf(c.daemon); 78 try shrinkSendBuf(c.daemon);
95 srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 }; 79 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 };
96 80
97 // Size the cap to a few snapshots' worth of backlog. Below one frame it 81 // Size the cap to a few snapshots' worth of backlog. Below one frame it
98 // would drop on the very first send, which would prove nothing about 82 // would drop on the very first send, which would prove nothing about
99 // bounding a backlog; the production default is 8 MiB. 83 // bounding a backlog; the production default is 8 MiB.
100 const state = try srv.sessions.table[0].?.eng.dumpState(alloc); 84 const state = try td.srv.sessions.table[0].?.eng.dumpState(alloc);
101 defer alloc.free(state); 85 defer alloc.free(state);
102 const frame_len = 5 + proto.snapshot_prefix_len + state.len; 86 const frame_len = 5 + proto.snapshot_prefix_len + state.len;
103 srv.pending_cap = 4 * frame_len; 87 td.srv.pending_cap = 4 * frame_len;
104 88
105 var rounds: usize = 0; 89 var rounds: usize = 0;
106 while (rounds < 256 and srv.clients[0] != null) : (rounds += 1) { 90 while (rounds < 256 and td.srv.clients[0] != null) : (rounds += 1) {
107 srv.sessions.table[0].?.eng.feed("cap-test output\r\n"); 91 td.srv.sessions.table[0].?.eng.feed("cap-test output\r\n");
108 srv.resyncSnapshot(0); 92 td.srv.resyncSnapshot(0);
109 } 93 }
110 94
111 // Past the cap the daemon drops the client rather than buffering for it 95 // Past the cap the daemon drops the client rather than buffering for it
112 // without bound. 96 // without bound.
113 try std.testing.expect(srv.clients[0] == null); 97 try std.testing.expect(td.srv.clients[0] == null);
114 // The cap is what ended it, not the loop bound: a four-frame cap behind 98 // The cap is what ended it, not the loop bound: a four-frame cap behind
115 // a socket buffer of about one frame cannot absorb many more than five. 99 // a socket buffer of about one frame cannot absorb many more than five.
116 try std.testing.expect(rounds <= 16); 100 try std.testing.expect(rounds <= 16);
@@ -127,49 +111,43 @@ test "Server: a client exceeding the pending cap is dropped" {
127 111
128 // And the session carries on without it: with nobody attached there is 112 // And the session carries on without it: with nobody attached there is
129 // no one to snapshot, so the counters stand still. 113 // no one to snapshot, so the counters stand still.
130 const snapshots_at_drop = srv.stats.snapshots; 114 const snapshots_at_drop = td.srv.stats.snapshots;
131 try std.testing.expect(snapshots_at_drop > 0); 115 try std.testing.expect(snapshots_at_drop > 0);
132 srv.sessions.table[0].?.eng.feed("after the drop\r\n"); 116 td.srv.sessions.table[0].?.eng.feed("after the drop\r\n");
133 srv.resyncSnapshot(0); 117 td.srv.resyncSnapshot(0);
134 try std.testing.expectEqual(snapshots_at_drop, srv.stats.snapshots); 118 try std.testing.expectEqual(snapshots_at_drop, td.srv.stats.snapshots);
135 } 119 }
136 120
137 test "Server: a writable backlog is flushed by poll, not mistaken for input" { 121 test "Server: a writable backlog is flushed by poll, not mistaken for input" {
138 const alloc = std.testing.allocator; 122 const alloc = std.testing.allocator;
139 123
140 var tmp = try TmpDir.make(); 124 var td = try h.TestDaemon.init(alloc, "pollout", .{ .shell = "/bin/sh" });
141 defer tmp.cleanup(); 125 defer td.deinit();
142 const dir_path = tmp.path();
143 const sock_path = try std.fmt.allocPrint(alloc, "{s}/pollout.sock", .{dir_path});
144 defer alloc.free(sock_path);
145
146 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
147 defer srv.deinit();
148 126
149 // Drain the shell's startup output first, so the pump below has exactly 127 // Drain the shell's startup output first, so the pump below has exactly
150 // one thing to react to: the client's writability. 128 // one thing to react to: the client's writability.
151 while (true) { 129 while (true) {
152 var pfd = [_]std.posix.pollfd{ 130 var pfd = [_]std.posix.pollfd{
153 .{ .fd = srv.sessions.table[0].?.pty.master, .events = std.posix.POLL.IN, .revents = 0 }, 131 .{ .fd = td.srv.sessions.table[0].?.pty.master, .events = std.posix.POLL.IN, .revents = 0 },
154 }; 132 };
155 if ((try std.posix.poll(&pfd, 300)) == 0) break; 133 if ((try std.posix.poll(&pfd, 300)) == 0) break;
156 var buf: [64 * 1024]u8 = undefined; 134 var buf: [64 * 1024]u8 = undefined;
157 _ = std.posix.read(srv.sessions.table[0].?.pty.master, &buf) catch break; 135 _ = std.posix.read(td.srv.sessions.table[0].?.pty.master, &buf) catch break;
158 } 136 }
159 137
160 const c = try connectedPair(); 138 const c = try connectedPair();
161 defer std.posix.close(c.peer); 139 defer std.posix.close(c.peer);
162 try shrinkSendBuf(c.daemon); 140 try shrinkSendBuf(c.daemon);
163 srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 }; 141 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 };
164 142
165 // Back the client up: the kernel stops taking bytes, so they queue. 143 // Back the client up: the kernel stops taking bytes, so they queue.
166 var rounds: usize = 0; 144 var rounds: usize = 0;
167 while (rounds < 64) : (rounds += 1) { 145 while (rounds < 64) : (rounds += 1) {
168 if (srv.clients[0].?.pending.items.len > 0) break; 146 if (td.srv.clients[0].?.pending.items.len > 0) break;
169 srv.sessions.table[0].?.eng.feed("pollout-test output\r\n"); 147 td.srv.sessions.table[0].?.eng.feed("pollout-test output\r\n");
170 srv.resyncSnapshot(0); 148 td.srv.resyncSnapshot(0);
171 } 149 }
172 const backlog = srv.clients[0].?.pending.items.len; 150 const backlog = td.srv.clients[0].?.pending.items.len;
173 try std.testing.expect(backlog > 0); 151 try std.testing.expect(backlog > 0);
174 152
175 // The client reads, so its socket is writable again — but it sends 153 // The client reads, so its socket is writable again — but it sends
@@ -179,29 +157,23 @@ test "Server: a writable backlog is flushed by poll, not mistaken for input" {
179 var drain: [16 * 1024]u8 = undefined; 157 var drain: [16 * 1024]u8 = undefined;
180 _ = try std.posix.read(c.peer, &drain); 158 _ = try std.posix.read(c.peer, &drain);
181 159
182 try srv.pumpOnce(50); 160 try td.srv.pumpOnce(50);
183 161
184 // The other half: the pump used the POLLOUT for what it was. 162 // The other half: the pump used the POLLOUT for what it was.
185 try std.testing.expect(srv.clients[0] != null); 163 try std.testing.expect(td.srv.clients[0] != null);
186 try std.testing.expect(srv.clients[0].?.pending.items.len < backlog); 164 try std.testing.expect(td.srv.clients[0].?.pending.items.len < backlog);
187 } 165 }
188 166
189 test "Server: a partially flushed queue delivers every byte exactly once" { 167 test "Server: a partially flushed queue delivers every byte exactly once" {
190 const alloc = std.testing.allocator; 168 const alloc = std.testing.allocator;
191 var tmp = try TmpDir.make(); 169 var td = try h.TestDaemon.init(alloc, "pf", .{ .shell = "/bin/sh" });
192 defer tmp.cleanup(); 170 defer td.deinit();
193 const dir_path = tmp.path();
194 const sock_path = try std.fmt.allocPrint(alloc, "{s}/pf.sock", .{dir_path});
195 defer alloc.free(sock_path);
196
197 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
198 defer srv.deinit();
199 171
200 const c = try connectedPair(); 172 const c = try connectedPair();
201 defer std.posix.close(c.peer); 173 defer std.posix.close(c.peer);
202 try shrinkSendBuf(c.daemon); 174 try shrinkSendBuf(c.daemon);
203 srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 }; 175 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 };
204 srv.pending_cap = 64 * 1024 * 1024; 176 td.srv.pending_cap = 64 * 1024 * 1024;
205 177
206 // Frames of varying length, queued while the peer is silent, so most of 178 // Frames of varying length, queued while the peer is silent, so most of
207 // them pile up behind a full socket buffer and every subsequent flush 179 // them pile up behind a full socket buffer and every subsequent flush
@@ -215,16 +187,16 @@ test "Server: a partially flushed queue delivers every byte exactly once" {
215 const full = try std.fmt.bufPrint(&pbuf, "frame-{d}-{s}", .{ k, "x" ** 320 }); 187 const full = try std.fmt.bufPrint(&pbuf, "frame-{d}-{s}", .{ k, "x" ** 320 });
216 const payload = full[0 .. 20 + (k % 280)]; 188 const payload = full[0 .. 20 + (k % 280)];
217 try proto.appendFrame(&expect, alloc, .dump_reply, payload); 189 try proto.appendFrame(&expect, alloc, .dump_reply, payload);
218 try std.testing.expect(srv.queueFrame(0, .dump_reply, payload)); 190 try std.testing.expect(td.srv.queueFrame(0, .dump_reply, payload));
219 } 191 }
220 try std.testing.expect(srv.clients[0].?.pending.items.len > 0); 192 try std.testing.expect(td.srv.clients[0].?.pending.items.len > 0);
221 193
222 // Drain and flush alternately: each flush starts where the last stopped. 194 // Drain and flush alternately: each flush starts where the last stopped.
223 var got: std.ArrayList(u8) = .empty; 195 var got: std.ArrayList(u8) = .empty;
224 defer got.deinit(alloc); 196 defer got.deinit(alloc);
225 var spins: usize = 0; 197 var spins: usize = 0;
226 while (srv.clients[0] != null and 198 while (td.srv.clients[0] != null and
227 srv.clients[0].?.pending.items.len > 0 and spins < 100000) : (spins += 1) 199 td.srv.clients[0].?.pending.items.len > 0 and spins < 100000) : (spins += 1)
228 { 200 {
229 var rbuf: [4096]u8 = undefined; 201 var rbuf: [4096]u8 = undefined;
230 var pfd = [_]std.posix.pollfd{.{ .fd = c.peer, .events = std.posix.POLL.IN, .revents = 0 }}; 202 var pfd = [_]std.posix.pollfd{.{ .fd = c.peer, .events = std.posix.POLL.IN, .revents = 0 }};
@@ -233,7 +205,7 @@ test "Server: a partially flushed queue delivers every byte exactly once" {
233 if (n == 0) break; 205 if (n == 0) break;
234 try got.appendSlice(alloc, rbuf[0..n]); 206 try got.appendSlice(alloc, rbuf[0..n]);
235 } 207 }
236 srv.flushClient(0); 208 td.srv.flushClient(0);
237 } 209 }
238 while (got.items.len < expect.items.len) { 210 while (got.items.len < expect.items.len) {
239 var rbuf: [4096]u8 = undefined; 211 var rbuf: [4096]u8 = undefined;
@@ -244,28 +216,19 @@ test "Server: a partially flushed queue delivers every byte exactly once" {
244 try got.appendSlice(alloc, rbuf[0..n]); 216 try got.appendSlice(alloc, rbuf[0..n]);
245 } 217 }
246 218
247 try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.pending.items.len); 219 try std.testing.expectEqual(@as(usize, 0), td.srv.clients[0].?.pending.items.len);
248 try std.testing.expectEqualSlices(u8, expect.items, got.items); 220 try std.testing.expectEqualSlices(u8, expect.items, got.items);
249 } 221 }
250 222
251 test "Server: the shell's exit status reaches an attached client" { 223 test "Server: the shell's exit status reaches an attached client" {
252 const alloc = std.testing.allocator; 224 const alloc = std.testing.allocator;
253 225
254 var tmp = try TmpDir.make(); 226 var td = try h.TestDaemon.init(alloc, "exit", .{ .shell = "/bin/sh" });
255 defer tmp.cleanup(); 227 defer td.deinit();
256 const dir_path = tmp.path();
257 const sock_path = try std.fmt.allocPrint(alloc, "{s}/exit.sock", .{dir_path});
258 defer alloc.free(sock_path);
259 228
260 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 229 try td.threaded();
261 defer srv.deinit();
262 230
263 var stop = std.atomic.Value(bool).init(false); 231 const c = try dial.dialAttach(td.sock_path, 80, 24);
264 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
265 defer th.join();
266 defer stop.store(true, .release);
267
268 const c = try dial.dialAttach(sock_path, 80, 24);
269 defer c.close(); 232 defer c.close();
270 233
271 // Settle first, so the shell is ready for input and the exit below is 234 // Settle first, so the shell is ready for input and the exit below is
@@ -293,20 +256,10 @@ test "Server: the shell's exit status reaches an attached client" {
293 try proto.writeFrame(c.handle, .input, "exit 7\n"); 256 try proto.writeFrame(c.handle, .input, "exit 7\n");
294 257
295 var status: ?u8 = null; 258 var status: ?u8 = null;
296 deadline_ms = 10_000; 259 if (try h.awaitFrameOn(alloc, c.handle, .exit_status, 10_000)) |frame| {
297 while (deadline_ms > 0 and status == null) {
298 var pfd = [_]std.posix.pollfd{
299 .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 },
300 };
301 const ready = try std.posix.poll(&pfd, 100);
302 deadline_ms -|= 100;
303 if (ready == 0) continue;
304 const frame = (try proto.readFrame(alloc, c.handle)) orelse break;
305 defer frame.deinit(alloc); 260 defer frame.deinit(alloc);
306 if (frame.type == .exit_status) { 261 try std.testing.expectEqual(@as(usize, 1), frame.payload.len);
307 try std.testing.expectEqual(@as(usize, 1), frame.payload.len); 262 status = frame.payload[0];
308 status = frame.payload[0];
309 }
310 } 263 }
311 try std.testing.expectEqual(@as(?u8, 7), status); 264 try std.testing.expectEqual(@as(?u8, 7), status);
312 } 265 }
@@ -342,43 +295,37 @@ const PeerDrainer = struct {
342 test "Server: the exit drain delivers a backlog once the peer resumes reading" { 295 test "Server: the exit drain delivers a backlog once the peer resumes reading" {
343 const alloc = std.testing.allocator; 296 const alloc = std.testing.allocator;
344 297
345 var tmp = try TmpDir.make(); 298 var td = try h.TestDaemon.init(alloc, "exitdrain", .{ .shell = "/bin/sh" });
346 defer tmp.cleanup(); 299 defer td.deinit();
347 const dir_path = tmp.path();
348 const sock_path = try std.fmt.allocPrint(alloc, "{s}/exitdrain.sock", .{dir_path});
349 defer alloc.free(sock_path);
350
351 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
352 defer srv.deinit();
353 300
354 const c = try connectedPair(); 301 const c = try connectedPair();
355 defer std.posix.close(c.peer); 302 defer std.posix.close(c.peer);
356 try shrinkSendBuf(c.daemon); 303 try shrinkSendBuf(c.daemon);
357 srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 }; 304 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 };
358 305
359 // Back the client up, so the exit_status queued next lands behind a 306 // Back the client up, so the exit_status queued next lands behind a
360 // backlog the kernel has already refused — the situation in which the 307 // backlog the kernel has already refused — the situation in which the
361 // frame would otherwise be lost. 308 // frame would otherwise be lost.
362 var rounds: usize = 0; 309 var rounds: usize = 0;
363 while (rounds < 64) : (rounds += 1) { 310 while (rounds < 64) : (rounds += 1) {
364 if (srv.clients[0].?.pending.items.len > 0) break; 311 if (td.srv.clients[0].?.pending.items.len > 0) break;
365 srv.sessions.table[0].?.eng.feed("exit-drain output\r\n"); 312 td.srv.sessions.table[0].?.eng.feed("exit-drain output\r\n");
366 srv.resyncSnapshot(0); 313 td.srv.resyncSnapshot(0);
367 } 314 }
368 try std.testing.expect(srv.clients[0].?.pending.items.len > 0); 315 try std.testing.expect(td.srv.clients[0].?.pending.items.len > 0);
369 try std.testing.expect(srv.queueFrame(0, .exit_status, &.{7})); 316 try std.testing.expect(td.srv.queueFrame(0, .exit_status, &.{7}));
370 try std.testing.expect(srv.clients[0].?.pending.items.len > 0); 317 try std.testing.expect(td.srv.clients[0].?.pending.items.len > 0);
371 318
372 var drainer = PeerDrainer{ .fd = c.peer, .alloc = alloc }; 319 var drainer = PeerDrainer{ .fd = c.peer, .alloc = alloc };
373 defer drainer.out.deinit(alloc); 320 defer drainer.out.deinit(alloc);
374 const th = try std.Thread.spawn(.{}, PeerDrainer.run, .{ &drainer, 700 }); 321 const th = try std.Thread.spawn(.{}, PeerDrainer.run, .{ &drainer, 700 });
375 322
376 srv.drainPending(5000); 323 td.srv.drainPending(5000);
377 th.join(); 324 th.join();
378 325
379 // Everything owed went out... 326 // Everything owed went out...
380 try std.testing.expect(srv.clients[0] != null); 327 try std.testing.expect(td.srv.clients[0] != null);
381 try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.pending.items.len); 328 try std.testing.expectEqual(@as(usize, 0), td.srv.clients[0].?.pending.items.len);
382 // ...and since the queue is FIFO and exit_status was queued last, the 329 // ...and since the queue is FIFO and exit_status was queued last, the
383 // stream ends with exactly its wire bytes. 330 // stream ends with exactly its wire bytes.
384 const tail = [_]u8{ 0x82, 1, 0, 0, 0, 7 }; 331 const tail = [_]u8{ 0x82, 1, 0, 0, 0, 7 };
@@ -393,37 +340,31 @@ test "Server: the exit drain delivers a backlog once the peer resumes reading" {
393 test "Server: the exit drain gives up on a peer that never reads" { 340 test "Server: the exit drain gives up on a peer that never reads" {
394 const alloc = std.testing.allocator; 341 const alloc = std.testing.allocator;
395 342
396 var tmp = try TmpDir.make(); 343 var td = try h.TestDaemon.init(alloc, "drain", .{ .shell = "/bin/sh" });
397 defer tmp.cleanup(); 344 defer td.deinit();
398 const dir_path = tmp.path();
399 const sock_path = try std.fmt.allocPrint(alloc, "{s}/drain.sock", .{dir_path});
400 defer alloc.free(sock_path);
401
402 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
403 defer srv.deinit();
404 345
405 const c = try connectedPair(); 346 const c = try connectedPair();
406 defer std.posix.close(c.peer); 347 defer std.posix.close(c.peer);
407 try shrinkSendBuf(c.daemon); 348 try shrinkSendBuf(c.daemon);
408 srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 }; 349 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon }, .session = 0 };
409 350
410 var rounds: usize = 0; 351 var rounds: usize = 0;
411 while (rounds < 64) : (rounds += 1) { 352 while (rounds < 64) : (rounds += 1) {
412 if (srv.clients[0].?.pending.items.len > 0) break; 353 if (td.srv.clients[0].?.pending.items.len > 0) break;
413 srv.sessions.table[0].?.eng.feed("drain-test output\r\n"); 354 td.srv.sessions.table[0].?.eng.feed("drain-test output\r\n");
414 srv.resyncSnapshot(0); 355 td.srv.resyncSnapshot(0);
415 } 356 }
416 try std.testing.expect(srv.clients[0].?.pending.items.len > 0); 357 try std.testing.expect(td.srv.clients[0].?.pending.items.len > 0);
417 358
418 // The peer is silent and its buffer is full, so this can never finish. 359 // The peer is silent and its buffer is full, so this can never finish.
419 // It must return on the deadline rather than hold the daemon open: a 360 // It must return on the deadline rather than hold the daemon open: a
420 // dead client must not be able to stop the session from exiting. 361 // dead client must not be able to stop the session from exiting.
421 var timer = try std.time.Timer.start(); 362 var timer = try std.time.Timer.start();
422 srv.drainPending(150); 363 td.srv.drainPending(150);
423 const elapsed_ms = timer.read() / std.time.ns_per_ms; 364 const elapsed_ms = timer.read() / std.time.ns_per_ms;
424 365
425 try std.testing.expect(srv.clients[0] != null); 366 try std.testing.expect(td.srv.clients[0] != null);
426 try std.testing.expect(srv.clients[0].?.pending.items.len > 0); 367 try std.testing.expect(td.srv.clients[0].?.pending.items.len > 0);
427 // Generous upper bound: the point is that it is bounded at all, not the 368 // Generous upper bound: the point is that it is bounded at all, not the
428 // precise number, which a loaded CI box would make flaky. 369 // precise number, which a loaded CI box would make flaky.
429 try std.testing.expect(elapsed_ms < 5000); 370 try std.testing.expect(elapsed_ms < 5000);
@@ -432,28 +373,22 @@ test "Server: the exit drain gives up on a peer that never reads" {
432 test "Server: broadcast stats count every send but the counterfactual once" { 373 test "Server: broadcast stats count every send but the counterfactual once" {
433 const alloc = std.testing.allocator; 374 const alloc = std.testing.allocator;
434 375
435 var tmp = try TmpDir.make(); 376 var td = try h.TestDaemon.init(alloc, "stats", .{ .shell = "/bin/sh" });
436 defer tmp.cleanup(); 377 defer td.deinit();
437 const dir_path = tmp.path();
438 const sock_path = try std.fmt.allocPrint(alloc, "{s}/stats.sock", .{dir_path});
439 defer alloc.free(sock_path);
440
441 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
442 defer srv.deinit();
443 378
444 // Two stand-in clients, driving the server directly so the accounting is 379 // Two stand-in clients, driving the server directly so the accounting is
445 // exact instead of hostage to shell timing. Real sockets, not pipes: the 380 // exact instead of hostage to shell timing. Real sockets, not pipes: the
446 // send path is send(2) now. srv.deinit() closes the daemon-side fds. 381 // send path is send(2) now. td.srv.deinit() closes the daemon-side fds.
447 const pa = try connectedPair(); 382 const pa = try connectedPair();
448 defer std.posix.close(pa.peer); 383 defer std.posix.close(pa.peer);
449 const pb = try connectedPair(); 384 const pb = try connectedPair();
450 defer std.posix.close(pb.peer); 385 defer std.posix.close(pb.peer);
451 srv.clients[0] = .{ .sink = .{ .socket = pa.daemon }, .session = 0 }; 386 td.srv.clients[0] = .{ .sink = .{ .socket = pa.daemon }, .session = 0 };
452 srv.clients[1] = .{ .sink = .{ .socket = pb.daemon }, .session = 0 }; 387 td.srv.clients[1] = .{ .sink = .{ .socket = pb.daemon }, .session = 0 };
453 388
454 try srv.sessions.table[0].?.tracker.rebuild(alloc, srv.sessions.table[0].?.eng, 24, 80); 389 try td.srv.sessions.table[0].?.tracker.rebuild(alloc, td.srv.sessions.table[0].?.eng, 24, 80);
455 srv.sessions.table[0].?.eng.feed("one update, two recipients"); 390 td.srv.sessions.table[0].?.eng.feed("one update, two recipients");
456 srv.sendUpdate(0); 391 td.srv.sendUpdate(0);
457 392
458 const fa = (try proto.readFrame(alloc, pa.peer)).?; 393 const fa = (try proto.readFrame(alloc, pa.peer)).?;
459 defer fa.deinit(alloc); 394 defer fa.deinit(alloc);
@@ -464,16 +399,16 @@ test "Server: broadcast stats count every send but the counterfactual once" {
464 try std.testing.expectEqualSlices(u8, fa.payload, fb.payload); 399 try std.testing.expectEqualSlices(u8, fa.payload, fb.payload);
465 400
466 // Actuals are per send: two clients, two deltas on the wire. 401 // Actuals are per send: two clients, two deltas on the wire.
467 try std.testing.expectEqual(@as(u64, 2), srv.stats.deltas); 402 try std.testing.expectEqual(@as(u64, 2), td.srv.stats.deltas);
468 try std.testing.expectEqual(@as(u64, @intCast(2 * fa.payload.len)), srv.stats.delta_bytes); 403 try std.testing.expectEqual(@as(u64, @intCast(2 * fa.payload.len)), td.srv.stats.delta_bytes);
469 // The counterfactual is per event: what a snapshot-only daemon would 404 // The counterfactual is per event: what a snapshot-only daemon would
470 // have sent for this one update is one snapshot, however many clients 405 // have sent for this one update is one snapshot, however many clients
471 // received it. 406 // received it.
472 const state = try srv.sessions.table[0].?.eng.dumpState(alloc); 407 const state = try td.srv.sessions.table[0].?.eng.dumpState(alloc);
473 defer alloc.free(state); 408 defer alloc.free(state);
474 try std.testing.expectEqual( 409 try std.testing.expectEqual(
475 @as(u64, @intCast(proto.snapshot_prefix_len + state.len)), 410 @as(u64, @intCast(proto.snapshot_prefix_len + state.len)),
476 srv.stats.snapshot_equiv_bytes, 411 td.srv.stats.snapshot_equiv_bytes,
477 ); 412 );
478 try std.testing.expectEqual(@as(u64, 0), srv.stats.snapshots); 413 try std.testing.expectEqual(@as(u64, 0), td.srv.stats.snapshots);
479 } 414 }
src/server/server_test_harness.zig
Old New
@@ -1,5 +1,6 @@
1 const std = @import("std"); 1 const std = @import("std");
2 const Engine = @import("term").engine.Engine; 2 const Engine = @import("term").engine.Engine;
3 const Pty = @import("pty").Pty;
3 const proto = @import("term").protocol; 4 const proto = @import("term").protocol;
4 const replica_mod = @import("term").replica; 5 const replica_mod = @import("term").replica;
5 const quic = @import("quic"); 6 const quic = @import("quic");
@@ -19,6 +20,124 @@ pub fn serverThread(srv: *Server, stop: *std.atomic.Value(bool)) void {
19 } 20 }
20 } 21 }
21 22
23 /// A daemon on a socket of its own, which is what nearly every test in this
24 /// folder opens with: a short-path temp directory (`testtmp`, because a unix
25 /// socket path caps at 108 bytes), a socket named inside it, and a `Server`
26 /// bound to that socket. It owns all three and takes them down in the order
27 /// they have to come down in.
28 ///
29 /// The pieces are still reachable — `td.srv` for the daemon, `td.sock_path`
30 /// for a dial, `td.tmp` for the tests that put an rc file, a key or an agent
31 /// socket in the same directory.
32 pub const TestDaemon = struct {
33 alloc: std.mem.Allocator,
34 tmp: TmpDir,
35 /// Owned here, not by the caller: `Server` KEEPS both of these slices
36 /// (`opts.sock_path` for the unlink at teardown, `opts.shell` for the
37 /// manifest and for the next session's spawn), so both have to outlive
38 /// `srv.deinit()`. A test that allocated them itself would free them on
39 /// its own defer, which runs first.
40 sock_path: []u8,
41 shell: [:0]u8,
42 /// Valid once `start` has run. `open` leaves it undefined on purpose so
43 /// that `td.srv` is the spelling everywhere, rather than `td.srv.?`.
44 srv: Server,
45 started: bool = false,
46 stop: std.atomic.Value(bool) = .init(false),
47 thread: ?std.Thread = null,
48
49 /// `Server.Options` minus `sock_path`, which is this fixture's to name.
50 /// Every other field is passed through rather than flattened: the tests
51 /// that ask for shell integration, a non-default grid or a version string
52 /// are asking for behaviour, not for convenience.
53 pub const Opts = struct {
54 shell: [:0]const u8,
55 cols: u16 = 80,
56 rows: u16 = 24,
57 shell_integration: bool = false,
58 extra_env: []const Pty.EnvPair = &.{},
59 version: []const u8 = "",
60 };
61
62 /// The directory and the socket path, with no daemon on them yet. For the
63 /// tests whose session shell is a script they have to write INTO that
64 /// directory first: they `open`, write the script, then `start` with it.
65 /// `init` is the same two steps for everyone else.
66 pub fn open(alloc: std.mem.Allocator, tag: []const u8) !TestDaemon {
67 var tmp = try TmpDir.make();
68 errdefer tmp.cleanup();
69 // After the move into the returned value would be wrong: `TmpDir.path`
70 // points into the struct's own buffer, so the slice is only good at the
71 // address it is read from. This is a copy, which travels.
72 const sock_path = try std.fmt.allocPrint(alloc, "{s}/{s}.sock", .{ tmp.path(), tag });
73 return .{
74 .alloc = alloc,
75 .tmp = tmp,
76 .sock_path = sock_path,
77 .shell = try alloc.dupeZ(u8, ""),
78 .srv = undefined,
79 };
80 }
81
82 pub fn start(self: *TestDaemon, opts: Opts) !void {
83 self.alloc.free(self.shell);
84 self.shell = try self.alloc.dupeZ(u8, opts.shell);
85 self.srv = try Server.init(self.alloc, .{
86 .sock_path = self.sock_path,
87 .shell = self.shell,
88 .cols = opts.cols,
89 .rows = opts.rows,
90 .shell_integration = opts.shell_integration,
91 .extra_env = opts.extra_env,
92 .version = opts.version,
93 });
94 self.started = true;
95 }
96
97 pub fn init(alloc: std.mem.Allocator, tag: []const u8, opts: Opts) !TestDaemon {
98 var self = try open(alloc, tag);
99 errdefer self.deinit();
100 try self.start(opts);
101 return self;
102 }
103
104 /// Run the daemon's pump on a thread, for the tests that talk to it over a
105 /// real socket instead of driving `pumpOnce` themselves.
106 pub fn threaded(self: *TestDaemon) !void {
107 self.thread = try std.Thread.spawn(.{}, serverThread, .{ &self.srv, &self.stop });
108 }
109
110 /// Take the daemon down but KEEP the directory, for the tests whose
111 /// question is what the teardown left on disk — a shim directory removed,
112 /// a planted file not removed. Those have to ask after the server is gone
113 /// and while its directory is still there, so they scope this and let the
114 /// `deinit` behind it do the rest; it is idempotent for that reason.
115 pub fn shutdown(self: *TestDaemon) void {
116 // Raise the flag BEFORE joining. `serverThread` only leaves its loop
117 // when it reads the flag, so a join that comes first waits on a thread
118 // nobody has asked to stop, and the test hangs until the runner is
119 // killed. Written as two statements in one place because as two
120 // `defer`s it was a declaration-order puzzle re-solved at every test
121 // that spawned a thread.
122 if (self.thread) |t| {
123 self.stop.store(true, .release);
124 t.join();
125 self.thread = null;
126 }
127 if (self.started) {
128 self.srv.deinit();
129 self.started = false;
130 }
131 }
132
133 pub fn deinit(self: *TestDaemon) void {
134 self.shutdown();
135 self.alloc.free(self.shell);
136 self.alloc.free(self.sock_path);
137 self.tmp.cleanup();
138 }
139 };
140
22 /// Bring a replica engine up to date with one daemon frame. The replay lives in 141 /// Bring a replica engine up to date with one daemon frame. The replay lives in
23 /// replica.zig — the production client's — so these tests replay through the 142 /// replica.zig — the production client's — so these tests replay through the
24 /// code the client ships and not a hand-rolled twin. 143 /// code the client ships and not a hand-rolled twin.
@@ -165,6 +284,46 @@ pub fn awaitFrame(
165 return null; 284 return null;
166 } 285 }
167 286
287 /// Threaded twin of `awaitFrame`: polls `fd` until a frame of type `want`
288 /// arrives or `timeout_ms` of wall clock passes. For the tests whose daemon
289 /// runs on `serverThread` — `awaitFrame` pumps the server itself, so a test
290 /// that has a thread doing that would be double-pumping.
291 ///
292 /// The budget is real elapsed time, read off `milliTimestamp`. The loops this
293 /// replaces spelled theirs as `deadline_ms -|= 100` once per `poll`, which
294 /// charges the full 100 ms even when the poll returned instantly because a
295 /// frame was already waiting: a socket with traffic on it spent a "5000 ms"
296 /// budget in fifty near-instant turns. Those budgets were frame counts wearing
297 /// a millisecond's name, and a slow box shortened them further rather than
298 /// giving them more time.
299 ///
300 /// Non-matching frames are dropped, the same as `awaitFrame` drops them, so
301 /// this is only right for "the first `want` to arrive" — not for a caller that
302 /// asserts on what came before it. Null means the budget ran out or the peer
303 /// closed; the caller is the one that knows which of those is a failure.
304 pub fn awaitFrameOn(
305 alloc: std.mem.Allocator,
306 fd: std.posix.fd_t,
307 want: proto.MsgType,
308 timeout_ms: i64,
309 ) !?proto.Frame {
310 const deadline = std.time.milliTimestamp() + timeout_ms;
311 while (true) {
312 const left = deadline - std.time.milliTimestamp();
313 if (left <= 0) return null;
314 var pfd = [_]std.posix.pollfd{
315 .{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 },
316 };
317 // Capped so the poll cannot outlast the budget, and so a long budget
318 // still wakes often enough to notice it has expired.
319 const ready = try std.posix.poll(&pfd, @intCast(@min(left, 100)));
320 if (ready == 0) continue;
321 const frame = (try proto.readFrame(alloc, fd)) orelse return null;
322 if (frame.type == want) return frame;
323 frame.deinit(alloc);
324 }
325 }
326
168 /// The liveness half: "no mark arrived" is worthless against a shell that 327 /// The liveness half: "no mark arrived" is worthless against a shell that
169 /// never started. 328 /// never started.
170 pub fn awaitGridText( 329 pub fn awaitGridText(
src/server/server_test_modes.zig
Old New
@@ -1,6 +1,5 @@
1 const std = @import("std"); 1 const std = @import("std");
2 const proto = @import("term").protocol; 2 const proto = @import("term").protocol;
3 const TmpDir = @import("testtmp").TmpDir;
4 const h = @import("server_test_harness.zig"); 3 const h = @import("server_test_harness.zig");
5 const dial = h.dial; 4 const dial = h.dial;
6 const srv_mod = @import("server.zig"); 5 const srv_mod = @import("server.zig");
@@ -41,16 +40,12 @@ fn drainModes(
41 test "Server: an attach arriving on an established connection is answered with the mode" { 40 test "Server: an attach arriving on an established connection is answered with the mode" {
42 const alloc = std.testing.allocator; 41 const alloc = std.testing.allocator;
43 42
44 var tmp = try TmpDir.make(); 43 var td = try h.TestDaemon.open(alloc, "reattach");
45 defer tmp.cleanup(); 44 defer td.deinit();
46 const dir_path = tmp.path();
47 const sock_path = try std.fmt.allocPrint(alloc, "{s}/reattach.sock", .{dir_path});
48 defer alloc.free(sock_path);
49 45
50 // /bin/cat: a session that produces no output of its own, so the only 46 // /bin/cat: a session that produces no output of its own, so the only
51 // frames on this socket are the ones the attach caused. 47 // frames on this socket are the ones the attach caused.
52 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 48 try td.start(.{ .shell = "/bin/cat" });
53 defer srv.deinit();
54 49
55 const c = try connectedPair(); 50 const c = try connectedPair();
56 defer std.posix.close(c.peer); 51 defer std.posix.close(c.peer);
@@ -58,7 +53,7 @@ test "Server: an attach arriving on an established connection is answered with t
58 // since its slot is created at handshake and its FIRST attach reaches 53 // since its slot is created at handshake and its FIRST attach reaches
59 // `handleFrame` rather than `serviceObserver`. The two attach paths are 54 // `handleFrame` rather than `serviceObserver`. The two attach paths are
60 // separate arms, each needing its own send. No session on purpose. 55 // separate arms, each needing its own send. No session on purpose.
61 srv.clients[0] = .{ .sink = .{ .socket = c.daemon } }; 56 td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon } };
62 57
63 var modes: std.ArrayList(proto.PtyModeFlags) = .empty; 58 var modes: std.ArrayList(proto.PtyModeFlags) = .empty;
64 defer modes.deinit(alloc); 59 defer modes.deinit(alloc);
@@ -69,7 +64,7 @@ test "Server: an attach arriving on an established connection is answered with t
69 64
70 // First attach, before any pump has run: this is the branch that reads 65 // First attach, before any pump has run: this is the branch that reads
71 // the pty for itself because nothing has been broadcast yet. 66 // the pty for itself because nothing has been broadcast yet.
72 srv.pushInbound(0, frame.items); 67 td.srv.pushInbound(0, frame.items);
73 try drainModes(alloc, c.peer, &modes); 68 try drainModes(alloc, c.peer, &modes);
74 try std.testing.expectEqual(@as(usize, 1), modes.items.len); 69 try std.testing.expectEqual(@as(usize, 1), modes.items.len);
75 try std.testing.expect(modes.items[0].icanon); 70 try std.testing.expect(modes.items[0].icanon);
@@ -78,9 +73,9 @@ test "Server: an attach arriving on an established connection is answered with t
78 // And again once a pump has been round, where the daemon already knows 73 // And again once a pump has been round, where the daemon already knows
79 // the mode and the fallback is not what is being exercised. A reattach 74 // the mode and the fallback is not what is being exercised. A reattach
80 // over a live connection is a real client's reconnect path. 75 // over a live connection is a real client's reconnect path.
81 try srv.pumpOnce(20); 76 try td.srv.pumpOnce(20);
82 modes.clearRetainingCapacity(); 77 modes.clearRetainingCapacity();
83 srv.pushInbound(0, frame.items); 78 td.srv.pushInbound(0, frame.items);
84 try drainModes(alloc, c.peer, &modes); 79 try drainModes(alloc, c.peer, &modes);
85 try std.testing.expectEqual(@as(usize, 1), modes.items.len); 80 try std.testing.expectEqual(@as(usize, 1), modes.items.len);
86 try std.testing.expect(modes.items[0].icanon); 81 try std.testing.expect(modes.items[0].icanon);
@@ -90,17 +85,14 @@ test "Server: an attach arriving on an established connection is answered with t
90 test "Server: the pty's mode bits reach a client on attach, and again only when they change" { 85 test "Server: the pty's mode bits reach a client on attach, and again only when they change" {
91 const alloc = std.testing.allocator; 86 const alloc = std.testing.allocator;
92 87
93 var tmp = try TmpDir.make(); 88 var td = try h.TestDaemon.open(alloc, "mode");
94 defer tmp.cleanup(); 89 defer td.deinit();
95 const dir_path = tmp.path();
96 const sock_path = try std.fmt.allocPrint(alloc, "{s}/mode.sock", .{dir_path});
97 defer alloc.free(sock_path);
98 90
99 // A SCRIPT, because readline takes the tty out of canonical mode per line 91 // A SCRIPT, because readline takes the tty out of canonical mode per line
100 // and puts it back, so the bits flap continuously and "changed exactly once" 92 // and puts it back, so the bits flap continuously and "changed exactly once"
101 // could not be asserted. It ends on a blocking read, so `deinit`'s SIGTERM 93 // could not be asserted. It ends on a blocking read, so `deinit`'s SIGTERM
102 // lands on the shell itself. 94 // lands on the shell itself.
103 try tmp.dir.writeFile(.{ 95 try td.tmp.dir.writeFile(.{
104 .sub_path = "session.sh", 96 .sub_path = "session.sh",
105 .data = 97 .data =
106 \\#!/bin/sh 98 \\#!/bin/sh
@@ -113,13 +105,12 @@ test "Server: the pty's mode bits reach a client on attach, and again only when
113 , 105 ,
114 .flags = .{ .mode = 0o755 }, 106 .flags = .{ .mode = 0o755 },
115 }); 107 });
116 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/session.sh", .{dir_path}, 0); 108 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/session.sh", .{td.tmp.path()}, 0);
117 defer alloc.free(script); 109 defer alloc.free(script);
118 110
119 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 111 try td.start(.{ .shell = script });
120 defer srv.deinit();
121 112
122 const c = try dial.dialAttach(sock_path, 80, 24); 113 const c = try dial.dialAttach(td.sock_path, 80, 24);
123 defer c.close(); 114 defer c.close();
124 115
125 var modes: std.ArrayList(proto.PtyModeFlags) = .empty; 116 var modes: std.ArrayList(proto.PtyModeFlags) = .empty;
@@ -130,7 +121,7 @@ test "Server: the pty's mode bits reach a client on attach, and again only when
130 // terminal is doing. 121 // terminal is doing.
131 var rounds: usize = 0; 122 var rounds: usize = 0;
132 while (rounds < 250 and modes.items.len == 0) : (rounds += 1) { 123 while (rounds < 250 and modes.items.len == 0) : (rounds += 1) {
133 try pumpAndCollectModes(alloc, &srv, c.handle, &modes); 124 try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes);
134 } 125 }
135 try std.testing.expectEqual(@as(usize, 1), modes.items.len); 126 try std.testing.expectEqual(@as(usize, 1), modes.items.len);
136 try std.testing.expect(modes.items[0].icanon); 127 try std.testing.expect(modes.items[0].icanon);
@@ -143,7 +134,7 @@ test "Server: the pty's mode bits reach a client on attach, and again only when
143 134
144 rounds = 0; 135 rounds = 0;
145 while (rounds < 500 and modes.items.len < 2) : (rounds += 1) { 136 while (rounds < 500 and modes.items.len < 2) : (rounds += 1) {
146 try pumpAndCollectModes(alloc, &srv, c.handle, &modes); 137 try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes);
147 } 138 }
148 try std.testing.expectEqual(@as(usize, 2), modes.items.len); 139 try std.testing.expectEqual(@as(usize, 2), modes.items.len);
149 try std.testing.expect(!modes.items[1].echo); 140 try std.testing.expect(!modes.items[1].echo);
@@ -159,7 +150,7 @@ test "Server: the pty's mode bits reach a client on attach, and again only when
159 150
160 rounds = 0; 151 rounds = 0;
161 while (rounds < 500 and modes.items.len < 3) : (rounds += 1) { 152 while (rounds < 500 and modes.items.len < 3) : (rounds += 1) {
162 try pumpAndCollectModes(alloc, &srv, c.handle, &modes); 153 try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes);
163 } 154 }
164 try std.testing.expectEqual(@as(usize, 3), modes.items.len); 155 try std.testing.expectEqual(@as(usize, 3), modes.items.len);
165 try std.testing.expect(!modes.items[2].icanon); 156 try std.testing.expect(!modes.items[2].icanon);
@@ -170,7 +161,7 @@ test "Server: the pty's mode bits reach a client on attach, and again only when
170 // on the very NEXT pump. 25 rounds, because round one catches it. 161 // on the very NEXT pump. 25 rounds, because round one catches it.
171 rounds = 0; 162 rounds = 0;
172 while (rounds < 25) : (rounds += 1) { 163 while (rounds < 25) : (rounds += 1) {
173 try pumpAndCollectModes(alloc, &srv, c.handle, &modes); 164 try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes);
174 } 165 }
175 try std.testing.expectEqual(@as(usize, 3), modes.items.len); 166 try std.testing.expectEqual(@as(usize, 3), modes.items.len);
176 } 167 }
@@ -178,21 +169,18 @@ test "Server: the pty's mode bits reach a client on attach, and again only when
178 test "Server: a BEL from the session reaches its client as a bell term_event" { 169 test "Server: a BEL from the session reaches its client as a bell term_event" {
179 const alloc = std.testing.allocator; 170 const alloc = std.testing.allocator;
180 171
181 var tmp = try TmpDir.make(); 172 var td = try h.TestDaemon.open(alloc, "bell");
182 defer tmp.cleanup(); 173 defer td.deinit();
183 const sock_path = try std.fmt.allocPrint(alloc, "{s}/bell.sock", .{tmp.path()});
184 defer alloc.free(sock_path);
185 174
186 // A sibling of the clipboard test rather than a leg inside it, which is named 175 // A sibling of the clipboard test rather than a leg inside it, which is named
187 // for clipboard SCOPING. What is untested is the drain's `.bell` arm: 176 // for clipboard SCOPING. What is untested is the drain's `.bell` arm:
188 // `encodeBellEvent` and the client's rendering are each pinned alone, and 177 // `encodeBellEvent` and the client's rendering are each pinned alone, and
189 // neither runs this function. 178 // neither runs this function.
190 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 179 try td.start(.{ .shell = "/bin/cat" });
191 defer srv.deinit();
192 180
193 const c = try dial.dialAttach(sock_path, 80, 24); 181 const c = try dial.dialAttach(td.sock_path, 80, 24);
194 defer c.close(); 182 defer c.close();
195 (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400) orelse 183 (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse
196 return error.NoSnapshot).deinit(alloc); 184 return error.NoSnapshot).deinit(alloc);
197 185
198 // The newline flushes the line to cat in canonical mode. cat writes the 186 // The newline flushes the line to cat in canonical mode. cat writes the
@@ -201,7 +189,7 @@ test "Server: a BEL from the session reaches its client as a bell term_event" {
201 // the first term_event is a bell either way. 189 // the first term_event is a bell either way.
202 try proto.writeFrame(c.handle, .input, "\x07\n"); 190 try proto.writeFrame(c.handle, .input, "\x07\n");
203 191
204 const ev = (try awaitFrame(alloc, &srv, c.handle, .term_event, 400)) orelse 192 const ev = (try awaitFrame(alloc, &td.srv, c.handle, .term_event, 400)) orelse
205 return error.NoTermEvent; 193 return error.NoTermEvent;
206 defer ev.deinit(alloc); 194 defer ev.deinit(alloc);
207 switch (try proto.decodeTermEvent(ev.payload)) { 195 switch (try proto.decodeTermEvent(ev.payload)) {
@@ -213,10 +201,8 @@ test "Server: a BEL from the session reaches its client as a bell term_event" {
213 test "Server: a burst of bells in one chunk is coalesced into one frame" { 201 test "Server: a burst of bells in one chunk is coalesced into one frame" {
214 const alloc = std.testing.allocator; 202 const alloc = std.testing.allocator;
215 203
216 var tmp = try TmpDir.make(); 204 var td = try h.TestDaemon.open(alloc, "bellburst");
217 defer tmp.cleanup(); 205 defer td.deinit();
218 const sock_path = try std.fmt.allocPrint(alloc, "{s}/bellburst.sock", .{tmp.path()});
219 defer alloc.free(sock_path);
220 206
221 // A script rather than `cat`, and the COUNT is the reason: typed input is 207 // A script rather than `cat`, and the COUNT is the reason: typed input is
222 // echoed, so five typed BELs can reach the engine twice in two chunks and 208 // echoed, so five typed BELs can reach the engine twice in two chunks and
@@ -225,7 +211,7 @@ test "Server: a burst of bells in one chunk is coalesced into one frame" {
225 // The `read` is not decoration: a script that rings at once races the attach, 211 // The `read` is not decoration: a script that rings at once races the attach,
226 // and the drain then has nobody to queue to — the event is recorded pending, 212 // and the drain then has nobody to queue to — the event is recorded pending,
227 // but a fresh attach takes the snapshot branch and replays nothing. 213 // but a fresh attach takes the snapshot branch and replays nothing.
228 try tmp.dir.writeFile(.{ 214 try td.tmp.dir.writeFile(.{
229 .sub_path = "bellburst.sh", 215 .sub_path = "bellburst.sh",
230 .data = 216 .data =
231 \\#!/bin/sh 217 \\#!/bin/sh
@@ -236,17 +222,16 @@ test "Server: a burst of bells in one chunk is coalesced into one frame" {
236 , 222 ,
237 .flags = .{ .mode = 0o755 }, 223 .flags = .{ .mode = 0o755 },
238 }); 224 });
239 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/bellburst.sh", .{tmp.path()}, 0); 225 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/bellburst.sh", .{td.tmp.path()}, 0);
240 defer alloc.free(script); 226 defer alloc.free(script);
241 227
242 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 228 try td.start(.{ .shell = script });
243 defer srv.deinit();
244 229
245 const c = try dial.dialAttach(sock_path, 80, 24); 230 const c = try dial.dialAttach(td.sock_path, 80, 24);
246 defer c.close(); 231 defer c.close();
247 // Awaited, not assumed: this is what makes the ring below strictly later 232 // Awaited, not assumed: this is what makes the ring below strictly later
248 // than the attach, and so what removes the race described above. 233 // than the attach, and so what removes the race described above.
249 (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400) orelse 234 (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse
250 return error.NoSnapshot).deinit(alloc); 235 return error.NoSnapshot).deinit(alloc);
251 236
252 // `go`, never a BEL: with a bell typed here the tty's own echo could 237 // `go`, never a BEL: with a bell typed here the tty's own echo could
@@ -254,7 +239,7 @@ test "Server: a burst of bells in one chunk is coalesced into one frame" {
254 // counting ECHOCTL rather than coalescing. 239 // counting ECHOCTL rather than coalescing.
255 try proto.writeFrame(c.handle, .input, "go\n"); 240 try proto.writeFrame(c.handle, .input, "go\n");
256 241
257 const ev = (try awaitFrame(alloc, &srv, c.handle, .term_event, 400)) orelse 242 const ev = (try awaitFrame(alloc, &td.srv, c.handle, .term_event, 400)) orelse
258 return error.NoTermEvent; 243 return error.NoTermEvent;
259 defer ev.deinit(alloc); 244 defer ev.deinit(alloc);
260 switch (try proto.decodeTermEvent(ev.payload)) { 245 switch (try proto.decodeTermEvent(ev.payload)) {
@@ -266,7 +251,7 @@ test "Server: a burst of bells in one chunk is coalesced into one frame" {
266 // vacuously: the other four rings must not have become four more frames. 251 // vacuously: the other four rings must not have become four more frames.
267 // The budget keeps pumping long past the drain that produced the first, 252 // The budget keeps pumping long past the drain that produced the first,
268 // which is the very pump the rest would have been queued in. 253 // which is the very pump the rest would have been queued in.
269 if (try awaitFrame(alloc, &srv, c.handle, .term_event, 60)) |extra| { 254 if (try awaitFrame(alloc, &td.srv, c.handle, .term_event, 60)) |extra| {
270 extra.deinit(alloc); 255 extra.deinit(alloc);
271 return error.BellsNotCoalesced; 256 return error.BellsNotCoalesced;
272 } 257 }
@@ -275,10 +260,8 @@ test "Server: a burst of bells in one chunk is coalesced into one frame" {
275 test "Server: a bell in a later chunk is its own frame, not folded into the first" { 260 test "Server: a bell in a later chunk is its own frame, not folded into the first" {
276 const alloc = std.testing.allocator; 261 const alloc = std.testing.allocator;
277 262
278 var tmp = try TmpDir.make(); 263 var td = try h.TestDaemon.open(alloc, "belltwice");
279 defer tmp.cleanup(); 264 defer td.deinit();
280 const sock_path = try std.fmt.allocPrint(alloc, "{s}/belltwice.sock", .{tmp.path()});
281 defer alloc.free(sock_path);
282 265
283 // Coalescing is PER DRAIN, and this is the half the burst test cannot see: 266 // Coalescing is PER DRAIN, and this is the half the burst test cannot see:
284 // hoisting the flag to session state would pass it and silence every ring 267 // hoisting the flag to session state would pass it and silence every ring
@@ -288,7 +271,7 @@ test "Server: a bell in a later chunk is its own frame, not folded into the firs
288 // chunks and both strictly after the attach. What is typed to release them 271 // chunks and both strictly after the attach. What is typed to release them
289 // is `go`, never a BEL: an echoed bell could arrive as a second event and 272 // is `go`, never a BEL: an echoed bell could arrive as a second event and
290 // pass this test on the leftovers of the first. 273 // pass this test on the leftovers of the first.
291 try tmp.dir.writeFile(.{ 274 try td.tmp.dir.writeFile(.{
292 .sub_path = "belltwice.sh", 275 .sub_path = "belltwice.sh",
293 .data = 276 .data =
294 \\#!/bin/sh 277 \\#!/bin/sh
@@ -301,25 +284,24 @@ test "Server: a bell in a later chunk is its own frame, not folded into the firs
301 , 284 ,
302 .flags = .{ .mode = 0o755 }, 285 .flags = .{ .mode = 0o755 },
303 }); 286 });
304 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/belltwice.sh", .{tmp.path()}, 0); 287 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/belltwice.sh", .{td.tmp.path()}, 0);
305 defer alloc.free(script); 288 defer alloc.free(script);
306 289
307 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 290 try td.start(.{ .shell = script });
308 defer srv.deinit();
309 291
310 const c = try dial.dialAttach(sock_path, 80, 24); 292 const c = try dial.dialAttach(td.sock_path, 80, 24);
311 defer c.close(); 293 defer c.close();
312 (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400) orelse 294 (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse
313 return error.NoSnapshot).deinit(alloc); 295 return error.NoSnapshot).deinit(alloc);
314 296
315 try proto.writeFrame(c.handle, .input, "one\n"); 297 try proto.writeFrame(c.handle, .input, "one\n");
316 const first = (try awaitFrame(alloc, &srv, c.handle, .term_event, 400)) orelse 298 const first = (try awaitFrame(alloc, &td.srv, c.handle, .term_event, 400)) orelse
317 return error.NoFirstBell; 299 return error.NoFirstBell;
318 first.deinit(alloc); 300 first.deinit(alloc);
319 301
320 try proto.writeFrame(c.handle, .input, "two\n"); 302 try proto.writeFrame(c.handle, .input, "two\n");
321 303
322 const second = (try awaitFrame(alloc, &srv, c.handle, .term_event, 400)) orelse 304 const second = (try awaitFrame(alloc, &td.srv, c.handle, .term_event, 400)) orelse
323 return error.SecondBellSwallowed; 305 return error.SecondBellSwallowed;
324 defer second.deinit(alloc); 306 defer second.deinit(alloc);
325 switch (try proto.decodeTermEvent(second.payload)) { 307 switch (try proto.decodeTermEvent(second.payload)) {
@@ -331,16 +313,14 @@ test "Server: a bell in a later chunk is its own frame, not folded into the firs
331 test "Server: a session enabling bracketed paste tells its clients, and not again per pump" { 313 test "Server: a session enabling bracketed paste tells its clients, and not again per pump" {
332 const alloc = std.testing.allocator; 314 const alloc = std.testing.allocator;
333 315
334 var tmp = try TmpDir.make(); 316 var td = try h.TestDaemon.open(alloc, "modes");
335 defer tmp.cleanup(); 317 defer td.deinit();
336 const sock_path = try std.fmt.allocPrint(alloc, "{s}/modes.sock", .{tmp.path()});
337 defer alloc.free(sock_path);
338 318
339 // Two pty chunks with one mode change between them: the DECSET, then — 319 // Two pty chunks with one mode change between them: the DECSET, then —
340 // once the test asks for it — output that changes nothing about the 320 // once the test asks for it — output that changes nothing about the
341 // modes. The second chunk is what makes "sent on change" falsifiable; 321 // modes. The second chunk is what makes "sent on change" falsifiable;
342 // without it a per-pump send would pass just as well. 322 // without it a per-pump send would pass just as well.
343 try tmp.dir.writeFile(.{ 323 try td.tmp.dir.writeFile(.{
344 .sub_path = "modes.sh", 324 .sub_path = "modes.sh",
345 .data = 325 .data =
346 \\#!/bin/sh 326 \\#!/bin/sh
@@ -352,19 +332,18 @@ test "Server: a session enabling bracketed paste tells its clients, and not agai
352 , 332 ,
353 .flags = .{ .mode = 0o755 }, 333 .flags = .{ .mode = 0o755 },
354 }); 334 });
355 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/modes.sh", .{tmp.path()}, 0); 335 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/modes.sh", .{td.tmp.path()}, 0);
356 defer alloc.free(script); 336 defer alloc.free(script);
357 337
358 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 338 try td.start(.{ .shell = script });
359 defer srv.deinit();
360 339
361 const c = try dial.dialAttach(sock_path, 80, 24); 340 const c = try dial.dialAttach(td.sock_path, 80, 24);
362 defer c.close(); 341 defer c.close();
363 342
364 // attach answers with the modes as they stand, which is before the shell 343 // attach answers with the modes as they stand, which is before the shell
365 // has run: keep pumping until one says the DECSET landed. Asserting on 344 // has run: keep pumping until one says the DECSET landed. Asserting on
366 // the FIRST term_modes would be asserting on the attach-time one. 345 // the FIRST term_modes would be asserting on the attach-time one.
367 const on = while (try awaitFrame(alloc, &srv, c.handle, .term_modes, 400)) |f| { 346 const on = while (try awaitFrame(alloc, &td.srv, c.handle, .term_modes, 400)) |f| {
368 defer f.deinit(alloc); 347 defer f.deinit(alloc);
369 if ((try proto.decodeTermModes(f.payload)).bracketed_paste) break true; 348 if ((try proto.decodeTermModes(f.payload)).bracketed_paste) break true;
370 } else false; 349 } else false;
@@ -381,7 +360,7 @@ test "Server: a session enabling bracketed paste tells its clients, and not agai
381 var i: usize = 0; 360 var i: usize = 0;
382 while (i < 400 and after < 60) : (i += 1) { 361 while (i < 400 and after < 60) : (i += 1) {
383 if (saw_content) after += 1; 362 if (saw_content) after += 1;
384 try srv.pumpOnce(5); 363 try td.srv.pumpOnce(5);
385 var pfd = [_]std.posix.pollfd{ 364 var pfd = [_]std.posix.pollfd{
386 .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 }, 365 .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 },
387 }; 366 };
@@ -401,12 +380,10 @@ test "Server: a session enabling bracketed paste tells its clients, and not agai
401 test "Server: a client attaching to a session already in bracketed paste is told, on either resync branch" { 380 test "Server: a client attaching to a session already in bracketed paste is told, on either resync branch" {
402 const alloc = std.testing.allocator; 381 const alloc = std.testing.allocator;
403 382
404 var tmp = try TmpDir.make(); 383 var td = try h.TestDaemon.open(alloc, "modesjoin");
405 defer tmp.cleanup(); 384 defer td.deinit();
406 const sock_path = try std.fmt.allocPrint(alloc, "{s}/modesjoin.sock", .{tmp.path()});
407 defer alloc.free(sock_path);
408 385
409 try tmp.dir.writeFile(.{ 386 try td.tmp.dir.writeFile(.{
410 .sub_path = "modesjoin.sh", 387 .sub_path = "modesjoin.sh",
411 .data = 388 .data =
412 \\#!/bin/sh 389 \\#!/bin/sh
@@ -416,18 +393,17 @@ test "Server: a client attaching to a session already in bracketed paste is told
416 , 393 ,
417 .flags = .{ .mode = 0o755 }, 394 .flags = .{ .mode = 0o755 },
418 }); 395 });
419 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/modesjoin.sh", .{tmp.path()}, 0); 396 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/modesjoin.sh", .{td.tmp.path()}, 0);
420 defer alloc.free(script); 397 defer alloc.free(script);
421 398
422 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 399 try td.start(.{ .shell = script });
423 defer srv.deinit();
424 400
425 // The first client is here to get the change SENT, not to observe it: 401 // The first client is here to get the change SENT, not to observe it:
426 // once term_modes_sent holds true, sampleTermModes will never fire again 402 // once term_modes_sent holds true, sampleTermModes will never fire again
427 // this session, so everything below can only be the resync's doing. 403 // this session, so everything below can only be the resync's doing.
428 const a = try dial.dialAttach(sock_path, 80, 24); 404 const a = try dial.dialAttach(td.sock_path, 80, 24);
429 defer a.close(); 405 defer a.close();
430 const enabled = while (try awaitFrame(alloc, &srv, a.handle, .term_modes, 400)) |f| { 406 const enabled = while (try awaitFrame(alloc, &td.srv, a.handle, .term_modes, 400)) |f| {
431 defer f.deinit(alloc); 407 defer f.deinit(alloc);
432 if ((try proto.decodeTermModes(f.payload)).bracketed_paste) break true; 408 if ((try proto.decodeTermModes(f.payload)).bracketed_paste) break true;
433 } else false; 409 } else false;
@@ -436,9 +412,9 @@ test "Server: a client attaching to a session already in bracketed paste is told
436 // Snapshot branch: a joiner holding nothing. It must be told what is 412 // Snapshot branch: a joiner holding nothing. It must be told what is
437 // true now, or it would paint a session whose application wants 413 // true now, or it would paint a session whose application wants
438 // bracketed paste while its host terminal has never heard of it. 414 // bracketed paste while its host terminal has never heard of it.
439 const b = try dial.dialAttach(sock_path, 80, 24); 415 const b = try dial.dialAttach(td.sock_path, 80, 24);
440 defer b.close(); 416 defer b.close();
441 var snap = try modesWithResync(alloc, &srv, b.handle); 417 var snap = try modesWithResync(alloc, &td.srv, b.handle);
442 try std.testing.expectEqual(proto.MsgType.snapshot, snap.content orelse 418 try std.testing.expectEqual(proto.MsgType.snapshot, snap.content orelse
443 return error.NoContentFrameOnJoin); 419 return error.NoContentFrameOnJoin);
444 try std.testing.expect(snap.modes orelse return error.NoTermModesOnSnapshotBranch); 420 try std.testing.expect(snap.modes orelse return error.NoTermModesOnSnapshotBranch);
@@ -447,9 +423,9 @@ test "Server: a client attaching to a session already in bracketed paste is told
447 // Seq and epoch read off the session rather than parsed back out of the 423 // Seq and epoch read off the session rather than parsed back out of the
448 // snapshot — the branch under test is chosen by exactly these two values, 424 // snapshot — the branch under test is chosen by exactly these two values,
449 // so handing them over verbatim is what makes the branch certain. 425 // so handing them over verbatim is what makes the branch certain.
450 const s = &srv.sessions.table[0].?; 426 const s = &td.srv.sessions.table[0].?;
451 try proto.writeFrame(b.handle, .attach, &proto.encodeAttach(80, 24, s.tracker.seq, s.epoch)); 427 try proto.writeFrame(b.handle, .attach, &proto.encodeAttach(80, 24, s.tracker.seq, s.epoch));
452 snap = try modesWithResync(alloc, &srv, b.handle); 428 snap = try modesWithResync(alloc, &td.srv, b.handle);
453 try std.testing.expectEqual(proto.MsgType.delta, snap.content orelse 429 try std.testing.expectEqual(proto.MsgType.delta, snap.content orelse
454 return error.NoContentFrameOnReattach); 430 return error.NoContentFrameOnReattach);
455 try std.testing.expect(snap.modes orelse return error.NoTermModesOnDeltaBranch); 431 try std.testing.expect(snap.modes orelse return error.NoTermModesOnDeltaBranch);
@@ -458,12 +434,10 @@ test "Server: a client attaching to a session already in bracketed paste is told
458 test "Server: a joiner that resizes the grid is still told the session's modes" { 434 test "Server: a joiner that resizes the grid is still told the session's modes" {
459 const alloc = std.testing.allocator; 435 const alloc = std.testing.allocator;
460 436
461 var tmp = try TmpDir.make(); 437 var td = try h.TestDaemon.open(alloc, "modesresize");
462 defer tmp.cleanup(); 438 defer td.deinit();
463 const sock_path = try std.fmt.allocPrint(alloc, "{s}/modesresize.sock", .{tmp.path()});
464 defer alloc.free(sock_path);
465 439
466 try tmp.dir.writeFile(.{ 440 try td.tmp.dir.writeFile(.{
467 .sub_path = "modesresize.sh", 441 .sub_path = "modesresize.sh",
468 .data = 442 .data =
469 \\#!/bin/sh 443 \\#!/bin/sh
@@ -473,18 +447,17 @@ test "Server: a joiner that resizes the grid is still told the session's modes"
473 , 447 ,
474 .flags = .{ .mode = 0o755 }, 448 .flags = .{ .mode = 0o755 },
475 }); 449 });
476 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/modesresize.sh", .{tmp.path()}, 0); 450 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/modesresize.sh", .{td.tmp.path()}, 0);
477 defer alloc.free(script); 451 defer alloc.free(script);
478 452
479 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 453 try td.start(.{ .shell = script });
480 defer srv.deinit();
481 454
482 // As in the resync-branch test above: the first client is here to latch 455 // As in the resync-branch test above: the first client is here to latch
483 // term_modes_sent, after which sampleTermModes can never fire again this 456 // term_modes_sent, after which sampleTermModes can never fire again this
484 // session and everything below is the resync's doing alone. 457 // session and everything below is the resync's doing alone.
485 const a = try dial.dialAttach(sock_path, 80, 24); 458 const a = try dial.dialAttach(td.sock_path, 80, 24);
486 defer a.close(); 459 defer a.close();
487 const enabled = while (try awaitFrame(alloc, &srv, a.handle, .term_modes, 400)) |f| { 460 const enabled = while (try awaitFrame(alloc, &td.srv, a.handle, .term_modes, 400)) |f| {
488 defer f.deinit(alloc); 461 defer f.deinit(alloc);
489 if ((try proto.decodeTermModes(f.payload)).bracketed_paste) break true; 462 if ((try proto.decodeTermModes(f.payload)).bracketed_paste) break true;
490 } else false; 463 } else false;
@@ -494,7 +467,7 @@ test "Server: a joiner that resizes the grid is still told the session's modes"
494 // joiner at a DIFFERENT size returns early through `resyncSnapshot`. Left 467 // joiner at a DIFFERENT size returns early through `resyncSnapshot`. Left
495 // bare, bracketed paste stops being mirrored after any window resize, and 468 // bare, bracketed paste stops being mirrored after any window resize, and
496 // nothing connects a shell echoing a literal `200~` to the resize. 469 // nothing connects a shell echoing a literal `200~` to the resize.
497 const b = try dial.dialAttach(sock_path, 100, 30); 470 const b = try dial.dialAttach(td.sock_path, 100, 30);
498 defer b.close(); 471 defer b.close();
499 472
500 // A's re-snapshot at B's size is the witness that this arm ran at all: 473 // A's re-snapshot at B's size is the witness that this arm ran at all:
@@ -505,7 +478,7 @@ test "Server: a joiner that resizes the grid is still told the session's modes"
505 var b_modes: ?bool = null; 478 var b_modes: ?bool = null;
506 var i: usize = 0; 479 var i: usize = 0;
507 while (i < 400 and !(a_resnapshotted and b_modes != null)) : (i += 1) { 480 while (i < 400 and !(a_resnapshotted and b_modes != null)) : (i += 1) {
508 try srv.pumpOnce(5); 481 try td.srv.pumpOnce(5);
509 var pfds = [_]std.posix.pollfd{ 482 var pfds = [_]std.posix.pollfd{
510 .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 }, 483 .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 },
511 .{ .fd = b.handle, .events = std.posix.POLL.IN, .revents = 0 }, 484 .{ .fd = b.handle, .events = std.posix.POLL.IN, .revents = 0 },
@@ -531,12 +504,10 @@ test "Server: a joiner that resizes the grid is still told the session's modes"
531 test "Server: a window title reaches clients on change, and only on change" { 504 test "Server: a window title reaches clients on change, and only on change" {
532 const alloc = std.testing.allocator; 505 const alloc = std.testing.allocator;
533 506
534 var tmp = try TmpDir.make(); 507 var td = try h.TestDaemon.open(alloc, "title");
535 defer tmp.cleanup(); 508 defer td.deinit();
536 const sock_path = try std.fmt.allocPrint(alloc, "{s}/title.sock", .{tmp.path()});
537 defer alloc.free(sock_path);
538 509
539 try tmp.dir.writeFile(.{ 510 try td.tmp.dir.writeFile(.{
540 .sub_path = "title.sh", 511 .sub_path = "title.sh",
541 // The leading `read` is what makes this about the SAMPLER: a shell that 512 // The leading `read` is what makes this about the SAMPLER: a shell that
542 // printed its title at once would race the attach, and the frame this 513 // printed its title at once would race the attach, and the frame this
@@ -554,19 +525,18 @@ test "Server: a window title reaches clients on change, and only on change" {
554 , 525 ,
555 .flags = .{ .mode = 0o755 }, 526 .flags = .{ .mode = 0o755 },
556 }); 527 });
557 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/title.sh", .{tmp.path()}, 0); 528 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/title.sh", .{td.tmp.path()}, 0);
558 defer alloc.free(script); 529 defer alloc.free(script);
559 530
560 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 531 try td.start(.{ .shell = script });
561 defer srv.deinit();
562 532
563 const c = try dial.dialAttach(sock_path, 80, 24); 533 const c = try dial.dialAttach(td.sock_path, 80, 24);
564 defer c.close(); 534 defer c.close();
565 535
566 // Queued behind the attach on the same socket, so the daemon has taken 536 // Queued behind the attach on the same socket, so the daemon has taken
567 // the attach before it can forward this — see the script's leading read. 537 // the attach before it can forward this — see the script's leading read.
568 try proto.writeFrame(c.handle, .input, "go\n"); 538 try proto.writeFrame(c.handle, .input, "go\n");
569 const first = (try awaitFrame(alloc, &srv, c.handle, .term_title, 400)) orelse 539 const first = (try awaitFrame(alloc, &td.srv, c.handle, .term_title, 400)) orelse
570 return error.NoTitleFrame; 540 return error.NoTitleFrame;
571 defer first.deinit(alloc); 541 defer first.deinit(alloc);
572 try std.testing.expectEqualStrings("first", first.payload); 542 try std.testing.expectEqualStrings("first", first.payload);
@@ -581,7 +551,7 @@ test "Server: a window title reaches clients on change, and only on change" {
581 var i: usize = 0; 551 var i: usize = 0;
582 while (i < 400 and after < 60) : (i += 1) { 552 while (i < 400 and after < 60) : (i += 1) {
583 if (saw_content) after += 1; 553 if (saw_content) after += 1;
584 try srv.pumpOnce(5); 554 try td.srv.pumpOnce(5);
585 var pfd = [_]std.posix.pollfd{ 555 var pfd = [_]std.posix.pollfd{
586 .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 }, 556 .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 },
587 }; 557 };
@@ -600,7 +570,7 @@ test "Server: a window title reaches clients on change, and only on change" {
600 // A DIFFERENT title does travel: the early return is a change filter, 570 // A DIFFERENT title does travel: the early return is a change filter,
601 // not a one-title-per-session latch. 571 // not a one-title-per-session latch.
602 try proto.writeFrame(c.handle, .input, "go\n"); 572 try proto.writeFrame(c.handle, .input, "go\n");
603 const second = (try awaitFrame(alloc, &srv, c.handle, .term_title, 400)) orelse 573 const second = (try awaitFrame(alloc, &td.srv, c.handle, .term_title, 400)) orelse
604 return error.NoSecondTitleFrame; 574 return error.NoSecondTitleFrame;
605 defer second.deinit(alloc); 575 defer second.deinit(alloc);
606 try std.testing.expectEqualStrings("second", second.payload); 576 try std.testing.expectEqualStrings("second", second.payload);
@@ -609,12 +579,10 @@ test "Server: a window title reaches clients on change, and only on change" {
609 test "Server: a session that never set a title has none sent for it" { 579 test "Server: a session that never set a title has none sent for it" {
610 const alloc = std.testing.allocator; 580 const alloc = std.testing.allocator;
611 581
612 var tmp = try TmpDir.make(); 582 var td = try h.TestDaemon.open(alloc, "notitle");
613 defer tmp.cleanup(); 583 defer td.deinit();
614 const sock_path = try std.fmt.allocPrint(alloc, "{s}/notitle.sock", .{tmp.path()});
615 defer alloc.free(sock_path);
616 584
617 try tmp.dir.writeFile(.{ 585 try td.tmp.dir.writeFile(.{
618 .sub_path = "notitle.sh", 586 .sub_path = "notitle.sh",
619 .data = 587 .data =
620 \\#!/bin/sh 588 \\#!/bin/sh
@@ -624,16 +592,15 @@ test "Server: a session that never set a title has none sent for it" {
624 , 592 ,
625 .flags = .{ .mode = 0o755 }, 593 .flags = .{ .mode = 0o755 },
626 }); 594 });
627 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/notitle.sh", .{tmp.path()}, 0); 595 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/notitle.sh", .{td.tmp.path()}, 0);
628 defer alloc.free(script); 596 defer alloc.free(script);
629 597
630 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 598 try td.start(.{ .shell = script });
631 defer srv.deinit();
632 599
633 // Two attaches, because the two paths that could send an empty title are 600 // Two attaches, because the two paths that could send an empty title are
634 // different code: the SAMPLER and the RESYNC. An empty title on either makes 601 // different code: the SAMPLER and the RESYNC. An empty title on either makes
635 // the client wipe the title bar of a terminal whose session said nothing. 602 // the client wipe the title bar of a terminal whose session said nothing.
636 const a = try dial.dialAttach(sock_path, 80, 24); 603 const a = try dial.dialAttach(td.sock_path, 80, 24);
637 defer a.close(); 604 defer a.close();
638 605
639 var saw_content = false; 606 var saw_content = false;
@@ -648,12 +615,12 @@ test "Server: a session that never set a title has none sent for it" {
648 // The joiner goes in only once the shell's output has landed, so 615 // The joiner goes in only once the shell's output has landed, so
649 // its resync reads a session that has genuinely run. 616 // its resync reads a session that has genuinely run.
650 if (!joined) { 617 if (!joined) {
651 b = try dial.dialAttach(sock_path, 80, 24); 618 b = try dial.dialAttach(td.sock_path, 80, 24);
652 joined = true; 619 joined = true;
653 } 620 }
654 after += 1; 621 after += 1;
655 } 622 }
656 try srv.pumpOnce(5); 623 try td.srv.pumpOnce(5);
657 // Sliced to what exists rather than padded with a placeholder. The 624 // Sliced to what exists rather than padded with a placeholder. The
658 // padding this replaces put A's fd in slot 1 until B joined and 625 // padding this replaces put A's fd in slot 1 until B joined and
659 // relied on a `continue` to skip it — and in a test whose verdict is 626 // relied on a `continue` to skip it — and in a test whose verdict is
@@ -683,12 +650,10 @@ test "Server: a session that never set a title has none sent for it" {
683 test "Server: a joiner that resizes the grid is still told the session's title" { 650 test "Server: a joiner that resizes the grid is still told the session's title" {
684 const alloc = std.testing.allocator; 651 const alloc = std.testing.allocator;
685 652
686 var tmp = try TmpDir.make(); 653 var td = try h.TestDaemon.open(alloc, "titleresize");
687 defer tmp.cleanup(); 654 defer td.deinit();
688 const sock_path = try std.fmt.allocPrint(alloc, "{s}/titleresize.sock", .{tmp.path()});
689 defer alloc.free(sock_path);
690 655
691 try tmp.dir.writeFile(.{ 656 try td.tmp.dir.writeFile(.{
692 .sub_path = "titleresize.sh", 657 .sub_path = "titleresize.sh",
693 .data = 658 .data =
694 \\#!/bin/sh 659 \\#!/bin/sh
@@ -698,18 +663,17 @@ test "Server: a joiner that resizes the grid is still told the session's title"
698 , 663 ,
699 .flags = .{ .mode = 0o755 }, 664 .flags = .{ .mode = 0o755 },
700 }); 665 });
701 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/titleresize.sh", .{tmp.path()}, 0); 666 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/titleresize.sh", .{td.tmp.path()}, 0);
702 defer alloc.free(script); 667 defer alloc.free(script);
703 668
704 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 669 try td.start(.{ .shell = script });
705 defer srv.deinit();
706 670
707 // The first client latches title_sent, after which sampleTermTitle can 671 // The first client latches title_sent, after which sampleTermTitle can
708 // never fire again for this title and everything below is the resync's 672 // never fire again for this title and everything below is the resync's
709 // doing alone. 673 // doing alone.
710 const a = try dial.dialAttach(sock_path, 80, 24); 674 const a = try dial.dialAttach(td.sock_path, 80, 24);
711 defer a.close(); 675 defer a.close();
712 const latched = (try awaitFrame(alloc, &srv, a.handle, .term_title, 400)) orelse 676 const latched = (try awaitFrame(alloc, &td.srv, a.handle, .term_title, 400)) orelse
713 return error.NoTitleFrame; 677 return error.NoTitleFrame;
714 defer latched.deinit(alloc); 678 defer latched.deinit(alloc);
715 try std.testing.expectEqualStrings("vim", latched.payload); 679 try std.testing.expectEqualStrings("vim", latched.payload);
@@ -718,7 +682,7 @@ test "Server: a joiner that resizes the grid is still told the session's title"
718 // through resyncSnapshot, before the delta/snapshot split. Left bare it 682 // through resyncSnapshot, before the delta/snapshot split. Left bare it
719 // is a real regression and a quiet one — the title bar would silently 683 // is a real regression and a quiet one — the title bar would silently
720 // stop matching the session after any window resize. 684 // stop matching the session after any window resize.
721 const b = try dial.dialAttach(sock_path, 100, 30); 685 const b = try dial.dialAttach(td.sock_path, 100, 30);
722 defer b.close(); 686 defer b.close();
723 687
724 // A's re-snapshot at B's size is the witness that this arm ran at all: 688 // A's re-snapshot at B's size is the witness that this arm ran at all:
@@ -729,7 +693,7 @@ test "Server: a joiner that resizes the grid is still told the session's title"
729 defer if (b_title) |t| alloc.free(t); 693 defer if (b_title) |t| alloc.free(t);
730 var i: usize = 0; 694 var i: usize = 0;
731 while (i < 400 and !(a_resnapshotted and b_title != null)) : (i += 1) { 695 while (i < 400 and !(a_resnapshotted and b_title != null)) : (i += 1) {
732 try srv.pumpOnce(5); 696 try td.srv.pumpOnce(5);
733 var pfds = [_]std.posix.pollfd{ 697 var pfds = [_]std.posix.pollfd{
734 .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 }, 698 .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 },
735 .{ .fd = b.handle, .events = std.posix.POLL.IN, .revents = 0 }, 699 .{ .fd = b.handle, .events = std.posix.POLL.IN, .revents = 0 },
@@ -780,22 +744,19 @@ fn modesWithResync(
780 744
781 test "Server: a resize reaches the pty as an in-band size report when the app asked for one" { 745 test "Server: a resize reaches the pty as an in-band size report when the app asked for one" {
782 const alloc = std.testing.allocator; 746 const alloc = std.testing.allocator;
783 var tmp = try TmpDir.make(); 747 var td = try h.TestDaemon.open(alloc, "inband");
784 defer tmp.cleanup(); 748 defer td.deinit();
785 const sock_path = try std.fmt.allocPrint(alloc, "{s}/inband.sock", .{tmp.path()});
786 defer alloc.free(sock_path);
787 // `cat`, not a shell: the oracle below is the line discipline's own echo, and 749 // `cat`, not a shell: the oracle below is the line discipline's own echo, and
788 // a shell's line editor turns ECHO off and reads the report as editing keys — 750 // a shell's line editor turns ECHO off and reads the report as editing keys —
789 // `ESC [` then digits is a numeric argument to readline. 751 // `ESC [` then digits is a numeric argument to readline.
790 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 752 try td.start(.{ .shell = "/bin/cat" });
791 defer srv.deinit();
792 753
793 const s = &srv.sessions.table[0].?; 754 const s = &td.srv.sessions.table[0].?;
794 // The app opts in. The engine queues the immediate report; drop it so 755 // The app opts in. The engine queues the immediate report; drop it so
795 // only the resize's own report is on trial below. 756 // only the resize's own report is on trial below.
796 s.eng.feed("\x1b[?2048h"); 757 s.eng.feed("\x1b[?2048h");
797 s.eng.clearPtyOutput(); 758 s.eng.clearPtyOutput();
798 try std.testing.expect(srv.applySize(0, 100, 30)); 759 try std.testing.expect(td.srv.applySize(0, 100, 30));
799 760
800 // The report is INPUT to the child, so the only place to see it from 761 // The report is INPUT to the child, so the only place to see it from
801 // here is the tty's own echo of it back on the master. Canonical mode 762 // here is the tty's own echo of it back on the master. Canonical mode
src/server/server_test_quic.zig
Old New
@@ -91,18 +91,14 @@ test "drainPending: the poll slice is floored at 1ms and capped by ngtcp2" {
91 91
92 test "Server: output reaches a silent QUIC client without waiting for it to speak" { 92 test "Server: output reaches a silent QUIC client without waiting for it to speak" {
93 const alloc = std.testing.allocator; 93 const alloc = std.testing.allocator;
94 var tmp = try TmpDir.make(); 94 var td = try h.TestDaemon.open(alloc, "qquiet");
95 defer tmp.cleanup(); 95 defer td.deinit();
96 const dir_path = tmp.path();
97 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qquiet.sock", .{dir_path});
98 defer alloc.free(sock_path);
99 96
100 // /bin/cat: a session that emits nothing on its own, so the only output 97 // /bin/cat: a session that emits nothing on its own, so the only output
101 // in this test is the output the test causes. 98 // in this test is the output the test causes.
102 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 99 try td.start(.{ .shell = "/bin/cat" });
103 defer srv.deinit();
104 const key: quic.Key = .{ .bytes = [_]u8{0x4D} ** quic.key_len }; 100 const key: quic.Key = .{ .bytes = [_]u8{0x4D} ** quic.key_len };
105 const q = try quicTestServer(&srv, key); 101 const q = try quicTestServer(&td.srv, key);
106 defer q.l.deinit(); 102 defer q.l.deinit();
107 103
108 var cl = try quic_server.TestPeer.init(q.addr, key); 104 var cl = try quic_server.TestPeer.init(q.addr, key);
@@ -113,7 +109,7 @@ test "Server: output reaches a silent QUIC client without waiting for it to spea
113 try attachOver(&cl, &out, alloc); 109 try attachOver(&cl, &out, alloc);
114 110
115 var only = [_]*quic_server.TestPeer{&cl}; 111 var only = [_]*quic_server.TestPeer{&cl};
116 try quicPump(&srv, &only, 8000, &cl, struct { 112 try quicPump(&td.srv, &only, 8000, &cl, struct {
117 fn f(t: *quic_server.TestPeer) bool { 113 fn f(t: *quic_server.TestPeer) bool {
118 return findFrame(t.cl.in.items, .snapshot) != null; 114 return findFrame(t.cl.in.items, .snapshot) != null;
119 } 115 }
@@ -134,7 +130,7 @@ test "Server: output reaches a silent QUIC client without waiting for it to spea
134 var settle: usize = 0; 130 var settle: usize = 0;
135 while (settle < 400) : (settle += 1) { 131 while (settle < 400) : (settle += 1) {
136 if (q.l.timeoutMs(1000) > 100) break; 132 if (q.l.timeoutMs(1000) > 100) break;
137 try srv.pumpOnce(5); 133 try td.srv.pumpOnce(5);
138 cl.drain(); 134 cl.drain();
139 var pfd = [_]std.posix.pollfd{ 135 var pfd = [_]std.posix.pollfd{
140 .{ .fd = cl.cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, 136 .{ .fd = cl.cl.fd, .events = std.posix.POLL.IN, .revents = 0 },
@@ -145,8 +141,8 @@ test "Server: output reaches a silent QUIC client without waiting for it to spea
145 141
146 const before = cl.echoed(); 142 const before = cl.echoed();
147 cl.out = &.{}; 143 cl.out = &.{};
148 srv.sessions.table[0].?.eng.feed("quiet-client-marker\r\n"); 144 td.srv.sessions.table[0].?.eng.feed("quiet-client-marker\r\n");
149 srv.sendUpdate(0); 145 td.srv.sendUpdate(0);
150 146
151 // EXACTLY ONE pump, and the client only ever listens. Both halves are 147 // EXACTLY ONE pump, and the client only ever listens. Both halves are
152 // the test, and both were learned by watching weaker versions pass: 148 // the test, and both were learned by watching weaker versions pass:
@@ -161,7 +157,7 @@ test "Server: output reaches a silent QUIC client without waiting for it to spea
161 // 157 //
162 // One pump answers the only question that isolates it: the frame was 158 // One pump answers the only question that isolates it: the frame was
163 // queued before this call, so did THIS call put it on the wire? 159 // queued before this call, so did THIS call put it on the wire?
164 try srv.pumpOnce(5); 160 try td.srv.pumpOnce(5);
165 161
166 // Reading is not transmitting: the client takes whatever already 162 // Reading is not transmitting: the client takes whatever already
167 // arrived, and never gives the server an inbound packet to react to. 163 // arrived, and never gives the server an inbound packet to react to.
@@ -179,16 +175,10 @@ test "Server: output reaches a silent QUIC client without waiting for it to spea
179 175
180 test "Server: a QUIC client still receives the shell's exit status" { 176 test "Server: a QUIC client still receives the shell's exit status" {
181 const alloc = std.testing.allocator; 177 const alloc = std.testing.allocator;
182 var tmp = try TmpDir.make(); 178 var td = try h.TestDaemon.init(alloc, "qexit", .{ .shell = "/bin/sh" });
183 defer tmp.cleanup(); 179 defer td.deinit();
184 const dir_path = tmp.path();
185 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qexit.sock", .{dir_path});
186 defer alloc.free(sock_path);
187
188 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
189 defer srv.deinit();
190 const key: quic.Key = .{ .bytes = [_]u8{0x31} ** quic.key_len }; 180 const key: quic.Key = .{ .bytes = [_]u8{0x31} ** quic.key_len };
191 const q = try quicTestServer(&srv, key); 181 const q = try quicTestServer(&td.srv, key);
192 defer q.l.deinit(); 182 defer q.l.deinit();
193 183
194 var cl = try quic_server.TestPeer.init(q.addr, key); 184 var cl = try quic_server.TestPeer.init(q.addr, key);
@@ -201,7 +191,7 @@ test "Server: a QUIC client still receives the shell's exit status" {
201 // Attached and carrying state: the frame path works before we start 191 // Attached and carrying state: the frame path works before we start
202 // asking about the harder one. 192 // asking about the harder one.
203 var only = [_]*quic_server.TestPeer{&cl}; 193 var only = [_]*quic_server.TestPeer{&cl};
204 try quicPump(&srv, &only, 8000, &cl, struct { 194 try quicPump(&td.srv, &only, 8000, &cl, struct {
205 fn f(t: *quic_server.TestPeer) bool { 195 fn f(t: *quic_server.TestPeer) bool {
206 return findFrame(t.cl.in.items, .snapshot) != null; 196 return findFrame(t.cl.in.items, .snapshot) != null;
207 } 197 }
@@ -215,7 +205,7 @@ test "Server: a QUIC client still receives the shell's exit status" {
215 cl.out = out.items; 205 cl.out = out.items;
216 cl.drain(); 206 cl.drain();
217 207
218 try quicPump(&srv, &only, 15000, &cl, struct { 208 try quicPump(&td.srv, &only, 15000, &cl, struct {
219 fn f(t: *quic_server.TestPeer) bool { 209 fn f(t: *quic_server.TestPeer) bool {
220 return findFrame(t.cl.in.items, .exit_status) != null; 210 return findFrame(t.cl.in.items, .exit_status) != null;
221 } 211 }
@@ -229,16 +219,10 @@ test "Server: a QUIC client still receives the shell's exit status" {
229 219
230 test "Server: one QUIC client leaving does not disturb the other" { 220 test "Server: one QUIC client leaving does not disturb the other" {
231 const alloc = std.testing.allocator; 221 const alloc = std.testing.allocator;
232 var tmp = try TmpDir.make(); 222 var td = try h.TestDaemon.init(alloc, "qtwo", .{ .shell = "/bin/sh" });
233 defer tmp.cleanup(); 223 defer td.deinit();
234 const dir_path = tmp.path();
235 const sock_path = try std.fmt.allocPrint(alloc, "{s}/qtwo.sock", .{dir_path});
236 defer alloc.free(sock_path);
237
238 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
239 defer srv.deinit();
240 const key: quic.Key = .{ .bytes = [_]u8{0x77} ** quic.key_len }; 224 const key: quic.Key = .{ .bytes = [_]u8{0x77} ** quic.key_len };
241 const q = try quicTestServer(&srv, key); 225 const q = try quicTestServer(&td.srv, key);
242 defer q.l.deinit(); 226 defer q.l.deinit();
243 227
244 var a = try quic_server.TestPeer.init(q.addr, key); 228 var a = try quic_server.TestPeer.init(q.addr, key);
@@ -254,7 +238,7 @@ test "Server: one QUIC client leaving does not disturb the other" {
254 try attachOver(&b, &bbuf, alloc); 238 try attachOver(&b, &bbuf, alloc);
255 239
256 var both = [_]*quic_server.TestPeer{ &a, &b }; 240 var both = [_]*quic_server.TestPeer{ &a, &b };
257 try quicPump(&srv, &both, 10000, &b, struct { 241 try quicPump(&td.srv, &both, 10000, &b, struct {
258 fn f(t: *quic_server.TestPeer) bool { 242 fn f(t: *quic_server.TestPeer) bool {
259 return findFrame(t.cl.in.items, .snapshot) != null; 243 return findFrame(t.cl.in.items, .snapshot) != null;
260 } 244 }
@@ -268,7 +252,7 @@ test "Server: one QUIC client leaving does not disturb the other" {
268 try proto.appendFrame(&abuf, alloc, .detach, ""); 252 try proto.appendFrame(&abuf, alloc, .detach, "");
269 a.out = abuf.items; 253 a.out = abuf.items;
270 a.drain(); 254 a.drain();
271 try quicPump(&srv, &both, 4000, &srv, struct { 255 try quicPump(&td.srv, &both, 4000, &td.srv, struct {
272 fn f(s: *Server) bool { 256 fn f(s: *Server) bool {
273 var n: usize = 0; 257 var n: usize = 0;
274 for (s.clients) |slot| { 258 for (s.clients) |slot| {
@@ -285,7 +269,7 @@ test "Server: one QUIC client leaving does not disturb the other" {
285 b.drain(); 269 b.drain();
286 var replica = try Engine.init(alloc, .{ .cols = 80, .rows = 24 }); 270 var replica = try Engine.init(alloc, .{ .cols = 80, .rows = 24 });
287 defer replica.deinit(); 271 defer replica.deinit();
288 try quicPump(&srv, &both, 15000, &b, struct { 272 try quicPump(&td.srv, &both, 15000, &b, struct {
289 fn f(t: *quic_server.TestPeer) bool { 273 fn f(t: *quic_server.TestPeer) bool {
290 return t.echoed() > 0 and findFrame(t.cl.in.items, .delta) != null; 274 return t.echoed() > 0 and findFrame(t.cl.in.items, .delta) != null;
291 } 275 }
src/server/server_test_session.zig
Old New
@@ -15,9 +15,9 @@ const max_sessions = srv_mod.max_sessions;
15 const shutdown_flag = &srv_mod.shutdown_flag; 15 const shutdown_flag = &srv_mod.shutdown_flag;
16 const applyFrame = h.applyFrame; 16 const applyFrame = h.applyFrame;
17 const awaitFrame = h.awaitFrame; 17 const awaitFrame = h.awaitFrame;
18 const awaitFrameOn = h.awaitFrameOn;
18 const connectedPair = h.connectedPair; 19 const connectedPair = h.connectedPair;
19 const firstStateFrame = h.firstStateFrame; 20 const firstStateFrame = h.firstStateFrame;
20 const serverThread = h.serverThread;
21 21
22 // folder rule 5 exemption: a test's daemon needs a shell to spawn, and this 22 // folder rule 5 exemption: a test's daemon needs a shell to spawn, and this
23 // file's fixture helpers sit outside the `test` blocks the rule skips. 23 // file's fixture helpers sit outside the `test` blocks the rule skips.
@@ -39,73 +39,57 @@ fn expectInitRefused(alloc: std.mem.Allocator, path: []const u8, want: anyerror)
39 test "Server: a second daemon refuses a live socket instead of stealing it" { 39 test "Server: a second daemon refuses a live socket instead of stealing it" {
40 const alloc = std.testing.allocator; 40 const alloc = std.testing.allocator;
41 41
42 var tmp = try TmpDir.make(); 42 var td = try h.TestDaemon.init(alloc, "live", .{ .shell = "/bin/sh" });
43 defer tmp.cleanup(); 43 defer td.deinit();
44 const dir_path = tmp.path();
45 const sock_path = try std.fmt.allocPrint(alloc, "{s}/live.sock", .{dir_path});
46 defer alloc.free(sock_path);
47
48 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
49 defer srv.deinit();
50 44
51 var stop = std.atomic.Value(bool).init(false); 45 try td.threaded();
52 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
53 defer th.join();
54 defer stop.store(true, .release);
55 46
56 // The incident: this used to unlink the path and bind over it, leaving 47 // The incident: this used to unlink the path and bind over it, leaving
57 // the first daemon running and unreachable. 48 // the first daemon running and unreachable.
58 try expectInitRefused(alloc, sock_path, error.DaemonAlreadyRunning); 49 try expectInitRefused(alloc, td.sock_path, error.DaemonAlreadyRunning);
59 50
60 // The loser touched nothing: the socket file is still there... 51 // The loser touched nothing: the socket file is still there...
61 const st = try std.posix.fstatat(std.posix.AT.FDCWD, sock_path, 0); 52 const st = try std.posix.fstatat(std.posix.AT.FDCWD, td.sock_path, 0);
62 try std.testing.expect(std.posix.S.ISSOCK(st.mode)); 53 try std.testing.expect(std.posix.S.ISSOCK(st.mode));
63 54
64 // ...and it still reaches the daemon that was already there. The epoch 55 // ...and it still reaches the daemon that was already there. The epoch
65 // is what makes that precise: it names one daemon *instance*, so 56 // is what makes that precise: it names one daemon *instance*, so
66 // matching it rules out having been handed a replacement. 57 // matching it rules out having been handed a replacement.
67 const c = try dial.dialAttach(sock_path, 80, 24); 58 const c = try dial.dialAttach(td.sock_path, 80, 24);
68 defer c.close(); 59 defer c.close();
69 const first = try firstStateFrame(alloc, c.handle, 10_000); 60 const first = try firstStateFrame(alloc, c.handle, 10_000);
70 try std.testing.expect(first != null); 61 try std.testing.expect(first != null);
71 try std.testing.expectEqual(proto.MsgType.snapshot, first.?.type); 62 try std.testing.expectEqual(proto.MsgType.snapshot, first.?.type);
72 try std.testing.expectEqual(srv.sessions.table[0].?.epoch, first.?.epoch); 63 try std.testing.expectEqual(td.srv.sessions.table[0].?.epoch, first.?.epoch);
73 } 64 }
74 65
75 test "Server: a dead daemon's leftover socket file is cleared and rebound" { 66 test "Server: a dead daemon's leftover socket file is cleared and rebound" {
76 const alloc = std.testing.allocator; 67 const alloc = std.testing.allocator;
77 68
78 var tmp = try TmpDir.make(); 69 var td = try h.TestDaemon.open(alloc, "stale");
79 defer tmp.cleanup(); 70 defer td.deinit();
80 const dir_path = tmp.path();
81 const sock_path = try std.fmt.allocPrint(alloc, "{s}/stale.sock", .{dir_path});
82 defer alloc.free(sock_path);
83 71
84 // What a daemon killed with SIGKILL leaves behind: a bound socket file 72 // What a daemon killed with SIGKILL leaves behind: a bound socket file
85 // whose listener is gone, since deinit's unlink never ran. 73 // whose listener is gone, since deinit's unlink never ran.
86 { 74 {
87 const addr = try std.net.Address.initUnix(sock_path); 75 const addr = try std.net.Address.initUnix(td.sock_path);
88 var dead = try addr.listen(.{}); 76 var dead = try addr.listen(.{});
89 dead.deinit(); // closes the fd; the file stays on disk 77 dead.deinit(); // closes the fd; the file stays on disk
90 } 78 }
91 const before = try std.posix.fstatat(std.posix.AT.FDCWD, sock_path, 0); 79 const before = try std.posix.fstatat(std.posix.AT.FDCWD, td.sock_path, 0);
92 try std.testing.expect(std.posix.S.ISSOCK(before.mode)); 80 try std.testing.expect(std.posix.S.ISSOCK(before.mode));
93 81
94 // Nobody answers there, so the path is ours: recovery, not refusal. 82 // Nobody answers there, so the path is ours: recovery, not refusal.
95 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 83 try td.start(.{ .shell = "/bin/sh" });
96 defer srv.deinit();
97 84
98 var stop = std.atomic.Value(bool).init(false); 85 try td.threaded();
99 const th = try std.Thread.spawn(.{}, serverThread, .{ &srv, &stop });
100 defer th.join();
101 defer stop.store(true, .release);
102 86
103 // And the rebind is real, not just a file that reappeared: it serves. 87 // And the rebind is real, not just a file that reappeared: it serves.
104 const c = try dial.dialAttach(sock_path, 80, 24); 88 const c = try dial.dialAttach(td.sock_path, 80, 24);
105 defer c.close(); 89 defer c.close();
106 const first = try firstStateFrame(alloc, c.handle, 10_000); 90 const first = try firstStateFrame(alloc, c.handle, 10_000);
107 try std.testing.expect(first != null); 91 try std.testing.expect(first != null);
108 try std.testing.expectEqual(srv.sessions.table[0].?.epoch, first.?.epoch); 92 try std.testing.expectEqual(td.srv.sessions.table[0].?.epoch, first.?.epoch);
109 } 93 }
110 94
111 test "Server: a path that cannot be bound fails as AddressInUse" { 95 test "Server: a path that cannot be bound fails as AddressInUse" {
@@ -169,16 +153,13 @@ test "Server: a non-socket at the path is refused, not deleted" {
169 test "Server: a listener attached by the caller survives srv.deinit — ownership stays with whoever bound it" { 153 test "Server: a listener attached by the caller survives srv.deinit — ownership stays with whoever bound it" {
170 const alloc = std.testing.allocator; 154 const alloc = std.testing.allocator;
171 155
172 var tmp = try TmpDir.make(); 156 var td = try h.TestDaemon.open(alloc, "ep3");
173 defer tmp.cleanup(); 157 defer td.deinit();
174 var kbuf: [128]u8 = undefined; 158 var kbuf: [128]u8 = undefined;
175 const key_path = try std.fmt.bufPrint(&kbuf, "{s}/key", .{tmp.path()}); 159 const key_path = try std.fmt.bufPrint(&kbuf, "{s}/key", .{td.tmp.path()});
176 try xdg.writeNewKey(key_path); 160 try xdg.writeNewKey(key_path);
177 const key = try quic.Key.load(key_path); 161 const key = try quic.Key.load(key_path);
178 162
179 var sbuf: [128]u8 = undefined;
180 const sock_path = try std.fmt.bufPrint(&sbuf, "{s}/ep3.sock", .{tmp.path()});
181
182 // Bound out here, as main.zig's explicit `--quic` path does. Deliberately NOT 163 // Bound out here, as main.zig's explicit `--quic` path does. Deliberately NOT
183 // deferred: which side frees this IS the claim, so the frees are written in 164 // deferred: which side frees this IS the claim, so the frees are written in
184 // the order the claim is about — a deferred free would be a double free the 165 // the order the claim is about — a deferred free would be a double free the
@@ -189,24 +170,24 @@ test "Server: a listener attached by the caller survives srv.deinit — ownershi
189 try std.testing.expect(port != 0); 170 try std.testing.expect(port != 0);
190 171
191 { 172 {
192 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 173 try td.start(.{ .shell = "/bin/sh" });
193 defer srv.deinit(); 174 defer td.shutdown();
194 175
195 // The two lines main.zig's run() uses, in its order. 176 // The two lines main.zig's run() uses, in its order.
196 l.setHandler(srv.quicHandler()); 177 l.setHandler(td.srv.quicHandler());
197 srv.attachQuic(l); 178 td.srv.attachQuic(l);
198 // attachQuic adopts the reference and nothing else: the tag that 179 // attachQuic adopts the reference and nothing else: the tag that
199 // decides who frees says borrowed, which is what the switch in 180 // decides who frees says borrowed, which is what the switch in
200 // deinit reads. 181 // deinit reads.
201 try std.testing.expect(srv.quic == .borrowed); 182 try std.testing.expect(td.srv.quic == .borrowed);
202 183
203 // The reporting path answers from the listener it was handed, with 184 // The reporting path answers from the listener it was handed, with
204 // no env and no key resolution — the same short circuit the lazy 185 // no env and no key resolution — the same short circuit the lazy
205 // path takes once it has bound. 186 // path takes once it has bound.
206 try std.testing.expectEqual(port, srv.endpointPortFrom(null, null, null)); 187 try std.testing.expectEqual(port, td.srv.endpointPortFrom(null, null, null));
207 } 188 }
208 189
209 // `srv.deinit()` has run and the listener must have survived it: the 190 // The daemon's `deinit` has run and the listener must have survived it: the
210 // process-global latch is still held, so a fresh bind is refused. Without 191 // process-global latch is still held, so a fresh bind is refused. Without
211 // this, a deinit that freed somebody else's listener shows up only as a hang. 192 // this, a deinit that freed somebody else's listener shows up only as a hang.
212 try std.testing.expectError( 193 try std.testing.expectError(
@@ -222,112 +203,96 @@ test "Server: a listener attached by the caller survives srv.deinit — ownershi
222 203
223 test "Server: a clean exit takes its socket file with it" { 204 test "Server: a clean exit takes its socket file with it" {
224 const alloc = std.testing.allocator; 205 const alloc = std.testing.allocator;
225 var tmp = try TmpDir.make(); 206 var td = try h.TestDaemon.init(alloc, "unlink", .{ .shell = "/bin/sh" });
226 defer tmp.cleanup(); 207 defer td.deinit();
227 const dir_path = tmp.path(); 208 try std.fs.cwd().access(td.sock_path, .{});
228 const sock_path = try std.fmt.allocPrint(alloc, "{s}/unlink.sock", .{dir_path}); 209 td.shutdown();
229 defer alloc.free(sock_path);
230
231 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
232 try std.fs.cwd().access(sock_path, .{});
233 srv.deinit();
234 210
235 // The EFFECT, not the reasoning: a guard comparing a sockfs inode against a 211 // The EFFECT, not the reasoning: a guard comparing a sockfs inode against a
236 // filesystem one is false every time, and the leftover socket is 212 // filesystem one is false every time, and the leftover socket is
237 // indistinguishable from a correct refusal to delete someone else's. 213 // indistinguishable from a correct refusal to delete someone else's.
238 try std.testing.expectError( 214 try std.testing.expectError(
239 error.FileNotFound, 215 error.FileNotFound,
240 std.fs.cwd().access(sock_path, .{}), 216 std.fs.cwd().access(td.sock_path, .{}),
241 ); 217 );
242 } 218 }
243 219
244 test "Server: a socket that replaced ours is not ours to delete" { 220 test "Server: a socket that replaced ours is not ours to delete" {
245 const alloc = std.testing.allocator; 221 const alloc = std.testing.allocator;
246 var tmp = try TmpDir.make(); 222 var td = try h.TestDaemon.init(alloc, "replaced", .{ .shell = "/bin/sh" });
247 defer tmp.cleanup(); 223 defer td.deinit();
248 const dir_path = tmp.path();
249 const sock_path = try std.fmt.allocPrint(alloc, "{s}/replaced.sock", .{dir_path});
250 defer alloc.free(sock_path);
251
252 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
253 224
254 // A second daemon takes the path over while the first is still up — 225 // A second daemon takes the path over while the first is still up —
255 // the shape of the field incident behind the socket-steal fix, and the 226 // the shape of the field incident behind the socket-steal fix, and the
256 // whole reason the unlink is guarded rather than unconditional. 227 // whole reason the unlink is guarded rather than unconditional.
257 try std.fs.cwd().deleteFile(sock_path); 228 try std.fs.cwd().deleteFile(td.sock_path);
258 const addr = try std.net.Address.initUnix(sock_path); 229 const addr = try std.net.Address.initUnix(td.sock_path);
259 var survivor = try addr.listen(.{}); 230 var survivor = try addr.listen(.{});
260 defer survivor.deinit(); 231 defer survivor.deinit();
261 232
262 srv.deinit(); 233 td.shutdown();
263 234
264 // The survivor's socket is still there. Deleting it would leave the 235 // The survivor's socket is still there. Deleting it would leave the
265 // daemon that owns it listening on a path nothing can reach. 236 // daemon that owns it listening on a path nothing can reach.
266 try std.fs.cwd().access(sock_path, .{}); 237 try std.fs.cwd().access(td.sock_path, .{});
267 const st = try std.posix.fstatat(std.posix.AT.FDCWD, sock_path, 0); 238 const st = try std.posix.fstatat(std.posix.AT.FDCWD, td.sock_path, 0);
268 try std.testing.expect(std.posix.S.ISSOCK(st.mode)); 239 try std.testing.expect(std.posix.S.ISSOCK(st.mode));
269 std.fs.cwd().deleteFile(sock_path) catch {}; 240 std.fs.cwd().deleteFile(td.sock_path) catch {};
270 } 241 }
271 242
272 test "Server: stats reports live client slots, and the number comes down again" { 243 test "Server: stats reports live client slots, and the number comes down again" {
273 const alloc = std.testing.allocator; 244 const alloc = std.testing.allocator;
274 var tmp = try TmpDir.make(); 245 var td = try h.TestDaemon.init(alloc, "gauge", .{ .shell = "/bin/sh" });
275 defer tmp.cleanup(); 246 defer td.deinit();
276 const dir_path = tmp.path();
277 const sock_path = try std.fmt.allocPrint(alloc, "{s}/gauge.sock", .{dir_path});
278 defer alloc.free(sock_path);
279
280 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
281 defer srv.deinit();
282 247
283 var buf: [Server.stats_text_len]u8 = undefined; 248 var buf: [Server.stats_text_len]u8 = undefined;
284 // indexOf rather than endsWith: the text no longer ends on the 249 // indexOf rather than endsWith: the text no longer ends on the
285 // global gauge — a per-session tail follows it — so the assertion has 250 // global gauge — a per-session tail follows it — so the assertion has
286 // to name the field it means rather than lean on it being last. 251 // to name the field it means rather than lean on it being last.
287 try std.testing.expect(std.mem.indexOf(u8, try srv.statsText(&buf), "clients=0") != null); 252 try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "clients=0") != null);
288 253
289 // Slots filled directly: what is under test is the gauge, not the 254 // Slots filled directly: what is under test is the gauge, not the
290 // machinery that fills them. Emptied by defer so that a failed 255 // machinery that fills them. Emptied by defer so that a failed
291 // assertion below still leaves teardown a valid Server — otherwise the 256 // assertion below still leaves teardown a valid Server — otherwise the
292 // real failure is buried under an abort from closing fd -1. 257 // real failure is buried under an abort from closing fd -1.
293 defer { 258 defer {
294 srv.clients[0] = null; 259 td.srv.clients[0] = null;
295 srv.clients[3] = null; 260 td.srv.clients[3] = null;
296 } 261 }
297 srv.clients[0] = .{ .sink = .{ .socket = -1 } }; 262 td.srv.clients[0] = .{ .sink = .{ .socket = -1 } };
298 srv.clients[3] = .{ .sink = .{ .socket = -1 } }; 263 td.srv.clients[3] = .{ .sink = .{ .socket = -1 } };
299 // Filled with no session (session: null, the promoted-but-unattached 264 // Filled with no session (session: null, the promoted-but-unattached
300 // shape), so this raises the daemon-wide gauge on the main line 265 // shape), so this raises the daemon-wide gauge on the main line
301 // without moving the default session's own per-session count — that 266 // without moving the default session's own per-session count — that
302 // is the boundary the next assertion pins. 267 // is the boundary the next assertion pins.
303 try std.testing.expect(std.mem.indexOf(u8, try srv.statsText(&buf), "clients=2") != null); 268 try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "clients=2") != null);
304 269
305 // A gauge, not a counter: the whole reason for adding it is watching 270 // A gauge, not a counter: the whole reason for adding it is watching
306 // occupancy clear, so it has to be able to go down. 271 // occupancy clear, so it has to be able to go down.
307 srv.clients[0] = null; 272 td.srv.clients[0] = null;
308 try std.testing.expect(std.mem.indexOf(u8, try srv.statsText(&buf), "clients=1") != null); 273 try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "clients=1") != null);
309 srv.clients[3] = null; 274 td.srv.clients[3] = null;
310 try std.testing.expect(std.mem.indexOf(u8, try srv.statsText(&buf), "clients=0") != null); 275 try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "clients=0") != null);
311 276
312 // ...and `attaches=` is the COUNTER beside it, which is why both exist: 277 // ...and `attaches=` is the COUNTER beside it, which is why both exist:
313 // occupancy went 0 → 2 → 0 above without an attach frame ever arriving, so 278 // occupancy went 0 → 2 → 0 above without an attach frame ever arriving, so
314 // this is still 0. It answers "did anyone attach since I last looked". 279 // this is still 0. It answers "did anyone attach since I last looked".
315 try std.testing.expect(std.mem.indexOf(u8, try srv.statsText(&buf), "attaches=0") != null); 280 try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "attaches=0") != null);
316 srv.stats.attaches += 1; 281 td.srv.stats.attaches += 1;
317 try std.testing.expect(std.mem.indexOf(u8, try srv.statsText(&buf), "attaches=1") != null); 282 try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "attaches=1") != null);
318 srv.clients[0] = .{ .sink = .{ .socket = -1 } }; 283 td.srv.clients[0] = .{ .sink = .{ .socket = -1 } };
319 // A second client seating itself does not move it either — only the 284 // A second client seating itself does not move it either — only the
320 // attach handler does, and a gauge moving is not an attach. 285 // attach handler does, and a gauge moving is not an attach.
321 const both = try srv.statsText(&buf); 286 const both = try td.srv.statsText(&buf);
322 try std.testing.expect(std.mem.indexOf(u8, both, "clients=1") != null); 287 try std.testing.expect(std.mem.indexOf(u8, both, "clients=1") != null);
323 try std.testing.expect(std.mem.indexOf(u8, both, "attaches=1") != null); 288 try std.testing.expect(std.mem.indexOf(u8, both, "attaches=1") != null);
324 srv.clients[0] = null; 289 td.srv.clients[0] = null;
325 290
326 // The fields the harnesses parse are still where they were: appended, 291 // The fields the harnesses parse are still where they were: appended,
327 // never reordered. The old leading `seq=` was one session's tracker 292 // never reordered. The old leading `seq=` was one session's tracker
328 // and moved into the per-session tail; the main line now starts 293 // and moved into the per-session tail; the main line now starts
329 // with the counters that were always daemon-global. 294 // with the counters that were always daemon-global.
330 const text = try srv.statsText(&buf); 295 const text = try td.srv.statsText(&buf);
331 try std.testing.expect(std.mem.startsWith(u8, text, "snapshots=")); 296 try std.testing.expect(std.mem.startsWith(u8, text, "snapshots="));
332 try std.testing.expect(std.mem.indexOf(u8, text, " snapshot_equiv_bytes=") != null); 297 try std.testing.expect(std.mem.indexOf(u8, text, " snapshot_equiv_bytes=") != null);
333 try std.testing.expect(std.mem.indexOf(u8, text, " sessions=1") != null); 298 try std.testing.expect(std.mem.indexOf(u8, text, " sessions=1") != null);
@@ -354,20 +319,17 @@ test "Server: the stats buffer holds the widest reply its format can print" {
354 test "Server: stop_req from a bare connection requests shutdown; run returns 0" { 319 test "Server: stop_req from a bare connection requests shutdown; run returns 0" {
355 const alloc = std.testing.allocator; 320 const alloc = std.testing.allocator;
356 321
357 var tmp = try TmpDir.make(); 322 var td = try h.TestDaemon.open(alloc, "stop");
358 defer tmp.cleanup(); 323 defer td.deinit();
359 var buf: [128]u8 = undefined;
360 const sock_path = try std.fmt.bufPrint(&buf, "{s}/stop.sock", .{tmp.path()});
361 324
362 // The flag is global by necessity (a signal handler shares it); reset 325 // The flag is global by necessity (a signal handler shares it); reset
363 // so this test neither inherits a stale request nor leaves one behind. 326 // so this test neither inherits a stale request nor leaves one behind.
364 shutdown_flag.store(false, .release); 327 shutdown_flag.store(false, .release);
365 defer shutdown_flag.store(false, .release); 328 defer shutdown_flag.store(false, .release);
366 329
367 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 330 try td.start(.{ .shell = "/bin/sh" });
368 defer srv.deinit();
369 331
370 const c = try dial.dial(sock_path); 332 const c = try dial.dial(td.sock_path);
371 defer c.close(); 333 defer c.close();
372 // No attach first: the frame must be honored from the OBSERVER 334 // No attach first: the frame must be honored from the OBSERVER
373 // dispatch, which is where a bare `mux d stop` connection lives. 335 // dispatch, which is where a bare `mux d stop` connection lives.
@@ -377,7 +339,7 @@ test "Server: stop_req from a bare connection requests shutdown; run returns 0"
377 // suite: 100 iterations x 50ms is the deadline, the flag is the exit. 339 // suite: 100 iterations x 50ms is the deadline, the flag is the exit.
378 var i: usize = 0; 340 var i: usize = 0;
379 while (i < 100 and !shutdown_flag.load(.acquire)) : (i += 1) { 341 while (i < 100 and !shutdown_flag.load(.acquire)) : (i += 1) {
380 try srv.pumpOnce(50); 342 try td.srv.pumpOnce(50);
381 } 343 }
382 try std.testing.expect(shutdown_flag.load(.acquire)); 344 try std.testing.expect(shutdown_flag.load(.acquire));
383 345
@@ -385,24 +347,21 @@ test "Server: stop_req from a bare connection requests shutdown; run returns 0"
385 // pumping. It is the one unbounded call here, safe only because the expect 347 // pumping. It is the one unbounded call here, safe only because the expect
386 // above guarantees the flag. 0: a daemon asked to stop did its job, and a 348 // above guarantees the flag. 0: a daemon asked to stop did its job, and a
387 // supervisor reads nonzero as a crash-loop. 349 // supervisor reads nonzero as a crash-loop.
388 try std.testing.expectEqual(@as(u8, 0), try srv.run()); 350 try std.testing.expectEqual(@as(u8, 0), try td.srv.run());
389 } 351 }
390 352
391 test "Server: stop_req from an attached client is honored too" { 353 test "Server: stop_req from an attached client is honored too" {
392 const alloc = std.testing.allocator; 354 const alloc = std.testing.allocator;
393 355
394 var tmp = try TmpDir.make(); 356 var td = try h.TestDaemon.open(alloc, "stop2");
395 defer tmp.cleanup(); 357 defer td.deinit();
396 var buf: [128]u8 = undefined;
397 const sock_path = try std.fmt.bufPrint(&buf, "{s}/stop2.sock", .{tmp.path()});
398 358
399 shutdown_flag.store(false, .release); 359 shutdown_flag.store(false, .release);
400 defer shutdown_flag.store(false, .release); 360 defer shutdown_flag.store(false, .release);
401 361
402 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 362 try td.start(.{ .shell = "/bin/sh" });
403 defer srv.deinit();
404 363
405 const c = try dial.dialAttach(sock_path, 80, 24); 364 const c = try dial.dialAttach(td.sock_path, 80, 24);
406 defer c.close(); 365 defer c.close();
407 // Both frames go out before any pump and the stop still lands in 366 // Both frames go out before any pump and the stop still lands in
408 // `handleFrame`: one observer read takes both into the buffer, the attach 367 // `handleFrame`: one observer read takes both into the buffer, the attach
@@ -412,7 +371,7 @@ test "Server: stop_req from an attached client is honored too" {
412 371
413 var i: usize = 0; 372 var i: usize = 0;
414 while (i < 100 and !shutdown_flag.load(.acquire)) : (i += 1) { 373 while (i < 100 and !shutdown_flag.load(.acquire)) : (i += 1) {
415 try srv.pumpOnce(50); 374 try td.srv.pumpOnce(50);
416 } 375 }
417 try std.testing.expect(shutdown_flag.load(.acquire)); 376 try std.testing.expect(shutdown_flag.load(.acquire));
418 } 377 }
@@ -420,37 +379,34 @@ test "Server: stop_req from an attached client is honored too" {
420 test "Server: endpoint_req binds a listener lazily, answers the same port on both dispatches, and deinit hands it back" { 379 test "Server: endpoint_req binds a listener lazily, answers the same port on both dispatches, and deinit hands it back" {
421 const alloc = std.testing.allocator; 380 const alloc = std.testing.allocator;
422 381
423 var tmp = try TmpDir.make(); 382 var td = try h.TestDaemon.open(alloc, "ep");
424 defer tmp.cleanup(); 383 defer td.deinit();
425 var kbuf: [128]u8 = undefined; 384 var kbuf: [128]u8 = undefined;
426 const key_path = try std.fmt.bufPrint(&kbuf, "{s}/key", .{tmp.path()}); 385 const key_path = try std.fmt.bufPrint(&kbuf, "{s}/key", .{td.tmp.path()});
427 try xdg.writeNewKey(key_path); 386 try xdg.writeNewKey(key_path);
428 const key = try quic.Key.load(key_path); 387 const key = try quic.Key.load(key_path);
429 388
430 var sbuf: [128]u8 = undefined; 389 // Scoped so the daemon's deinit runs before the release check below, and still
431 const sock_path = try std.fmt.bufPrint(&sbuf, "{s}/ep.sock", .{tmp.path()});
432
433 // Scoped so srv.deinit() runs before the release check below, and still
434 // runs if an assertion inside fails. 390 // runs if an assertion inside fails.
435 var first_port: u16 = 0; 391 var first_port: u16 = 0;
436 { 392 {
437 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 393 try td.start(.{ .shell = "/bin/sh" });
438 defer srv.deinit(); 394 defer td.shutdown();
439 395
440 // Key resolution first, through the env seam: MUX_KEY_FILE's value 396 // Key resolution first, through the env seam: MUX_KEY_FILE's value
441 // handed in rather than set, since tests cannot setenv. 397 // handed in rather than set, since tests cannot setenv.
442 first_port = srv.endpointPortFrom(key_path, null, null); 398 first_port = td.srv.endpointPortFrom(key_path, null, null);
443 try std.testing.expect(first_port != 0); 399 try std.testing.expect(first_port != 0);
444 try std.testing.expect(srv.quicListener() != null); 400 try std.testing.expect(td.srv.quicListener() != null);
445 401
446 // Now the wire, on a listener that already exists — which is what 402 // Now the wire, on a listener that already exists — which is what
447 // keeps this half from depending on the test machine having a real 403 // keeps this half from depending on the test machine having a real
448 // ~/.config/mux/key. The observer arm is the load-bearing one: 404 // ~/.config/mux/key. The observer arm is the load-bearing one:
449 // `mux d endpoint` never attaches. 405 // `mux d endpoint` never attaches.
450 const obs = try dial.dial(sock_path); 406 const obs = try dial.dial(td.sock_path);
451 defer obs.close(); 407 defer obs.close();
452 try proto.writeFrame(obs.handle, .endpoint_req, ""); 408 try proto.writeFrame(obs.handle, .endpoint_req, "");
453 const reply = (try awaitFrame(alloc, &srv, obs.handle, .endpoint_reply, 200)) orelse 409 const reply = (try awaitFrame(alloc, &td.srv, obs.handle, .endpoint_reply, 200)) orelse
454 return error.NoEndpointReply; 410 return error.NoEndpointReply;
455 defer reply.deinit(alloc); 411 defer reply.deinit(alloc);
456 try std.testing.expectEqual(first_port, try proto.decodeEndpointReply(reply.payload)); 412 try std.testing.expectEqual(first_port, try proto.decodeEndpointReply(reply.payload));
@@ -459,17 +415,17 @@ test "Server: endpoint_req binds a listener lazily, answers the same port on bot
459 // refused by the process-global latch and surface as port 0, so 415 // refused by the process-global latch and surface as port 0, so
460 // equality is the stronger claim than "nonzero again". 416 // equality is the stronger claim than "nonzero again".
461 try proto.writeFrame(obs.handle, .endpoint_req, ""); 417 try proto.writeFrame(obs.handle, .endpoint_req, "");
462 const reply2 = (try awaitFrame(alloc, &srv, obs.handle, .endpoint_reply, 200)) orelse 418 const reply2 = (try awaitFrame(alloc, &td.srv, obs.handle, .endpoint_reply, 200)) orelse
463 return error.NoSecondEndpointReply; 419 return error.NoSecondEndpointReply;
464 defer reply2.deinit(alloc); 420 defer reply2.deinit(alloc);
465 try std.testing.expectEqual(first_port, try proto.decodeEndpointReply(reply2.payload)); 421 try std.testing.expectEqual(first_port, try proto.decodeEndpointReply(reply2.payload));
466 422
467 // The attached-client arm answers the same verb the same way, on a 423 // The attached-client arm answers the same verb the same way, on a
468 // separate connection that attaches first. 424 // separate connection that attaches first.
469 const cl = try dial.dialAttach(sock_path, 80, 24); 425 const cl = try dial.dialAttach(td.sock_path, 80, 24);
470 defer cl.close(); 426 defer cl.close();
471 try proto.writeFrame(cl.handle, .endpoint_req, ""); 427 try proto.writeFrame(cl.handle, .endpoint_req, "");
472 const reply3 = (try awaitFrame(alloc, &srv, cl.handle, .endpoint_reply, 400)) orelse 428 const reply3 = (try awaitFrame(alloc, &td.srv, cl.handle, .endpoint_reply, 400)) orelse
473 return error.NoAttachedEndpointReply; 429 return error.NoAttachedEndpointReply;
474 defer reply3.deinit(alloc); 430 defer reply3.deinit(alloc);
475 try std.testing.expectEqual(first_port, try proto.decodeEndpointReply(reply3.payload)); 431 try std.testing.expectEqual(first_port, try proto.decodeEndpointReply(reply3.payload));
@@ -486,59 +442,51 @@ test "Server: endpoint_req binds a listener lazily, answers the same port on bot
486 test "Server: endpointPortFrom refuses without a key, survives it, and prefers a listener already bound" { 442 test "Server: endpointPortFrom refuses without a key, survives it, and prefers a listener already bound" {
487 const alloc = std.testing.allocator; 443 const alloc = std.testing.allocator;
488 444
489 var tmp = try TmpDir.make(); 445 var td = try h.TestDaemon.open(alloc, "ep2");
490 defer tmp.cleanup(); 446 defer td.deinit();
491 var kbuf: [128]u8 = undefined; 447 var kbuf: [128]u8 = undefined;
492 const key_path = try std.fmt.bufPrint(&kbuf, "{s}/key", .{tmp.path()}); 448 const key_path = try std.fmt.bufPrint(&kbuf, "{s}/key", .{td.tmp.path()});
493 try xdg.writeNewKey(key_path); 449 try xdg.writeNewKey(key_path);
494 const key = try quic.Key.load(key_path); 450 const key = try quic.Key.load(key_path);
495 451
496 var sbuf: [128]u8 = undefined; 452 try td.start(.{ .shell = "/bin/sh" });
497 const sock_path = try std.fmt.bufPrint(&sbuf, "{s}/ep2.sock", .{tmp.path()});
498
499 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
500 defer srv.deinit();
501 453
502 // No env at all: keyPathFrom has nothing to build a default from, so 454 // No env at all: keyPathFrom has nothing to build a default from, so
503 // there is no key and the answer is 0 rather than a crash. 455 // there is no key and the answer is 0 rather than a crash.
504 try std.testing.expectEqual(@as(u16, 0), srv.endpointPortFrom(null, null, null)); 456 try std.testing.expectEqual(@as(u16, 0), td.srv.endpointPortFrom(null, null, null));
505 // Named but absent: Key.load fails, same answer. 457 // Named but absent: Key.load fails, same answer.
506 var mbuf: [128]u8 = undefined; 458 var mbuf: [128]u8 = undefined;
507 const missing = try std.fmt.bufPrint(&mbuf, "{s}/absent-key", .{tmp.path()}); 459 const missing = try std.fmt.bufPrint(&mbuf, "{s}/absent-key", .{td.tmp.path()});
508 try std.testing.expectEqual(@as(u16, 0), srv.endpointPortFrom(missing, null, null)); 460 try std.testing.expectEqual(@as(u16, 0), td.srv.endpointPortFrom(missing, null, null));
509 // A default path pointing at a directory with no key in it: resolvable, 461 // A default path pointing at a directory with no key in it: resolvable,
510 // not present, so the access check refuses before Key.load is asked. 462 // not present, so the access check refuses before Key.load is asked.
511 try std.testing.expectEqual(@as(u16, 0), srv.endpointPortFrom(null, tmp.path(), null)); 463 try std.testing.expectEqual(@as(u16, 0), td.srv.endpointPortFrom(null, td.tmp.path(), null));
512 464
513 // Refusing left no half-state behind: nothing was bound, and the daemon 465 // Refusing left no half-state behind: nothing was bound, and the daemon
514 // is still able to bind when a real key does turn up. 466 // is still able to bind when a real key does turn up.
515 try std.testing.expect(srv.quic == .none); 467 try std.testing.expect(td.srv.quic == .none);
516 const port = try srv.lazyBindQuic(key); 468 const port = try td.srv.lazyBindQuic(key);
517 try std.testing.expect(port != 0); 469 try std.testing.expect(port != 0);
518 470
519 // With a listener in hand the answer is the bound port, and key 471 // With a listener in hand the answer is the bound port, and key
520 // resolution is never reached — all-null env would otherwise return 0. 472 // resolution is never reached — all-null env would otherwise return 0.
521 try std.testing.expectEqual(port, srv.endpointPortFrom(null, null, null)); 473 try std.testing.expectEqual(port, td.srv.endpointPortFrom(null, null, null));
522 } 474 }
523 475
524 test "Server: status_req is answered on an attached client and on a bare observer" { 476 test "Server: status_req is answered on an attached client and on a bare observer" {
525 const alloc = std.testing.allocator; 477 const alloc = std.testing.allocator;
526 478
527 var tmp = try TmpDir.make(); 479 var td = try h.TestDaemon.open(alloc, "status");
528 defer tmp.cleanup(); 480 defer td.deinit();
529 const dir_path = tmp.path();
530 const sock_path = try std.fmt.allocPrint(alloc, "{s}/status.sock", .{dir_path});
531 defer alloc.free(sock_path);
532 481
533 // /bin/cat: a session that produces no output of its own, so nothing 482 // /bin/cat: a session that produces no output of its own, so nothing
534 // moves the state these assertions describe. 483 // moves the state these assertions describe.
535 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 484 try td.start(.{ .shell = "/bin/cat" });
536 defer srv.deinit();
537 485
538 const c = try dial.dialAttach(sock_path, 80, 24); 486 const c = try dial.dialAttach(td.sock_path, 80, 24);
539 defer c.close(); 487 defer c.close();
540 try proto.writeFrame(c.handle, .status_req, ""); 488 try proto.writeFrame(c.handle, .status_req, "");
541 const f = (try awaitFrame(alloc, &srv, c.handle, .status_reply, 400)) orelse 489 const f = (try awaitFrame(alloc, &td.srv, c.handle, .status_reply, 400)) orelse
542 return error.NoAttachedStatusReply; 490 return error.NoAttachedStatusReply;
543 defer f.deinit(alloc); 491 defer f.deinit(alloc);
544 const st = try proto.decodeStatusReply(f.payload); 492 const st = try proto.decodeStatusReply(f.payload);
@@ -554,10 +502,10 @@ test "Server: status_req is answered on an attached client and on a bare observe
554 // `mux a status` never attaches, so the observer arm is the load-bearing 502 // `mux a status` never attaches, so the observer arm is the load-bearing
555 // one — same reasoning as endpoint_req's, and the same failure if it is 503 // one — same reasoning as endpoint_req's, and the same failure if it is
556 // missing: the frame falls into `else => {}` and the caller hangs. 504 // missing: the frame falls into `else => {}` and the caller hangs.
557 const obs = try dial.dial(sock_path); 505 const obs = try dial.dial(td.sock_path);
558 defer obs.close(); 506 defer obs.close();
559 try proto.writeFrame(obs.handle, .status_req, ""); 507 try proto.writeFrame(obs.handle, .status_req, "");
560 const f2 = (try awaitFrame(alloc, &srv, obs.handle, .status_reply, 400)) orelse 508 const f2 = (try awaitFrame(alloc, &td.srv, obs.handle, .status_reply, 400)) orelse
561 return error.NoObserverStatusReply; 509 return error.NoObserverStatusReply;
562 defer f2.deinit(alloc); 510 defer f2.deinit(alloc);
563 const st2 = try proto.decodeStatusReply(f2.payload); 511 const st2 = try proto.decodeStatusReply(f2.payload);
@@ -596,19 +544,16 @@ fn pumpUntilReplicaSees(
596 test "Server: two named sessions hold two shells with independent content" { 544 test "Server: two named sessions hold two shells with independent content" {
597 const alloc = std.testing.allocator; 545 const alloc = std.testing.allocator;
598 546
599 var tmp = try TmpDir.make(); 547 var td = try h.TestDaemon.open(alloc, "twonames");
600 defer tmp.cleanup(); 548 defer td.deinit();
601 const sock_path = try std.fmt.allocPrint(alloc, "{s}/twonames.sock", .{tmp.path()});
602 defer alloc.free(sock_path);
603 549
604 // /bin/cat sessions: what is typed comes straight back and nothing else 550 // /bin/cat sessions: what is typed comes straight back and nothing else
605 // ever prints, so each grid holds exactly its own client's marker. 551 // ever prints, so each grid holds exactly its own client's marker.
606 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 552 try td.start(.{ .shell = "/bin/cat" });
607 defer srv.deinit();
608 553
609 const ca = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 554 const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
610 defer ca.close(); 555 defer ca.close();
611 const cb = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 556 const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
612 defer cb.close(); 557 defer cb.close();
613 558
614 try proto.writeFrame(ca.handle, .input, "MARKER-ALPHA\n"); 559 try proto.writeFrame(ca.handle, .input, "MARKER-ALPHA\n");
@@ -621,60 +566,55 @@ test "Server: two named sessions hold two shells with independent content" {
621 566
622 // Liveness first: each client converges on its own marker. 567 // Liveness first: each client converges on its own marker.
623 try std.testing.expect( 568 try std.testing.expect(
624 try pumpUntilReplicaSees(alloc, &srv, ca.handle, rep_a, "MARKER-ALPHA", 400), 569 try pumpUntilReplicaSees(alloc, &td.srv, ca.handle, rep_a, "MARKER-ALPHA", 400),
625 ); 570 );
626 try std.testing.expect( 571 try std.testing.expect(
627 try pumpUntilReplicaSees(alloc, &srv, cb.handle, rep_b, "MARKER-BETA", 400), 572 try pumpUntilReplicaSees(alloc, &td.srv, cb.handle, rep_b, "MARKER-BETA", 400),
628 ); 573 );
629 574
630 // The boundary: keep pumping so any misdirected broadcast has every 575 // The boundary: keep pumping so any misdirected broadcast has every
631 // chance to arrive, then assert the other session's marker never does. 576 // chance to arrive, then assert the other session's marker never does.
632 try std.testing.expect( 577 try std.testing.expect(
633 !try pumpUntilReplicaSees(alloc, &srv, ca.handle, rep_a, "MARKER-BETA", 30), 578 !try pumpUntilReplicaSees(alloc, &td.srv, ca.handle, rep_a, "MARKER-BETA", 30),
634 ); 579 );
635 try std.testing.expect( 580 try std.testing.expect(
636 !try pumpUntilReplicaSees(alloc, &srv, cb.handle, rep_b, "MARKER-ALPHA", 30), 581 !try pumpUntilReplicaSees(alloc, &td.srv, cb.handle, rep_b, "MARKER-ALPHA", 30),
637 ); 582 );
638 583
639 // Two real sessions stood up beside the default, each under its name. 584 // Two real sessions stood up beside the default, each under its name.
640 try std.testing.expect(srv.sessions.table[1] != null); 585 try std.testing.expect(td.srv.sessions.table[1] != null);
641 try std.testing.expect(srv.sessions.table[2] != null); 586 try std.testing.expect(td.srv.sessions.table[2] != null);
642 const si_a = srv.sessions.find("a") orelse return error.SessionAMissing; 587 const si_a = td.srv.sessions.find("a") orelse return error.SessionAMissing;
643 const si_b = srv.sessions.find("b") orelse return error.SessionBMissing; 588 const si_b = td.srv.sessions.find("b") orelse return error.SessionBMissing;
644 try std.testing.expect(si_a != si_b); 589 try std.testing.expect(si_a != si_b);
645 try std.testing.expect(si_a != 0 and si_b != 0); 590 try std.testing.expect(si_a != 0 and si_b != 0);
646 try std.testing.expectEqualStrings("a", srv.sessions.table[si_a].?.name()); 591 try std.testing.expectEqualStrings("a", td.srv.sessions.table[si_a].?.name());
647 try std.testing.expectEqualStrings("b", srv.sessions.table[si_b].?.name()); 592 try std.testing.expectEqualStrings("b", td.srv.sessions.table[si_b].?.name());
648 } 593 }
649 594
650 test "SessionTable: a name read from the table is that session's own, after the table changes again" { 595 test "SessionTable: a name read from the table is that session's own, after the table changes again" {
651 const alloc = std.testing.allocator; 596 const alloc = std.testing.allocator;
652 597
653 var tmp = try TmpDir.make(); 598 var td = try h.TestDaemon.init(alloc, "ownname", .{ .shell = "/bin/sh" });
654 defer tmp.cleanup(); 599 defer td.deinit();
655 const sock_path = try std.fmt.allocPrint(alloc, "{s}/ownname.sock", .{tmp.path()});
656 defer alloc.free(sock_path);
657
658 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
659 defer srv.deinit();
660 600
661 // Four slots, no two names the same length. The escape this pins gave 601 // Four slots, no two names the same length. The escape this pins gave
662 // every session the LAST one's bytes truncated to its own length, so a 602 // every session the LAST one's bytes truncated to its own length, so a
663 // table of equal-length names sees nothing, and a table of one sees less. 603 // table of equal-length names sees nothing, and a table of one sees less.
664 srv.sessions.table[1] = try SessionTable.create(alloc, srv.spawn_plan, "aaa", 80, 24, null); 604 td.srv.sessions.table[1] = try SessionTable.create(alloc, td.srv.spawn_plan, "aaa", 80, 24, null);
665 srv.sessions.table[2] = try SessionTable.create(alloc, srv.spawn_plan, "bb", 80, 24, null); 605 td.srv.sessions.table[2] = try SessionTable.create(alloc, td.srv.spawn_plan, "bb", 80, 24, null);
666 606
667 // Read out and HELD, which is the shape that broke: a name is a slice, 607 // Read out and HELD, which is the shape that broke: a name is a slice,
668 // and the question is whose bytes it points at once the walk that 608 // and the question is whose bytes it points at once the walk that
669 // produced it is over. Inline storage per slot is what makes this safe; 609 // produced it is over. Inline storage per slot is what makes this safe;
670 // one buffer shared by the table would not be. 610 // one buffer shared by the table would not be.
671 const held = [_][]const u8{ srv.ses(0).name(), srv.ses(1).name(), srv.ses(2).name() }; 611 const held = [_][]const u8{ td.srv.ses(0).name(), td.srv.ses(1).name(), td.srv.ses(2).name() };
672 612
673 // The table changes under the slices, and the newest name is READ — 613 // The table changes under the slices, and the newest name is READ —
674 // the read is the half that matters, because that is what would write 614 // the read is the half that matters, because that is what would write
675 // over a buffer the table shared. 615 // over a buffer the table shared.
676 srv.sessions.table[3] = try SessionTable.create(alloc, srv.spawn_plan, "zzzzz", 80, 24, null); 616 td.srv.sessions.table[3] = try SessionTable.create(alloc, td.srv.spawn_plan, "zzzzz", 80, 24, null);
677 try std.testing.expectEqualStrings("zzzzz", srv.ses(3).name()); 617 try std.testing.expectEqualStrings("zzzzz", td.srv.ses(3).name());
678 618
679 try std.testing.expectEqualStrings(proto.default_session, held[0]); 619 try std.testing.expectEqualStrings(proto.default_session, held[0]);
680 try std.testing.expectEqualStrings("aaa", held[1]); 620 try std.testing.expectEqualStrings("aaa", held[1]);
@@ -684,13 +624,8 @@ test "SessionTable: a name read from the table is that session's own, after the
684 test "Server: every session shell is told the socket it lives on and its own name" { 624 test "Server: every session shell is told the socket it lives on and its own name" {
685 const alloc = std.testing.allocator; 625 const alloc = std.testing.allocator;
686 626
687 var tmp = try TmpDir.make(); 627 var td = try h.TestDaemon.init(alloc, "selfenv", .{ .shell = "/bin/sh" });
688 defer tmp.cleanup(); 628 defer td.deinit();
689 const sock_path = try std.fmt.allocPrint(alloc, "{s}/selfenv.sock", .{tmp.path()});
690 defer alloc.free(sock_path);
691
692 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
693 defer srv.deinit();
694 629
695 // The shell does the comparing, so a pass means the CHILD saw the 630 // The shell does the comparing, so a pass means the CHILD saw the
696 // daemon's own path — not that this test can rebuild the string. The 631 // daemon's own path — not that this test can rebuild the string. The
@@ -699,13 +634,13 @@ test "Server: every session shell is told the socket it lives on and its own nam
699 const probe = try std.fmt.allocPrint( 634 const probe = try std.fmt.allocPrint(
700 alloc, 635 alloc,
701 "[ \"$MUX_SOCK\" = \"{s}\" ] && printf 'SOCK%s-%s\\n' OK \"$MUX_SESSION\"\n", 636 "[ \"$MUX_SOCK\" = \"{s}\" ] && printf 'SOCK%s-%s\\n' OK \"$MUX_SESSION\"\n",
702 .{sock_path}, 637 .{td.sock_path},
703 ); 638 );
704 defer alloc.free(probe); 639 defer alloc.free(probe);
705 640
706 const c0 = try dial.dialAttachNamed(sock_path, 80, 24, ""); 641 const c0 = try dial.dialAttachNamed(td.sock_path, 80, 24, "");
707 defer c0.close(); 642 defer c0.close();
708 const ca = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 643 const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
709 defer ca.close(); 644 defer ca.close();
710 645
711 try proto.writeFrame(c0.handle, .input, probe); 646 try proto.writeFrame(c0.handle, .input, probe);
@@ -720,50 +655,40 @@ test "Server: every session shell is told the socket it lives on and its own nam
720 // told is the RESOLVED name, because that is what a client comparing 655 // told is the RESOLVED name, because that is what a client comparing
721 // `--session` against it will have resolved too. 656 // `--session` against it will have resolved too.
722 try std.testing.expect( 657 try std.testing.expect(
723 try pumpUntilReplicaSees(alloc, &srv, c0.handle, rep_0, "SOCKOK-0", 600), 658 try pumpUntilReplicaSees(alloc, &td.srv, c0.handle, rep_0, "SOCKOK-0", 600),
724 ); 659 );
725 try std.testing.expect( 660 try std.testing.expect(
726 try pumpUntilReplicaSees(alloc, &srv, ca.handle, rep_a, "SOCKOK-a", 600), 661 try pumpUntilReplicaSees(alloc, &td.srv, ca.handle, rep_a, "SOCKOK-a", 600),
727 ); 662 );
728 } 663 }
729 664
730 test "Server: a bare 20-byte attach lands in the default session" { 665 test "Server: a bare 20-byte attach lands in the default session" {
731 const alloc = std.testing.allocator; 666 const alloc = std.testing.allocator;
732 667
733 var tmp = try TmpDir.make(); 668 var td = try h.TestDaemon.init(alloc, "bare", .{ .shell = "/bin/cat" });
734 defer tmp.cleanup(); 669 defer td.deinit();
735 const sock_path = try std.fmt.allocPrint(alloc, "{s}/bare.sock", .{tmp.path()});
736 defer alloc.free(sock_path);
737
738 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
739 defer srv.deinit();
740 670
741 // The wire spelling from before named sessions, byte for byte: no name 671 // The wire spelling from before named sessions, byte for byte: no name
742 // tail at all. An 672 // tail at all. An
743 // old client must land in the default session, not create a nameless 673 // old client must land in the default session, not create a nameless
744 // one beside it. 674 // one beside it.
745 const c = try dial.dialAttach(sock_path, 80, 24); 675 const c = try dial.dialAttach(td.sock_path, 80, 24);
746 defer c.close(); 676 defer c.close();
747 const f = (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400)) orelse 677 const f = (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400)) orelse
748 return error.NoSnapshotOnBareAttach; 678 return error.NoSnapshotOnBareAttach;
749 f.deinit(alloc); 679 f.deinit(alloc);
750 680
751 const si = srv.clients[0].?.session orelse return error.SlotHoldsNoSession; 681 const si = td.srv.clients[0].?.session orelse return error.SlotHoldsNoSession;
752 try std.testing.expectEqualStrings(proto.default_session, srv.sessions.table[si].?.name()); 682 try std.testing.expectEqualStrings(proto.default_session, td.srv.sessions.table[si].?.name());
753 // And nothing was created for it: the default was already there. 683 // And nothing was created for it: the default was already there.
754 for (srv.sessions.table[1..]) |slot| try std.testing.expect(slot == null); 684 for (td.srv.sessions.table[1..]) |slot| try std.testing.expect(slot == null);
755 } 685 }
756 686
757 test "Server: an attach past max_sessions is refused with exit_status, sessions intact" { 687 test "Server: an attach past max_sessions is refused with exit_status, sessions intact" {
758 const alloc = std.testing.allocator; 688 const alloc = std.testing.allocator;
759 689
760 var tmp = try TmpDir.make(); 690 var td = try h.TestDaemon.init(alloc, "tablefull", .{ .shell = "/bin/cat" });
761 defer tmp.cleanup(); 691 defer td.deinit();
762 const sock_path = try std.fmt.allocPrint(alloc, "{s}/tablefull.sock", .{tmp.path()});
763 defer alloc.free(sock_path);
764
765 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
766 defer srv.deinit();
767 692
768 // The default session holds slot 0, so `max_sessions - 1` more fill the 693 // The default session holds slot 0, so `max_sessions - 1` more fill the
769 // table. Each attach is confirmed before the next, so the refusal below is 694 // table. Each attach is confirmed before the next, so the refusal below is
@@ -773,30 +698,30 @@ test "Server: an attach past max_sessions is refused with exit_status, sessions
773 for (1..max_sessions) |i| { 698 for (1..max_sessions) |i| {
774 var nb: [8]u8 = undefined; 699 var nb: [8]u8 = undefined;
775 const nm = try std.fmt.bufPrint(&nb, "s{d}", .{i}); 700 const nm = try std.fmt.bufPrint(&nb, "s{d}", .{i});
776 const c = try dial.dialAttachNamed(sock_path, 80, 24, nm); 701 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, nm);
777 defer c.close(); 702 defer c.close();
778 const f = (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400)) orelse 703 const f = (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400)) orelse
779 return error.NoSnapshotFillingTable; 704 return error.NoSnapshotFillingTable;
780 f.deinit(alloc); 705 f.deinit(alloc);
781 } 706 }
782 // The closes above are only visible to the daemon once it polls, and a 707 // The closes above are only visible to the daemon once it polls, and a
783 // client slot it still believes is live would refuse the probe below 708 // client slot it still believes is live would refuse the probe below
784 // before the session table ever got asked. 709 // before the session table ever got asked.
785 for (0..8) |_| try srv.pumpOnce(1); 710 for (0..8) |_| try td.srv.pumpOnce(1);
786 711
787 // One name past the table gets the same honest no a full client table 712 // One name past the table gets the same honest no a full client table
788 // gives. 713 // gives.
789 const extra = try dial.dialAttachNamed(sock_path, 80, 24, "extra"); 714 const extra = try dial.dialAttachNamed(td.sock_path, 80, 24, "extra");
790 defer extra.close(); 715 defer extra.close();
791 const f = (try awaitFrame(alloc, &srv, extra.handle, .exit_status, 400)) orelse 716 const f = (try awaitFrame(alloc, &td.srv, extra.handle, .exit_status, 400)) orelse
792 return error.NoRefusal; 717 return error.NoRefusal;
793 defer f.deinit(alloc); 718 defer f.deinit(alloc);
794 try std.testing.expect(f.payload.len == 1 and f.payload[0] == 1); 719 try std.testing.expect(f.payload.len == 1 and f.payload[0] == 1);
795 720
796 // And it cost nobody anything: every slot's shell is live, none exited. 721 // And it cost nobody anything: every slot's shell is live, none exited.
797 for (0..max_sessions) |si| { 722 for (0..max_sessions) |si| {
798 try std.testing.expect(srv.sessions.table[si] != null); 723 try std.testing.expect(td.srv.sessions.table[si] != null);
799 try std.testing.expect(srv.sessions.table[si].?.pty.checkExited() == null); 724 try std.testing.expect(td.srv.sessions.table[si].?.pty.checkExited() == null);
800 } 725 }
801 } 726 }
802 727
@@ -806,15 +731,13 @@ test "Server: an attach past max_sessions is refused with exit_status, sessions
806 test "Server: a table of TERM-ignoring shells costs one grace, not one each" { 731 test "Server: a table of TERM-ignoring shells costs one grace, not one each" {
807 const alloc = std.testing.allocator; 732 const alloc = std.testing.allocator;
808 733
809 var tmp = try TmpDir.make(); 734 var td = try h.TestDaemon.open(alloc, "stubborn");
810 defer tmp.cleanup(); 735 defer td.deinit();
811 const sock_path = try std.fmt.allocPrint(alloc, "{s}/stubborn.sock", .{tmp.path()});
812 defer alloc.free(sock_path);
813 736
814 // A real shell ignoring the two signals teardown relies on — HUP from 737 // A real shell ignoring the two signals teardown relies on — HUP from
815 // the master's close, then TERM — which is what an interactive shell 738 // the master's close, then TERM — which is what an interactive shell
816 // does with TERM anyway. Only these children pay the grace at all. 739 // does with TERM anyway. Only these children pay the grace at all.
817 const shim = try std.fmt.allocPrintSentinel(alloc, "{s}/stubborn.sh", .{tmp.path()}, 0); 740 const shim = try std.fmt.allocPrintSentinel(alloc, "{s}/stubborn.sh", .{td.tmp.path()}, 0);
818 defer alloc.free(shim); 741 defer alloc.free(shim);
819 { 742 {
820 var f = try std.fs.cwd().createFile(shim, .{ .mode = 0o755 }); 743 var f = try std.fs.cwd().createFile(shim, .{ .mode = 0o755 });
@@ -822,16 +745,16 @@ test "Server: a table of TERM-ignoring shells costs one grace, not one each" {
822 try f.writeAll("#!/bin/sh\ntrap '' HUP TERM\nwhile :; do sleep 1; done\n"); 745 try f.writeAll("#!/bin/sh\ntrap '' HUP TERM\nwhile :; do sleep 1; done\n");
823 } 746 }
824 747
825 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = shim }); 748 try td.start(.{ .shell = shim });
826 749
827 // Eight beside the default: enough that serial grace (9 x 500ms) and 750 // Eight beside the default: enough that serial grace (9 x 500ms) and
828 // shared grace (500ms) cannot be confused, cheap enough to spawn. 751 // shared grace (500ms) cannot be confused, cheap enough to spawn.
829 for (1..9) |i| { 752 for (1..9) |i| {
830 var nb: [8]u8 = undefined; 753 var nb: [8]u8 = undefined;
831 const nm = try std.fmt.bufPrint(&nb, "s{d}", .{i}); 754 const nm = try std.fmt.bufPrint(&nb, "s{d}", .{i});
832 const c = try dial.dialAttachNamed(sock_path, 80, 24, nm); 755 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, nm);
833 defer c.close(); 756 defer c.close();
834 const f = (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400)) orelse 757 const f = (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400)) orelse
835 return error.NoSnapshotFillingTable; 758 return error.NoSnapshotFillingTable;
836 f.deinit(alloc); 759 f.deinit(alloc);
837 } 760 }
@@ -842,7 +765,7 @@ test "Server: a table of TERM-ignoring shells costs one grace, not one each" {
842 765
843 var kids: [max_sessions]std.posix.pid_t = undefined; 766 var kids: [max_sessions]std.posix.pid_t = undefined;
844 var n: usize = 0; 767 var n: usize = 0;
845 for (&srv.sessions.table) |*slot| { 768 for (&td.srv.sessions.table) |*slot| {
846 if (slot.* == null) continue; 769 if (slot.* == null) continue;
847 const s = &slot.*.?; 770 const s = &slot.*.?;
848 try std.testing.expect(s.pty.checkExited() == null); 771 try std.testing.expect(s.pty.checkExited() == null);
@@ -852,7 +775,7 @@ test "Server: a table of TERM-ignoring shells costs one grace, not one each" {
852 try std.testing.expectEqual(@as(usize, 9), n); 775 try std.testing.expectEqual(@as(usize, 9), n);
853 776
854 var t = try std.time.Timer.start(); 777 var t = try std.time.Timer.start();
855 srv.deinit(); 778 td.shutdown();
856 const elapsed_ms = t.read() / std.time.ns_per_ms; 779 const elapsed_ms = t.read() / std.time.ns_per_ms;
857 if (elapsed_ms >= 1500) { 780 if (elapsed_ms >= 1500) {
858 std.debug.print( 781 std.debug.print(
@@ -875,23 +798,18 @@ test "Server: a table of TERM-ignoring shells costs one grace, not one each" {
875 test "Server: an invalid name on the wire is refused, not created" { 798 test "Server: an invalid name on the wire is refused, not created" {
876 const alloc = std.testing.allocator; 799 const alloc = std.testing.allocator;
877 800
878 var tmp = try TmpDir.make(); 801 var td = try h.TestDaemon.init(alloc, "badname", .{ .shell = "/bin/cat" });
879 defer tmp.cleanup(); 802 defer td.deinit();
880 const sock_path = try std.fmt.allocPrint(alloc, "{s}/badname.sock", .{tmp.path()});
881 defer alloc.free(sock_path);
882
883 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
884 defer srv.deinit();
885 803
886 // A raw-wire client can spell any bytes it likes; a name no tool could 804 // A raw-wire client can spell any bytes it likes; a name no tool could
887 // ever address must die here, not become a session. 805 // ever address must die here, not become a session.
888 const c = try dial.dialAttachNamed(sock_path, 80, 24, "has space"); 806 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "has space");
889 defer c.close(); 807 defer c.close();
890 const f = (try awaitFrame(alloc, &srv, c.handle, .exit_status, 400)) orelse 808 const f = (try awaitFrame(alloc, &td.srv, c.handle, .exit_status, 400)) orelse
891 return error.NoRefusal; 809 return error.NoRefusal;
892 defer f.deinit(alloc); 810 defer f.deinit(alloc);
893 try std.testing.expect(f.payload.len == 1 and f.payload[0] == 1); 811 try std.testing.expect(f.payload.len == 1 and f.payload[0] == 1);
894 try std.testing.expect(srv.sessions.table[1] == null); 812 try std.testing.expect(td.srv.sessions.table[1] == null);
895 } 813 }
896 814
897 test "Server: safeName refuses to hand the console a peer's bytes" { 815 test "Server: safeName refuses to hand the console a peer's bytes" {
@@ -933,27 +851,23 @@ fn writeMortalScript(tmp: *TmpDir) !void {
933 test "Server: one session's shell exiting drops only its clients; the daemon carries on" { 851 test "Server: one session's shell exiting drops only its clients; the daemon carries on" {
934 const alloc = std.testing.allocator; 852 const alloc = std.testing.allocator;
935 853
936 var tmp = try TmpDir.make(); 854 var td = try h.TestDaemon.open(alloc, "onedies");
937 defer tmp.cleanup(); 855 defer td.deinit();
938 const dir_path = tmp.path();
939 const sock_path = try std.fmt.allocPrint(alloc, "{s}/onedies.sock", .{dir_path});
940 defer alloc.free(sock_path);
941 856
942 try writeMortalScript(&tmp); 857 try writeMortalScript(&td.tmp);
943 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/mortal.sh", .{dir_path}, 0); 858 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/mortal.sh", .{td.tmp.path()}, 0);
944 defer alloc.free(script); 859 defer alloc.free(script);
945 860
946 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 861 try td.start(.{ .shell = script });
947 defer srv.deinit();
948 862
949 const ca = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 863 const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
950 defer ca.close(); 864 defer ca.close();
951 const fa = (try awaitFrame(alloc, &srv, ca.handle, .snapshot, 400)) orelse 865 const fa = (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400)) orelse
952 return error.NoSnapshotA; 866 return error.NoSnapshotA;
953 fa.deinit(alloc); 867 fa.deinit(alloc);
954 const si_a = srv.sessions.find("a") orelse return error.SessionAMissing; 868 const si_a = td.srv.sessions.find("a") orelse return error.SessionAMissing;
955 869
956 const cb = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 870 const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
957 defer cb.close(); 871 defer cb.close();
958 872
959 // b's liveness established BEFORE a dies, so its survival below is a 873 // b's liveness established BEFORE a dies, so its survival below is a
@@ -962,34 +876,25 @@ test "Server: one session's shell exiting drops only its clients; the daemon car
962 defer rep_b.deinit(); 876 defer rep_b.deinit();
963 try proto.writeFrame(cb.handle, .input, "pre\n"); 877 try proto.writeFrame(cb.handle, .input, "pre\n");
964 try std.testing.expect( 878 try std.testing.expect(
965 try pumpUntilReplicaSees(alloc, &srv, cb.handle, rep_b, "echo:pre", 400), 879 try pumpUntilReplicaSees(alloc, &td.srv, cb.handle, rep_b, "echo:pre", 400),
966 ); 880 );
967 881
968 // Kill a's shell and pump until its client hears so. b is what makes 882 // Kill a's shell and pump until its client hears so. b is what makes
969 // this a comparison: a's death must reach a's client and nobody else's. 883 // this a comparison: a's death must reach a's client and nobody else's.
970 try proto.writeFrame(ca.handle, .input, "die 0\n"); 884 try proto.writeFrame(ca.handle, .input, "die 0\n");
971 var status: ?u8 = null; 885 var status: ?u8 = null;
972 var rounds: usize = 0; 886 if (try awaitFrame(alloc, &td.srv, ca.handle, .exit_status, 500)) |frame| {
973 while (rounds < 500 and status == null) : (rounds += 1) {
974 try srv.pumpOnce(5);
975 var pfd = [_]std.posix.pollfd{
976 .{ .fd = ca.handle, .events = std.posix.POLL.IN, .revents = 0 },
977 };
978 if ((std.posix.poll(&pfd, 1) catch 0) == 0) continue;
979 const frame = (try proto.readFrame(alloc, ca.handle)) orelse break;
980 defer frame.deinit(alloc); 887 defer frame.deinit(alloc);
981 if (frame.type == .exit_status) { 888 try std.testing.expectEqual(@as(usize, 1), frame.payload.len);
982 try std.testing.expectEqual(@as(usize, 1), frame.payload.len); 889 status = frame.payload[0];
983 status = frame.payload[0];
984 }
985 } 890 }
986 try std.testing.expectEqual(@as(?u8, 0), status); 891 try std.testing.expectEqual(@as(?u8, 0), status);
987 892
988 // The session is gone — slot null, name free — and no client slot still 893 // The session is gone — slot null, name free — and no client slot still
989 // points where it was. 894 // points where it was.
990 try std.testing.expect(srv.sessions.table[si_a] == null); 895 try std.testing.expect(td.srv.sessions.table[si_a] == null);
991 try std.testing.expect(srv.sessions.find("a") == null); 896 try std.testing.expect(td.srv.sessions.find("a") == null);
992 for (srv.clients) |slot| { 897 for (td.srv.clients) |slot| {
993 const cs = slot orelse continue; 898 const cs = slot orelse continue;
994 if (cs.session) |si| try std.testing.expect(si != si_a); 899 if (cs.session) |si| try std.testing.expect(si != si_a);
995 } 900 }
@@ -1007,7 +912,7 @@ test "Server: one session's shell exiting drops only its clients; the daemon car
1007 // And b never noticed: still attached, still echoing. 912 // And b never noticed: still attached, still echoing.
1008 try proto.writeFrame(cb.handle, .input, "post\n"); 913 try proto.writeFrame(cb.handle, .input, "post\n");
1009 try std.testing.expect( 914 try std.testing.expect(
1010 try pumpUntilReplicaSees(alloc, &srv, cb.handle, rep_b, "echo:post", 400), 915 try pumpUntilReplicaSees(alloc, &td.srv, cb.handle, rep_b, "echo:post", 400),
1011 ); 916 );
1012 } 917 }
1013 918
@@ -1019,28 +924,6 @@ fn runThread(srv: *Server, out: *?u8) void {
1019 }; 924 };
1020 } 925 }
1021 926
1022 /// Not `awaitFrame`: that one pumps the server, and here a thread does.
1023 fn awaitFrameThreaded(
1024 alloc: std.mem.Allocator,
1025 fd: std.posix.fd_t,
1026 want: proto.MsgType,
1027 timeout_ms: u64,
1028 ) !?proto.Frame {
1029 var left = timeout_ms;
1030 while (left > 0) {
1031 var pfd = [_]std.posix.pollfd{
1032 .{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 },
1033 };
1034 const ready = try std.posix.poll(&pfd, 50);
1035 left -|= 50;
1036 if (ready == 0) continue;
1037 const frame = (try proto.readFrame(alloc, fd)) orelse return null;
1038 if (frame.type == want) return frame;
1039 frame.deinit(alloc);
1040 }
1041 return null;
1042 }
1043
1044 /// Ask the daemon for its stats until `want` appears. A connection of its OWN 927 /// Ask the daemon for its stats until `want` appears. A connection of its OWN
1045 /// per call, which is the assertion's teeth: a reply that misses its 200 ms 928 /// per call, which is the assertion's teeth: a reply that misses its 200 ms
1046 /// stays buffered, so a shared socket lets a later question be answered by an 929 /// stays buffered, so a shared socket lets a later question be answered by an
@@ -1051,7 +934,7 @@ fn waitStats(alloc: std.mem.Allocator, sock_path: []const u8, want: []const u8)
1051 var tries: usize = 0; 934 var tries: usize = 0;
1052 while (tries < 40) : (tries += 1) { 935 while (tries < 40) : (tries += 1) {
1053 try proto.writeFrame(obs.handle, .stats_req, ""); 936 try proto.writeFrame(obs.handle, .stats_req, "");
1054 const frame = (try awaitFrameThreaded(alloc, obs.handle, .stats_reply, 200)) orelse continue; 937 const frame = (try awaitFrameOn(alloc, obs.handle, .stats_reply, 200)) orelse continue;
1055 defer frame.deinit(alloc); 938 defer frame.deinit(alloc);
1056 if (std.mem.indexOf(u8, frame.payload, want) != null) return; 939 if (std.mem.indexOf(u8, frame.payload, want) != null) return;
1057 } 940 }
@@ -1069,7 +952,7 @@ fn probeEmptiedDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !void {
1069 // retired one this shell's code WAS the daemon's. 952 // retired one this shell's code WAS the daemon's.
1070 const c1 = try dial.dialAttachNamed(sock_path, 80, 24, proto.default_session); 953 const c1 = try dial.dialAttachNamed(sock_path, 80, 24, proto.default_session);
1071 defer c1.close(); 954 defer c1.close();
1072 const snap = (try awaitFrameThreaded(alloc, c1.handle, .snapshot, 4000)) orelse 955 const snap = (try awaitFrameOn(alloc, c1.handle, .snapshot, 4000)) orelse
1073 return error.NoFirstSnapshot; 956 return error.NoFirstSnapshot;
1074 snap.deinit(alloc); 957 snap.deinit(alloc);
1075 try proto.writeFrame(c1.handle, .input, "die 7\n"); 958 try proto.writeFrame(c1.handle, .input, "die 7\n");
@@ -1081,7 +964,7 @@ fn probeEmptiedDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !void {
1081 // sends. 964 // sends.
1082 const c2 = try dial.dialAttachNamed(sock_path, 80, 24, proto.default_session); 965 const c2 = try dial.dialAttachNamed(sock_path, 80, 24, proto.default_session);
1083 defer c2.close(); 966 defer c2.close();
1084 const reborn = (try awaitFrameThreaded(alloc, c2.handle, .snapshot, 4000)) orelse 967 const reborn = (try awaitFrameOn(alloc, c2.handle, .snapshot, 4000)) orelse
1085 return error.EmptyDaemonRefusedABirth; 968 return error.EmptyDaemonRefusedABirth;
1086 reborn.deinit(alloc); 969 reborn.deinit(alloc);
1087 try waitStats(alloc, sock_path, "sessions=1"); 970 try waitStats(alloc, sock_path, "sessions=1");
@@ -1090,14 +973,11 @@ fn probeEmptiedDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !void {
1090 test "Server: a daemon outlives its last session and ends only on stop" { 973 test "Server: a daemon outlives its last session and ends only on stop" {
1091 const alloc = std.testing.allocator; 974 const alloc = std.testing.allocator;
1092 975
1093 var tmp = try TmpDir.make(); 976 var td = try h.TestDaemon.open(alloc, "livesempty");
1094 defer tmp.cleanup(); 977 defer td.deinit();
1095 const dir_path = tmp.path();
1096 const sock_path = try std.fmt.allocPrint(alloc, "{s}/livesempty.sock", .{dir_path});
1097 defer alloc.free(sock_path);
1098 978
1099 try writeMortalScript(&tmp); 979 try writeMortalScript(&td.tmp);
1100 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/mortal.sh", .{dir_path}, 0); 980 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/mortal.sh", .{td.tmp.path()}, 0);
1101 defer alloc.free(script); 981 defer alloc.free(script);
1102 982
1103 // The flag is global by necessity (a signal handler shares it); reset 983 // The flag is global by necessity (a signal handler shares it); reset
@@ -1105,15 +985,14 @@ test "Server: a daemon outlives its last session and ends only on stop" {
1105 shutdown_flag.store(false, .release); 985 shutdown_flag.store(false, .release);
1106 defer shutdown_flag.store(false, .release); 986 defer shutdown_flag.store(false, .release);
1107 987
1108 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 988 try td.start(.{ .shell = script });
1109 defer srv.deinit();
1110 989
1111 var code: ?u8 = null; 990 var code: ?u8 = null;
1112 const th = try std.Thread.spawn(.{}, runThread, .{ &srv, &code }); 991 const th = try std.Thread.spawn(.{}, runThread, .{ &td.srv, &code });
1113 // Captured, not propagated: nothing may return between the spawn and 992 // Captured, not propagated: nothing may return between the spawn and
1114 // the join, because `deinit` is a demolition list and must never run 993 // the join, because `deinit` is a demolition list and must never run
1115 // under a live pump thread. 994 // under a live pump thread.
1116 const probed = probeEmptiedDaemon(alloc, sock_path); 995 const probed = probeEmptiedDaemon(alloc, td.sock_path);
1117 shutdown_flag.store(true, .release); 996 shutdown_flag.store(true, .release);
1118 th.join(); 997 th.join();
1119 try probed; 998 try probed;
@@ -1128,25 +1007,21 @@ test "Server: a daemon outlives its last session and ends only on stop" {
1128 test "Server: a dead name re-attaches as a fresh session with a new epoch" { 1007 test "Server: a dead name re-attaches as a fresh session with a new epoch" {
1129 const alloc = std.testing.allocator; 1008 const alloc = std.testing.allocator;
1130 1009
1131 var tmp = try TmpDir.make(); 1010 var td = try h.TestDaemon.open(alloc, "reborn");
1132 defer tmp.cleanup(); 1011 defer td.deinit();
1133 const dir_path = tmp.path();
1134 const sock_path = try std.fmt.allocPrint(alloc, "{s}/reborn.sock", .{dir_path});
1135 defer alloc.free(sock_path);
1136 1012
1137 try writeMortalScript(&tmp); 1013 try writeMortalScript(&td.tmp);
1138 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/mortal.sh", .{dir_path}, 0); 1014 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/mortal.sh", .{td.tmp.path()}, 0);
1139 defer alloc.free(script); 1015 defer alloc.free(script);
1140 1016
1141 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 1017 try td.start(.{ .shell = script });
1142 defer srv.deinit();
1143 1018
1144 // First instance of "a": hold on to what its attach answered — the 1019 // First instance of "a": hold on to what its attach answered — the
1145 // epoch its snapshots were stamped with, and the newest seq counted 1020 // epoch its snapshots were stamped with, and the newest seq counted
1146 // under it. That pair is exactly what a surviving client would quote. 1021 // under it. That pair is exactly what a surviving client would quote.
1147 const c1 = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 1022 const c1 = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
1148 defer c1.close(); 1023 defer c1.close();
1149 const f1 = (try awaitFrame(alloc, &srv, c1.handle, .snapshot, 400)) orelse 1024 const f1 = (try awaitFrame(alloc, &td.srv, c1.handle, .snapshot, 400)) orelse
1150 return error.NoFirstSnapshot; 1025 return error.NoFirstSnapshot;
1151 defer f1.deinit(alloc); 1026 defer f1.deinit(alloc);
1152 const p1 = try proto.readSnapshotPrefix(f1.payload); 1027 const p1 = try proto.readSnapshotPrefix(f1.payload);
@@ -1155,15 +1030,15 @@ test "Server: a dead name re-attaches as a fresh session with a new epoch" {
1155 // Kill it, and pump until the name is free. 1030 // Kill it, and pump until the name is free.
1156 try proto.writeFrame(c1.handle, .input, "die 0\n"); 1031 try proto.writeFrame(c1.handle, .input, "die 0\n");
1157 var rounds: usize = 0; 1032 var rounds: usize = 0;
1158 while (rounds < 500 and srv.sessions.find("a") != null) : (rounds += 1) { 1033 while (rounds < 500 and td.srv.sessions.find("a") != null) : (rounds += 1) {
1159 try srv.pumpOnce(5); 1034 try td.srv.pumpOnce(5);
1160 } 1035 }
1161 try std.testing.expect(srv.sessions.find("a") == null); 1036 try std.testing.expect(td.srv.sessions.find("a") == null);
1162 1037
1163 // Re-attach the dead name quoting the DEAD instance's seq and epoch — the 1038 // Re-attach the dead name quoting the DEAD instance's seq and epoch — the
1164 // reconnect a client that missed the death sends. A fresh session cannot 1039 // reconnect a client that missed the death sends. A fresh session cannot
1165 // delta-serve seqs counted by a shell it never was. 1040 // delta-serve seqs counted by a shell it never was.
1166 const c2 = try dial.dial(sock_path); 1041 const c2 = try dial.dial(td.sock_path);
1167 defer c2.close(); 1042 defer c2.close();
1168 var abuf: [proto.attach_max_len]u8 = undefined; 1043 var abuf: [proto.attach_max_len]u8 = undefined;
1169 try proto.writeFrame( 1044 try proto.writeFrame(
@@ -1174,7 +1049,7 @@ test "Server: a dead name re-attaches as a fresh session with a new epoch" {
1174 var second: ?proto.SnapshotPrefix = null; 1049 var second: ?proto.SnapshotPrefix = null;
1175 rounds = 0; 1050 rounds = 0;
1176 while (rounds < 400 and second == null) : (rounds += 1) { 1051 while (rounds < 400 and second == null) : (rounds += 1) {
1177 try srv.pumpOnce(5); 1052 try td.srv.pumpOnce(5);
1178 var pfd = [_]std.posix.pollfd{ 1053 var pfd = [_]std.posix.pollfd{
1179 .{ .fd = c2.handle, .events = std.posix.POLL.IN, .revents = 0 }, 1054 .{ .fd = c2.handle, .events = std.posix.POLL.IN, .revents = 0 },
1180 }; 1055 };
@@ -1191,7 +1066,7 @@ test "Server: a dead name re-attaches as a fresh session with a new epoch" {
1191 const p2 = second orelse return error.NoRebornSnapshot; 1066 const p2 = second orelse return error.NoRebornSnapshot;
1192 try std.testing.expect(p2.epoch != 0); 1067 try std.testing.expect(p2.epoch != 0);
1193 try std.testing.expect(p2.epoch != p1.epoch); 1068 try std.testing.expect(p2.epoch != p1.epoch);
1194 try std.testing.expect(srv.sessions.find("a") != null); 1069 try std.testing.expect(td.srv.sessions.find("a") != null);
1195 } 1070 }
1196 1071
1197 // --------------------------------------------------------------------------- 1072 // ---------------------------------------------------------------------------
@@ -1202,17 +1077,12 @@ test "Server: a dead name re-attaches as a fresh session with a new epoch" {
1202 test "Server: dump names a session; an unknown name answers in words" { 1077 test "Server: dump names a session; an unknown name answers in words" {
1203 const alloc = std.testing.allocator; 1078 const alloc = std.testing.allocator;
1204 1079
1205 var tmp = try TmpDir.make(); 1080 var td = try h.TestDaemon.init(alloc, "dumpnames", .{ .shell = "/bin/cat" });
1206 defer tmp.cleanup(); 1081 defer td.deinit();
1207 const sock_path = try std.fmt.allocPrint(alloc, "{s}/dumpnames.sock", .{tmp.path()});
1208 defer alloc.free(sock_path);
1209
1210 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
1211 defer srv.deinit();
1212 1082
1213 const ca = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 1083 const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
1214 defer ca.close(); 1084 defer ca.close();
1215 const cb = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 1085 const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
1216 defer cb.close(); 1086 defer cb.close();
1217 1087
1218 try proto.writeFrame(ca.handle, .input, "MARKER-ALPHA\n"); 1088 try proto.writeFrame(ca.handle, .input, "MARKER-ALPHA\n");
@@ -1223,16 +1093,16 @@ test "Server: dump names a session; an unknown name answers in words" {
1223 var rep_b = try Engine.init(alloc, .{ .cols = 80, .rows = 24 }); 1093 var rep_b = try Engine.init(alloc, .{ .cols = 80, .rows = 24 });
1224 defer rep_b.deinit(); 1094 defer rep_b.deinit();
1225 try std.testing.expect( 1095 try std.testing.expect(
1226 try pumpUntilReplicaSees(alloc, &srv, ca.handle, rep_a, "MARKER-ALPHA", 400), 1096 try pumpUntilReplicaSees(alloc, &td.srv, ca.handle, rep_a, "MARKER-ALPHA", 400),
1227 ); 1097 );
1228 try std.testing.expect( 1098 try std.testing.expect(
1229 try pumpUntilReplicaSees(alloc, &srv, cb.handle, rep_b, "MARKER-BETA", 400), 1099 try pumpUntilReplicaSees(alloc, &td.srv, cb.handle, rep_b, "MARKER-BETA", 400),
1230 ); 1100 );
1231 1101
1232 // Asked from a's own connection, for b: dump answers the payload's 1102 // Asked from a's own connection, for b: dump answers the payload's
1233 // tail, never the asker's own session. 1103 // tail, never the asker's own session.
1234 try proto.writeFrame(ca.handle, .debug_dump, &[_]u8{ 1, 'b' }); 1104 try proto.writeFrame(ca.handle, .debug_dump, &[_]u8{ 1, 'b' });
1235 const reply = (try awaitFrame(alloc, &srv, ca.handle, .dump_reply, 400)) orelse 1105 const reply = (try awaitFrame(alloc, &td.srv, ca.handle, .dump_reply, 400)) orelse
1236 return error.NoDumpReply; 1106 return error.NoDumpReply;
1237 defer reply.deinit(alloc); 1107 defer reply.deinit(alloc);
1238 try std.testing.expect(std.mem.indexOf(u8, reply.payload, "MARKER-BETA") != null); 1108 try std.testing.expect(std.mem.indexOf(u8, reply.payload, "MARKER-BETA") != null);
@@ -1241,7 +1111,7 @@ test "Server: dump names a session; an unknown name answers in words" {
1241 // An unknown name is a dump_reply in words, not a fake connection loss 1111 // An unknown name is a dump_reply in words, not a fake connection loss
1242 // — the only channel this verb has for "no" is the reply itself. 1112 // — the only channel this verb has for "no" is the reply itself.
1243 try proto.writeFrame(ca.handle, .debug_dump, &[_]u8{ 1, 'z' }); 1113 try proto.writeFrame(ca.handle, .debug_dump, &[_]u8{ 1, 'z' });
1244 const bad = (try awaitFrame(alloc, &srv, ca.handle, .dump_reply, 400)) orelse 1114 const bad = (try awaitFrame(alloc, &td.srv, ca.handle, .dump_reply, 400)) orelse
1245 return error.NoDumpReplyForBadName; 1115 return error.NoDumpReplyForBadName;
1246 defer bad.deinit(alloc); 1116 defer bad.deinit(alloc);
1247 try std.testing.expectEqualStrings("mux d: no such session: z\n", bad.payload); 1117 try std.testing.expectEqualStrings("mux d: no such session: z\n", bad.payload);
@@ -1250,16 +1120,13 @@ test "Server: dump names a session; an unknown name answers in words" {
1250 test "Server: an observer's status_req names a session by tail" { 1120 test "Server: an observer's status_req names a session by tail" {
1251 const alloc = std.testing.allocator; 1121 const alloc = std.testing.allocator;
1252 1122
1253 var tmp = try TmpDir.make(); 1123 var td = try h.TestDaemon.open(alloc, "obsstatus");
1254 defer tmp.cleanup(); 1124 defer td.deinit();
1255 const dir_path = tmp.path();
1256 const sock_path = try std.fmt.allocPrint(alloc, "{s}/obsstatus.sock", .{dir_path});
1257 defer alloc.free(sock_path);
1258 1125
1259 // The one-mark-on-command scripted shell, same shape as the OSC 133 1126 // The one-mark-on-command scripted shell, same shape as the OSC 133
1260 // test: it answers to being told, not on its own, so a running push 1127 // test: it answers to being told, not on its own, so a running push
1261 // against b is unambiguously b's and never a's idle default. 1128 // against b is unambiguously b's and never a's idle default.
1262 try tmp.dir.writeFile(.{ 1129 try td.tmp.dir.writeFile(.{
1263 .sub_path = "marks.sh", 1130 .sub_path = "marks.sh",
1264 .data = 1131 .data =
1265 \\#!/bin/sh 1132 \\#!/bin/sh
@@ -1270,36 +1137,35 @@ test "Server: an observer's status_req names a session by tail" {
1270 , 1137 ,
1271 .flags = .{ .mode = 0o755 }, 1138 .flags = .{ .mode = 0o755 },
1272 }); 1139 });
1273 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/marks.sh", .{dir_path}, 0); 1140 const script = try std.fmt.allocPrintSentinel(alloc, "{s}/marks.sh", .{td.tmp.path()}, 0);
1274 defer alloc.free(script); 1141 defer alloc.free(script);
1275 1142
1276 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 1143 try td.start(.{ .shell = script });
1277 defer srv.deinit();
1278 1144
1279 const a = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 1145 const a = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
1280 defer a.close(); 1146 defer a.close();
1281 const b = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 1147 const b = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
1282 defer b.close(); 1148 defer b.close();
1283 const fa = (try awaitFrame(alloc, &srv, a.handle, .snapshot, 400)) orelse 1149 const fa = (try awaitFrame(alloc, &td.srv, a.handle, .snapshot, 400)) orelse
1284 return error.NoSnapshotA; 1150 return error.NoSnapshotA;
1285 fa.deinit(alloc); 1151 fa.deinit(alloc);
1286 const fb = (try awaitFrame(alloc, &srv, b.handle, .snapshot, 400)) orelse 1152 const fb = (try awaitFrame(alloc, &td.srv, b.handle, .snapshot, 400)) orelse
1287 return error.NoSnapshotB; 1153 return error.NoSnapshotB;
1288 fb.deinit(alloc); 1154 fb.deinit(alloc);
1289 1155
1290 // Drive b into its C — a's shell is never told to run anything, so a 1156 // Drive b into its C — a's shell is never told to run anything, so a
1291 // stays at its idle default and the two sessions read differently. 1157 // stays at its idle default and the two sessions read differently.
1292 try proto.writeFrame(b.handle, .input, "go\n"); 1158 try proto.writeFrame(b.handle, .input, "go\n");
1293 const push = (try awaitFrame(alloc, &srv, b.handle, .cmd_state, 500)) orelse 1159 const push = (try awaitFrame(alloc, &td.srv, b.handle, .cmd_state, 500)) orelse
1294 return error.NoRunningPush; 1160 return error.NoRunningPush;
1295 push.deinit(alloc); 1161 push.deinit(alloc);
1296 1162
1297 // A bare connection — an observer, never attached — asks about b by 1163 // A bare connection — an observer, never attached — asks about b by
1298 // name alone. 1164 // name alone.
1299 const obs = try dial.dial(sock_path); 1165 const obs = try dial.dial(td.sock_path);
1300 defer obs.close(); 1166 defer obs.close();
1301 try proto.writeFrame(obs.handle, .status_req, "b"); 1167 try proto.writeFrame(obs.handle, .status_req, "b");
1302 const reply = (try awaitFrame(alloc, &srv, obs.handle, .status_reply, 400)) orelse 1168 const reply = (try awaitFrame(alloc, &td.srv, obs.handle, .status_reply, 400)) orelse
1303 return error.NoStatusReply; 1169 return error.NoStatusReply;
1304 defer reply.deinit(alloc); 1170 defer reply.deinit(alloc);
1305 const st = try proto.decodeStatusReply(reply.payload); 1171 const st = try proto.decodeStatusReply(reply.payload);
@@ -1310,7 +1176,7 @@ test "Server: an observer's status_req names a session by tail" {
1310 // for words — but it says `exit_status` 1 FIRST. Silence made 1176 // for words — but it says `exit_status` 1 FIRST. Silence made
1311 // `mux a status --session nosuch` report a dead daemon. 1177 // `mux a status --session nosuch` report a dead daemon.
1312 try proto.writeFrame(obs.handle, .status_req, "z"); 1178 try proto.writeFrame(obs.handle, .status_req, "z");
1313 const refusal = (try awaitFrame(alloc, &srv, obs.handle, .exit_status, 400)) orelse 1179 const refusal = (try awaitFrame(alloc, &td.srv, obs.handle, .exit_status, 400)) orelse
1314 return error.NoRefusalForUnknownSession; 1180 return error.NoRefusalForUnknownSession;
1315 defer refusal.deinit(alloc); 1181 defer refusal.deinit(alloc);
1316 try std.testing.expectEqualSlices(u8, &.{1}, refusal.payload); 1182 try std.testing.expectEqualSlices(u8, &.{1}, refusal.payload);
@@ -1318,7 +1184,7 @@ test "Server: an observer's status_req names a session by tail" {
1318 var saw_eof = false; 1184 var saw_eof = false;
1319 var iters: usize = 0; 1185 var iters: usize = 0;
1320 while (iters < 400 and !saw_eof) : (iters += 1) { 1186 while (iters < 400 and !saw_eof) : (iters += 1) {
1321 try srv.pumpOnce(5); 1187 try td.srv.pumpOnce(5);
1322 var pfd = [_]std.posix.pollfd{ 1188 var pfd = [_]std.posix.pollfd{
1323 .{ .fd = obs.handle, .events = std.posix.POLL.IN, .revents = 0 }, 1189 .{ .fd = obs.handle, .events = std.posix.POLL.IN, .revents = 0 },
1324 }; 1190 };
@@ -1333,29 +1199,24 @@ test "Server: an observer's status_req names a session by tail" {
1333 test "Server: stats names every live session" { 1199 test "Server: stats names every live session" {
1334 const alloc = std.testing.allocator; 1200 const alloc = std.testing.allocator;
1335 1201
1336 var tmp = try TmpDir.make(); 1202 var td = try h.TestDaemon.init(alloc, "statsnames", .{ .shell = "/bin/cat" });
1337 defer tmp.cleanup(); 1203 defer td.deinit();
1338 const sock_path = try std.fmt.allocPrint(alloc, "{s}/statsnames.sock", .{tmp.path()});
1339 defer alloc.free(sock_path);
1340
1341 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
1342 defer srv.deinit();
1343 1204
1344 // The default session (bare attach) plus one named session: two live 1205 // The default session (bare attach) plus one named session: two live
1345 // sessions total, both of which must show up by name. 1206 // sessions total, both of which must show up by name.
1346 const ca = try dial.dialAttach(sock_path, 80, 24); 1207 const ca = try dial.dialAttach(td.sock_path, 80, 24);
1347 defer ca.close(); 1208 defer ca.close();
1348 const cb = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 1209 const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
1349 defer cb.close(); 1210 defer cb.close();
1350 const fa = (try awaitFrame(alloc, &srv, ca.handle, .snapshot, 400)) orelse 1211 const fa = (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400)) orelse
1351 return error.NoSnapshotA; 1212 return error.NoSnapshotA;
1352 fa.deinit(alloc); 1213 fa.deinit(alloc);
1353 const fb = (try awaitFrame(alloc, &srv, cb.handle, .snapshot, 400)) orelse 1214 const fb = (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400)) orelse
1354 return error.NoSnapshotB; 1215 return error.NoSnapshotB;
1355 fb.deinit(alloc); 1216 fb.deinit(alloc);
1356 1217
1357 var buf: [Server.stats_text_len]u8 = undefined; 1218 var buf: [Server.stats_text_len]u8 = undefined;
1358 const text = try srv.statsText(&buf); 1219 const text = try td.srv.statsText(&buf);
1359 try std.testing.expect(std.mem.indexOf(u8, text, "sessions=2") != null); 1220 try std.testing.expect(std.mem.indexOf(u8, text, "sessions=2") != null);
1360 try std.testing.expect(std.mem.indexOf(u8, text, "session " ++ proto.default_session ++ " ") != null); 1221 try std.testing.expect(std.mem.indexOf(u8, text, "session " ++ proto.default_session ++ " ") != null);
1361 try std.testing.expect(std.mem.indexOf(u8, text, "session b ") != null); 1222 try std.testing.expect(std.mem.indexOf(u8, text, "session b ") != null);
@@ -1364,29 +1225,24 @@ test "Server: stats names every live session" {
1364 test "Server: sessions_req answers every live name, whoever asks" { 1225 test "Server: sessions_req answers every live name, whoever asks" {
1365 const alloc = std.testing.allocator; 1226 const alloc = std.testing.allocator;
1366 1227
1367 var tmp = try TmpDir.make(); 1228 var td = try h.TestDaemon.init(alloc, "sessreq", .{ .shell = "/bin/cat" });
1368 defer tmp.cleanup(); 1229 defer td.deinit();
1369 const sock_path = try std.fmt.allocPrint(alloc, "{s}/sessreq.sock", .{tmp.path()});
1370 defer alloc.free(sock_path);
1371
1372 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
1373 defer srv.deinit();
1374 1230
1375 const ca = try dial.dialAttach(sock_path, 80, 24); 1231 const ca = try dial.dialAttach(td.sock_path, 80, 24);
1376 defer ca.close(); 1232 defer ca.close();
1377 const cb = try dial.dialAttachNamed(sock_path, 80, 24, "work"); 1233 const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "work");
1378 defer cb.close(); 1234 defer cb.close();
1379 const fa = (try awaitFrame(alloc, &srv, ca.handle, .snapshot, 400)) orelse 1235 const fa = (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400)) orelse
1380 return error.NoSnapshotA; 1236 return error.NoSnapshotA;
1381 fa.deinit(alloc); 1237 fa.deinit(alloc);
1382 const fb = (try awaitFrame(alloc, &srv, cb.handle, .snapshot, 400)) orelse 1238 const fb = (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400)) orelse
1383 return error.NoSnapshotB; 1239 return error.NoSnapshotB;
1384 fb.deinit(alloc); 1240 fb.deinit(alloc);
1385 1241
1386 // Asked from the DEFAULT session's connection, and the answer still 1242 // Asked from the DEFAULT session's connection, and the answer still
1387 // names both: the reply is about the daemon, not about the asker. 1243 // names both: the reply is about the daemon, not about the asker.
1388 try proto.writeFrame(ca.handle, .sessions_req, ""); 1244 try proto.writeFrame(ca.handle, .sessions_req, "");
1389 const reply = (try awaitFrame(alloc, &srv, ca.handle, .sessions_reply, 400)) orelse 1245 const reply = (try awaitFrame(alloc, &td.srv, ca.handle, .sessions_reply, 400)) orelse
1390 return error.NoSessionsReply; 1246 return error.NoSessionsReply;
1391 defer reply.deinit(alloc); 1247 defer reply.deinit(alloc);
1392 try std.testing.expectEqualStrings(proto.default_session ++ "\nwork", reply.payload); 1248 try std.testing.expectEqualStrings(proto.default_session ++ "\nwork", reply.payload);
@@ -1395,22 +1251,17 @@ test "Server: sessions_req answers every live name, whoever asks" {
1395 test "Server: an attached client's mismatched status tail is ignored" { 1251 test "Server: an attached client's mismatched status tail is ignored" {
1396 const alloc = std.testing.allocator; 1252 const alloc = std.testing.allocator;
1397 1253
1398 var tmp = try TmpDir.make(); 1254 var td = try h.TestDaemon.init(alloc, "mismatch", .{ .shell = "/bin/cat" });
1399 defer tmp.cleanup(); 1255 defer td.deinit();
1400 const sock_path = try std.fmt.allocPrint(alloc, "{s}/mismatch.sock", .{tmp.path()});
1401 defer alloc.free(sock_path);
1402
1403 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
1404 defer srv.deinit();
1405 1256
1406 const ca = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 1257 const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
1407 defer ca.close(); 1258 defer ca.close();
1408 const cb = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 1259 const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
1409 defer cb.close(); 1260 defer cb.close();
1410 const fa = (try awaitFrame(alloc, &srv, ca.handle, .snapshot, 400)) orelse 1261 const fa = (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400)) orelse
1411 return error.NoSnapshotA; 1262 return error.NoSnapshotA;
1412 fa.deinit(alloc); 1263 fa.deinit(alloc);
1413 const fb = (try awaitFrame(alloc, &srv, cb.handle, .snapshot, 400)) orelse 1264 const fb = (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400)) orelse
1414 return error.NoSnapshotB; 1265 return error.NoSnapshotB;
1415 fb.deinit(alloc); 1266 fb.deinit(alloc);
1416 1267
@@ -1418,7 +1269,7 @@ test "Server: an attached client's mismatched status tail is ignored" {
1418 // attached to gets silence, never an answer about the tail instead: 1269 // attached to gets silence, never an answer about the tail instead:
1419 // bounded negative probe, since the absence is what is under test. 1270 // bounded negative probe, since the absence is what is under test.
1420 try proto.writeFrame(ca.handle, .status_req, "b"); 1271 try proto.writeFrame(ca.handle, .status_req, "b");
1421 if (try awaitFrame(alloc, &srv, ca.handle, .status_reply, 30)) |leak| { 1272 if (try awaitFrame(alloc, &td.srv, ca.handle, .status_reply, 30)) |leak| {
1422 leak.deinit(alloc); 1273 leak.deinit(alloc);
1423 return error.MismatchedTailAnswered; 1274 return error.MismatchedTailAnswered;
1424 } 1275 }
@@ -1426,7 +1277,7 @@ test "Server: an attached client's mismatched status tail is ignored" {
1426 // The empty tail is the "no opinion" spelling and always matches: the 1277 // The empty tail is the "no opinion" spelling and always matches: the
1427 // reply arrives, and it is a's own state, not b's. 1278 // reply arrives, and it is a's own state, not b's.
1428 try proto.writeFrame(ca.handle, .status_req, ""); 1279 try proto.writeFrame(ca.handle, .status_req, "");
1429 const ok = (try awaitFrame(alloc, &srv, ca.handle, .status_reply, 400)) orelse 1280 const ok = (try awaitFrame(alloc, &td.srv, ca.handle, .status_reply, 400)) orelse
1430 return error.NoReplyForEmptyTail; 1281 return error.NoReplyForEmptyTail;
1431 ok.deinit(alloc); 1282 ok.deinit(alloc);
1432 } 1283 }
@@ -1434,19 +1285,13 @@ test "Server: an attached client's mismatched status tail is ignored" {
1434 test "Server: a session-less slot's status_req resolves the tail like an observer's" { 1285 test "Server: a session-less slot's status_req resolves the tail like an observer's" {
1435 const alloc = std.testing.allocator; 1286 const alloc = std.testing.allocator;
1436 1287
1437 var tmp = try TmpDir.make(); 1288 var td = try h.TestDaemon.init(alloc, "quicstatus", .{ .shell = "/bin/cat" });
1438 defer tmp.cleanup(); 1289 defer td.deinit();
1439 const dir_path = tmp.path();
1440 const sock_path = try std.fmt.allocPrint(alloc, "{s}/quicstatus.sock", .{dir_path});
1441 defer alloc.free(sock_path);
1442
1443 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
1444 defer srv.deinit();
1445 1290
1446 // Stand up session "b" for real, over an ordinary attach. 1291 // Stand up session "b" for real, over an ordinary attach.
1447 const cb = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 1292 const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
1448 defer cb.close(); 1293 defer cb.close();
1449 const fb = (try awaitFrame(alloc, &srv, cb.handle, .snapshot, 400)) orelse 1294 const fb = (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400)) orelse
1450 return error.NoSnapshotB; 1295 return error.NoSnapshotB;
1451 fb.deinit(alloc); 1296 fb.deinit(alloc);
1452 1297
@@ -1457,12 +1302,12 @@ test "Server: a session-less slot's status_req resolves the tail like an observe
1457 // and stomping it would leak its queued snapshot. 1302 // and stomping it would leak its queued snapshot.
1458 const c = try connectedPair(); 1303 const c = try connectedPair();
1459 defer std.posix.close(c.peer); 1304 defer std.posix.close(c.peer);
1460 srv.clients[1] = .{ .sink = .{ .socket = c.daemon } }; 1305 td.srv.clients[1] = .{ .sink = .{ .socket = c.daemon } };
1461 1306
1462 var frame: std.ArrayList(u8) = .empty; 1307 var frame: std.ArrayList(u8) = .empty;
1463 defer frame.deinit(alloc); 1308 defer frame.deinit(alloc);
1464 try proto.appendFrame(&frame, alloc, .status_req, "b"); 1309 try proto.appendFrame(&frame, alloc, .status_req, "b");
1465 srv.pushInbound(1, frame.items); 1310 td.srv.pushInbound(1, frame.items);
1466 1311
1467 // queueFrame flushes synchronously (see its doc comment), so the reply 1312 // queueFrame flushes synchronously (see its doc comment), so the reply
1468 // is already on the wire — no pump needed, same as the other 1313 // is already on the wire — no pump needed, same as the other
@@ -1480,8 +1325,8 @@ test "Server: a session-less slot's status_req resolves the tail like an observe
1480 _ = try proto.decodeStatusReply(reply.payload); 1325 _ = try proto.decodeStatusReply(reply.payload);
1481 1326
1482 // A one-shot ask, not an attach: the slot still holds no session. 1327 // A one-shot ask, not an attach: the slot still holds no session.
1483 try std.testing.expect(srv.clients[1] != null); 1328 try std.testing.expect(td.srv.clients[1] != null);
1484 try std.testing.expect(srv.clients[1].?.session == null); 1329 try std.testing.expect(td.srv.clients[1].?.session == null);
1485 1330
1486 // An unknown name refuses with `exit_status` 1 and then drops the CLIENT. The 1331 // An unknown name refuses with `exit_status` 1 and then drops the CLIENT. The
1487 // frame has to PRECEDE the close, or `mux a status --quic --session nosuch` 1332 // frame has to PRECEDE the close, or `mux a status --quic --session nosuch`
@@ -1490,8 +1335,8 @@ test "Server: a session-less slot's status_req resolves the tail like an observe
1490 // which a unit test cannot stand up. 1335 // which a unit test cannot stand up.
1491 frame.clearRetainingCapacity(); 1336 frame.clearRetainingCapacity();
1492 try proto.appendFrame(&frame, alloc, .status_req, "zz"); 1337 try proto.appendFrame(&frame, alloc, .status_req, "zz");
1493 srv.pushInbound(1, frame.items); 1338 td.srv.pushInbound(1, frame.items);
1494 try std.testing.expect(srv.clients[1] == null); 1339 try std.testing.expect(td.srv.clients[1] == null);
1495 1340
1496 const refusal = (try proto.readFrame(alloc, c.peer)) orelse 1341 const refusal = (try proto.readFrame(alloc, c.peer)) orelse
1497 return error.NoRefusalForUnknownSession; 1342 return error.NoRefusalForUnknownSession;
@@ -1521,31 +1366,26 @@ fn alive(pid: std.posix.pid_t) bool {
1521 test "Server: end_req with another client attached is refused with the count; forced, both see the exit" { 1366 test "Server: end_req with another client attached is refused with the count; forced, both see the exit" {
1522 const alloc = std.testing.allocator; 1367 const alloc = std.testing.allocator;
1523 1368
1524 var tmp = try TmpDir.make(); 1369 var td = try h.TestDaemon.init(alloc, "endreq", .{ .shell = "/bin/sh" });
1525 defer tmp.cleanup(); 1370 defer td.deinit();
1526 const sock_path = try std.fmt.allocPrint(alloc, "{s}/endreq.sock", .{tmp.path()});
1527 defer alloc.free(sock_path);
1528
1529 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
1530 defer srv.deinit();
1531 1371
1532 const a1 = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 1372 const a1 = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
1533 defer a1.close(); 1373 defer a1.close();
1534 (try awaitFrame(alloc, &srv, a1.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); 1374 (try awaitFrame(alloc, &td.srv, a1.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
1535 const a2 = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 1375 const a2 = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
1536 defer a2.close(); 1376 defer a2.close();
1537 (try awaitFrame(alloc, &srv, a2.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); 1377 (try awaitFrame(alloc, &td.srv, a2.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
1538 const b1 = try dial.dialAttachNamed(sock_path, 80, 24, "b"); 1378 const b1 = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
1539 defer b1.close(); 1379 defer b1.close();
1540 (try awaitFrame(alloc, &srv, b1.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); 1380 (try awaitFrame(alloc, &td.srv, b1.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
1541 const pid_a = shellPidOf(&srv, "a"); 1381 const pid_a = shellPidOf(&td.srv, "a");
1542 1382
1543 // an observer asks: every attached client is "other" 1383 // an observer asks: every attached client is "other"
1544 const obs = try dial.dial(sock_path); 1384 const obs = try dial.dial(td.sock_path);
1545 defer obs.close(); 1385 defer obs.close();
1546 var rq: [proto.end_req_max_len]u8 = undefined; 1386 var rq: [proto.end_req_max_len]u8 = undefined;
1547 try proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, false, "a")); 1387 try proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, false, "a"));
1548 const r1 = (try awaitFrame(alloc, &srv, obs.handle, .end_reply, 200)) orelse return error.NoEndReply; 1388 const r1 = (try awaitFrame(alloc, &td.srv, obs.handle, .end_reply, 200)) orelse return error.NoEndReply;
1549 defer r1.deinit(alloc); 1389 defer r1.deinit(alloc);
1550 const v1 = proto.parseEndReply(r1.payload) orelse return error.BadEndReply; 1390 const v1 = proto.parseEndReply(r1.payload) orelse return error.BadEndReply;
1551 try std.testing.expect(!v1.accepted); 1391 try std.testing.expect(!v1.accepted);
@@ -1554,59 +1394,54 @@ test "Server: end_req with another client attached is refused with the count; fo
1554 1394
1555 // a1 asks over its own link: it is not its own "other" 1395 // a1 asks over its own link: it is not its own "other"
1556 try proto.writeFrame(a1.handle, .end_req, proto.encodeEndReq(&rq, false, "a")); 1396 try proto.writeFrame(a1.handle, .end_req, proto.encodeEndReq(&rq, false, "a"));
1557 const r2 = (try awaitFrame(alloc, &srv, a1.handle, .end_reply, 200)) orelse return error.NoEndReply; 1397 const r2 = (try awaitFrame(alloc, &td.srv, a1.handle, .end_reply, 200)) orelse return error.NoEndReply;
1558 defer r2.deinit(alloc); 1398 defer r2.deinit(alloc);
1559 try std.testing.expectEqual(@as(u8, 1), (proto.parseEndReply(r2.payload) orelse return error.BadEndReply).others); 1399 try std.testing.expectEqual(@as(u8, 1), (proto.parseEndReply(r2.payload) orelse return error.BadEndReply).others);
1560 try std.testing.expect(alive(pid_a)); 1400 try std.testing.expect(alive(pid_a));
1561 1401
1562 // forced: accepted, the shell dies, BOTH clients on "a" get exit_status, "b" is untouched 1402 // forced: accepted, the shell dies, BOTH clients on "a" get exit_status, "b" is untouched
1563 try proto.writeFrame(a1.handle, .end_req, proto.encodeEndReq(&rq, true, "a")); 1403 try proto.writeFrame(a1.handle, .end_req, proto.encodeEndReq(&rq, true, "a"));
1564 const r3 = (try awaitFrame(alloc, &srv, a1.handle, .end_reply, 200)) orelse return error.NoEndReply; 1404 const r3 = (try awaitFrame(alloc, &td.srv, a1.handle, .end_reply, 200)) orelse return error.NoEndReply;
1565 defer r3.deinit(alloc); 1405 defer r3.deinit(alloc);
1566 try std.testing.expect((proto.parseEndReply(r3.payload) orelse return error.BadEndReply).accepted); 1406 try std.testing.expect((proto.parseEndReply(r3.payload) orelse return error.BadEndReply).accepted);
1567 const x2 = (try awaitFrame(alloc, &srv, a2.handle, .exit_status, 400)) orelse return error.NoExitOnSibling; 1407 const x2 = (try awaitFrame(alloc, &td.srv, a2.handle, .exit_status, 400)) orelse return error.NoExitOnSibling;
1568 x2.deinit(alloc); 1408 x2.deinit(alloc);
1569 var waited: u32 = 0; 1409 var waited: u32 = 0;
1570 while (alive(pid_a) and waited < 3000) : (waited += 50) { 1410 while (alive(pid_a) and waited < 3000) : (waited += 50) {
1571 try srv.pumpOnce(20); 1411 try td.srv.pumpOnce(20);
1572 std.Thread.sleep(30 * std.time.ns_per_ms); 1412 std.Thread.sleep(30 * std.time.ns_per_ms);
1573 } 1413 }
1574 try std.testing.expect(!alive(pid_a)); 1414 try std.testing.expect(!alive(pid_a));
1575 try std.testing.expect(alive(shellPidOf(&srv, "b"))); 1415 try std.testing.expect(alive(shellPidOf(&td.srv, "b")));
1576 } 1416 }
1577 1417
1578 test "Server: end_req alone on a session ends it at once, and an unknown name is refused" { 1418 test "Server: end_req alone on a session ends it at once, and an unknown name is refused" {
1579 const alloc = std.testing.allocator; 1419 const alloc = std.testing.allocator;
1580 1420
1581 var tmp = try TmpDir.make(); 1421 var td = try h.TestDaemon.init(alloc, "endsolo", .{ .shell = "/bin/sh" });
1582 defer tmp.cleanup(); 1422 defer td.deinit();
1583 const sock_path = try std.fmt.allocPrint(alloc, "{s}/endsolo.sock", .{tmp.path()});
1584 defer alloc.free(sock_path);
1585 1423
1586 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 1424 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "solo");
1587 defer srv.deinit();
1588
1589 const c = try dial.dialAttachNamed(sock_path, 80, 24, "solo");
1590 defer c.close(); 1425 defer c.close();
1591 (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); 1426 (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
1592 1427
1593 const pid = shellPidOf(&srv, "solo"); 1428 const pid = shellPidOf(&td.srv, "solo");
1594 var rq: [proto.end_req_max_len]u8 = undefined; 1429 var rq: [proto.end_req_max_len]u8 = undefined;
1595 try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "solo")); 1430 try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "solo"));
1596 const r = (try awaitFrame(alloc, &srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply; 1431 const r = (try awaitFrame(alloc, &td.srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply;
1597 defer r.deinit(alloc); 1432 defer r.deinit(alloc);
1598 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted); 1433 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);
1599 var waited: u32 = 0; 1434 var waited: u32 = 0;
1600 while (alive(pid) and waited < 3000) : (waited += 50) { 1435 while (alive(pid) and waited < 3000) : (waited += 50) {
1601 try srv.pumpOnce(20); 1436 try td.srv.pumpOnce(20);
1602 std.Thread.sleep(30 * std.time.ns_per_ms); 1437 std.Thread.sleep(30 * std.time.ns_per_ms);
1603 } 1438 }
1604 try std.testing.expect(!alive(pid)); 1439 try std.testing.expect(!alive(pid));
1605 1440
1606 const obs = try dial.dial(sock_path); 1441 const obs = try dial.dial(td.sock_path);
1607 defer obs.close(); 1442 defer obs.close();
1608 try proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, true, "nosuch")); 1443 try proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, true, "nosuch"));
1609 const r2 = (try awaitFrame(alloc, &srv, obs.handle, .end_reply, 200)) orelse return error.NoEndReply; 1444 const r2 = (try awaitFrame(alloc, &td.srv, obs.handle, .end_reply, 200)) orelse return error.NoEndReply;
1610 defer r2.deinit(alloc); 1445 defer r2.deinit(alloc);
1611 const v = proto.parseEndReply(r2.payload) orelse return error.BadEndReply; 1446 const v = proto.parseEndReply(r2.payload) orelse return error.BadEndReply;
1612 try std.testing.expect(!v.accepted); 1447 try std.testing.expect(!v.accepted);
@@ -1616,25 +1451,20 @@ test "Server: end_req alone on a session ends it at once, and an unknown name is
1616 test "Server: an observer's sessions_req is answered with every live name" { 1451 test "Server: an observer's sessions_req is answered with every live name" {
1617 const alloc = std.testing.allocator; 1452 const alloc = std.testing.allocator;
1618 1453
1619 var tmp = try TmpDir.make(); 1454 var td = try h.TestDaemon.init(alloc, "obssess", .{ .shell = "/bin/sh" });
1620 defer tmp.cleanup(); 1455 defer td.deinit();
1621 const sock_path = try std.fmt.allocPrint(alloc, "{s}/obssess.sock", .{tmp.path()});
1622 defer alloc.free(sock_path);
1623 1456
1624 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" }); 1457 const cx = try dial.dialAttachNamed(td.sock_path, 80, 24, "x");
1625 defer srv.deinit();
1626
1627 const cx = try dial.dialAttachNamed(sock_path, 80, 24, "x");
1628 defer cx.close(); 1458 defer cx.close();
1629 (try awaitFrame(alloc, &srv, cx.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); 1459 (try awaitFrame(alloc, &td.srv, cx.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
1630 const cy = try dial.dialAttachNamed(sock_path, 80, 24, "y"); 1460 const cy = try dial.dialAttachNamed(td.sock_path, 80, 24, "y");
1631 defer cy.close(); 1461 defer cy.close();
1632 (try awaitFrame(alloc, &srv, cy.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); 1462 (try awaitFrame(alloc, &td.srv, cy.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
1633 1463
1634 const obs = try dial.dial(sock_path); 1464 const obs = try dial.dial(td.sock_path);
1635 defer obs.close(); 1465 defer obs.close();
1636 try proto.writeFrame(obs.handle, .sessions_req, ""); 1466 try proto.writeFrame(obs.handle, .sessions_req, "");
1637 const r = (try awaitFrame(alloc, &srv, obs.handle, .sessions_reply, 200)) orelse return error.NoSessionsReply; 1467 const r = (try awaitFrame(alloc, &td.srv, obs.handle, .sessions_reply, 200)) orelse return error.NoSessionsReply;
1638 defer r.deinit(alloc); 1468 defer r.deinit(alloc);
1639 // The whole payload, not two substrings: `"xy"`, a truncation after two 1469 // The whole payload, not two substrings: `"xy"`, a truncation after two
1640 // names, and a name that aliased its neighbour's buffer all satisfy an 1470 // names, and a name that aliased its neighbour's buffer all satisfy an
@@ -1665,35 +1495,30 @@ fn awaitGridIn(
1665 test "Server: an accepted end kills a shell that ignores TERM and HUP" { 1495 test "Server: an accepted end kills a shell that ignores TERM and HUP" {
1666 const alloc = std.testing.allocator; 1496 const alloc = std.testing.allocator;
1667 1497
1668 var tmp = try TmpDir.make(); 1498 var td = try h.TestDaemon.init(alloc, "endkill", .{ .shell = "/bin/sh" });
1669 defer tmp.cleanup(); 1499 defer td.deinit();
1670 const sock_path = try std.fmt.allocPrint(alloc, "{s}/endkill.sock", .{tmp.path()});
1671 defer alloc.free(sock_path);
1672
1673 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
1674 defer srv.deinit();
1675 1500
1676 const c = try dial.dialAttachNamed(sock_path, 80, 24, "stubborn"); 1501 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "stubborn");
1677 defer c.close(); 1502 defer c.close();
1678 (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); 1503 (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
1679 1504
1680 // The shell ignores both signals `requestExit` has, and never reads stdin 1505 // The shell ignores both signals `requestExit` has, and never reads stdin
1681 // again — so closing the master cannot end it either. TRAP"PED" is typed 1506 // again — so closing the master cannot end it either. TRAP"PED" is typed
1682 // and TRAPPED is printed, so the needle can only be the shell's output 1507 // and TRAPPED is printed, so the needle can only be the shell's output
1683 // and never the echo of the line that asked for it. 1508 // and never the echo of the line that asked for it.
1684 try proto.writeFrame(c.handle, .input, "trap '' TERM HUP; echo TRAP\"PED\"; while :; do sleep 1; done\n"); 1509 try proto.writeFrame(c.handle, .input, "trap '' TERM HUP; echo TRAP\"PED\"; while :; do sleep 1; done\n");
1685 try std.testing.expect(try awaitGridIn(alloc, &srv, "stubborn", "TRAPPED", 5000)); 1510 try std.testing.expect(try awaitGridIn(alloc, &td.srv, "stubborn", "TRAPPED", 5000));
1686 1511
1687 const pid = shellPidOf(&srv, "stubborn"); 1512 const pid = shellPidOf(&td.srv, "stubborn");
1688 var rq: [proto.end_req_max_len]u8 = undefined; 1513 var rq: [proto.end_req_max_len]u8 = undefined;
1689 try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "stubborn")); 1514 try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "stubborn"));
1690 const r = (try awaitFrame(alloc, &srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply; 1515 const r = (try awaitFrame(alloc, &td.srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply;
1691 defer r.deinit(alloc); 1516 defer r.deinit(alloc);
1692 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted); 1517 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);
1693 1518
1694 var waited: u32 = 0; 1519 var waited: u32 = 0;
1695 while (alive(pid) and waited < 3000) : (waited += 50) { 1520 while (alive(pid) and waited < 3000) : (waited += 50) {
1696 try srv.pumpOnce(20); 1521 try td.srv.pumpOnce(20);
1697 std.Thread.sleep(30 * std.time.ns_per_ms); 1522 std.Thread.sleep(30 * std.time.ns_per_ms);
1698 } 1523 }
1699 try std.testing.expect(!alive(pid)); 1524 try std.testing.expect(!alive(pid));
@@ -1702,23 +1527,20 @@ test "Server: an accepted end kills a shell that ignores TERM and HUP" {
1702 test "Server: a keystroke into an ending session does not cost that client its exit_status" { 1527 test "Server: a keystroke into an ending session does not cost that client its exit_status" {
1703 const alloc = std.testing.allocator; 1528 const alloc = std.testing.allocator;
1704 1529
1705 var tmp = try TmpDir.make(); 1530 var td = try h.TestDaemon.open(alloc, "endtype");
1706 defer tmp.cleanup(); 1531 defer td.deinit();
1707 const sock_path = try std.fmt.allocPrint(alloc, "{s}/endtype.sock", .{tmp.path()}); 1532 const script = try h.writeStubbornShell(alloc, &td.tmp);
1708 defer alloc.free(sock_path);
1709 const script = try h.writeStubbornShell(alloc, &tmp);
1710 defer alloc.free(script); 1533 defer alloc.free(script);
1711 1534
1712 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 1535 try td.start(.{ .shell = script });
1713 defer srv.deinit();
1714 1536
1715 const c = try dial.dialAttachNamed(sock_path, 80, 24, "typing"); 1537 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "typing");
1716 defer c.close(); 1538 defer c.close();
1717 (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); 1539 (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
1718 1540
1719 var rq: [proto.end_req_max_len]u8 = undefined; 1541 var rq: [proto.end_req_max_len]u8 = undefined;
1720 try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "typing")); 1542 try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "typing"));
1721 const r = (try awaitFrame(alloc, &srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply; 1543 const r = (try awaitFrame(alloc, &td.srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply;
1722 defer r.deinit(alloc); 1544 defer r.deinit(alloc);
1723 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted); 1545 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);
1724 1546
@@ -1726,42 +1548,39 @@ test "Server: a keystroke into an ending session does not cost that client its e
1726 // reaches a session with nowhere to put it. Dropping the client for it 1548 // reaches a session with nowhere to put it. Dropping the client for it
1727 // would trade the exit code for "connection to the daemon lost". 1549 // would trade the exit code for "connection to the daemon lost".
1728 try proto.writeFrame(c.handle, .input, "x"); 1550 try proto.writeFrame(c.handle, .input, "x");
1729 const x = (try awaitFrame(alloc, &srv, c.handle, .exit_status, 600)) orelse return error.NoExitStatus; 1551 const x = (try awaitFrame(alloc, &td.srv, c.handle, .exit_status, 600)) orelse return error.NoExitStatus;
1730 x.deinit(alloc); 1552 x.deinit(alloc);
1731 } 1553 }
1732 1554
1733 test "Server: a repeated end_req does not push the SIGKILL deadline out" { 1555 test "Server: a repeated end_req does not push the SIGKILL deadline out" {
1734 const alloc = std.testing.allocator; 1556 const alloc = std.testing.allocator;
1735 1557
1736 var tmp = try TmpDir.make(); 1558 var td = try h.TestDaemon.open(alloc, "endagain");
1737 defer tmp.cleanup(); 1559 defer td.deinit();
1738 const sock_path = try std.fmt.allocPrint(alloc, "{s}/endagain.sock", .{tmp.path()}); 1560 const script = try h.writeStubbornShell(alloc, &td.tmp);
1739 defer alloc.free(sock_path);
1740 const script = try h.writeStubbornShell(alloc, &tmp);
1741 defer alloc.free(script); 1561 defer alloc.free(script);
1742 1562
1743 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); 1563 try td.start(.{ .shell = script });
1744 defer srv.deinit();
1745 1564
1746 const c = try dial.dialAttachNamed(sock_path, 80, 24, "nag"); 1565 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "nag");
1747 defer c.close(); 1566 defer c.close();
1748 (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); 1567 (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
1749 1568
1750 const pid = shellPidOf(&srv, "nag"); 1569 const pid = shellPidOf(&td.srv, "nag");
1751 var rq: [proto.end_req_max_len]u8 = undefined; 1570 var rq: [proto.end_req_max_len]u8 = undefined;
1752 try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "nag")); 1571 try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "nag"));
1753 const r = (try awaitFrame(alloc, &srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply; 1572 const r = (try awaitFrame(alloc, &td.srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply;
1754 defer r.deinit(alloc); 1573 defer r.deinit(alloc);
1755 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted); 1574 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);
1756 1575
1757 // An observer nagging faster than the grace: if each accept restarted the 1576 // An observer nagging faster than the grace: if each accept restarted the
1758 // clock, the shell would outlive every deadline it was ever given. 1577 // clock, the shell would outlive every deadline it was ever given.
1759 const obs = try dial.dial(sock_path); 1578 const obs = try dial.dial(td.sock_path);
1760 defer obs.close(); 1579 defer obs.close();
1761 var waited: u32 = 0; 1580 var waited: u32 = 0;
1762 while (alive(pid) and waited < 3000) : (waited += 80) { 1581 while (alive(pid) and waited < 3000) : (waited += 80) {
1763 proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, true, "nag")) catch {}; 1582 proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, true, "nag")) catch {};
1764 try srv.pumpOnce(20); 1583 try td.srv.pumpOnce(20);
1765 std.Thread.sleep(60 * std.time.ns_per_ms); 1584 std.Thread.sleep(60 * std.time.ns_per_ms);
1766 } 1585 }
1767 try std.testing.expect(!alive(pid)); 1586 try std.testing.expect(!alive(pid));
src/server/server_test_upgrade.zig
Old New
@@ -12,24 +12,19 @@ const awaitFrame = h.awaitFrame;
12 test "writeManifestTo: every session's own name crosses, not the last one's" { 12 test "writeManifestTo: every session's own name crosses, not the last one's" {
13 const alloc = std.testing.allocator; 13 const alloc = std.testing.allocator;
14 14
15 var tmp = try TmpDir.make(); 15 var td = try h.TestDaemon.init(alloc, "names", .{ .shell = "/bin/sh" });
16 defer tmp.cleanup(); 16 defer td.deinit();
17 const sock_path = try std.fmt.allocPrint(alloc, "{s}/names.sock", .{tmp.path()});
18 defer alloc.free(sock_path);
19
20 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
21 defer srv.deinit();
22 17
23 // Three sessions, each with a name of its own. One session cannot catch 18 // Three sessions, each with a name of its own. One session cannot catch
24 // a name that aliases its neighbour's buffer, which is how a by-value 19 // a name that aliases its neighbour's buffer, which is how a by-value
25 // `Session` copy in the gather loop shipped every record pointing at 20 // `Session` copy in the gather loop shipped every record pointing at
26 // the last name: right length, wrong bytes. 21 // the last name: right length, wrong bytes.
27 srv.sessions.table[1] = try SessionTable.create(alloc, srv.spawn_plan, "work", 80, 24, null); 22 td.srv.sessions.table[1] = try SessionTable.create(alloc, td.srv.spawn_plan, "work", 80, 24, null);
28 srv.sessions.table[2] = try SessionTable.create(alloc, srv.spawn_plan, "logs", 80, 24, null); 23 td.srv.sessions.table[2] = try SessionTable.create(alloc, td.srv.spawn_plan, "logs", 80, 24, null);
29 24
30 const memfd = try std.posix.memfd_create("mux-names-test", 0); 25 const memfd = try std.posix.memfd_create("mux-names-test", 0);
31 defer std.posix.close(memfd); 26 defer std.posix.close(memfd);
32 try srv.writeManifestTo(memfd, "0.0.1-99"); 27 try td.srv.writeManifestTo(memfd, "0.0.1-99");
33 28
34 var file = std.fs.File{ .handle = memfd }; 29 var file = std.fs.File{ .handle = memfd };
35 try file.seekTo(0); 30 try file.seekTo(0);
@@ -48,21 +43,14 @@ test "writeManifestTo: every session's own name crosses, not the last one's" {
48 test "writeManifestTo: what crosses is what a session cannot rebuild" { 43 test "writeManifestTo: what crosses is what a session cannot rebuild" {
49 const alloc = std.testing.allocator; 44 const alloc = std.testing.allocator;
50 45
51 var tmp = try TmpDir.make(); 46 var td = try h.TestDaemon.init(alloc, "manif", .{
52 defer tmp.cleanup();
53 const dir_path = tmp.path();
54 const sock_path = try std.fmt.allocPrint(alloc, "{s}/manif.sock", .{dir_path});
55 defer alloc.free(sock_path);
56
57 var srv = try Server.init(alloc, .{
58 .sock_path = sock_path,
59 .shell = "/bin/sh", 47 .shell = "/bin/sh",
60 .shell_integration = true, 48 .shell_integration = true,
61 .extra_env = &.{.{ .key = "PS1", .value = ">>" }}, 49 .extra_env = &.{.{ .key = "PS1", .value = ">>" }},
62 }); 50 });
63 defer srv.deinit(); 51 defer td.deinit();
64 52
65 const s = &srv.sessions.table[0].?; 53 const s = &td.srv.sessions.table[0].?;
66 // Feed the engine a title so the manifest must carry it. 54 // Feed the engine a title so the manifest must carry it.
67 s.eng.feed("\x1b]0;t1\x07"); 55 s.eng.feed("\x1b]0;t1\x07");
68 // Feed some content so dumpState is non-empty. 56 // Feed some content so dumpState is non-empty.
@@ -71,7 +59,7 @@ test "writeManifestTo: what crosses is what a session cannot rebuild" {
71 const memfd = try std.posix.memfd_create("mux-upgrade-test", 0); 59 const memfd = try std.posix.memfd_create("mux-upgrade-test", 0);
72 defer std.posix.close(memfd); 60 defer std.posix.close(memfd);
73 61
74 try srv.writeManifestTo(memfd, "0.0.1-99"); 62 try td.srv.writeManifestTo(memfd, "0.0.1-99");
75 63
76 // Read back and parse. 64 // Read back and parse.
77 var file = std.fs.File{ .handle = memfd }; 65 var file = std.fs.File{ .handle = memfd };
@@ -83,7 +71,7 @@ test "writeManifestTo: what crosses is what a session cannot rebuild" {
83 defer parsed.deinit(); 71 defer parsed.deinit();
84 72
85 // Daemon section: sock_path and writer identity cross. 73 // Daemon section: sock_path and writer identity cross.
86 try std.testing.expectEqualStrings(sock_path, parsed.daemon.sock_path); 74 try std.testing.expectEqualStrings(td.sock_path, parsed.daemon.sock_path);
87 try std.testing.expectEqualStrings("0.0.1-99", parsed.daemon.writer_version); 75 try std.testing.expectEqualStrings("0.0.1-99", parsed.daemon.writer_version);
88 // The rollback target must be THIS process's binary — resolved inside 76 // The rollback target must be THIS process's binary — resolved inside
89 // writeManifestTo, never taken from a caller, because the first draft 77 // writeManifestTo, never taken from a caller, because the first draft
@@ -243,16 +231,10 @@ test "initFromManifest: the return watermark is re-stamped, never carried across
243 test "validateUpgrade: same version without the flag names both versions" { 231 test "validateUpgrade: same version without the flag names both versions" {
244 const alloc = std.testing.allocator; 232 const alloc = std.testing.allocator;
245 233
246 var tmp = try TmpDir.make(); 234 var td = try h.TestDaemon.init(alloc, "val1", .{ .shell = "/bin/sh" });
247 defer tmp.cleanup(); 235 defer td.deinit();
248 const dir_path = tmp.path();
249 const sock_path = try std.fmt.allocPrint(alloc, "{s}/val1.sock", .{dir_path});
250 defer alloc.free(sock_path);
251
252 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
253 defer srv.deinit();
254 236
255 const reason = srv.validateUpgrade(.{ 237 const reason = td.srv.validateUpgrade(.{
256 .allow_same_version = false, 238 .allow_same_version = false,
257 .version = "0.0.1-13", 239 .version = "0.0.1-13",
258 .path = "/bin/sh", 240 .path = "/bin/sh",
@@ -265,16 +247,10 @@ test "validateUpgrade: same version without the flag names both versions" {
265 test "validateUpgrade: a relative path is refused" { 247 test "validateUpgrade: a relative path is refused" {
266 const alloc = std.testing.allocator; 248 const alloc = std.testing.allocator;
267 249
268 var tmp = try TmpDir.make(); 250 var td = try h.TestDaemon.init(alloc, "val2", .{ .shell = "/bin/sh" });
269 defer tmp.cleanup(); 251 defer td.deinit();
270 const dir_path = tmp.path();
271 const sock_path = try std.fmt.allocPrint(alloc, "{s}/val2.sock", .{dir_path});
272 defer alloc.free(sock_path);
273
274 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
275 defer srv.deinit();
276 252
277 const reason = srv.validateUpgrade(.{ 253 const reason = td.srv.validateUpgrade(.{
278 .allow_same_version = true, 254 .allow_same_version = true,
279 .version = "0.0.1-14", 255 .version = "0.0.1-14",
280 .path = "relative/mux", 256 .path = "relative/mux",
@@ -286,17 +262,11 @@ test "validateUpgrade: a relative path is refused" {
286 test "validateUpgrade: a non-executable path is refused" { 262 test "validateUpgrade: a non-executable path is refused" {
287 const alloc = std.testing.allocator; 263 const alloc = std.testing.allocator;
288 264
289 var tmp = try TmpDir.make(); 265 var td = try h.TestDaemon.init(alloc, "val3", .{ .shell = "/bin/sh" });
290 defer tmp.cleanup(); 266 defer td.deinit();
291 const dir_path = tmp.path();
292 const sock_path = try std.fmt.allocPrint(alloc, "{s}/val3.sock", .{dir_path});
293 defer alloc.free(sock_path);
294
295 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/sh" });
296 defer srv.deinit();
297 267
298 // /dev/null exists but is not executable. 268 // /dev/null exists but is not executable.
299 const reason = srv.validateUpgrade(.{ 269 const reason = td.srv.validateUpgrade(.{
300 .allow_same_version = true, 270 .allow_same_version = true,
301 .version = "0.0.1-14", 271 .version = "0.0.1-14",
302 .path = "/dev/null", 272 .path = "/dev/null",
@@ -392,32 +362,29 @@ test "sealAdoptedFds: the adopted fds are CLOEXEC again, and not one step before
392 test "Server: an upgrade asked for during a session's hangup is refused, not attempted" { 362 test "Server: an upgrade asked for during a session's hangup is refused, not attempted" {
393 const alloc = std.testing.allocator; 363 const alloc = std.testing.allocator;
394 364
395 var tmp = try TmpDir.make(); 365 var td = try h.TestDaemon.open(alloc, "upending");
396 defer tmp.cleanup(); 366 defer td.deinit();
397 const sock_path = try std.fmt.allocPrint(alloc, "{s}/upending.sock", .{tmp.path()}); 367 const script = try h.writeStubbornShell(alloc, &td.tmp);
398 defer alloc.free(sock_path);
399 const script = try h.writeStubbornShell(alloc, &tmp);
400 defer alloc.free(script); 368 defer alloc.free(script);
401 369
402 // A real version, so every other check would PASS: without the ending 370 // A real version, so every other check would PASS: without the ending
403 // session this upgrade is one the daemon would go through with. 371 // session this upgrade is one the daemon would go through with.
404 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script, .version = "0.0.1-1" }); 372 try td.start(.{ .shell = script, .version = "0.0.1-1" });
405 defer srv.deinit();
406 373
407 // The shell outlives the hangup by the whole grace, so the exec's 374 // The shell outlives the hangup by the whole grace, so the exec's
408 // clearCloexec and the manifest's pty_fd would both see -1. 375 // clearCloexec and the manifest's pty_fd would both see -1.
409 const ender = try dial.dial(sock_path); 376 const ender = try dial.dial(td.sock_path);
410 defer ender.close(); 377 defer ender.close();
411 var rq: [proto.end_req_max_len]u8 = undefined; 378 var rq: [proto.end_req_max_len]u8 = undefined;
412 try proto.writeFrame(ender.handle, .end_req, proto.encodeEndReq(&rq, true, "")); 379 try proto.writeFrame(ender.handle, .end_req, proto.encodeEndReq(&rq, true, ""));
413 const r = (try awaitFrame(alloc, &srv, ender.handle, .end_reply, 200)) orelse return error.NoEndReply; 380 const r = (try awaitFrame(alloc, &td.srv, ender.handle, .end_reply, 200)) orelse return error.NoEndReply;
414 defer r.deinit(alloc); 381 defer r.deinit(alloc);
415 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted); 382 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);
416 383
417 // A candidate that passes every OTHER check, so the refusal can only be 384 // A candidate that passes every OTHER check, so the refusal can only be
418 // the ending session: a wrong version or an unrunnable path would refuse 385 // the ending session: a wrong version or an unrunnable path would refuse
419 // this upgrade whether or not anything was hanging up. 386 // this upgrade whether or not anything was hanging up.
420 try tmp.dir.writeFile(.{ 387 try td.tmp.dir.writeFile(.{
421 .sub_path = "fakemux.sh", 388 .sub_path = "fakemux.sh",
422 .data = 389 .data =
423 \\#!/bin/sh 390 \\#!/bin/sh
@@ -429,10 +396,10 @@ test "Server: an upgrade asked for during a session's hangup is refused, not att
429 , 396 ,
430 .flags = .{ .mode = 0o755 }, 397 .flags = .{ .mode = 0o755 },
431 }); 398 });
432 const cand = try std.fmt.allocPrint(alloc, "{s}/fakemux.sh", .{tmp.path()}); 399 const cand = try std.fmt.allocPrint(alloc, "{s}/fakemux.sh", .{td.tmp.path()});
433 defer alloc.free(cand); 400 defer alloc.free(cand);
434 401
435 const obs = try dial.dial(sock_path); 402 const obs = try dial.dial(td.sock_path);
436 defer obs.close(); 403 defer obs.close();
437 var ureq: [512]u8 = undefined; 404 var ureq: [512]u8 = undefined;
438 try proto.writeFrame(obs.handle, .upgrade_req, try proto.encodeUpgradeReq(&ureq, .{ 405 try proto.writeFrame(obs.handle, .upgrade_req, try proto.encodeUpgradeReq(&ureq, .{
@@ -440,7 +407,7 @@ test "Server: an upgrade asked for during a session's hangup is refused, not att
440 .version = "9.9.9", 407 .version = "9.9.9",
441 .path = cand, 408 .path = cand,
442 })); 409 }));
443 const reply = (try awaitFrame(alloc, &srv, obs.handle, .upgrade_reply, 400)) orelse return error.NoUpgradeReply; 410 const reply = (try awaitFrame(alloc, &td.srv, obs.handle, .upgrade_reply, 400)) orelse return error.NoUpgradeReply;
444 defer reply.deinit(alloc); 411 defer reply.deinit(alloc);
445 try std.testing.expect(reply.payload.len > 1); 412 try std.testing.expect(reply.payload.len > 1);
446 try std.testing.expectEqual(@as(u8, 1), reply.payload[0]); 413 try std.testing.expectEqual(@as(u8, 1), reply.payload[0]);
@@ -545,24 +512,21 @@ test "Server: an EMPTY daemon upgrades — the manifest names no session and the
545 test "Server: an accepted end cancels a pending upgrade — the exec never sees a hung-up master" { 512 test "Server: an accepted end cancels a pending upgrade — the exec never sees a hung-up master" {
546 const alloc = std.testing.allocator; 513 const alloc = std.testing.allocator;
547 514
548 var tmp = try TmpDir.make(); 515 var td = try h.TestDaemon.open(alloc, "upcancel");
549 defer tmp.cleanup(); 516 defer td.deinit();
550 const sock_path = try std.fmt.allocPrint(alloc, "{s}/upcancel.sock", .{tmp.path()}); 517 const script = try h.writeStubbornShell(alloc, &td.tmp);
551 defer alloc.free(sock_path);
552 const script = try h.writeStubbornShell(alloc, &tmp);
553 defer alloc.free(script); 518 defer alloc.free(script);
554 519
555 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script, .version = "0.0.1-1" }); 520 try td.start(.{ .shell = script, .version = "0.0.1-1" });
556 defer srv.deinit();
557 521
558 // Plural: the end takes ONE session and the upgrade would have carried 522 // Plural: the end takes ONE session and the upgrade would have carried
559 // the others, so a guard that only looked at the ending slot's own 523 // the others, so a guard that only looked at the ending slot's own
560 // clients would still pass here. 524 // clients would still pass here.
561 const ca = try dial.dialAttachNamed(sock_path, 80, 24, "a"); 525 const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
562 defer ca.close(); 526 defer ca.close();
563 (try awaitFrame(alloc, &srv, ca.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); 527 (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
564 528
565 try tmp.dir.writeFile(.{ 529 try td.tmp.dir.writeFile(.{
566 .sub_path = "fakemux.sh", 530 .sub_path = "fakemux.sh",
567 .data = 531 .data =
568 \\#!/bin/sh 532 \\#!/bin/sh
@@ -574,18 +538,18 @@ test "Server: an accepted end cancels a pending upgrade — the exec never sees
574 , 538 ,
575 .flags = .{ .mode = 0o755 }, 539 .flags = .{ .mode = 0o755 },
576 }); 540 });
577 const cand = try std.fmt.allocPrint(alloc, "{s}/fakemux.sh", .{tmp.path()}); 541 const cand = try std.fmt.allocPrint(alloc, "{s}/fakemux.sh", .{td.tmp.path()});
578 defer alloc.free(cand); 542 defer alloc.free(cand);
579 543
580 // Two observers, seated in slot order BEFORE either speaks: the hazard 544 // Two observers, seated in slot order BEFORE either speaks: the hazard
581 // is one pump servicing an upgrade at a lower slot and an end at a 545 // is one pump servicing an upgrade at a lower slot and an end at a
582 // higher one, after which `run` execs on a master that is already -1. 546 // higher one, after which `run` execs on a master that is already -1.
583 const up = try dial.dial(sock_path); 547 const up = try dial.dial(td.sock_path);
584 defer up.close(); 548 defer up.close();
585 try srv.pumpOnce(20); 549 try td.srv.pumpOnce(20);
586 const ender = try dial.dial(sock_path); 550 const ender = try dial.dial(td.sock_path);
587 defer ender.close(); 551 defer ender.close();
588 try srv.pumpOnce(20); 552 try td.srv.pumpOnce(20);
589 553
590 var ureq: [512]u8 = undefined; 554 var ureq: [512]u8 = undefined;
591 try proto.writeFrame(up.handle, .upgrade_req, try proto.encodeUpgradeReq(&ureq, .{ 555 try proto.writeFrame(up.handle, .upgrade_req, try proto.encodeUpgradeReq(&ureq, .{
@@ -593,15 +557,15 @@ test "Server: an accepted end cancels a pending upgrade — the exec never sees
593 .version = "9.9.9", 557 .version = "9.9.9",
594 .path = cand, 558 .path = cand,
595 })); 559 }));
596 const reply = (try awaitFrame(alloc, &srv, up.handle, .upgrade_reply, 800)) orelse 560 const reply = (try awaitFrame(alloc, &td.srv, up.handle, .upgrade_reply, 800)) orelse
597 return error.NoUpgradeReply; 561 return error.NoUpgradeReply;
598 defer reply.deinit(alloc); 562 defer reply.deinit(alloc);
599 try std.testing.expectEqual(@as(u8, 0), reply.payload[0]); // accepted 563 try std.testing.expectEqual(@as(u8, 0), reply.payload[0]); // accepted
600 try std.testing.expect(srv.pending_upgrade != null); 564 try std.testing.expect(td.srv.pending_upgrade != null);
601 565
602 var rq: [proto.end_req_max_len]u8 = undefined; 566 var rq: [proto.end_req_max_len]u8 = undefined;
603 try proto.writeFrame(ender.handle, .end_req, proto.encodeEndReq(&rq, true, "a")); 567 try proto.writeFrame(ender.handle, .end_req, proto.encodeEndReq(&rq, true, "a"));
604 const r = (try awaitFrame(alloc, &srv, ender.handle, .end_reply, 400)) orelse 568 const r = (try awaitFrame(alloc, &td.srv, ender.handle, .end_reply, 400)) orelse
605 return error.NoEndReply; 569 return error.NoEndReply;
606 defer r.deinit(alloc); 570 defer r.deinit(alloc);
607 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted); 571 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);
@@ -609,5 +573,5 @@ test "Server: an accepted end cancels a pending upgrade — the exec never sees
609 // The claim. `clearCloexec(-1)` is `unreachable` in the pinned std, so 573 // The claim. `clearCloexec(-1)` is `unreachable` in the pinned std, so
610 // an exec that still ran here would panic the daemon and take every 574 // an exec that still ran here would panic the daemon and take every
611 // session with it — a refusal is what the operator must get instead. 575 // session with it — a refusal is what the operator must get instead.
612 try std.testing.expect(srv.pending_upgrade == null); 576 try std.testing.expect(td.srv.pending_upgrade == null);
613 } 577 }