repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
ed69a16
parent
23203f8
author
Eric Bower
date
2025-10-12 22:25:24 -0400 EDT
fix: vt.resize on attach

fix: filter out characters ghostty cannot handle
1 files changed,  +50, -28
M src/daemon.zig
+50, -28
  1@@ -304,7 +304,7 @@ fn handleMessage(client: *Client, data: []const u8) !void {
  2         .attach_session_request => {
  3             const parsed = try protocol.parseMessage(protocol.AttachSessionRequest, client.allocator, data);
  4             defer parsed.deinit();
  5-            std.debug.print("Handling attach request for session: {s}\n", .{parsed.value.payload.session_name});
  6+            std.debug.print("Handling attach request for session: {s} ({}x{})\n", .{ parsed.value.payload.session_name, parsed.value.payload.cols, parsed.value.payload.rows });
  7             try handleAttachSession(client.server_ctx, client, parsed.value.payload.session_name, parsed.value.payload.rows, parsed.value.payload.cols);
  8         },
  9         .detach_session_request => {
 10@@ -551,19 +551,21 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
 11         break :blk new_session;
 12     };
 13 
 14-    // Update libghostty-vt terminal size
 15-    try session.vt.resize(session.allocator, cols, rows);
 16+    // Update libghostty-vt terminal size and PTY window size
 17+    // Only resize if we have valid dimensions
 18+    if (rows > 0 and cols > 0) {
 19+        try session.vt.resize(session.allocator, cols, rows);
 20 
 21-    // Update PTY window size
 22-    var ws = c.struct_winsize{
 23-        .ws_row = rows,
 24-        .ws_col = cols,
 25-        .ws_xpixel = 0,
 26-        .ws_ypixel = 0,
 27-    };
 28-    const result = c.ioctl(session.pty_master_fd, c.TIOCSWINSZ, &ws);
 29-    if (result < 0) {
 30-        return error.IoctlFailed;
 31+        var ws = c.struct_winsize{
 32+            .ws_row = rows,
 33+            .ws_col = cols,
 34+            .ws_xpixel = 0,
 35+            .ws_ypixel = 0,
 36+        };
 37+        const result = c.ioctl(session.pty_master_fd, c.TIOCSWINSZ, &ws);
 38+        if (result < 0) {
 39+            return error.IoctlFailed;
 40+        }
 41     }
 42 
 43     // Mark client as attached
 44@@ -591,21 +593,28 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
 45     // If reattaching, send the scrollback buffer (raw PTY output with colors)
 46     // Limit to last 64KB to avoid huge JSON messages
 47     if (is_reattach and session.buffer.items.len > 0) {
 48-        std.debug.print("Sending scrollback buffer: {d} bytes total\n", .{session.buffer.items.len});
 49-
 50-        const max_buffer_size = 64 * 1024;
 51-        const buffer_start = if (session.buffer.items.len > max_buffer_size)
 52-            session.buffer.items.len - max_buffer_size
 53-        else
 54-            0;
 55-        const buffer_slice = session.buffer.items[buffer_start..];
 56-
 57-        std.debug.print("Sending slice: {d} bytes (from offset {d})\n", .{ buffer_slice.len, buffer_start });
 58-
 59+        const buffer_slice = try session.vt.plainStringUnwrapped(client.allocator);
 60         try protocol.writeJson(ctx.allocator, client.fd, .pty_out, protocol.PtyOutput{
 61             .text = buffer_slice,
 62         });
 63         std.debug.print("Sent scrollback buffer to client fd={d}\n", .{client.fd});
 64+        defer client.allocator.free(buffer_slice);
 65+        //
 66+        // std.debug.print("Sending scrollback buffer: {d} bytes total\n", .{session.buffer.items.len});
 67+        //
 68+        // const max_buffer_size = 64 * 1024;
 69+        // const buffer_start = if (session.buffer.items.len > max_buffer_size)
 70+        //     session.buffer.items.len - max_buffer_size
 71+        // else
 72+        //     0;
 73+        // const buffer_slice = session.buffer.items[buffer_start..];
 74+        //
 75+        // std.debug.print("Sending slice: {d} bytes (from offset {d})\n", .{ buffer_slice.len, buffer_start });
 76+        //
 77+        // try protocol.writeJson(ctx.allocator, client.fd, .pty_out, protocol.PtyOutput{
 78+        //     .text = buffer_slice,
 79+        // });
 80+        // std.debug.print("Sent scrollback buffer to client fd={d}\n", .{client.fd});
 81     }
 82 }
 83 
 84@@ -852,10 +861,23 @@ fn readPtyCallback(
 85             std.debug.print("Buffer append error: {s}\n", .{@errorName(err)});
 86         };
 87 
 88-        // ALWAYS parse through libghostty-vt to maintain state
 89-        session.vt_stream.nextSlice(valid_data) catch |err| {
 90-            std.debug.print("VT parse error: {s}\n", .{@errorName(err)});
 91-        };
 92+        // Parse through libghostty-vt byte-by-byte to handle invalid data
 93+        // This is necessary because binary data (like /dev/urandom) can cause
 94+        // panics in @enumFromInt when high bytes appear during escape sequences
 95+        for (valid_data) |byte| {
 96+            // Skip high bytes when parser is not in ground state to avoid
 97+            // @enumFromInt panic in execute() which expects u7 (0-127)
 98+            if (session.vt_stream.parser.state != .ground and byte > 127) {
 99+                // Reset to ground state and skip this byte
100+                session.vt_stream.parser.state = .ground;
101+                continue;
102+            }
103+            session.vt_stream.next(byte) catch |err| {
104+                std.debug.print("VT parse error at byte 0x{x}: {s}\n", .{ byte, @errorName(err) });
105+                // Reset to ground state on any error
106+                session.vt_stream.parser.state = .ground;
107+            };
108+        }
109 
110         // Only proxy to clients if someone is attached
111         if (session.attached_clients.count() > 0 and valid_len > 0) {