repos / zmx

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

commit
b2b7ece
parent
79f25b5
author
Eric Bower
date
2025-10-11 17:03:26 -0400 EDT
fix: memory leak
6 files changed,  +61, -12
M src/attach.zig
+3, -2
 1@@ -53,7 +53,7 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
 2 
 3     const session_name = res.positionals[0] orelse {
 4         std.debug.print("Usage: zmx attach <session-name>\n", .{});
 5-        std.process.exit(1);
 6+        return error.MissingSessionName;
 7     };
 8 
 9     var thread_pool = xevg.ThreadPool.init(.{});
10@@ -75,8 +75,9 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
11     posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen()) catch |err| {
12         if (err == error.ConnectionRefused) {
13             std.debug.print("Error: Unable to connect to zmx daemon at {s}\nPlease start the daemon first with: zmx daemon\n", .{socket_path});
14+            return err;
15         }
16-        std.process.exit(1);
17+        return err;
18     };
19 
20     // Set raw mode after successful connection
M src/config.zig
+3, -1
 1@@ -3,6 +3,7 @@ const toml = @import("toml");
 2 
 3 pub const Config = struct {
 4     socket_path: []const u8 = "/tmp/zmx.sock",
 5+    socket_path_allocated: bool = false,
 6 
 7     pub fn load(allocator: std.mem.Allocator) !Config {
 8         const config_path = getConfigPath(allocator) catch |err| {
 9@@ -26,12 +27,13 @@ pub const Config = struct {
10 
11         const config = Config{
12             .socket_path = try allocator.dupe(u8, result.value.socket_path),
13+            .socket_path_allocated = true,
14         };
15         return config;
16     }
17 
18     pub fn deinit(self: *Config, allocator: std.mem.Allocator) void {
19-        if (!std.mem.eql(u8, self.socket_path, "/tmp/zmx.sock")) {
20+        if (self.socket_path_allocated) {
21             allocator.free(self.socket_path);
22         }
23     }
M src/daemon.zig
+50, -1
 1@@ -715,6 +715,41 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
 2     return output.toOwnedSlice(allocator);
 3 }
 4 
 5+fn notifyAttachedClientsAndCleanup(session: *Session, ctx: *ServerContext, reason: []const u8) void {
 6+    std.debug.print("Session '{s}' ending: {s}\n", .{ session.name, reason });
 7+
 8+    // Notify all attached clients
 9+    var it = session.attached_clients.keyIterator();
10+    while (it.next()) |client_fd| {
11+        const client = ctx.clients.get(client_fd.*) orelse continue;
12+        protocol.writeJson(
13+            client.allocator,
14+            client.fd,
15+            .kill_notification,
16+            protocol.KillNotification{ .session_name = session.name },
17+        ) catch |err| {
18+            std.debug.print("Failed to notify client {d}: {s}\n", .{ client_fd.*, @errorName(err) });
19+        };
20+        // Clear client's attached session reference
21+        if (client.attached_session) |attached| {
22+            client.allocator.free(attached);
23+            client.attached_session = null;
24+        }
25+    }
26+
27+    // Close PTY master fd
28+    posix.close(session.pty_master_fd);
29+
30+    // Remove from sessions map BEFORE cleaning up (session.deinit frees session.name)
31+    const session_name_copy = ctx.allocator.dupe(u8, session.name) catch return;
32+    defer ctx.allocator.free(session_name_copy);
33+    _ = ctx.sessions.remove(session_name_copy);
34+
35+    // Clean up session
36+    session.deinit();
37+    ctx.allocator.destroy(session);
38+}
39+
40 fn readPtyCallback(
41     pty_ctx_opt: ?*PtyReadContext,
42     loop: *xev.Loop,
43@@ -729,7 +764,10 @@ fn readPtyCallback(
44 
45     if (read_result) |bytes_read| {
46         if (bytes_read == 0) {
47-            std.debug.print("pty closed\n", .{});
48+            std.debug.print("PTY closed (EOF)\n", .{});
49+            notifyAttachedClientsAndCleanup(session, ctx, "PTY closed");
50+            ctx.allocator.destroy(pty_ctx);
51+            ctx.allocator.destroy(completion);
52             return .disarm;
53         }
54 
55@@ -871,7 +909,18 @@ fn readPtyCallback(
56             return .disarm;
57         }
58 
59+        // Fatal error - notify clients and clean up
60         std.debug.print("PTY read error: {s}\n", .{@errorName(err)});
61+        const error_msg = std.fmt.allocPrint(
62+            ctx.allocator,
63+            "PTY read error: {s}",
64+            .{@errorName(err)},
65+        ) catch "PTY read error";
66+        defer if (!std.mem.eql(u8, error_msg, "PTY read error")) ctx.allocator.free(error_msg);
67+
68+        notifyAttachedClientsAndCleanup(session, ctx, error_msg);
69+        ctx.allocator.destroy(pty_ctx);
70+        ctx.allocator.destroy(completion);
71         return .disarm;
72     }
73     unreachable;
M src/detach.zig
+3, -4
 1@@ -39,7 +39,7 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
 2     // Look for .zmx_client_fd_* files
 3     var dir = std.fs.cwd().openDir(home_dir, .{ .iterate = true }) catch {
 4         std.debug.print("Error: Cannot access home directory\n", .{});
 5-        std.process.exit(1);
 6+        return error.CannotAccessHomeDirectory;
 7     };
 8     defer dir.close();
 9 
10@@ -72,7 +72,7 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
11     if (session_name == null) {
12         std.debug.print("Error: Not currently attached to any session\n", .{});
13         std.debug.print("Use Ctrl-b d to detach from within an attached session\n", .{});
14-        std.process.exit(1);
15+        return error.NotAttached;
16     }
17     defer if (session_name) |name| allocator.free(name);
18 
19@@ -83,7 +83,6 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
20     posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen()) catch |err| {
21         if (err == error.ConnectionRefused) {
22             std.debug.print("Error: Unable to connect to zmx daemon at {s}\nPlease start the daemon first with: zmx daemon\n", .{socket_path});
23-            std.process.exit(1);
24         }
25         return err;
26     };
27@@ -115,6 +114,6 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
28     } else {
29         const error_msg = parsed.value.payload.error_message orelse "Unknown error";
30         std.debug.print("Failed to detach: {s}\n", .{error_msg});
31-        std.process.exit(1);
32+        return error.DetachFailed;
33     }
34 }
M src/kill.zig
+2, -3
 1@@ -33,7 +33,7 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
 2 
 3     const session_name = res.positionals[0] orelse {
 4         std.debug.print("Usage: zmx kill <session-name>\n", .{});
 5-        std.process.exit(1);
 6+        return error.MissingSessionName;
 7     };
 8 
 9     const unix_addr = try std.net.Address.initUnix(socket_path);
10@@ -43,7 +43,6 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
11     posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen()) catch |err| {
12         if (err == error.ConnectionRefused) {
13             std.debug.print("Error: Unable to connect to zmx daemon at {s}\nPlease start the daemon first with: zmx daemon\n", .{socket_path});
14-            std.process.exit(1);
15         }
16         return err;
17     };
18@@ -75,6 +74,6 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
19     } else {
20         const error_msg = parsed.value.payload.error_message orelse "Unknown error";
21         std.debug.print("Failed to kill session: {s}\n", .{error_msg});
22-        std.process.exit(1);
23+        return error.KillFailed;
24     }
25 }
M src/list.zig
+0, -1
1@@ -37,7 +37,6 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
2     posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen()) catch |err| {
3         if (err == error.ConnectionRefused) {
4             std.debug.print("Error: Unable to connect to zmx daemon at {s}\nPlease start the daemon first with: zmx daemon\n", .{socket_path});
5-            std.process.exit(1);
6         }
7         return err;
8     };