repos / zmx

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

commit
8bdbac6
parent
f973821
author
Eric Bower
date
2025-11-23 20:47:08 -0500 EST
feat: list command
2 files changed,  +71, -5
M src/ipc.zig
+6, -0
 1@@ -8,6 +8,7 @@ pub const Tag = enum(u8) {
 2     Detach = 3,
 3     DetachAll = 4,
 4     Kill = 5,
 5+    Info = 6,
 6 };
 7 
 8 pub const Header = packed struct {
 9@@ -20,6 +21,11 @@ pub const Resize = packed struct {
10     cols: u16,
11 };
12 
13+pub const Info = packed struct {
14+    clients_len: usize,
15+    pid: i32,
16+};
17+
18 pub fn expectedLength(data: []const u8) ?usize {
19     if (data.len < @sizeOf(Header)) return null;
20     const header = std.mem.bytesToValue(Header, data[0..@sizeOf(Header)]);
M src/main.zig
+65, -5
  1@@ -65,6 +65,7 @@ const Daemon = struct {
  2     session_name: []const u8,
  3     socket_path: []const u8,
  4     running: bool,
  5+    pid: i32,
  6 
  7     pub fn deinit(self: *Daemon) void {
  8         self.clients.deinit(self.alloc);
  9@@ -114,6 +115,8 @@ pub fn main() !void {
 10 
 11     if (std.mem.eql(u8, cmd, "help")) {
 12         return help();
 13+    } else if (std.mem.eql(u8, cmd, "list")) {
 14+        return list(&cfg);
 15     } else if (std.mem.eql(u8, cmd, "detach")) {
 16         return detachAll(&cfg);
 17     } else if (std.mem.eql(u8, cmd, "kill")) {
 18@@ -135,6 +138,7 @@ pub fn main() !void {
 19             .clients = clients,
 20             .session_name = session_name,
 21             .socket_path = undefined,
 22+            .pid = undefined,
 23         };
 24         daemon.socket_path = try getSocketPath(alloc, cfg.socket_dir, session_name);
 25         std.log.info("socket path={s}", .{daemon.socket_path});
 26@@ -148,8 +152,53 @@ fn help() !void {
 27     std.log.info("running cmd=help", .{});
 28 }
 29 
 30-fn list(_: *Cfg) !void {
 31+fn list(cfg: *Cfg) !void {
 32     std.log.info("running cmd=list", .{});
 33+    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
 34+    defer _ = gpa.deinit();
 35+    const alloc = gpa.allocator();
 36+
 37+    var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{ .iterate = true });
 38+    defer dir.close();
 39+    var iter = dir.iterate();
 40+    while (try iter.next()) |entry| {
 41+        if (try sessionExists(dir, entry.name)) {
 42+            const socket_path = try getSocketPath(alloc, cfg.socket_dir, entry.name);
 43+            defer alloc.free(socket_path);
 44+
 45+            const fd = sessionConnect(socket_path) catch |err| {
 46+                std.log.warn("could not connect to session {s}: {s}", .{ entry.name, @errorName(err) });
 47+                continue;
 48+            };
 49+            defer posix.close(fd);
 50+
 51+            try ipc.send(fd, .Info, "");
 52+
 53+            var sb = try ipc.SocketBuffer.init(alloc);
 54+            defer sb.deinit();
 55+
 56+            var info: ?ipc.Info = null;
 57+            read_loop: while (true) {
 58+                const n = try sb.read(fd);
 59+                if (n == 0) break; // EOF
 60+
 61+                while (sb.next()) |msg| {
 62+                    if (msg.header.tag == .Info) {
 63+                        if (msg.payload.len == @sizeOf(ipc.Info)) {
 64+                            info = std.mem.bytesToValue(ipc.Info, msg.payload[0..@sizeOf(ipc.Info)]);
 65+                            break :read_loop;
 66+                        }
 67+                    }
 68+                }
 69+            }
 70+
 71+            if (info) |i| {
 72+                std.log.info("session_name={s}\tpid={d}\tclients={d}", .{ entry.name, i.pid, i.clients_len });
 73+            } else {
 74+                std.log.info("session_name={s}\tpid=?\tclients=?", .{entry.name});
 75+            }
 76+        }
 77+    }
 78 }
 79 
 80 fn detachAll(cfg: *Cfg) !void {
 81@@ -499,14 +548,14 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 82                 const n = client.read_buf.read(client.socket_fd) catch |err| {
 83                     if (err == error.WouldBlock) continue;
 84                     std.log.warn("client read error err={s}", .{@errorName(err)});
 85-                    const last = daemon.closeClient(client, i, true);
 86+                    const last = daemon.closeClient(client, i, false);
 87                     if (last) should_exit = true;
 88                     continue;
 89                 };
 90 
 91                 if (n == 0) {
 92                     // Client closed connection
 93-                    const last = daemon.closeClient(client, i, true);
 94+                    const last = daemon.closeClient(client, i, false);
 95                     if (last) should_exit = true;
 96                     continue;
 97                 }
 98@@ -547,6 +596,16 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 99                             should_exit = true;
100                             break :clients_loop;
101                         },
102+                        .Info => {
103+                            // subtract current client since it's just fetching info
104+                            const clients_len = daemon.clients.items.len - 1;
105+                            const info = ipc.Info{
106+                                .clients_len = clients_len,
107+                                .pid = daemon.pid,
108+                            };
109+                            try ipc.appendMessage(daemon.alloc, &client.write_buf, .Info, std.mem.asBytes(&info));
110+                            client.has_pending_output = true;
111+                        },
112                         .Output => {}, // Clients shouldn't send output
113                     }
114                 }
115@@ -557,7 +616,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
116                 const n = posix.write(client.socket_fd, client.write_buf.items) catch |err| blk: {
117                     if (err == error.WouldBlock) break :blk 0;
118                     // Error on write, close client
119-                    const last = daemon.closeClient(client, i, true);
120+                    const last = daemon.closeClient(client, i, false);
121                     if (last) should_exit = true;
122                     continue;
123                 };
124@@ -572,7 +631,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
125             }
126 
127             if (revents & (posix.POLL.HUP | posix.POLL.ERR | posix.POLL.NVAL) != 0) {
128-                const last = daemon.closeClient(client, i, true);
129+                const last = daemon.closeClient(client, i, false);
130                 if (last) should_exit = true;
131             }
132         }
133@@ -611,6 +670,7 @@ fn spawnPty(daemon: *Daemon) !c_int {
134     }
135     // master pid code path
136 
137+    daemon.pid = pid;
138     std.log.info("created pty session: session_name={s} master_pid={d} child_pid={d}", .{ daemon.session_name, master_fd, pid });
139 
140     // make pty non-blocking