d3ff0d67
fix: mux hosts rm takes the daemon's panes out of the layout too
a73x 2026-09-03 05:20
Commit message
src/cli/mux_main.zig
| Old | New | ||
|---|---|---|---|
| @@ -16,6 +16,7 @@ const handoff = @import("client").handoff; | |||
| 16 | const sockpath = @import("sockpath"); | 16 | const sockpath = @import("sockpath"); |
| 17 | const wall = @import("wall"); | 17 | const wall = @import("wall"); |
| 18 | const hosts = @import("client").hosts; | 18 | const hosts = @import("client").hosts; |
| 19 | const layoutfile = @import("client").layoutfile; | ||
| 19 | const cliflags = @import("cliflags"); | 20 | const cliflags = @import("cliflags"); |
| 20 | const TmpDir = @import("testtmp").TmpDir; | 21 | const TmpDir = @import("testtmp").TmpDir; |
| 21 | 22 | ||
| @@ -620,12 +621,36 @@ fn hostsEdit( | |||
| 620 | rc = 1; | 621 | rc = 1; |
| 621 | } | 622 | } |
| 622 | } | 623 | } |
| 624 | // The panes go with the daemon. A layout leaf is `HOST#SESSION` and its | ||
| 625 | // host part must be a line of the hosts file, so a leaf left behind here | ||
| 626 | // refuses the WHOLE layout at the next start — one `mux hosts rm box` | ||
| 627 | // and the user's other four panes are gone with a printed line they did | ||
| 628 | // not ask for. The sessions themselves keep running; this is the file, | ||
| 629 | // not the daemon. `wall_picker.pickForget` is the same edit typed from | ||
| 630 | // inside a wall, made there by the wall's own save. | ||
| 631 | if (layoutBeside(arena, path)) |lp| { | ||
| 632 | if (layoutfile.forgetHosts(arena, lp, spellings.items, "mux hosts")) |_| {} else |err| { | ||
| 633 | // Not a failure of the removal: the hosts file is already | ||
| 634 | // written and the daemon is already off the wall. Said, because | ||
| 635 | // the next `mux` will refuse a layout this could not repair. | ||
| 636 | std.debug.print("mux hosts rm: {s}: {s}\n", .{ lp, hosts.reason(err) }); | ||
| 637 | } | ||
| 638 | } | ||
| 623 | // `forgetMany` matches exact lines. On failure, print the file so a | 639 | // `forgetMany` matches exact lines. On failure, print the file so a |
| 624 | // hand-edited entry can be copied byte for byte. | 640 | // hand-edited entry can be copied byte for byte. |
| 625 | if (rc != 0) showFile(arena, path); | 641 | if (rc != 0) showFile(arena, path); |
| 626 | return rc; | 642 | return rc; |
| 627 | } | 643 | } |
| 628 | 644 | ||
| 645 | /// The layout file beside a given hosts file. Both are `xdg.statePath` names | ||
| 646 | /// in one directory (`hosts.statePath`, `hosts.layoutPath`), so the sibling | ||
| 647 | /// is the layout for THIS hosts file — including the tmp-directory pair a | ||
| 648 | /// test drives, which an `XDG_STATE_HOME` lookup here would miss. | ||
| 649 | fn layoutBeside(arena: std.mem.Allocator, hosts_path: []const u8) ?[]const u8 { | ||
| 650 | const dir = std.fs.path.dirname(hosts_path) orelse return null; | ||
| 651 | return std.fs.path.join(arena, &.{ dir, "layout" }) catch null; | ||
| 652 | } | ||
| 653 | |||
| 629 | /// Print the hosts file verbatim for use with an exact-match removal. | 654 | /// Print the hosts file verbatim for use with an exact-match removal. |
| 630 | fn showFile(arena: std.mem.Allocator, path: []const u8) void { | 655 | fn showFile(arena: std.mem.Allocator, path: []const u8) void { |
| 631 | const lines = hosts.loadLines(arena, path) catch return; | 656 | const lines = hosts.loadLines(arena, path) catch return; |
| @@ -778,6 +803,34 @@ test "hosts: a line the file cannot hold exits 2 wherever the file is read" { | |||
| 778 | try std.testing.expectEqual(@as(u8, 0), try hostsMain(alloc, &[_][:0]const u8{ "add", "other" }, path)); | 803 | try std.testing.expectEqual(@as(u8, 0), try hostsMain(alloc, &[_][:0]const u8{ "add", "other" }, path)); |
| 779 | } | 804 | } |
| 780 | 805 | ||
| 806 | test "hosts rm: the removed daemon's panes leave the layout beside the hosts file" { | ||
| 807 | const alloc = std.testing.allocator; | ||
| 808 | var tmp = try TmpDir.make(); | ||
| 809 | defer tmp.cleanup(); | ||
| 810 | var buf: [256]u8 = undefined; | ||
| 811 | const path = try std.fmt.bufPrint(&buf, "{s}/hosts", .{tmp.path()}); | ||
| 812 | var lbuf: [256]u8 = undefined; | ||
| 813 | const lpath = try std.fmt.bufPrint(&lbuf, "{s}/layout", .{tmp.path()}); | ||
| 814 | try hosts.saveBytes(path, "box\n--sock /tmp/a.sock\n"); | ||
| 815 | // Two hosts, three panes: the removal must take the two that name `box` | ||
| 816 | // and leave the third exactly where it was. | ||
| 817 | try hosts.saveBytes(lpath, | ||
| 818 | \\mux-layout 1 | ||
| 819 | \\beside 0 | ||
| 820 | \\ leaf 60 box#0 | ||
| 821 | \\ stacked 40 | ||
| 822 | \\ leaf 50 --sock /tmp/a.sock#work | ||
| 823 | \\ leaf 50 box#two | ||
| 824 | \\ | ||
| 825 | ); | ||
| 826 | try std.testing.expectEqual(@as(u8, 0), try hostsMain(alloc, &[_][:0]const u8{ "rm", "box" }, path)); | ||
| 827 | const after = try std.fs.cwd().readFileAlloc(alloc, lpath, 4096); | ||
| 828 | defer alloc.free(after); | ||
| 829 | // What the next `mux` reads: one leaf, and a file it does not refuse. | ||
| 830 | try std.testing.expect(std.mem.indexOf(u8, after, "--sock /tmp/a.sock#work") != null); | ||
| 831 | try std.testing.expect(std.mem.indexOf(u8, after, "box#") == null); | ||
| 832 | } | ||
| 833 | |||
| 781 | test "hosts list: a line the grammar refuses is named, not a refusal of the listing" { | 834 | test "hosts list: a line the grammar refuses is named, not a refusal of the listing" { |
| 782 | const alloc = std.testing.allocator; | 835 | const alloc = std.testing.allocator; |
| 783 | var tmp = try TmpDir.make(); | 836 | var tmp = try TmpDir.make(); |
src/client/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -27,6 +27,7 @@ const link_mod = @import("link"); | |||
| 27 | pub const hosts = @import("hosts.zig"); | 27 | pub const hosts = @import("hosts.zig"); |
| 28 | pub const handoff = @import("handoff.zig"); | 28 | pub const handoff = @import("handoff.zig"); |
| 29 | pub const layout = @import("layout.zig"); | 29 | pub const layout = @import("layout.zig"); |
| 30 | pub const layoutfile = @import("layoutfile.zig"); | ||
| 30 | // The seam carries `keymap.detach_key` and nothing else: the dial here and | 31 | // The seam carries `keymap.detach_key` and nothing else: the dial here and |
| 31 | // the prefix filter in interact.zig both watch for that one abort byte. It | 32 | // the prefix filter in interact.zig both watch for that one abort byte. It |
| 32 | // is keymap's rather than the filter's because a dial has no session to | 33 | // is keymap's rather than the filter's because a dial has no session to |
src/client/layoutfile.zig
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,186 @@ | |||
| 1 | //! The layout file edited from OUTSIDE a wall: a read-modify-write over | ||
| 2 | //! `hosts.saveBytes`' atomic rename, for the fronts that change what the | ||
| 3 | //! wall will hold without one on the screen. | ||
| 4 | //! | ||
| 5 | //! The wall's own saves are `wall_layout.persist`, which serializes the tree | ||
| 6 | //! it is painting; the hub's own add is `webhub.appendLeaf`. What lives here | ||
| 7 | //! is the edit neither of those can make, because it is made by a command | ||
| 8 | //! that never opens a wall — `mux hosts rm`, which takes a daemon off the | ||
| 9 | //! device and must take that daemon's panes with it. A leaf naming a host | ||
| 10 | //! the hosts file does not list refuses the WHOLE layout at the next start | ||
| 11 | //! (`wall_layout.seedLayout`), so a `rm` that edited one file and not the | ||
| 12 | //! other cost the user every other pane they had authored. | ||
| 13 | //! | ||
| 14 | //! A `client`-row child rather than part of `hosts.zig`: this knows the | ||
| 15 | //! layout grammar, and `hosts.zig` deliberately does not — it owns the | ||
| 16 | //! hosts file and the path beside it, and says so. Reachable from | ||
| 17 | //! `src/cli/mux_main.zig` and from `webhub.zig`, which are the two fronts | ||
| 18 | //! that edit the file without a wall. | ||
| 19 | const std = @import("std"); | ||
| 20 | const hosts = @import("hosts.zig"); | ||
| 21 | const layout = @import("layout.zig"); | ||
| 22 | const TmpDir = @import("testtmp").TmpDir; | ||
| 23 | |||
| 24 | /// What a removal did to the file. | ||
| 25 | pub const Outcome = union(enum) { | ||
| 26 | /// No file, or no leaf named any of those hosts: nothing was written. | ||
| 27 | /// A save that changes nothing is still a write another writer can lose | ||
| 28 | /// an update to, which is `hosts.forgetMany`'s rule as well. | ||
| 29 | unchanged, | ||
| 30 | /// How many leaves went. The file was rewritten, or DELETED when the | ||
| 31 | /// removal took the last one: an empty tree serializes to a header with | ||
| 32 | /// no root, and `layout.parseReporting` refuses that file — so writing | ||
| 33 | /// one would leave behind exactly the refusal this whole module exists | ||
| 34 | /// to prevent. No panes is no layout. | ||
| 35 | rewrote: usize, | ||
| 36 | /// The file is not a layout. Nothing is written and the line it gave up | ||
| 37 | /// on has been printed: a file mux cannot read is a file mux must not | ||
| 38 | /// rewrite, and the user's own editor is the tool for it. | ||
| 39 | refused, | ||
| 40 | }; | ||
| 41 | |||
| 42 | /// Every leaf whose HOST part is one of `spellings`, gone from the tree. | ||
| 43 | /// The match is byte for byte against the hosts-file spelling, the same key | ||
| 44 | /// `seedLayout` matches a leaf by, so a `rm` removes exactly the leaves the | ||
| 45 | /// next start would have refused the file for. | ||
| 46 | /// | ||
| 47 | /// `who` is the command saying so, for the one line this prints. | ||
| 48 | pub fn forgetHosts( | ||
| 49 | alloc: std.mem.Allocator, | ||
| 50 | path: []const u8, | ||
| 51 | spellings: []const []const u8, | ||
| 52 | who: []const u8, | ||
| 53 | ) !Outcome { | ||
| 54 | const bytes = std.fs.cwd().readFileAlloc(alloc, path, 1024 * 1024) catch |e| switch (e) { | ||
| 55 | // A device that has never opened a wall has no layout, and removing | ||
| 56 | // a host from its hosts file is not the moment to invent one. | ||
| 57 | error.FileNotFound => return .unchanged, | ||
| 58 | else => return e, | ||
| 59 | }; | ||
| 60 | defer alloc.free(bytes); | ||
| 61 | var bad_line: []const u8 = ""; | ||
| 62 | var parsed = layout.parseReporting(alloc, bytes, &bad_line) orelse { | ||
| 63 | std.debug.print("{s}: layout ignored ({s}): {s}\n", .{ who, path, bad_line }); | ||
| 64 | return .refused; | ||
| 65 | }; | ||
| 66 | defer parsed.deinit(alloc); | ||
| 67 | |||
| 68 | var removed: usize = 0; | ||
| 69 | for (parsed.spellings.items, 0..) |sp, id| { | ||
| 70 | const cut = std.mem.lastIndexOfScalar(u8, sp, '#') orelse continue; | ||
| 71 | for (spellings) |want| { | ||
| 72 | if (!std.mem.eql(u8, sp[0..cut], want)) continue; | ||
| 73 | // The leaf id IS its index here: `parseReporting` hands ids out | ||
| 74 | // in encounter order. The spelling stays in the array — the | ||
| 75 | // tree no longer names it, and `serialize` walks the tree. | ||
| 76 | parsed.tree.remove(@intCast(id)); | ||
| 77 | if (parsed.focus) |f| { | ||
| 78 | if (f == id) parsed.focus = null; | ||
| 79 | } | ||
| 80 | removed += 1; | ||
| 81 | break; | ||
| 82 | } | ||
| 83 | } | ||
| 84 | if (removed == 0) return .unchanged; | ||
| 85 | if (parsed.tree.root == null) { | ||
| 86 | std.fs.cwd().deleteFile(path) catch |e| switch (e) { | ||
| 87 | error.FileNotFound => {}, | ||
| 88 | else => return e, | ||
| 89 | }; | ||
| 90 | return .{ .rewrote = removed }; | ||
| 91 | } | ||
| 92 | var buf: std.ArrayListUnmanaged(u8) = .{}; | ||
| 93 | defer buf.deinit(alloc); | ||
| 94 | try parsed.tree.serialize(parsed.spellings.items, parsed.focus, buf.writer(alloc)); | ||
| 95 | try hosts.saveBytes(path, buf.items); | ||
| 96 | return .{ .rewrote = removed }; | ||
| 97 | } | ||
| 98 | |||
| 99 | test "forgetHosts: one host's leaves go and the other host's keep their shape" { | ||
| 100 | const alloc = std.testing.allocator; | ||
| 101 | var tmp = try TmpDir.make(); | ||
| 102 | defer tmp.cleanup(); | ||
| 103 | const path = try std.fmt.allocPrint(alloc, "{s}/layout", .{tmp.path()}); | ||
| 104 | defer alloc.free(path); | ||
| 105 | |||
| 106 | // Two hosts and three leaves, the plural default: a fixture with one | ||
| 107 | // leaf per host could not tell "removed the right leaves" from | ||
| 108 | // "removed a host". The cut is a `beside` over a `stacked`, and the | ||
| 109 | // survivors must come out in the same shape. | ||
| 110 | try hosts.saveBytes(path, | ||
| 111 | \\mux-layout 1 | ||
| 112 | \\beside 0 | ||
| 113 | \\ leaf 60 box#0 | ||
| 114 | \\ stacked 40 | ||
| 115 | \\ leaf 50 --sock /tmp/a.sock#work | ||
| 116 | \\ leaf 50 box#two | ||
| 117 | \\focus 2 | ||
| 118 | \\ | ||
| 119 | ); | ||
| 120 | const out = try forgetHosts(alloc, path, &.{"box"}, "test"); | ||
| 121 | try std.testing.expectEqual(@as(usize, 2), out.rewrote); | ||
| 122 | |||
| 123 | const after = try std.fs.cwd().readFileAlloc(alloc, path, 4096); | ||
| 124 | defer alloc.free(after); | ||
| 125 | var line: []const u8 = ""; | ||
| 126 | var re = layout.parseReporting(alloc, after, &line) orelse return error.Unparsable; | ||
| 127 | defer re.deinit(alloc); | ||
| 128 | try std.testing.expectEqual(@as(usize, 1), re.spellings.items.len); | ||
| 129 | try std.testing.expectEqualStrings("--sock /tmp/a.sock#work", re.spellings.items[0]); | ||
| 130 | // The removed leaf held the focus record; it may not survive as an | ||
| 131 | // index into a file that no longer has that many leaves. | ||
| 132 | try std.testing.expect(re.focus == null or re.focus.? == 0); | ||
| 133 | } | ||
| 134 | |||
| 135 | test "forgetHosts: a host no leaf names writes nothing, and a file that is not a layout is left alone" { | ||
| 136 | const alloc = std.testing.allocator; | ||
| 137 | var tmp = try TmpDir.make(); | ||
| 138 | defer tmp.cleanup(); | ||
| 139 | const path = try std.fmt.allocPrint(alloc, "{s}/layout", .{tmp.path()}); | ||
| 140 | defer alloc.free(path); | ||
| 141 | |||
| 142 | const good = | ||
| 143 | \\mux-layout 1 | ||
| 144 | \\beside 0 | ||
| 145 | \\ leaf 60 box#0 | ||
| 146 | \\ leaf 40 --sock /tmp/a.sock#work | ||
| 147 | \\ | ||
| 148 | ; | ||
| 149 | try hosts.saveBytes(path, good); | ||
| 150 | try std.testing.expect(try forgetHosts(alloc, path, &.{"elsewhere"}, "test") == .unchanged); | ||
| 151 | const same = try std.fs.cwd().readFileAlloc(alloc, path, 4096); | ||
| 152 | defer alloc.free(same); | ||
| 153 | try std.testing.expectEqualStrings(good, same); | ||
| 154 | |||
| 155 | // Not a layout: the file is the user's to repair, and a rewrite here | ||
| 156 | // would destroy the line they have to find. | ||
| 157 | try hosts.saveBytes(path, "bogus 9\n"); | ||
| 158 | try std.testing.expect(try forgetHosts(alloc, path, &.{"box"}, "test") == .refused); | ||
| 159 | const junk = try std.fs.cwd().readFileAlloc(alloc, path, 4096); | ||
| 160 | defer alloc.free(junk); | ||
| 161 | try std.testing.expectEqualStrings("bogus 9\n", junk); | ||
| 162 | |||
| 163 | // A missing file is a no-op, not an error: a device that never opened a | ||
| 164 | // wall still runs `mux hosts rm`. | ||
| 165 | try std.fs.cwd().deleteFile(path); | ||
| 166 | try std.testing.expect(try forgetHosts(alloc, path, &.{"box"}, "test") == .unchanged); | ||
| 167 | } | ||
| 168 | |||
| 169 | test "forgetHosts: taking the last leaf deletes the file rather than writing one nothing can read" { | ||
| 170 | const alloc = std.testing.allocator; | ||
| 171 | var tmp = try TmpDir.make(); | ||
| 172 | defer tmp.cleanup(); | ||
| 173 | const path = try std.fmt.allocPrint(alloc, "{s}/layout", .{tmp.path()}); | ||
| 174 | defer alloc.free(path); | ||
| 175 | |||
| 176 | try hosts.saveBytes(path, | ||
| 177 | \\mux-layout 1 | ||
| 178 | \\beside 0 | ||
| 179 | \\ leaf 50 box#0 | ||
| 180 | \\ leaf 50 box#two | ||
| 181 | \\ | ||
| 182 | ); | ||
| 183 | const out = try forgetHosts(alloc, path, &.{"box"}, "test"); | ||
| 184 | try std.testing.expectEqual(@as(usize, 2), out.rewrote); | ||
| 185 | try std.testing.expectError(error.FileNotFound, std.fs.cwd().access(path, .{})); | ||
| 186 | } | ||
src/tui/wall_picker.zig
| Old | New | ||
|---|---|---|---|
| @@ -655,12 +655,18 @@ pub fn pickForget(w: Wall, sel: usize, path: ?[]const u8) void { | |||
| 655 | // record would put every one of them back on the next start. | 655 | // record would put every one of them back on the next start. |
| 656 | wall_layout.persist(w); | 656 | wall_layout.persist(w); |
| 657 | var buf: [96]u8 = undefined; | 657 | var buf: [96]u8 = undefined; |
| 658 | // A run that saves no layout (`Shared.layout_path` null — a pipe, a | ||
| 659 | // `--via` wall, a file this run refused) has just taken the panes off | ||
| 660 | // the SCREEN and nothing off the file, so the next start puts every one | ||
| 661 | // of them back. Said here, where the forget is, rather than left for | ||
| 662 | // the user to discover on a wall they thought they had changed. | ||
| 663 | const tail: []const u8 = if (w.shared.layout_path == null) " - layout not updated" else ""; | ||
| 658 | const said = if (why) |e| | 664 | const said = if (why) |e| |
| 659 | std.fmt.bufPrint(&buf, "[hosts file not updated: {s}]", .{hosts.reason(e)}) catch "[hosts file not updated]" | 665 | std.fmt.bufPrint(&buf, "[hosts file not updated: {s}{s}]", .{ hosts.reason(e), tail }) catch "[hosts file not updated]" |
| 660 | else if (!gone_from_file) | 666 | else if (!gone_from_file) |
| 661 | std.fmt.bufPrint(&buf, "[{s} was not on the wall]", .{h.spec.spelling}) catch "[that host was not on the wall]" | 667 | std.fmt.bufPrint(&buf, "[{s} was not on the wall{s}]", .{ h.spec.spelling, tail }) catch "[that host was not on the wall]" |
| 662 | else | 668 | else |
| 663 | std.fmt.bufPrint(&buf, "[forgot {s}]", .{h.spec.spelling}) catch "[forgot the host]"; | 669 | std.fmt.bufPrint(&buf, "[forgot {s}{s}]", .{ h.spec.spelling, tail }) catch "[forgot the host]"; |
| 664 | wv.setNotice(w.shared, said); | 670 | wv.setNotice(w.shared, said); |
| 665 | } | 671 | } |
| 666 | 672 | ||
test/e2e_09_hosts.sh
| Old | New | ||
|---|---|---|---|
| @@ -798,6 +798,14 @@ for _hc in "$SOCKH1:2" "$SOCKH2:1"; do | |||
| 798 | echo "e2e FAIL: hosts: 'mux hosts' does not say $_hcs holds $_hcn sessions:" | 798 | echo "e2e FAIL: hosts: 'mux hosts' does not say $_hcs holds $_hcn sessions:" |
| 799 | cat "$OUT.hlist"; exit 1; } | 799 | cat "$OUT.hlist"; exit 1; } |
| 800 | done | 800 | done |
| 801 | # The layout is the wall, so the removal has a second file to keep true. | ||
| 802 | # A leaf's host part must be a line of the hosts file — `seedLayout` | ||
| 803 | # refuses the WHOLE layout over one that is not — so a `rm` that edited the | ||
| 804 | # hosts file alone left the user's OTHER panes to be thrown away at the | ||
| 805 | # next start, with a printed line and no undo. Two hosts and three leaves: | ||
| 806 | # the removal has to take two of them and leave the third exactly as it is. | ||
| 807 | seed_layout "$HSTATE" stacked \ | ||
| 808 | "--sock $SOCKH1#0" "--sock $SOCKH1#b" "--sock $SOCKH2#0" | ||
| 801 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts rm "--sock $SOCKH1" > "$OUT.hrm" 2>&1 || { | 809 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts rm "--sock $SOCKH1" > "$OUT.hrm" 2>&1 || { |
| 802 | echo "e2e FAIL: hosts rm: removing a listed daemon failed:"; cat "$OUT.hrm"; exit 1; } | 810 | echo "e2e FAIL: hosts rm: removing a listed daemon failed:"; cat "$OUT.hrm"; exit 1; } |
| 803 | if grep -qF -- "$SOCKH1" "$HSTATE/mux/hosts"; then | 811 | if grep -qF -- "$SOCKH1" "$HSTATE/mux/hosts"; then |
| @@ -818,6 +826,36 @@ kill -0 "$BSHELL" 2>/dev/null || { | |||
| 818 | wait_sessions "$SOCKH1" 2 "hosts rm: daemon 1's sessions must survive being forgotten" | 826 | wait_sessions "$SOCKH1" 2 "hosts rm: daemon 1's sessions must survive being forgotten" |
| 819 | wait_grid "$SOCKH1" "hb-pin" "hosts rm: session b's grid outlived the wall line" b | 827 | wait_grid "$SOCKH1" "hb-pin" "hosts rm: session b's grid outlived the wall line" b |
| 820 | 828 | ||
| 829 | # What the NEXT `mux` on this device gets — the assertion the file edit is | ||
| 830 | # for. The surviving host's pane, on a wall that read its layout without a | ||
| 831 | # word: a `rm` that left the removed daemon's leaves behind would have this | ||
| 832 | # run print `layout ignored` and open the empty wall's picker instead. | ||
| 833 | set +e | ||
| 834 | XDG_STATE_HOME="$HSTATE" timeout 60 "$PTYCLIENT" --cols 80 --rows 44 \ | ||
| 835 | --out "$OUT.hrmcap" --err "$OUT.hrmcap.err" -- "$MUX" > "$OUT.hrmpc" 2>&1 <<EOF | ||
| 836 | expect --sock $SOCKH2#0 [up] 25000 | ||
| 837 | settle 800 25000 | ||
| 838 | send \x1cd | ||
| 839 | waitexit 15000 | ||
| 840 | EOF | ||
| 841 | RC=$? | ||
| 842 | set -e | ||
| 843 | rc0 "hosts rm: the wall after the removal exited $RC:" "$OUT.hrmpc" "$OUT.hrmcap.err" | ||
| 844 | if grep -qF "layout ignored" "$OUT.hrmcap.err"; then | ||
| 845 | echo "e2e FAIL: hosts rm: the layout was refused after the removal, so the" | ||
| 846 | echo " removal left leaves naming the daemon it took off the wall:" | ||
| 847 | cat "$OUT.hrmcap.err"; exit 1 | ||
| 848 | fi | ||
| 849 | "$RENDER" --cols 80 --rows 44 < "$OUT.hrmcap" > "$OUT.hrmcap.final" | ||
| 850 | grep -q -- "--sock $SOCKH2#0 \[up\]" "$OUT.hrmcap.final" || { | ||
| 851 | echo "e2e FAIL: hosts rm: the surviving host's pane is not on the wall the" | ||
| 852 | echo " removal left behind:" | ||
| 853 | cat "$OUT.hrmcap.final"; exit 1; } | ||
| 854 | if grep -q -- "--sock $SOCKH1" "$OUT.hrmcap.final"; then | ||
| 855 | echo "e2e FAIL: hosts rm: a pane on the removed daemon is still on the wall:" | ||
| 856 | cat "$OUT.hrmcap.final"; exit 1 | ||
| 857 | fi | ||
| 858 | |||
| 821 | set +e | 859 | set +e |
| 822 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts rm "--sock $SOCKH1" > "$OUT.hrm2" 2>&1 | 860 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts rm "--sock $SOCKH1" > "$OUT.hrm2" 2>&1 |
| 823 | RC=$? | 861 | RC=$? |