repos / zmx

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

commit
686ee94
parent
c2e856c
author
Eric Bower
date
2025-11-26 14:50:43 -0500 EST
refactor: cleanup logs
2 files changed,  +30, -35
M src/log.zig
+1, -1
1@@ -62,7 +62,7 @@ pub const LogSystem = struct {
2             self.current_size += total_len;
3 
4             var buf: [4096]u8 = undefined;
5-            var w = f.writer(&buf);
6+            var w = f.writerStreaming(&buf);
7             w.interface.print(prefix ++ format ++ "\n", prefix_args ++ args) catch {};
8             w.interface.flush() catch {};
9         }
M src/main.zig
+29, -34
  1@@ -97,12 +97,12 @@ const Daemon = struct {
  2     }
  3 
  4     pub fn closeClient(self: *Daemon, client: *Client, i: usize, shutdown_on_last: bool) bool {
  5-        std.log.info("closing client idx={d}", .{i});
  6+        const fd = client.socket_fd;
  7         client.deinit();
  8         self.alloc.destroy(client);
  9         _ = self.clients.orderedRemove(i);
 10+        std.log.info("client disconnected fd={d} remaining={d}", .{ fd, self.clients.items.len });
 11         if (shutdown_on_last and self.clients.items.len == 0) {
 12-            std.log.info("last client disconnected, shutting down", .{});
 13             self.shutdown();
 14             return true;
 15         }
 16@@ -126,8 +126,6 @@ pub fn main() !void {
 17     try log_system.init(alloc, log_path);
 18     defer log_system.deinit();
 19 
 20-    std.log.info("running cli", .{});
 21-
 22     const cmd = args.next() orelse {
 23         return list(&cfg);
 24     };
 25@@ -168,11 +166,23 @@ pub fn main() !void {
 26 }
 27 
 28 fn help() !void {
 29-    std.log.info("running cmd=help", .{});
 30+    const help_text =
 31+        \\zmx - session persistence for terminal processes
 32+        \\
 33+        \\Usage: zmx <command> [args]
 34+        \\
 35+        \\Commands:
 36+        \\  attach <name>   Create or attach to a session
 37+        \\  detach          Detach from current session (or Ctrl+\)
 38+        \\  list            List active sessions
 39+        \\  kill <name>     Kill a session and all attached clients
 40+        \\  help            Show this help message
 41+        \\
 42+    ;
 43+    try std.fs.File.stdout().writeAll(help_text);
 44 }
 45 
 46 fn list(cfg: *Cfg) !void {
 47-    std.log.info("running cmd=list", .{});
 48     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
 49     defer _ = gpa.deinit();
 50     const alloc = gpa.allocator();
 51@@ -236,7 +246,6 @@ fn list(cfg: *Cfg) !void {
 52 }
 53 
 54 fn detachAll(cfg: *Cfg) !void {
 55-    std.log.info("running cmd=detach", .{});
 56     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
 57     defer _ = gpa.deinit();
 58     const alloc = gpa.allocator();
 59@@ -260,7 +269,6 @@ fn detachAll(cfg: *Cfg) !void {
 60 }
 61 
 62 fn kill(cfg: *Cfg, session_name: []const u8) !void {
 63-    std.log.info("running cmd=kill session_name={s}", .{session_name});
 64     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
 65     defer _ = gpa.deinit();
 66     const alloc = gpa.allocator();
 67@@ -289,8 +297,6 @@ fn kill(cfg: *Cfg, session_name: []const u8) !void {
 68 }
 69 
 70 fn attach(daemon: *Daemon) !void {
 71-    std.log.info("running cmd=attach {s}", .{daemon.session_name});
 72-
 73     var dir = try std.fs.openDirAbsolute(daemon.cfg.socket_dir, .{});
 74     defer dir.close();
 75 
 76@@ -298,7 +304,6 @@ fn attach(daemon: *Daemon) !void {
 77     var should_create = !exists;
 78 
 79     if (exists) {
 80-        std.log.info("reattaching to session session_name={s}", .{daemon.session_name});
 81         const fd = sessionConnect(daemon.socket_path) catch |err| switch (err) {
 82             error.ConnectionRefused => blk: {
 83                 std.log.warn("stale socket found, cleaning up fname={s}", .{daemon.socket_path});
 84@@ -314,9 +319,8 @@ fn attach(daemon: *Daemon) !void {
 85     }
 86 
 87     if (should_create) {
 88-        std.log.info("creating session session_name={s}", .{daemon.session_name});
 89+        std.log.info("creating session={s}", .{daemon.session_name});
 90         const server_sock_fd = try createSocket(daemon.socket_path);
 91-        std.log.info("unix socket created server_sock_fd={d}", .{server_sock_fd});
 92 
 93         const pid = try posix.fork();
 94         if (pid == 0) { // child
 95@@ -345,8 +349,7 @@ fn attach(daemon: *Daemon) !void {
 96     }
 97 
 98     const client_sock = try sessionConnect(daemon.socket_path);
 99-
100-    std.log.info("setting client stdin to raw mode", .{});
101+    std.log.info("attached session={s}", .{daemon.session_name});
102     //  this is typically used with tcsetattr() to modify terminal settings.
103     //      - you first get the current settings with tcgetattr()
104     //      - modify the desired attributes in the termios structure
105@@ -388,7 +391,6 @@ fn attach(daemon: *Daemon) !void {
106 }
107 
108 fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
109-    std.log.info("starting client loop client_sock_fd={d}", .{client_sock_fd});
110     // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
111     const alloc = std.heap.c_allocator;
112 
113@@ -483,14 +485,12 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
114             const n = read_buf.read(client_sock_fd) catch |err| {
115                 if (err == error.WouldBlock) continue;
116                 if (err == error.ConnectionResetByPeer or err == error.BrokenPipe) {
117-                    std.log.info("client daemon disconnected", .{});
118                     return;
119                 }
120-                std.log.err("client read error from daemon: {s}", .{@errorName(err)});
121+                std.log.err("daemon read err={s}", .{@errorName(err)});
122                 return err;
123             };
124             if (n == 0) {
125-                std.log.info("client daemon closed connection", .{});
126                 return; // Server closed connection
127             }
128 
129@@ -523,7 +523,7 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
130 }
131 
132 fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
133-    std.log.info("starting daemon loop server_sock_fd={d} pty_fd={d}", .{ server_sock_fd, pty_fd });
134+    std.log.info("daemon started session={s} pty_fd={d}", .{ daemon.session_name, pty_fd });
135     var should_exit = false;
136     var poll_fds = try std.ArrayList(posix.pollfd).initCapacity(daemon.alloc, 8);
137     defer poll_fds.deinit(daemon.alloc);
138@@ -564,7 +564,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
139         };
140 
141         if (poll_fds.items[0].revents & (posix.POLL.ERR | posix.POLL.HUP | posix.POLL.NVAL) != 0) {
142-            std.log.err("server socket error revents={}", .{poll_fds.items[0].revents});
143+            std.log.err("server socket error revents={d}", .{poll_fds.items[0].revents});
144             should_exit = true;
145         } else if (poll_fds.items[0].revents & posix.POLL.IN != 0) {
146             const client_fd = try posix.accept(server_sock_fd, null, null, posix.SOCK.NONBLOCK | posix.SOCK.CLOEXEC);
147@@ -577,6 +577,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
148             };
149             client.write_buf = try std.ArrayList(u8).initCapacity(client.alloc, 4096);
150             try daemon.clients.append(daemon.alloc, client);
151+            std.log.info("client connected fd={d} total={d}", .{ client_fd, daemon.clients.items.len });
152 
153             // TODO: Replace this SIGWINCH hack with proper terminal state restoration
154             // using ghostty-vt to maintain a virtual terminal buffer and replay it
155@@ -634,7 +635,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
156             if (revents & posix.POLL.IN != 0) {
157                 const n = client.read_buf.read(client.socket_fd) catch |err| {
158                     if (err == error.WouldBlock) continue;
159-                    std.log.warn("client read error err={s}", .{@errorName(err)});
160+                    std.log.debug("client read err={s} fd={d}", .{ @errorName(err), client.socket_fd });
161                     const last = daemon.closeClient(client, i, false);
162                     if (last) should_exit = true;
163                     continue;
164@@ -664,13 +665,16 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
165                                     .ws_ypixel = 0,
166                                 };
167                                 _ = c.ioctl(pty_fd, c.TIOCSWINSZ, &ws);
168+                                std.log.debug("resize rows={d} cols={d}", .{ resize.rows, resize.cols });
169                             }
170                         },
171                         .Detach => {
172+                            std.log.info("client detach fd={d}", .{client.socket_fd});
173                             _ = daemon.closeClient(client, i, false);
174                             break :clients_loop;
175                         },
176                         .DetachAll => {
177+                            std.log.info("detach all clients={d}", .{daemon.clients.items.len});
178                             for (daemon.clients.items) |client_to_close| {
179                                 client_to_close.deinit();
180                                 daemon.alloc.destroy(client_to_close);
181@@ -679,6 +683,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
182                             break :clients_loop;
183                         },
184                         .Kill => {
185+                            std.log.info("kill received session={s}", .{daemon.session_name});
186                             daemon.shutdown();
187                             should_exit = true;
188                             break :clients_loop;
189@@ -726,7 +731,6 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
190 }
191 
192 fn spawnPty(daemon: *Daemon) !c_int {
193-    std.log.info("spawning pty session_name={s}", .{daemon.session_name});
194     // Get terminal size
195     var orig_ws: c.struct_winsize = undefined;
196     const result = c.ioctl(posix.STDOUT_FILENO, c.TIOCGWINSZ, &orig_ws);
197@@ -756,9 +760,8 @@ fn spawnPty(daemon: *Daemon) !c_int {
198         std.posix.exit(1);
199     }
200     // master pid code path
201-
202     daemon.pid = pid;
203-    std.log.info("created pty session: session_name={s} master_pid={d} child_pid={d}", .{ daemon.session_name, master_fd, pid });
204+    std.log.info("pty spawned session={s} pid={d}", .{ daemon.session_name, pid });
205 
206     // make pty non-blocking
207     const flags = try posix.fcntl(master_fd, posix.F.GETFL, 0);
208@@ -770,14 +773,7 @@ fn sessionConnect(fname: []const u8) !i32 {
209     var unix_addr = try std.net.Address.initUnix(fname);
210     const socket_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0);
211     errdefer posix.close(socket_fd);
212-    posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen()) catch |err| {
213-        if (err == error.ConnectionRefused) {
214-            std.log.err("unable to connect to unix socket fname={s}", .{fname});
215-            return err;
216-        }
217-        return err;
218-    };
219-    std.log.info("unix socket connected client_socket_fd={d}", .{socket_fd});
220+    try posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen());
221     return socket_fd;
222 }
223 
224@@ -793,7 +789,6 @@ fn sessionExists(dir: std.fs.Dir, name: []const u8) !bool {
225 }
226 
227 fn createSocket(fname: []const u8) !i32 {
228-    std.log.info("creating unix socket fname={s}", .{fname});
229     // AF.UNIX: Unix domain socket for local IPC with client processes
230     // SOCK.STREAM: Reliable, bidirectional communication
231     // SOCK.NONBLOCK: Set socket to non-blocking