c2004563
The harness owns a running daemon and a threaded await
a73x 2026-08-31 16:32
Commit message
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 | } |