repos / zmx

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

commit
377c062
parent
6e6bc9c
author
Eric Bower
date
2026-01-20 16:03:54 -0500 EST
fix: gracefully shutdown daemon process

This change properly handles when the daemon process receives a SIGTERM.

Also during the kill process we properly forward SIGTERM to all children processes of the daemon.
1 files changed,  +31, -11
M src/main.zig
+31, -11
  1@@ -55,6 +55,7 @@ else
  2     c.forkpty;
  3 
  4 var sigwinch_received: std.atomic.Value(bool) = std.atomic.Value(bool).init(false);
  5+var sigterm_received: std.atomic.Value(bool) = std.atomic.Value(bool).init(false);
  6 
  7 const Client = struct {
  8     alloc: std.mem.Allocator,
  9@@ -241,7 +242,8 @@ const Daemon = struct {
 10 
 11     pub fn handleKill(self: *Daemon) void {
 12         std.log.info("kill received session={s}", .{self.session_name});
 13-        posix.kill(self.pid, posix.SIG.TERM) catch |err| {
 14+        // negative pid means kill process and children
 15+        posix.kill(-self.pid, posix.SIG.TERM) catch |err| {
 16             std.log.warn("failed to send SIGTERM to pty child err={s}", .{@errorName(err)});
 17         };
 18         self.shutdown();
 19@@ -680,6 +682,7 @@ fn ensureSession(daemon: *Daemon) !EnsureSessionResult {
 20                 };
 21             }
 22             try daemonLoop(daemon, server_sock_fd, pty_fd);
 23+            daemon.handleKill();
 24             _ = posix.waitpid(daemon.pid, 0);
 25             daemon.deinit();
 26             return .{ .created = true, .is_daemon = true };
 27@@ -1007,7 +1010,7 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
 28 
 29 fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 30     std.log.info("daemon started session={s} pty_fd={d}", .{ daemon.session_name, pty_fd });
 31-    var should_exit = false;
 32+    setupSigtermHandler();
 33     var poll_fds = try std.ArrayList(posix.pollfd).initCapacity(daemon.alloc, 8);
 34     defer poll_fds.deinit(daemon.alloc);
 35 
 36@@ -1021,7 +1024,12 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 37     var vt_stream = term.vtStream();
 38     defer vt_stream.deinit();
 39 
 40-    while (!should_exit and daemon.running) {
 41+    daemon_loop: while (daemon.running) {
 42+        if (sigterm_received.swap(false, .acq_rel)) {
 43+            std.log.info("SIGTERM received, shutting down gracefully session={s}", .{daemon.session_name});
 44+            break :daemon_loop;
 45+        }
 46+
 47         poll_fds.clearRetainingCapacity();
 48 
 49         try poll_fds.append(daemon.alloc, .{
 50@@ -1054,7 +1062,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 51 
 52         if (poll_fds.items[0].revents & (posix.POLL.ERR | posix.POLL.HUP | posix.POLL.NVAL) != 0) {
 53             std.log.err("server socket error revents={d}", .{poll_fds.items[0].revents});
 54-            should_exit = true;
 55+            break :daemon_loop;
 56         } else if (poll_fds.items[0].revents & posix.POLL.IN != 0) {
 57             const client_fd = try posix.accept(server_sock_fd, null, null, posix.SOCK.NONBLOCK | posix.SOCK.CLOEXEC);
 58             const client = try daemon.alloc.create(Client);
 59@@ -1081,7 +1089,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 60                 if (n == 0) {
 61                     // EOF: Shell exited
 62                     std.log.info("shell exited pty_fd={d}", .{pty_fd});
 63-                    should_exit = true;
 64+                    break :daemon_loop;
 65                 } else {
 66                     // Feed PTY output to terminal emulator for state tracking
 67                     try vt_stream.nextSlice(buf[0..n]);
 68@@ -1119,14 +1127,14 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 69                     if (err == error.WouldBlock) continue;
 70                     std.log.debug("client read err={s} fd={d}", .{ @errorName(err), client.socket_fd });
 71                     const last = daemon.closeClient(client, i, false);
 72-                    if (last) should_exit = true;
 73+                    if (last) break :daemon_loop;
 74                     continue;
 75                 };
 76 
 77                 if (n == 0) {
 78                     // Client closed connection
 79                     const last = daemon.closeClient(client, i, false);
 80-                    if (last) should_exit = true;
 81+                    if (last) break :daemon_loop;
 82                     continue;
 83                 }
 84 
 85@@ -1145,8 +1153,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 86                         },
 87                         .Kill => {
 88                             daemon.handleKill();
 89-                            should_exit = true;
 90-                            break :clients_loop;
 91+                            break :daemon_loop;
 92                         },
 93                         .Info => try daemon.handleInfo(client),
 94                         .History => try daemon.handleHistory(client, &term, msg.payload),
 95@@ -1162,7 +1169,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 96                     if (err == error.WouldBlock) break :blk 0;
 97                     // Error on write, close client
 98                     const last = daemon.closeClient(client, i, false);
 99-                    if (last) should_exit = true;
100+                    if (last) break :daemon_loop;
101                     continue;
102                 };
103 
104@@ -1177,7 +1184,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
105 
106             if (revents & (posix.POLL.HUP | posix.POLL.ERR | posix.POLL.NVAL) != 0) {
107                 const last = daemon.closeClient(client, i, false);
108-                if (last) should_exit = true;
109+                if (last) break :daemon_loop;
110             }
111         }
112     }
113@@ -1334,6 +1341,10 @@ fn handleSigwinch(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c
114     sigwinch_received.store(true, .release);
115 }
116 
117+fn handleSigterm(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void {
118+    sigterm_received.store(true, .release);
119+}
120+
121 fn setupSigwinchHandler() void {
122     const act: posix.Sigaction = .{
123         .handler = .{ .sigaction = handleSigwinch },
124@@ -1343,6 +1354,15 @@ fn setupSigwinchHandler() void {
125     posix.sigaction(posix.SIG.WINCH, &act, null);
126 }
127 
128+fn setupSigtermHandler() void {
129+    const act: posix.Sigaction = .{
130+        .handler = .{ .sigaction = handleSigterm },
131+        .mask = posix.sigemptyset(),
132+        .flags = posix.SA.SIGINFO,
133+    };
134+    posix.sigaction(posix.SIG.TERM, &act, null);
135+}
136+
137 fn getTerminalSize(fd: i32) ipc.Resize {
138     var ws: c.struct_winsize = undefined;
139     if (c.ioctl(fd, c.TIOCGWINSZ, &ws) == 0 and ws.ws_row > 0 and ws.ws_col > 0) {