Commit 43e6f0f

Eric Bower  ·  2026-04-03 14:22:23 -0400 EDT
parent 66c8e0e
feat(attach): support switching sessions

Previously we did not allow users to switch to a session from within a session
via `zmx attach`.

Now users are able to run `zmx attach` from within a session and it'll properly
detach and then reattach to the new session.

References: https://github.com/neurosnap/zmx/issues/91
2 files changed,  +114, -8
+1, -0
......@@ -15,6 +15,7 @@ pub const Tag = enum(u8) {
1515 History = 8,
1616 Run = 9,
1717 Ack = 10,
18+ Switch = 11,
1819 // Non-exhaustive: this enum comes off the wire via bytesToValue and
1920 // @enumFromInt, so out-of-range values (11-255) are representable
2021 // rather than UB. Switches must handle `_` (unknown tag).
+113, -8
......@@ -635,6 +635,27 @@ const Daemon = struct {
635635 }
636636 }
637637
638+ pub fn handleSwitch(self: *Daemon, session_name: []const u8) !void {
639+ for (self.clients.items) |client| {
640+ if (self.leader_client_fd == client.socket_fd) {
641+ ipc.appendMessage(
642+ self.alloc,
643+ &client.write_buf,
644+ .Switch,
645+ session_name,
646+ ) catch |err| {
647+ std.log.warn(
648+ "failed to buffer terminal state for client err={s}",
649+ .{@errorName(err)},
650+ );
651+ };
652+ client.has_pending_output = true;
653+ return;
654+ }
655+ }
656+ return error.NoLeaderFound;
657+ }
658+
638659 pub fn handleInit(
639660 self: *Daemon,
640661 client: *Client,
......@@ -1197,10 +1218,46 @@ fn history(cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !voi
11971218 }
11981219 }
11991220
1221+fn switchSesh(daemon: *Daemon, current_sesh: []const u8) !void {
1222+ // we want daemon.session_name because that's the session name the user provided during zmx attach
1223+ // instead of the name of the session they are currently inside of.
1224+ const next_session = daemon.session_name;
1225+
1226+ const socket_path = socket.getSocketPath(daemon.alloc, daemon.cfg.socket_dir, current_sesh) catch |err| switch (err) {
1227+ error.NameTooLong => return socket.printSessionNameTooLong(current_sesh, daemon.cfg.socket_dir),
1228+ error.OutOfMemory => return err,
1229+ };
1230+ defer daemon.alloc.free(socket_path);
1231+
1232+ var dir = try std.fs.openDirAbsolute(daemon.cfg.socket_dir, .{});
1233+ defer dir.close();
1234+
1235+ const exists = try socket.sessionExists(dir, current_sesh);
1236+ if (!exists) {
1237+ var buf: [4096]u8 = undefined;
1238+ var w = std.fs.File.stderr().writer(&buf);
1239+ w.interface.print("error: session \"{s}\" does not exist\n", .{current_sesh}) catch {};
1240+ w.interface.flush() catch {};
1241+ return error.SessionNotFound;
1242+ }
1243+ const result = ipc.probeSession(daemon.alloc, socket_path) catch |err| {
1244+ std.log.err("session unresponsive: {s}", .{@errorName(err)});
1245+ if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, current_sesh);
1246+ return;
1247+ };
1248+ defer posix.close(result.fd);
1249+
1250+ ipc.send(result.fd, .Switch, next_session) catch |err| switch (err) {
1251+ error.BrokenPipe, error.ConnectionResetByPeer => return,
1252+ else => return err,
1253+ };
1254+}
1255+
12001256 fn attach(daemon: *Daemon) !void {
12011257 const sesh = socket.getSeshNameFromEnv();
12021258 if (sesh.len > 0) {
1203- return error.CannotAttachToSessionInSession;
1259+ return switchSesh(daemon, sesh);
1260+ // return error.CannotAttachToSessionInSession;
12041261 }
12051262
12061263 const result = try daemon.ensureSession();
......@@ -1264,7 +1321,42 @@ fn attach(daemon: *Daemon) !void {
12641321 const clear_seq = "\x1b[2J\x1b[H";
12651322 _ = try posix.write(posix.STDOUT_FILENO, clear_seq);
12661323
1267- try clientLoop(client_sock);
1324+ const looper = try clientLoop(client_sock);
1325+ switch (looper.kind) {
1326+ .detach => return,
1327+ .switch_session => {
1328+ if (looper.session_name) |session_name| {
1329+ var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
1330+ const cwd = std.posix.getcwd(&cwd_buf) catch "";
1331+ const target_path = socket.getSocketPath(
1332+ daemon.alloc,
1333+ daemon.cfg.socket_dir,
1334+ session_name,
1335+ ) catch |err| switch (err) {
1336+ error.NameTooLong => return socket.printSessionNameTooLong(
1337+ session_name,
1338+ daemon.cfg.socket_dir,
1339+ ),
1340+ error.OutOfMemory => return err,
1341+ };
1342+
1343+ const clients = try std.ArrayList(*Client).initCapacity(daemon.alloc, 10);
1344+ var target_daemon = Daemon{
1345+ .running = true,
1346+ .cfg = daemon.cfg,
1347+ .alloc = daemon.alloc,
1348+ .clients = clients,
1349+ .session_name = session_name,
1350+ .socket_path = target_path,
1351+ .pid = undefined,
1352+ .cwd = cwd,
1353+ .created_at = @intCast(std.time.timestamp()),
1354+ .leader_client_fd = null,
1355+ };
1356+ return attach(&target_daemon);
1357+ }
1358+ },
1359+ }
12681360 }
12691361
12701362 fn run(daemon: *Daemon, command_args: [][]const u8) !void {
......@@ -1398,9 +1490,17 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void {
13981490 return error.NoAckReceived;
13991491 }
14001492
1493+const ClientResult = struct {
1494+ kind: enum {
1495+ detach,
1496+ switch_session,
1497+ },
1498+ session_name: ?[]const u8,
1499+};
1500+
14011501 /// clientLoop sends ipc commands to its corresponding daemon. It uses poll() as its non-blocking
14021502 /// mechanism. It will send stdin to the daemon and receive stdout from the daemon.
1403-fn clientLoop(client_sock_fd: i32) !void {
1503+fn clientLoop(client_sock_fd: i32) !ClientResult {
14041504 // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
14051505 const alloc = std.heap.c_allocator;
14061506 defer posix.close(client_sock_fd);
......@@ -1496,7 +1596,7 @@ fn clientLoop(client_sock_fd: i32) !void {
14961596 }
14971597 } else {
14981598 // EOF on stdin
1499- return;
1599+ return ClientResult{ .kind = .detach, .session_name = null };
15001600 }
15011601 }
15021602 }
......@@ -1506,13 +1606,14 @@ fn clientLoop(client_sock_fd: i32) !void {
15061606 const n = read_buf.read(client_sock_fd) catch |err| {
15071607 if (err == error.WouldBlock) continue;
15081608 if (err == error.ConnectionResetByPeer or err == error.BrokenPipe) {
1509- return;
1609+ return ClientResult{ .kind = .detach, .session_name = null };
15101610 }
15111611 std.log.err("daemon read err={s}", .{@errorName(err)});
15121612 return err;
15131613 };
15141614 if (n == 0) {
1515- return; // Server closed connection
1615+ // Server closed connection
1616+ return ClientResult{ .kind = .detach, .session_name = null };
15161617 }
15171618
15181619 while (read_buf.next()) |msg| {
......@@ -1533,6 +1634,9 @@ fn clientLoop(client_sock_fd: i32) !void {
15331634 std.mem.asBytes(&next_size),
15341635 );
15351636 },
1637+ .Switch => {
1638+ return ClientResult{ .kind = .switch_session, .session_name = try alloc.dupe(u8, msg.payload) };
1639+ },
15361640 else => {},
15371641 }
15381642 }
......@@ -1544,7 +1648,7 @@ fn clientLoop(client_sock_fd: i32) !void {
15441648 const n = posix.write(client_sock_fd, sock_write_buf.items) catch |err| blk: {
15451649 if (err == error.WouldBlock) break :blk 0;
15461650 if (err == error.ConnectionResetByPeer or err == error.BrokenPipe) {
1547- return;
1651+ return ClientResult{ .kind = .detach, .session_name = null };
15481652 }
15491653 return err;
15501654 };
......@@ -1565,7 +1669,7 @@ fn clientLoop(client_sock_fd: i32) !void {
15651669 }
15661670
15671671 if (poll_fds.items[1].revents & (posix.POLL.HUP | posix.POLL.ERR | posix.POLL.NVAL) != 0) {
1568- return;
1672+ return ClientResult{ .kind = .detach, .session_name = null };
15691673 }
15701674 }
15711675 }
......@@ -1766,6 +1870,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
17661870 switch (msg.header.tag) {
17671871 .Input => try daemon.handleInput(client, msg.payload),
17681872 .Init => try daemon.handleInit(client, pty_fd, &term, msg.payload),
1873+ .Switch => try daemon.handleSwitch(msg.payload),
17691874 .Resize => try daemon.handleResize(client, pty_fd, &term, msg.payload),
17701875 .Detach => {
17711876 daemon.handleDetach(client, i);