- commit
- b2b7ece
- parent
- 79f25b5
- author
- Eric Bower
- date
- 2025-10-11 17:03:26 -0400 EDT
fix: memory leak
6 files changed,
+61,
-12
+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
+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 }
+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;
+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 }
+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 }
+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 };