repos / zmx

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

commit
34e2e3b
parent
f03cdc1
author
Eric Bower
date
2025-10-11 16:03:12 -0400 EDT
fix: use xev for pty writes
1 files changed,  +54, -4
M src/daemon.zig
+54, -4
 1@@ -40,6 +40,12 @@ const PtyReadContext = struct {
 2     server_ctx: *ServerContext,
 3 };
 4 
 5+// Context for PTY write callbacks
 6+const PtyWriteContext = struct {
 7+    allocator: std.mem.Allocator,
 8+    message: []u8,
 9+};
10+
11 // A PTY session that manages a persistent shell process
12 // Stores the PTY master file descriptor, shell process PID, scrollback buffer,
13 // and a read buffer for async I/O with libxev
14@@ -894,14 +900,35 @@ fn readPtyCallback(
15             response_buf.appendSlice(session.allocator, "\"}}\n") catch return .disarm;
16             const response = response_buf.items;
17 
18-            // Send to all attached clients
19+            // Send to all attached clients using async write
20             var it = session.attached_clients.keyIterator();
21             while (it.next()) |client_fd| {
22                 const attached_client = ctx.clients.get(client_fd.*) orelse continue;
23-                std.debug.print("Sending response to client fd={d}\n", .{client_fd.*});
24-                _ = posix.write(attached_client.fd, response) catch |err| {
25-                    std.debug.print("Error writing to fd={d}: {s}\n", .{ client_fd.*, @errorName(err) });
26+                const owned_response = session.allocator.dupe(u8, response) catch continue;
27+
28+                const write_ctx = session.allocator.create(PtyWriteContext) catch {
29+                    session.allocator.free(owned_response);
30+                    continue;
31+                };
32+                write_ctx.* = .{
33+                    .allocator = session.allocator,
34+                    .message = owned_response,
35                 };
36+
37+                const write_completion = session.allocator.create(xev.Completion) catch {
38+                    session.allocator.free(owned_response);
39+                    session.allocator.destroy(write_ctx);
40+                    continue;
41+                };
42+
43+                attached_client.stream.write(
44+                    loop,
45+                    write_completion,
46+                    .{ .slice = owned_response },
47+                    PtyWriteContext,
48+                    write_ctx,
49+                    ptyWriteCallback,
50+                );
51             }
52         }
53 
54@@ -935,6 +962,29 @@ fn readPtyCallback(
55     unreachable;
56 }
57 
58+fn ptyWriteCallback(
59+    write_ctx_opt: ?*PtyWriteContext,
60+    _: *xev.Loop,
61+    completion: *xev.Completion,
62+    _: xev.Stream,
63+    _: xev.WriteBuffer,
64+    write_result: xev.WriteError!usize,
65+) xev.CallbackAction {
66+    const write_ctx = write_ctx_opt.?;
67+    const allocator = write_ctx.allocator;
68+
69+    if (write_result) |_| {
70+        // Successfully sent PTY output to client
71+    } else |_| {
72+        // Silently ignore write errors to prevent log spam
73+    }
74+
75+    allocator.free(write_ctx.message);
76+    allocator.destroy(write_ctx);
77+    allocator.destroy(completion);
78+    return .disarm;
79+}
80+
81 fn execShellWithPrompt(allocator: std.mem.Allocator, session_name: []const u8, shell: [*:0]const u8) noreturn {
82     // Detect shell type and add prompt injection
83     const shell_name = std.fs.path.basename(std.mem.span(shell));