fed80c17
refactor: the hub resolves a tile through the one resolver
a73x 2026-08-29 10:01
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -282,7 +282,7 @@ const mod_table = [_]ModSpec{ | |||
| 282 | // @embedFiles them), so its tests build no artifacts. | 282 | // @embedFiles them), so its tests build no artifacts. |
| 283 | // sockpath is the sun_path bound a `--sock` tile is refused against — | 283 | // sockpath is the sun_path bound a `--sock` tile is refused against — |
| 284 | // the check argv used to make before the Hub owned resolution. | 284 | // the check argv used to make before the Hub owned resolution. |
| 285 | .{ .name = "webhub", .path = "src/client/webhub.zig", .layer = 4, .imports = &.{ "protocol", "client", "wall", "handoff", "xdg", "sockpath" }, .quic_tests = true }, | 285 | .{ .name = "webhub", .path = "src/client/webhub.zig", .layer = 4, .imports = &.{ "protocol", "client", "wall", "handoff" }, .quic_tests = true }, |
| 286 | // The CLI wall (`mux wall`): multiattach stripes in one terminal, one of | 286 | // The CLI wall (`mux wall`): multiattach stripes in one terminal, one of |
| 287 | // which can be ZOOMED — promoted to the terminal's size and typed | 287 | // which can be ZOOMED — promoted to the terminal's size and typed |
| 288 | // through. Same layer as webhub for the same reason — both sit on | 288 | // through. Same layer as webhub for the same reason — both sit on |
src/client/webhub.zig
| Old | New | ||
|---|---|---|---|
| @@ -14,8 +14,6 @@ const proto = @import("protocol"); | |||
| 14 | const client = @import("client"); | 14 | const client = @import("client"); |
| 15 | const wall = @import("wall"); | 15 | const wall = @import("wall"); |
| 16 | const handoff = @import("handoff"); | 16 | const handoff = @import("handoff"); |
| 17 | const xdg = @import("xdg"); | ||
| 18 | const sockpath = @import("sockpath"); | ||
| 19 | 17 | ||
| 20 | pub const default_port: u16 = 7681; | 18 | pub const default_port: u16 = 7681; |
| 21 | 19 | ||
| @@ -78,8 +76,9 @@ const HubTile = struct { | |||
| 78 | pub const AddError = wall.ParseError || ResolveError || error{PersistFailed}; | 76 | pub const AddError = wall.ParseError || ResolveError || error{PersistFailed}; |
| 79 | 77 | ||
| 80 | /// The half of AddError a spelling can fail at resolution time — shared | 78 | /// The half of AddError a spelling can fail at resolution time — shared |
| 81 | /// with `Hub.init`, which resolves the whole wall before serving. | 79 | /// with `Hub.init`, which resolves the whole wall before serving. The |
| 82 | pub const ResolveError = error{ MissingKey, SockPathTooLong, OutOfMemory }; | 80 | /// resolver's own set: one door, one list of ways through it to fail. |
| 81 | pub const ResolveError = client.SpecError; | ||
| 83 | 82 | ||
| 84 | /// Resolves as argv does at startup, so a runtime tile means what the | 83 | /// Resolves as argv does at startup, so a runtime tile means what the |
| 85 | /// command line means. | 84 | /// command line means. |
| @@ -98,55 +97,11 @@ fn resolveTile( | |||
| 98 | // page shows what was typed, not a rebuilt approximation of it. | 97 | // page shows what was typed, not a rebuilt approximation of it. |
| 99 | const label = try arena.dupe(u8, spelling); | 98 | const label = try arena.dupe(u8, spelling); |
| 100 | const session = try arena.dupe(u8, p.session); | 99 | const session = try arena.dupe(u8, p.session); |
| 101 | const target: client.Target = switch (p.spec) { | 100 | // The hub is never the ask: a tile redials for as long as the page is |
| 102 | // sun_path is a fixed array in the kernel's struct: a longer path | 101 | // open and nobody is sitting in front of it. Said HERE and not only |
| 103 | // cannot be dialed at all, so it is refused here rather than at a | 102 | // where the pump clears it — a permission cleared downstream is one a |
| 104 | // connect that fails with a truncated name nobody typed. The hub's | 103 | // new road can miss. |
| 105 | // argv path made this check before the Hub existed; a POSTed | 104 | const target = try client.Target.fromSpec(arena, p.spec, key, idle_ms, false); |
| 106 | // spelling gets the same answer. | ||
| 107 | .sock => |path| if (path.len > sockpath.max_sun_path) | ||
| 108 | return error.SockPathTooLong | ||
| 109 | else | ||
| 110 | .{ .sock = try arena.dupe(u8, path) }, | ||
| 111 | .host => |h| blk: { | ||
| 112 | const hd = try arena.dupe(u8, h); | ||
| 113 | const r = try handoff.recipeFor(arena, hd, false); | ||
| 114 | break :blk .{ | ||
| 115 | .hand = .{ | ||
| 116 | .host = hd, | ||
| 117 | .ssh_argv = r.ssh_argv, | ||
| 118 | .start_argv = r.start_argv, | ||
| 119 | .cache_path = r.cache_path, | ||
| 120 | .idle_ms = idle_ms, | ||
| 121 | // The hub is never the ask: a tile redials for as long as | ||
| 122 | // the page is open and nobody is sitting in front of it. | ||
| 123 | // Said HERE and not only where the pump clears it — a | ||
| 124 | // permission cleared downstream is one a new road can miss. | ||
| 125 | .asked = false, | ||
| 126 | }, | ||
| 127 | }; | ||
| 128 | }, | ||
| 129 | .quic => |hp| blk: { | ||
| 130 | const key_path = switch (xdg.resolveKeyPath(arena, key) catch |err| switch (err) { | ||
| 131 | // No HOME is no default key path, which is the same | ||
| 132 | // outcome for this caller as a default that isn't there: | ||
| 133 | // nothing to authenticate the dial with. | ||
| 134 | error.NoHome => return error.MissingKey, | ||
| 135 | else => |e| return e, | ||
| 136 | }) { | ||
| 137 | // `.given` borrows from argv/env, which outlives nothing | ||
| 138 | // in particular from the tile's point of view. | ||
| 139 | .given => |kp| try arena.dupe(u8, kp), | ||
| 140 | .default => |kp| kp, | ||
| 141 | .missing => return error.MissingKey, | ||
| 142 | }; | ||
| 143 | break :blk .{ .quic = .{ | ||
| 144 | .host_port = try arena.dupe(u8, hp), | ||
| 145 | .key_path = key_path, | ||
| 146 | .idle_ms = idle_ms, | ||
| 147 | } }; | ||
| 148 | }, | ||
| 149 | }; | ||
| 150 | return .{ .target = target, .label = label, .session = session }; | 105 | return .{ .target = target, .label = label, .session = session }; |
| 151 | } | 106 | } |
| 152 | 107 | ||
| @@ -1421,6 +1376,27 @@ test "hub: checkout copies the target into the caller's arena; release unregiste | |||
| 1421 | try std.testing.expectError(error.UnknownId, hub.checkoutTile(0, 7, arena.allocator())); | 1376 | try std.testing.expectError(error.UnknownId, hub.checkoutTile(0, 7, arena.allocator())); |
| 1422 | } | 1377 | } |
| 1423 | 1378 | ||
| 1379 | test "hub: a HOST tile is never the ask, whether it came from the wall file or a POST" { | ||
| 1380 | const alloc = std.testing.allocator; | ||
| 1381 | var w = wall.Wall{}; | ||
| 1382 | _ = try w.add(alloc, "box"); | ||
| 1383 | var hub = try Hub.init(alloc, w, null, null, client.quic_idle_ms_default); | ||
| 1384 | defer hub.deinit(); | ||
| 1385 | // Both mouths, because the startup wall and the runtime POST are two | ||
| 1386 | // roads to `resolveTile` and only one of them was ever walked here. | ||
| 1387 | _ = try hub.addTile("gate"); | ||
| 1388 | |||
| 1389 | var arena = std.heap.ArenaAllocator.init(alloc); | ||
| 1390 | defer arena.deinit(); | ||
| 1391 | for ([_]u32{ 0, 1 }) |id| { | ||
| 1392 | const t = try hub.checkoutTile(id, 7, arena.allocator()); | ||
| 1393 | // Nobody is sitting in front of a browser tile: it may not start a | ||
| 1394 | // daemon on a box whose owner just stopped one, and it may not | ||
| 1395 | // print an ssh-fallback line into a page that has no stderr. | ||
| 1396 | try std.testing.expect(!t.target.hand.asked); | ||
| 1397 | } | ||
| 1398 | } | ||
| 1399 | |||
| 1424 | test "hub: a checkout carries the tile's session name, which the pump may have to create" { | 1400 | test "hub: a checkout carries the tile's session name, which the pump may have to create" { |
| 1425 | const alloc = std.testing.allocator; | 1401 | const alloc = std.testing.allocator; |
| 1426 | var w = wall.Wall{}; | 1402 | var w = wall.Wall{}; |