This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch tymux
in repository terminology.
View the commit online.
commit d2006452da70d34ea72cdb4127199847c9114dde
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 15 18:27:06 2026 -0600
feat: implement termsrv utilities for tymux session recovery IPC
Adds pure POSIX implementation of termsrv IPC utilities to support
persistent terminal session recovery. Includes socket path helpers,
sessions directory management, and message send/recv with 8-byte
framing for the IPC protocol. Implements SCM_RIGHTS file descriptor
passing to transfer memfd handles between session server and clients.
Comprehensive TDD test suite validates path construction, message
round-trip correctness, and fd passing with functional verification
(write/read across transferred fd).
All code guarded by HAVE_TYMUX to maintain compatibility with older
builds.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
src/bin/termsrv.c | 161 +++++++++++++++++++++++++++++++++++++++++++++++
src/tests/test_termsrv.c | 111 ++++++++++++++++++++++++++++++++
2 files changed, 272 insertions(+)
diff --git a/src/bin/termsrv.c b/src/bin/termsrv.c
new file mode 100644
index 00000000..16ca44f4
--- /dev/null
+++ b/src/bin/termsrv.c
@@ -0,0 +1,161 @@
+#include "config.h"
+
+#ifdef HAVE_TYMUX
+
+#include "termsrv.h"
+
+/* Sanity cap on incoming message payload size. No tymux message should
+ * ever approach this; it guards against a malformed hdr.size of ~4 GB. */
+#define TERMSRV_MAX_MSG_PAYLOAD (1u << 20) /* 1 MiB */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+
+static const char *
+_xdg(void)
+{
+ const char *d = getenv("XDG_RUNTIME_DIR");
+ return (d && *d) ? d : "/tmp";
+}
+
+int
+termsrv_socket_path(const char *session_name, char *buf, size_t bufsz)
+{
+ if (!session_name || session_name[0] == '\0' || strchr(session_name, '/')) return -1;
+ int n = snprintf(buf, bufsz, "%s/terminology/tymux/%s.sock",
+ _xdg(), session_name);
+ return (n > 0 && (size_t)n < bufsz) ? 0 : -1;
+}
+
+int
+termsrv_sessions_dir(char *buf, size_t bufsz)
+{
+ int n = snprintf(buf, bufsz, "%s/terminology/tymux", _xdg());
+ return (n > 0 && (size_t)n < bufsz) ? 0 : -1;
+}
+
+int
+termsrv_ensure_sessions_dir(void)
+{
+ char buf[512];
+ /* $XDG/terminology */
+ if (snprintf(buf, sizeof(buf), "%s/terminology", _xdg())
+ >= (int)sizeof(buf)) return -1;
+ if (mkdir(buf, 0700) < 0 && errno != EEXIST) return -1;
+ /* $XDG/terminology/tymux */
+ if (snprintf(buf, sizeof(buf), "%s/terminology/tymux", _xdg())
+ >= (int)sizeof(buf)) return -1;
+ if (mkdir(buf, 0700) < 0 && errno != EEXIST) return -1;
+ return 0;
+}
+
+/* NOTE: write()/read() calls below assume the socket is used in blocking
+ * mode and that messages are small enough to complete atomically. For
+ * payloads below PIPE_BUF (4096 B on Linux) this holds on AF_UNIX.
+ * Larger payloads would require a write/read loop for full robustness. */
+
+int
+termsrv_msg_send(int fd, TermSrvMsgType type,
+ const void *payload, uint32_t size)
+{
+ TermSrvMsgHdr hdr = { .type = (uint32_t)type, .size = size };
+ if (write(fd, &hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) return -1;
+ if (size && payload)
+ if (write(fd, payload, size) != (ssize_t)size) return -1;
+ return 0;
+}
+
+int
+termsrv_msg_recv(int fd, void **payload_out, uint32_t *size_out)
+{
+ TermSrvMsgHdr hdr;
+ *payload_out = NULL;
+ *size_out = 0;
+
+ ssize_t r = read(fd, &hdr, sizeof(hdr));
+ if (r == 0) return -1; /* EOF */
+ if (r != (ssize_t)sizeof(hdr)) return -1;
+
+ if (hdr.size > TERMSRV_MAX_MSG_PAYLOAD) return -1;
+
+ if (hdr.size > 0)
+ {
+ void *buf = malloc(hdr.size);
+ if (!buf) return -1;
+ if (read(fd, buf, hdr.size) != (ssize_t)hdr.size)
+ { free(buf); return -1; }
+ *payload_out = buf;
+ }
+ *size_out = hdr.size;
+ return (int)hdr.type;
+}
+
+int
+termsrv_send_attached(int sock_fd, int shm_fd,
+ uint32_t cols, uint32_t rows)
+{
+ TermSrvMsgHdr hdr = { .type = TSRV_MSG_ATTACHED,
+ .size = sizeof(TermSrvMsgAttached) };
+ TermSrvMsgAttached pl = { .cols = cols, .rows = rows };
+
+ struct iovec iov[2] = {
+ { &hdr, sizeof(hdr) },
+ { &pl, sizeof(pl) }
+ };
+ char cmsgbuf[CMSG_SPACE(sizeof(int))];
+ memset(cmsgbuf, 0, sizeof(cmsgbuf));
+ struct msghdr msg = {
+ .msg_iov = iov,
+ .msg_iovlen = 2,
+ .msg_control = cmsgbuf,
+ .msg_controllen = sizeof(cmsgbuf)
+ };
+ struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
+ cm->cmsg_level = SOL_SOCKET;
+ cm->cmsg_type = SCM_RIGHTS;
+ cm->cmsg_len = CMSG_LEN(sizeof(int));
+ memcpy(CMSG_DATA(cm), &shm_fd, sizeof(int));
+
+ return (sendmsg(sock_fd, &msg, 0) < 0) ? -1 : 0;
+}
+
+int
+termsrv_recv_attached(int sock_fd, int *shm_fd_out,
+ uint32_t *cols_out, uint32_t *rows_out)
+{
+ TermSrvMsgHdr hdr;
+ TermSrvMsgAttached pl;
+ char cmsgbuf[CMSG_SPACE(sizeof(int))];
+ memset(cmsgbuf, 0, sizeof(cmsgbuf));
+ struct iovec iov[2] = {
+ { &hdr, sizeof(hdr) },
+ { &pl, sizeof(pl) }
+ };
+ struct msghdr msg = {
+ .msg_iov = iov,
+ .msg_iovlen = 2,
+ .msg_control = cmsgbuf,
+ .msg_controllen = sizeof(cmsgbuf)
+ };
+
+ *shm_fd_out = -1;
+ if (recvmsg(sock_fd, &msg, 0) < 0) return -1;
+ if (msg.msg_flags & MSG_CTRUNC) return -1;
+ if (hdr.type != TSRV_MSG_ATTACHED) return -1;
+
+ struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
+ if (!cm || cm->cmsg_type != SCM_RIGHTS) return -1;
+ memcpy(shm_fd_out, CMSG_DATA(cm), sizeof(int));
+
+ *cols_out = pl.cols;
+ *rows_out = pl.rows;
+ return 0;
+}
+
+#endif /* HAVE_TYMUX */
diff --git a/src/tests/test_termsrv.c b/src/tests/test_termsrv.c
new file mode 100644
index 00000000..62d15297
--- /dev/null
+++ b/src/tests/test_termsrv.c
@@ -0,0 +1,111 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include "termsrv.h"
+
+static void
+test_socket_path(void)
+{
+ char buf[512];
+ setenv("XDG_RUNTIME_DIR", "/tmp/test_xdg_ty", 1);
+
+ assert(termsrv_socket_path("mysession", buf, sizeof(buf)) == 0);
+ assert(strcmp(buf,
+ "/tmp/test_xdg_ty/terminology/tymux/mysession.sock") == 0);
+
+ /* buf too small */
+ char tiny[5];
+ assert(termsrv_socket_path("x", tiny, sizeof(tiny)) == -1);
+
+ /* empty name is rejected */
+ assert(termsrv_socket_path("", buf, sizeof(buf)) == -1);
+
+ /* slash in name is rejected */
+ assert(termsrv_socket_path("a/b", buf, sizeof(buf)) == -1);
+
+ printf("PASS: test_socket_path\n");
+}
+
+static void
+test_sessions_dir(void)
+{
+ char buf[512];
+ setenv("XDG_RUNTIME_DIR", "/tmp/test_xdg_ty", 1);
+
+ assert(termsrv_sessions_dir(buf, sizeof(buf)) == 0);
+ assert(strcmp(buf, "/tmp/test_xdg_ty/terminology/tymux") == 0);
+
+ printf("PASS: test_sessions_dir\n");
+}
+
+static void
+test_msg_round_trip(void)
+{
+ int sv[2];
+ assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0);
+
+ TermSrvMsgResize pl_send = { .cols = 80, .rows = 24 };
+ assert(termsrv_msg_send(sv[0], TSRV_MSG_RESIZE,
+ &pl_send, sizeof(pl_send)) == 0);
+
+ void *payload = NULL;
+ uint32_t size = 0;
+ int type = termsrv_msg_recv(sv[1], &payload, &size);
+ assert(type == TSRV_MSG_RESIZE);
+ assert(size == sizeof(TermSrvMsgResize));
+ TermSrvMsgResize *r = payload;
+ assert(r->cols == 80 && r->rows == 24);
+ free(payload);
+
+ /* zero-payload message */
+ assert(termsrv_msg_send(sv[0], TSRV_MSG_BELL, NULL, 0) == 0);
+ type = termsrv_msg_recv(sv[1], &payload, &size);
+ assert(type == TSRV_MSG_BELL && size == 0 && payload == NULL);
+
+ close(sv[0]); close(sv[1]);
+ printf("PASS: test_msg_round_trip\n");
+}
+
+static void
+test_fd_passing(void)
+{
+ int sv[2];
+ assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0);
+ int pipefd[2];
+ assert(pipe(pipefd) == 0);
+
+ assert(termsrv_send_attached(sv[0], pipefd[0], 132, 43) == 0);
+
+ int shm_fd = -1;
+ uint32_t cols = 0, rows = 0;
+ assert(termsrv_recv_attached(sv[1], &shm_fd, &cols, &rows) == 0);
+ assert(shm_fd >= 0 && shm_fd != pipefd[0]); /* received a dup */
+ assert(cols == 132 && rows == 43);
+
+ /* verify the transferred fd is functional: write to pipefd[1],
+ * read back through shm_fd (the dup of pipefd[0]) */
+ const char msg[] = "tymux";
+ assert(write(pipefd[1], msg, sizeof(msg)) == (ssize_t)sizeof(msg));
+ char rbuf[sizeof(msg)];
+ assert(read(shm_fd, rbuf, sizeof(rbuf)) == (ssize_t)sizeof(rbuf));
+ assert(memcmp(rbuf, msg, sizeof(msg)) == 0);
+
+ close(sv[0]); close(sv[1]);
+ close(pipefd[0]); close(pipefd[1]);
+ close(shm_fd);
+ printf("PASS: test_fd_passing\n");
+}
+
+int
+main(void)
+{
+ test_socket_path();
+ test_sessions_dir();
+ test_msg_round_trip();
+ test_fd_passing();
+ printf("All tests passed.\n");
+ return 0;
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.