c3ab0e21
fix: a hosts row is printed whole, so rm can match the line the list just showed
a73x 2026-08-28 19:53
Commit message
src/cli/mux_main.zig
| Old | New | ||
|---|---|---|---|
| @@ -588,19 +588,30 @@ fn hostsList(arena: std.mem.Allocator, path: []const u8, out_fd: std.posix.fd_t) | |||
| 588 | const key = std.posix.getenv(xdg.key_env); | 588 | const key = std.posix.getenv(xdg.key_env); |
| 589 | for (lines.items) |line| { | 589 | for (lines.items) |line| { |
| 590 | const spec = wallview.resolveHost(arena, line, key, client.quic_idle_ms_default) catch |err| { | 590 | const spec = wallview.resolveHost(arena, line, key, client.quic_idle_ms_default) catch |err| { |
| 591 | printOut(out_fd, "{s}\t[bad host: {s}]\n", .{ line, hosts.reason(err) }); | 591 | printRow(out_fd, line, "\t[bad host: {s}]\n", .{hosts.reason(err)}); |
| 592 | continue; | 592 | continue; |
| 593 | }; | 593 | }; |
| 594 | var out: [proto.sessions_text_max]u8 = undefined; | 594 | var out: [proto.sessions_text_max]u8 = undefined; |
| 595 | const list = client.listSessions(arena, spec.poll_target, &out, hosts_list_ms, null) catch { | 595 | const list = client.listSessions(arena, spec.poll_target, &out, hosts_list_ms, null) catch { |
| 596 | printOut(out_fd, "{s}\t[unreachable]\n", .{line}); | 596 | printRow(out_fd, line, "\t[unreachable]\n", .{}); |
| 597 | continue; | 597 | continue; |
| 598 | }; | 598 | }; |
| 599 | printOut(out_fd, "{s}\t{d}\n", .{ line, countSessions(list) }); | 599 | printRow(out_fd, line, "\t{d}\n", .{countSessions(list)}); |
| 600 | } | 600 | } |
| 601 | return 0; | 601 | return 0; |
| 602 | } | 602 | } |
| 603 | 603 | ||
| 604 | /// One listing row: the file's line verbatim, then a formatted verdict. | ||
| 605 | fn printRow(fd: std.posix.fd_t, line: []const u8, comptime fmt: []const u8, args: anytype) void { | ||
| 606 | // The line is written straight through rather than formatted into a | ||
| 607 | // buffer with the verdict. `wall.loadLines` accepts lines up to a MiB | ||
| 608 | // and `mux hosts rm` matches byte for byte, so a row that overflowed a | ||
| 609 | // fixed buffer used to vanish from the one command whose job is showing | ||
| 610 | // the user a line they then have to type back. | ||
| 611 | proto.writeAllFd(fd, line) catch return; | ||
| 612 | printOut(fd, fmt, args); | ||
| 613 | } | ||
| 614 | |||
| 604 | /// A listing someone ASKED for is output, like `--help`; every refusal | 615 | /// A listing someone ASKED for is output, like `--help`; every refusal |
| 605 | /// above and below stays on stderr. | 616 | /// above and below stays on stderr. |
| 606 | fn printOut(fd: std.posix.fd_t, comptime fmt: []const u8, args: anytype) void { | 617 | fn printOut(fd: std.posix.fd_t, comptime fmt: []const u8, args: anytype) void { |
| @@ -913,6 +924,43 @@ test "hosts list: a line the grammar refuses is named, not a refusal of the list | |||
| 913 | try std.testing.expect(std.mem.indexOf(u8, text, "\t[unreachable]") != null); | 924 | try std.testing.expect(std.mem.indexOf(u8, text, "\t[unreachable]") != null); |
| 914 | } | 925 | } |
| 915 | 926 | ||
| 927 | test "hosts list: a line longer than the row buffer is still shown, because rm matches what was shown" { | ||
| 928 | // `wall.loadLines` takes lines up to a MiB and `mux hosts rm` matches | ||
| 929 | // byte for byte, so a row this command drops is a line the user can | ||
| 930 | // neither see nor type back — the two halves of the repair loop have to | ||
| 931 | // agree about which lines exist. | ||
| 932 | const alloc = std.testing.allocator; | ||
| 933 | var tmp = try TmpDir.make(); | ||
| 934 | defer tmp.cleanup(); | ||
| 935 | var buf: [512]u8 = undefined; | ||
| 936 | const path = try std.fmt.bufPrint(&buf, "{s}/hosts", .{tmp.path()}); | ||
| 937 | |||
| 938 | // A `--sock` path no `sun_path` could hold: refused at usage altitude, | ||
| 939 | // so the row is produced without a dial and the check is about the | ||
| 940 | // printing and nothing else. | ||
| 941 | var long: [900]u8 = undefined; | ||
| 942 | @memcpy(long[0..8], "--sock /"); | ||
| 943 | @memset(long[8..], 'h'); | ||
| 944 | var line_buf: [1024]u8 = undefined; | ||
| 945 | try wall.saveBytes(path, try std.fmt.bufPrint(&line_buf, "{s}\n", .{long})); | ||
| 946 | |||
| 947 | var arena_state = std.heap.ArenaAllocator.init(alloc); | ||
| 948 | defer arena_state.deinit(); | ||
| 949 | const arena = arena_state.allocator(); | ||
| 950 | var out_buf: [512]u8 = undefined; | ||
| 951 | const out_path = try std.fmt.bufPrint(&out_buf, "{s}/out", .{tmp.path()}); | ||
| 952 | const out = try std.fs.createFileAbsolute(out_path, .{}); | ||
| 953 | const rc = try hostsList(arena, path, out.handle); | ||
| 954 | out.close(); | ||
| 955 | try std.testing.expectEqual(@as(u8, 0), rc); | ||
| 956 | |||
| 957 | const text = try std.fs.cwd().readFileAlloc(alloc, out_path, 8192); | ||
| 958 | defer alloc.free(text); | ||
| 959 | // Whole, and with its verdict: a truncated line is one `rm` cannot take. | ||
| 960 | try std.testing.expect(std.mem.startsWith(u8, text, &long)); | ||
| 961 | try std.testing.expect(std.mem.endsWith(u8, text, "\t[bad host: socket path too long to bind]\n")); | ||
| 962 | } | ||
| 963 | |||
| 916 | test "wall: a LISTED local daemon that nothing answers on is one this wall starts" { | 964 | test "wall: a LISTED local daemon that nothing answers on is one this wall starts" { |
| 917 | const alloc = std.testing.allocator; | 965 | const alloc = std.testing.allocator; |
| 918 | var tmp = try TmpDir.make(); | 966 | var tmp = try TmpDir.make(); |