d51f6359
One round trip asks a Transport a question
a73x 2026-08-31 16:43
Commit message
src/client/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -1183,6 +1183,44 @@ fn awaitFrames(tr: *Transport, deadline: i64) !bool { | |||
| 1183 | return fds[0].revents != 0 or tr.link == .quic; | 1183 | return fds[0].revents != 0 or tr.link == .quic; |
| 1184 | } | 1184 | } |
| 1185 | 1185 | ||
| 1186 | /// One question on a side connection: send `req`, then hand back the first | ||
| 1187 | /// frame whose type is one the caller named, deinit-ing every other frame | ||
| 1188 | /// the daemon says on the way. The frame returned is the caller's to deinit. | ||
| 1189 | /// | ||
| 1190 | /// The errors are thin on purpose — `Timeout` for a budget spent, `Closed` | ||
| 1191 | /// for a peer that hung up before answering, `Transport` for a poll that | ||
| 1192 | /// failed, `OutOfMemory` for this machine's allocator. What one MEANS is | ||
| 1193 | /// the caller's to say: the same close is a refusal to a birth and a box | ||
| 1194 | /// that is down to a poll. | ||
| 1195 | fn roundTrip( | ||
| 1196 | tr: *Transport, | ||
| 1197 | alloc: std.mem.Allocator, | ||
| 1198 | req: proto.MsgType, | ||
| 1199 | payload: []const u8, | ||
| 1200 | want: []const proto.MsgType, | ||
| 1201 | deadline: i64, | ||
| 1202 | ) !proto.Frame { | ||
| 1203 | try tr.writeFrame(req, payload); | ||
| 1204 | while (true) { | ||
| 1205 | if (!try awaitFrames(tr, deadline)) continue; | ||
| 1206 | while (true) { | ||
| 1207 | // `readFrame` turns every wire fault into `.closed`, so the one | ||
| 1208 | // error it can still raise is this machine's own allocator. | ||
| 1209 | switch (try tr.readFrame(alloc)) { | ||
| 1210 | .incomplete => break, | ||
| 1211 | .closed => return error.Closed, | ||
| 1212 | .frame => |f| { | ||
| 1213 | for (want) |w| if (f.type == w) return f; | ||
| 1214 | f.deinit(alloc); | ||
| 1215 | }, | ||
| 1216 | } | ||
| 1217 | // One readable event is one frame on a socket link, and a | ||
| 1218 | // second read would block until the daemon spoke again. | ||
| 1219 | if (tr.link != .quic) break; | ||
| 1220 | } | ||
| 1221 | } | ||
| 1222 | } | ||
| 1223 | |||
| 1186 | /// Creates `name` on `target` over a connection of its own, then leaves. | 1224 | /// Creates `name` on `target` over a connection of its own, then leaves. |
| 1187 | pub fn birthSession( | 1225 | pub fn birthSession( |
| 1188 | alloc: std.mem.Allocator, | 1226 | alloc: std.mem.Allocator, |
| @@ -1201,38 +1239,27 @@ pub fn birthSession( | |||
| 1201 | var buf: [proto.attach_max_len]u8 = undefined; | 1239 | var buf: [proto.attach_max_len]u8 = undefined; |
| 1202 | // Resume args 0/0: this connection holds nothing and wants the | 1240 | // Resume args 0/0: this connection holds nothing and wants the |
| 1203 | // cheapest thing the daemon can answer with. | 1241 | // cheapest thing the daemon can answer with. |
| 1204 | try tr.writeFrame(.attach, proto.encodeAttachNamed(&buf, cols, rows, 0, 0, proto.wireName(name))); | 1242 | const attach = proto.encodeAttachNamed(&buf, cols, rows, 0, 0, proto.wireName(name)); |
| 1205 | const deadline = std.time.milliTimestamp() + birth_budget_ms; | 1243 | const deadline = std.time.milliTimestamp() + birth_budget_ms; |
| 1206 | while (true) { | 1244 | // Both answers are asked for, because both end the wait: |
| 1207 | if (!try awaitFrames(&tr, deadline)) continue; | 1245 | // `server_sessions.resolve` refuses with an `exit_status` before any |
| 1208 | while (true) { | 1246 | // snapshot, and waiting past it for a snapshot that is not coming would |
| 1209 | // Not `Refused`: a read that failed is a transport that | 1247 | // spend the whole budget on an answer already given. |
| 1210 | // broke, and telling the browser its attach was refused for | 1248 | const f = roundTrip(&tr, alloc, .attach, attach, &.{ .snapshot, .exit_status }, deadline) catch |e| return switch (e) { |
| 1211 | // something nothing refused is a lie the page then shows. | 1249 | // A daemon that hung up before it answered refused this attach. |
| 1212 | switch (tr.readFrame(alloc) catch return error.Transport) { | 1250 | error.Closed => error.Refused, |
| 1213 | .incomplete => break, | 1251 | error.Timeout => error.Timeout, |
| 1214 | .closed => return error.Refused, | 1252 | // Not `Refused`: a read that failed is a transport that broke, and |
| 1215 | .frame => |f| { | 1253 | // telling the browser its attach was refused for something nothing |
| 1216 | defer f.deinit(alloc); | 1254 | // refused is a lie the page then shows. |
| 1217 | switch (f.type) { | 1255 | else => error.Transport, |
| 1218 | // The daemon made a grid, so the session exists. | 1256 | }; |
| 1219 | .snapshot => { | 1257 | defer f.deinit(alloc); |
| 1220 | tr.writeFrame(.detach, "") catch {}; | 1258 | if (f.type != .snapshot) return error.Refused; |
| 1221 | return; | 1259 | // The daemon made a grid, so the session exists — and this connection |
| 1222 | }, | 1260 | // leaves, because the session outlives it and a lingering client would |
| 1223 | // `server_sessions.resolve` refuses before any | 1261 | // hold a slot the browser tile needs. |
| 1224 | // snapshot; anything else is narration this | 1262 | tr.writeFrame(.detach, "") catch {}; |
| 1225 | // connection has no use for. | ||
| 1226 | .exit_status => return error.Refused, | ||
| 1227 | else => {}, | ||
| 1228 | } | ||
| 1229 | }, | ||
| 1230 | } | ||
| 1231 | // One readable event is one frame on a socket link, and a | ||
| 1232 | // second read would block until the daemon spoke again. | ||
| 1233 | if (tr.link != .quic) break; | ||
| 1234 | } | ||
| 1235 | } | ||
| 1236 | } | 1263 | } |
| 1237 | 1264 | ||
| 1238 | /// Every way of not reaching a daemon, except running out of memory. | 1265 | /// Every way of not reaching a daemon, except running out of memory. |
| @@ -1266,27 +1293,17 @@ pub fn listSessions( | |||
| 1266 | defer tr.close(); | 1293 | defer tr.close(); |
| 1267 | // The handoff picks its own link, so only the success case knows it. | 1294 | // The handoff picks its own link, so only the success case knows it. |
| 1268 | if (answered) |a| a.* = tr.link; | 1295 | if (answered) |a| a.* = tr.link; |
| 1269 | try tr.writeFrame(.sessions_req, ""); | ||
| 1270 | const deadline = std.time.milliTimestamp() + budget_ms; | 1296 | const deadline = std.time.milliTimestamp() + budget_ms; |
| 1271 | while (true) { | 1297 | // A daemon too old for the verb answers nothing at all, so the timeout is |
| 1272 | if (!try awaitFrames(&tr, deadline)) continue; | 1298 | // what names it; anything else on this connection is narration the poll |
| 1273 | while (true) { | 1299 | // never asked for, which `roundTrip` drops. A daemon that hung up is a |
| 1274 | switch (tr.readFrame(alloc) catch |e| return oomOrTransport(e)) { | 1300 | // box the wall cannot reach, the same as one that never came up. |
| 1275 | .incomplete => break, | 1301 | const f = roundTrip(&tr, alloc, .sessions_req, "", &.{.sessions_reply}, deadline) catch |e| |
| 1276 | .closed => return error.Transport, | 1302 | return if (e == error.Timeout) e else oomOrTransport(e); |
| 1277 | .frame => |f| { | 1303 | defer f.deinit(alloc); |
| 1278 | defer f.deinit(alloc); | 1304 | if (f.payload.len > out.len) return error.Transport; |
| 1279 | // A daemon too old for the verb answers nothing at all, | 1305 | @memcpy(out[0..f.payload.len], f.payload); |
| 1280 | // so the timeout is what names it; anything else on this | 1306 | return out[0..f.payload.len]; |
| 1281 | // connection is narration it never asked for. | ||
| 1282 | if (f.type != .sessions_reply) continue; | ||
| 1283 | if (f.payload.len > out.len) return error.Transport; | ||
| 1284 | @memcpy(out[0..f.payload.len], f.payload); | ||
| 1285 | return out[0..f.payload.len]; | ||
| 1286 | }, | ||
| 1287 | } | ||
| 1288 | } | ||
| 1289 | } | ||
| 1290 | } | 1307 | } |
| 1291 | 1308 | ||
| 1292 | /// A daemon on a wall: what to dial, and the line that named it. The | 1309 | /// A daemon on a wall: what to dial, and the line that named it. The |