repos / zmx

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

commit
52dda8a
parent
55084ff
author
Eric Bower
date
2025-10-13 14:18:26 -0400 EDT
refactor(daemon): we dont need to buffer pty read for partial utf8

ghostty already handles this for us.
1 files changed,  +7, -52
M src/daemon.zig
+7, -52
  1@@ -86,10 +86,6 @@ const Session = struct {
  2     vt_handler: VTHandler,
  3     attached_clients: std.AutoHashMap(std.posix.fd_t, void),
  4 
  5-    // Buffer for incomplete UTF-8 sequences from previous read
  6-    utf8_partial: [3]u8,
  7-    utf8_partial_len: usize,
  8-
  9     fn deinit(self: *Session) void {
 10         self.allocator.free(self.name);
 11         self.vt.deinit(self.allocator);
 12@@ -308,7 +304,7 @@ fn readCallback(
 13 }
 14 
 15 fn handleMessage(client: *Client, data: []const u8) !void {
 16-    std.debug.print("Received message from client fd={d}: {s}", .{ client.fd, data });
 17+    std.debug.print("Received message from client fd={d}: {s}\n", .{ client.fd, data });
 18 
 19     // Parse message type first for dispatching
 20     const type_parsed = try protocol.parseMessageType(client.allocator, data);
 21@@ -547,7 +543,7 @@ fn handleListSessions(ctx: *ServerContext, client: *Client) !void {
 22 
 23     try response.appendSlice(client.allocator, "]}}\n");
 24 
 25-    std.debug.print("Sending list response to client fd={d}: {s}", .{ client.fd, response.items });
 26+    std.debug.print("Sending list response to client fd={d}: {s}\n", .{ client.fd, response.items });
 27 
 28     const written = posix.write(client.fd, response.items) catch |err| {
 29         std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
 30@@ -814,57 +810,18 @@ fn readPtyCallback(
 31             return .disarm;
 32         }
 33 
 34-        // Combine any partial UTF-8 from previous read with new data
 35-        var combined_buf: [4096 + 3]u8 = undefined;
 36-        const total_len = session.utf8_partial_len + bytes_read;
 37-
 38-        if (session.utf8_partial_len > 0) {
 39-            @memcpy(combined_buf[0..session.utf8_partial_len], session.utf8_partial[0..session.utf8_partial_len]);
 40-            @memcpy(combined_buf[session.utf8_partial_len..total_len], read_buffer.slice[0..bytes_read]);
 41-        } else {
 42-            @memcpy(combined_buf[0..bytes_read], read_buffer.slice[0..bytes_read]);
 43-        }
 44-
 45-        const data = combined_buf[0..total_len];
 46-        std.debug.print("PTY output ({d} bytes, {d} from partial)\n", .{ bytes_read, session.utf8_partial_len });
 47-
 48-        // Check for incomplete UTF-8 sequence at end
 49-        var valid_len = total_len;
 50-        session.utf8_partial_len = 0;
 51-
 52-        if (total_len > 0) {
 53-            // Scan backwards to find if we have a partial UTF-8 sequence
 54-            var i = total_len;
 55-            const scan_start = if (total_len >= 4) total_len - 4 else 0;
 56-            while (i > 0 and i > scan_start) {
 57-                i -= 1;
 58-                const byte = data[i];
 59-                // Check if this is a UTF-8 start byte
 60-                if (byte & 0x80 == 0) break; // ASCII, we're good
 61-                if (byte & 0xC0 == 0xC0) {
 62-                    // This is a UTF-8 start byte, check if sequence is complete
 63-                    const expected_len: usize = if (byte & 0xE0 == 0xC0) 2 else if (byte & 0xF0 == 0xE0) 3 else if (byte & 0xF8 == 0xF0) 4 else 1;
 64-                    if (i + expected_len > total_len) {
 65-                        // Save partial sequence for next read
 66-                        session.utf8_partial_len = total_len - i;
 67-                        @memcpy(session.utf8_partial[0..session.utf8_partial_len], data[i..total_len]);
 68-                        valid_len = i;
 69-                    }
 70-                    break;
 71-                }
 72-            }
 73-        }
 74-
 75-        const valid_data = data[0..valid_len];
 76+        const total_len = bytes_read;
 77+        const data = read_buffer.slice[0..bytes_read];
 78+        std.debug.print("PTY output ({d} bytes)\n", .{bytes_read});
 79 
 80         // Build a sanitized buffer that only includes bytes we can safely send
 81-        var sanitized_buf = std.ArrayList(u8).initCapacity(session.allocator, valid_len) catch return .disarm;
 82+        var sanitized_buf = std.ArrayList(u8).initCapacity(session.allocator, total_len) catch return .disarm;
 83         defer sanitized_buf.deinit(session.allocator);
 84 
 85         // Parse through libghostty-vt byte-by-byte to handle invalid data
 86         // This is necessary because binary data (like /dev/urandom) can cause
 87         // panics in @enumFromInt when high bytes appear during escape sequences
 88-        for (valid_data) |byte| {
 89+        for (data) |byte| {
 90             // Skip high bytes when parser is not in ground state to avoid
 91             // @enumFromInt panic in execute() which expects u7 (0-127)
 92             if (session.vt_stream.parser.state != .ground and byte > 127) {
 93@@ -1103,8 +1060,6 @@ fn createSession(allocator: std.mem.Allocator, session_name: []const u8) !*Sessi
 94         },
 95         .vt_stream = undefined,
 96         .attached_clients = std.AutoHashMap(std.posix.fd_t, void).init(allocator),
 97-        .utf8_partial = undefined,
 98-        .utf8_partial_len = 0,
 99     };
100 
101     // Initialize the stream after session is created since handler needs terminal pointer