Skip to content

Commit ef553e7

Browse files
zlice devclaude
andcommitted
Fix close_tab state corruption, active_panes routing, and blocking client writes
- Guard close_tab to reject closing the last tab before destroying pane state, preventing irreversible state corruption on a normal user command - Initialize active_panes for all existing tabs on client hello, and propagate new tab's focused pane to all clients to prevent cross-tab input misrouting - Make client sockets nonblocking (SOCK_NONBLOCK) and disconnect clients on write failure to prevent one stalled client from freezing the entire server Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9039669 commit ef553e7

2 files changed

Lines changed: 50 additions & 8 deletions

File tree

src/pane.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ fn replaceNodeInParent(
405405
}
406406
}
407407

408-
fn firstLeafId(node: *const LayoutNode) PaneId {
408+
pub fn firstLeafId(node: *const LayoutNode) PaneId {
409409
return switch (node.*) {
410410
.leaf => |l| l.id,
411411
.split => |s| firstLeafId(s.first),

src/server.zig

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ pub const ClientState = struct {
134134
mode: mode_mod.Mode = .normal,
135135
recv_buf: [RECV_BUF_SIZE]u8 = undefined,
136136
recv_len: usize = 0,
137+
write_failed: bool = false,
137138
allocator: std.mem.Allocator,
138139

139140
pub fn init(allocator: std.mem.Allocator, id: u16, fd: posix.fd_t) !*ClientState {
@@ -347,7 +348,7 @@ pub const Server = struct {
347348

348349
if (tag == TAG_LISTEN) {
349350
// Accept a new client connection
350-
const new_fd = posix.accept(self.listen_fd, null, null, posix.SOCK.CLOEXEC) catch continue;
351+
const new_fd = posix.accept(self.listen_fd, null, null, posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK) catch continue;
351352
if (self.clients.count() >= MAX_CLIENTS) {
352353
posix.close(new_fd);
353354
continue;
@@ -448,6 +449,12 @@ pub const Server = struct {
448449
cs.rows = hp.rows;
449450
cs.screen.resize(hp.cols, hp.rows) catch {};
450451
cs.screen.invalidate();
452+
// Initialize active_panes for all existing tabs
453+
for (self.tab_manager.tabs, 0..) |maybe_tab, i| {
454+
if (maybe_tab) |t| {
455+
cs.active_panes[@intCast(i)] = pane_mod.firstLeafId(t.pane_tree.root);
456+
}
457+
}
451458
if (self.active_client == null) {
452459
self.active_client = client_id;
453460
}
@@ -557,15 +564,21 @@ pub const Server = struct {
557564
const new_tab_idx = self.tab_manager.createTab(new_pane_id) catch return;
558565
self.spawnPaneState(new_pane_id) catch return;
559566
cs.active_tab = new_tab_idx;
560-
cs.active_panes[new_tab_idx] = new_pane_id;
567+
// Propagate new tab's focused pane to ALL clients
568+
var it = self.clients.valueIterator();
569+
while (it.next()) |other_cs| {
570+
other_cs.*.active_panes[new_tab_idx] = new_pane_id;
571+
}
561572
self.invalidateAllClients();
562573
self.composeAll();
563574
},
564575
.close_tab => {
565-
self.destroyAllPanesInTab(cs.active_tab);
566-
const nearest = self.tab_manager.closeTab(cs.active_tab) orelse return;
567-
// Update ALL clients viewing the closed tab
576+
// Reject closing the last tab before destroying any state
577+
if (self.tab_manager.tabCount() <= 1) return;
568578
const closed_tab = cs.active_tab;
579+
self.destroyAllPanesInTab(closed_tab);
580+
const nearest = self.tab_manager.closeTab(closed_tab) orelse return;
581+
// Update ALL clients viewing the closed tab
569582
var it = self.clients.valueIterator();
570583
while (it.next()) |other_cs| {
571584
if (other_cs.*.active_tab == closed_tab) {
@@ -822,6 +835,8 @@ pub const Server = struct {
822835
while (it.next()) |cs| {
823836
self.composeForClient(cs.*);
824837
}
838+
// Disconnect clients that failed to write (corrupted protocol stream)
839+
self.sweepFailedClients();
825840
}
826841

827842
// ── resizePtysForClient ──────────────────────────────────────────────
@@ -1185,8 +1200,14 @@ pub const Server = struct {
11851200
const n = try protocol.encodeFrame(&buf, msg_type, payload);
11861201
var sent: usize = 0;
11871202
while (sent < n) {
1188-
const w = try posix.write(cs.fd, buf[sent..n]);
1189-
if (w == 0) return error.BrokenPipe;
1203+
const w = posix.write(cs.fd, buf[sent..n]) catch |err| {
1204+
cs.write_failed = true;
1205+
return err;
1206+
};
1207+
if (w == 0) {
1208+
cs.write_failed = true;
1209+
return error.BrokenPipe;
1210+
}
11901211
sent += w;
11911212
}
11921213
}
@@ -1213,6 +1234,27 @@ pub const Server = struct {
12131234
}
12141235
}
12151236

1237+
// ── sweepFailedClients ─────────────────────────────────────────────
1238+
1239+
/// Disconnect clients whose write_failed flag is set (protocol stream corrupted).
1240+
/// Safe to call after iteration since it collects IDs first.
1241+
fn sweepFailedClients(self: *Server) void {
1242+
var to_remove: [MAX_CLIENTS]u16 = undefined;
1243+
var count: u8 = 0;
1244+
var it = self.clients.iterator();
1245+
while (it.next()) |entry| {
1246+
if (entry.value_ptr.*.write_failed) {
1247+
if (count < MAX_CLIENTS) {
1248+
to_remove[count] = entry.key_ptr.*;
1249+
count += 1;
1250+
}
1251+
}
1252+
}
1253+
for (to_remove[0..count]) |cid| {
1254+
self.disconnectClient(cid);
1255+
}
1256+
}
1257+
12161258
// ── destroyAllPanesInTab ─────────────────────────────────────────────
12171259

12181260
fn destroyAllPanesInTab(self: *Server, tab_idx: u8) void {

0 commit comments

Comments
 (0)