Commit 3b0e4e8
Eric Bower
·
2026-03-22 22:12:51 -0400 EDT
parent 89e2728
docs: doc strings
1 files changed,
+47,
-15
+47,
-15
| ... | ... | @@ -187,6 +187,9 @@ pub fn main() !void { | |
| 187 | 187 | } | |
| 188 | 188 | } | |
| 189 | 189 | ||
| 190 | + | /// Client represents each terminal that has connected to a session. | |
| 191 | + | /// | |
| 192 | + | /// Multiple Clients can connect to a single session. | |
| 190 | 193 | const Client = struct { | |
| 191 | 194 | alloc: std.mem.Allocator, | |
| 192 | 195 | socket_fd: i32, |
| ... | ... | @@ -201,23 +204,16 @@ const Client = struct { | |
| 201 | 204 | } | |
| 202 | 205 | }; | |
| 203 | 206 | ||
| 207 | + | /// Cfg is zmx's configuration container. | |
| 208 | + | /// | |
| 209 | + | /// The purpose of this container is to hold anything that can be modified by the user. | |
| 204 | 210 | const Cfg = struct { | |
| 205 | 211 | socket_dir: []const u8, | |
| 206 | 212 | log_dir: []const u8, | |
| 207 | 213 | max_scrollback: usize = 10_000_000, | |
| 208 | 214 | ||
| 209 | 215 | pub fn init(alloc: std.mem.Allocator) !Cfg { | |
| 210 | - | const tmpdir = std.mem.trimRight(u8, posix.getenv("TMPDIR") orelse "/tmp", "/"); | |
| 211 | - | const uid = posix.getuid(); | |
| 212 | - | ||
| 213 | - | const socket_dir: []const u8 = if (posix.getenv("ZMX_DIR")) |zmxdir| | |
| 214 | - | try alloc.dupe(u8, zmxdir) | |
| 215 | - | else if (posix.getenv("XDG_RUNTIME_DIR")) |xdg_runtime| | |
| 216 | - | try std.fmt.allocPrint(alloc, "{s}/zmx", .{xdg_runtime}) | |
| 217 | - | else | |
| 218 | - | try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid }); | |
| 219 | - | errdefer alloc.free(socket_dir); | |
| 220 | - | ||
| 216 | + | const socket_dir = try socketDir(alloc); | |
| 221 | 217 | const log_dir = try std.fmt.allocPrint(alloc, "{s}/logs", .{socket_dir}); | |
| 222 | 218 | errdefer alloc.free(log_dir); | |
| 223 | 219 |
| ... | ... | @@ -231,6 +227,21 @@ const Cfg = struct { | |
| 231 | 227 | return cfg; | |
| 232 | 228 | } | |
| 233 | 229 | ||
| 230 | + | fn socketDir(alloc: std.mem.Allocator) ![]const u8 { | |
| 231 | + | const tmpdir = std.mem.trimRight(u8, posix.getenv("TMPDIR") orelse "/tmp", "/"); | |
| 232 | + | const uid = posix.getuid(); | |
| 233 | + | ||
| 234 | + | const socket_dir: []const u8 = if (posix.getenv("ZMX_DIR")) |zmxdir| | |
| 235 | + | try alloc.dupe(u8, zmxdir) | |
| 236 | + | else if (posix.getenv("XDG_RUNTIME_DIR")) |xdg_runtime| | |
| 237 | + | try std.fmt.allocPrint(alloc, "{s}/zmx", .{xdg_runtime}) | |
| 238 | + | else | |
| 239 | + | try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid }); | |
| 240 | + | errdefer alloc.free(socket_dir); | |
| 241 | + | ||
| 242 | + | return socket_dir; | |
| 243 | + | } | |
| 244 | + | ||
| 234 | 245 | pub fn deinit(self: *Cfg, alloc: std.mem.Allocator) void { | |
| 235 | 246 | if (self.socket_dir.len > 0) alloc.free(self.socket_dir); | |
| 236 | 247 | if (self.log_dir.len > 0) alloc.free(self.log_dir); |
| ... | ... | @@ -254,6 +265,14 @@ const EnsureSessionResult = struct { | |
| 254 | 265 | is_daemon: bool, | |
| 255 | 266 | }; | |
| 256 | 267 | ||
| 268 | + | /// Daemon is responsible for managing a zmx session. | |
| 269 | + | /// | |
| 270 | + | /// It holds all the state for a running session. Instead of a single daemon for all sessions, we | |
| 271 | + | /// create a daemon for every session. This has some benefits. The ipc communication between | |
| 272 | + | /// session clients and the daemon doesn't need to be tagged with the session name. If a daemon | |
| 273 | + | /// crashes for one session won't crash all the other sessions. | |
| 274 | + | /// | |
| 275 | + | /// Conceptually it's also much simpler to reason about. | |
| 257 | 276 | const Daemon = struct { | |
| 258 | 277 | cfg: *Cfg, | |
| 259 | 278 | alloc: std.mem.Allocator, |
| ... | ... | @@ -343,6 +362,7 @@ const Daemon = struct { | |
| 343 | 362 | std.posix.exit(1); | |
| 344 | 363 | } | |
| 345 | 364 | ||
| 365 | + | /// spawnPty runs forkpty() and executes the shell or shell command the user provides. | |
| 346 | 366 | fn spawnPty(self: *Daemon) !c_int { | |
| 347 | 367 | const size = ipc.getTerminalSize(posix.STDOUT_FILENO); | |
| 348 | 368 | var ws: cross.c.struct_winsize = .{ |
| ... | ... | @@ -379,6 +399,8 @@ const Daemon = struct { | |
| 379 | 399 | return master_fd; | |
| 380 | 400 | } | |
| 381 | 401 | ||
| 402 | + | /// ensureSession "upserts" a session by checking if the unix socket exists already. | |
| 403 | + | /// If not it creates one and spawns the daemon. | |
| 382 | 404 | fn ensureSession(self: *Daemon) !EnsureSessionResult { | |
| 383 | 405 | var dir = try std.fs.openDirAbsolute(self.cfg.socket_dir, .{}); | |
| 384 | 406 | defer dir.close(); |
| ... | ... | @@ -411,8 +433,10 @@ const Daemon = struct { | |
| 411 | 433 | std.log.info("creating session={s}", .{self.session_name}); | |
| 412 | 434 | const server_sock_fd = try socket.createSocket(self.socket_path); | |
| 413 | 435 | ||
| 436 | + | // creates the daemon | |
| 414 | 437 | const pid = try posix.fork(); | |
| 415 | 438 | if (pid == 0) { // child (daemon) | |
| 439 | + | // becomes the session leader and detaches process from its controlling terminal | |
| 416 | 440 | _ = try posix.setsid(); | |
| 417 | 441 | ||
| 418 | 442 | log_system.deinit(); |
| ... | ... | @@ -695,10 +719,10 @@ fn help() !void { | |
| 695 | 719 | \\ [r]un <name> [command...] Send command without attaching, creating session if needed | |
| 696 | 720 | \\ [d]etach Detach all clients from current session (ctrl+\ for current client) | |
| 697 | 721 | \\ [l]ist [--short] List active sessions | |
| 698 | - | \\ [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish) | |
| 699 | 722 | \\ [k]ill <name> Kill a session and all attached clients | |
| 700 | 723 | \\ [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences) | |
| 701 | 724 | \\ [w]ait <name>... Wait for session tasks to complete | |
| 725 | + | \\ [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish) | |
| 702 | 726 | \\ [v]ersion Show version information | |
| 703 | 727 | \\ [h]elp Show this help message | |
| 704 | 728 | \\ |
| ... | ... | @@ -707,7 +731,7 @@ fn help() !void { | |
| 707 | 731 | \\ - ZMX_DIR Controls which folder is used to store unix socket files (prio: 1) | |
| 708 | 732 | \\ - XDG_RUNTIME_DIR Controls which folder is used to store unix socket files (prio: 2) | |
| 709 | 733 | \\ - TMPDIR Controls which folder is used to store unix socket files (prio: 3) | |
| 710 | - | \\ - ZMX_SESSION This variable is injected into every zmx session automatically | |
| 734 | + | \\ - ZMX_SESSION The session name we inject into every zmx session automatically | |
| 711 | 735 | \\ - ZMX_SESSION_PREFIX Adds this value to the start of every session name for all commands | |
| 712 | 736 | \\ | |
| 713 | 737 | ; |
| ... | ... | @@ -1001,7 +1025,7 @@ fn attach(daemon: *Daemon) !void { | |
| 1001 | 1025 | ||
| 1002 | 1026 | const client_sock = try socket.sessionConnect(daemon.socket_path); | |
| 1003 | 1027 | std.log.info("attached session={s}", .{daemon.session_name}); | |
| 1004 | - | // this is typically used with tcsetattr() to modify terminal settings. | |
| 1028 | + | // This is typically used with tcsetattr() to modify terminal settings. | |
| 1005 | 1029 | // - you first get the current settings with tcgetattr() | |
| 1006 | 1030 | // - modify the desired attributes in the termios structure | |
| 1007 | 1031 | // - then apply the changes with tcsetattr(). |
| ... | ... | @@ -1079,6 +1103,10 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void { | |
| 1079 | 1103 | ||
| 1080 | 1104 | const shell = util.detectShell(); | |
| 1081 | 1105 | const shell_basename = std.fs.path.basename(shell); | |
| 1106 | + | // We append a task marker so we can: | |
| 1107 | + | // - know when the command finishes | |
| 1108 | + | // - capture its exit status | |
| 1109 | + | // This information is retrived when running `zmx list` | |
| 1082 | 1110 | const inline_task_marker = if (std.mem.eql(u8, shell_basename, "fish")) | |
| 1083 | 1111 | "; echo ZMX_TASK_COMPLETED:$status" | |
| 1084 | 1112 | else |
| ... | ... | @@ -1185,6 +1213,8 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void { | |
| 1185 | 1213 | return error.NoAckReceived; | |
| 1186 | 1214 | } | |
| 1187 | 1215 | ||
| 1216 | + | /// clientLoop sends ipc commands to its corresponding daemon. It uses poll() as its non-blocking | |
| 1217 | + | /// mechanism. It will send stdin to the daemon and receive stdout from the daemon. | |
| 1188 | 1218 | fn clientLoop(client_sock_fd: i32) !void { | |
| 1189 | 1219 | // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking | |
| 1190 | 1220 | const alloc = std.heap.c_allocator; |
| ... | ... | @@ -1343,6 +1373,8 @@ fn clientLoop(client_sock_fd: i32) !void { | |
| 1343 | 1373 | } | |
| 1344 | 1374 | } | |
| 1345 | 1375 | ||
| 1376 | + | /// dameonLoop is what the daemon runs to send and receive ipc commands from its corresponding | |
| 1377 | + | /// clients. It uses poll() as its non-blocking mechanism. | |
| 1346 | 1378 | fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void { | |
| 1347 | 1379 | std.log.info("daemon started session={s} pty_fd={d}", .{ daemon.session_name, pty_fd }); | |
| 1348 | 1380 | setupSigtermHandler(); |
| ... | ... | @@ -1554,7 +1586,7 @@ fn handleSigterm(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) | |
| 1554 | 1586 | sigterm_received.store(true, .release); | |
| 1555 | 1587 | } | |
| 1556 | 1588 | ||
| 1557 | - | // No SA_RESTART on these: we WANT the signal to interrupt poll() so the | |
| 1589 | + | // No SA_RESTART: we want the signal to interrupt poll() so the | |
| 1558 | 1590 | // loop can check the flag. On BSD/macOS, SA_RESTART makes poll restartable, | |
| 1559 | 1591 | // which would leave an idle daemon deaf to SIGTERM until other I/O wakes it. | |
| 1560 | 1592 | fn setupSigwinchHandler() void { |