This is an automated email from the git hooks/post-receive script.
git pushed a commit to reference refs/pull/36/head
in repository terminology.
View the commit online.
commit 74c8f8bdf4ce03fa888e37fe93de46bdd5daee79
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 15 21:23:43 2026 -0600
feat: implement tymux daemon session with memfd-backed shared memory
Introduces TermDSession: the core daemon-side session logic that manages
one PTY and a memfd-based circular buffer for screen state. Each session
forks a shell, creates a shared memory buffer, and tracks all connected
clients via Ecore fd handlers.
Key components:
- TermDSession struct: owns PTY, mmap'd shm header/cells, client list
- memfd-backed shm buffer with atomic write_seq counter for ABA-safe updates
- Screen buffer flattening: converts Termpty's circular buffer layout to
linear shm cells, handling both primary and alternate screen modes
- Four Termpty callbacks relay screen changes, title, bell, and exit events
to all connected clients via NOTIFY/TITLE/BELL/EXIT messages
- Client fd handler dispatches INPUT/RESIZE/DETACH messages back to PTY
- Synthetic Config object for headless operation (no GUI needed)
Also fixes termsrv.c first include from config.h to private.h (was
resolving to wrong header, breaking HAVE_TYMUX preprocessor gate).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
src/bin/termsrv.c | 2 +-
src/bin/tymux_session.c | 437 ++++++++++++++++++++++++++++++++++++++++++++++++
src/bin/tymux_session.h | 22 ++-
3 files changed, 459 insertions(+), 2 deletions(-)
diff --git a/src/bin/termsrv.c b/src/bin/termsrv.c
index 16ca44f4..c3ac3845 100644
--- a/src/bin/termsrv.c
+++ b/src/bin/termsrv.c
@@ -1,4 +1,4 @@
-#include "config.h"
+#include "private.h"
#ifdef HAVE_TYMUX
diff --git a/src/bin/tymux_session.c b/src/bin/tymux_session.c
index a0c719ae..873a59e9 100644
--- a/src/bin/tymux_session.c
+++ b/src/bin/tymux_session.c
@@ -1,2 +1,439 @@
#include "private.h"
+
+#include <errno.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <linux/memfd.h>
+
+#include <Eina.h>
+#include <Ecore.h>
+
#include "tymux_session.h"
+#include "termpty.h"
+#include "termsrv.h"
+
+/* ── log domain ─────────────────────────────────────────────────── */
+
+static int _log_dom_session = -1;
+
+#undef ERR
+#undef WRN
+#undef INF
+#undef DBG
+#define ERR(...) EINA_LOG_DOM_ERR(_log_dom_session, __VA_ARGS__)
+#define WRN(...) EINA_LOG_DOM_WARN(_log_dom_session, __VA_ARGS__)
+#define INF(...) EINA_LOG_DOM_INFO(_log_dom_session, __VA_ARGS__)
+#define DBG(...) EINA_LOG_DOM_DBG(_log_dom_session, __VA_ARGS__)
+
+/* ── memfd_create wrapper ────────────────────────────────────────── */
+
+#ifndef __NR_memfd_create
+# error "memfd_create syscall number unknown for this arch"
+#endif
+
+static int
+_memfd_create(const char *name, unsigned int flags)
+{
+ return (int)syscall(__NR_memfd_create, name, flags);
+}
+
+/* ── per-client state ────────────────────────────────────────────── */
+
+typedef struct _TermDClient
+{
+ int fd;
+ Ecore_Fd_Handler *handler;
+ TermDSession *sess;
+} TermDClient;
+
+/* ── session state ───────────────────────────────────────────────── */
+
+struct _TermDSession
+{
+ char *name;
+ Termpty *pty;
+ Config *config;
+ int shm_fd;
+ TermShmHeader *shm;
+ Eina_List *clients;
+};
+
+/* ── synthetic Config for headless use ───────────────────────────── */
+
+static Config *
+_default_config_new(void)
+{
+ Config *cfg = calloc(1, sizeof(Config));
+ if (!cfg)
+ return NULL;
+
+ cfg->scrollback = 10000;
+ cfg->login_shell = EINA_FALSE;
+ cfg->xterm_256color = EINA_TRUE;
+ return cfg;
+}
+
+/* ── shm flush ───────────────────────────────────────────────────── */
+
+static void
+_flush_screen_to_shm(TermDSession *sess)
+{
+ Termpty *ty = sess->pty;
+ TermShmHeader *hdr = sess->shm;
+ Termcell *dst = TERMSRV_SHM_CELLS(hdr);
+ int w = ty->w;
+ int h = ty->h;
+ int cols, rows, y;
+ Termcell *scr;
+ int circ_off;
+
+ /* Select primary or alternate screen buffer */
+ if (ty->altbuf)
+ {
+ scr = ty->screen2;
+ circ_off = ty->circular_offset2;
+ }
+ else
+ {
+ scr = ty->screen;
+ circ_off = ty->circular_offset;
+ }
+
+ /* Clamp to shm capacity */
+ cols = (w < (int)TERMSRV_MAX_COLS) ? w : (int)TERMSRV_MAX_COLS;
+ rows = (h < (int)TERMSRV_MAX_ROWS) ? h : (int)TERMSRV_MAX_ROWS;
+
+ hdr->cols = (uint32_t)cols;
+ hdr->rows = (uint32_t)rows;
+ hdr->cursor_x = (uint32_t)((ty->cursor_state.cx < cols) ? ty->cursor_state.cx : cols - 1);
+ hdr->cursor_y = (uint32_t)((ty->cursor_state.cy < rows) ? ty->cursor_state.cy : rows - 1);
+ hdr->cursor_visible = ty->termstate.hide_cursor ? 0 : 1;
+
+ for (y = 0; y < rows; y++)
+ {
+ int src_row = (y + circ_off) % h;
+ Termcell *src = "" + (src_row * w);
+ Termcell *d = dst + (y * cols);
+ memcpy(d, src, (size_t)cols * sizeof(Termcell));
+ }
+
+ uint64_t seq = atomic_load_explicit(&hdr->write_seq, memory_order_relaxed);
+ atomic_store_explicit(&hdr->write_seq, seq + 1, memory_order_release);
+}
+
+/* ── forward declarations ────────────────────────────────────────── */
+
+static void _client_remove(TermDSession *sess, TermDClient *client);
+
+/* ── Termpty callbacks ───────────────────────────────────────────── */
+
+static void
+_pty_change_cb(void *data)
+{
+ TermDSession *sess = data;
+ TermSrvMsgNotify pay;
+ Eina_List *l;
+ TermDClient *c;
+
+ _flush_screen_to_shm(sess);
+
+ pay.write_seq = atomic_load_explicit(&sess->shm->write_seq,
+ memory_order_acquire);
+ EINA_LIST_FOREACH(sess->clients, l, c)
+ termsrv_msg_send(c->fd, TSRV_MSG_NOTIFY, &pay, sizeof(pay));
+}
+
+static void
+_pty_title_cb(void *data)
+{
+ TermDSession *sess = data;
+ const char *title = sess->pty->prop.title;
+ Eina_List *l;
+ TermDClient *c;
+
+ if (!title) title = "";
+ EINA_LIST_FOREACH(sess->clients, l, c)
+ termsrv_msg_send(c->fd, TSRV_MSG_TITLE, title,
+ (uint32_t)(strlen(title) + 1));
+}
+
+static void
+_pty_bell_cb(void *data)
+{
+ TermDSession *sess = data;
+ Eina_List *l;
+ TermDClient *c;
+
+ EINA_LIST_FOREACH(sess->clients, l, c)
+ termsrv_msg_send(c->fd, TSRV_MSG_BELL, NULL, 0);
+}
+
+static void
+_pty_exit_cb(void *data)
+{
+ TermDSession *sess = data;
+ TermSrvMsgExit pay;
+ Eina_List *l;
+ TermDClient *c;
+
+ pay.exit_code = (int32_t)sess->pty->exit_code;
+ EINA_LIST_FOREACH(sess->clients, l, c)
+ termsrv_msg_send(c->fd, TSRV_MSG_EXIT, &pay, sizeof(pay));
+
+ ecore_main_loop_quit();
+}
+
+/* ── client fd handler ───────────────────────────────────────────── */
+
+static Eina_Bool
+_cb_client_data(void *data, Ecore_Fd_Handler *fd_handler EINA_UNUSED)
+{
+ TermDClient *client = data;
+ TermDSession *sess = client->sess;
+ void *payload;
+ uint32_t size;
+ int type;
+
+ type = termsrv_msg_recv(client->fd, &payload, &size);
+ if (type < 0)
+ {
+ _client_remove(sess, client);
+ return ECORE_CALLBACK_CANCEL;
+ }
+
+ switch ((TermSrvMsgType)type)
+ {
+ case TSRV_MSG_INPUT:
+ if (payload && size > 0)
+ termpty_write(sess->pty, (const char *)payload, (int)size);
+ break;
+
+ case TSRV_MSG_RESIZE:
+ if (payload && size >= sizeof(TermSrvMsgResize))
+ {
+ TermSrvMsgResize *r = payload;
+ uint32_t cols = r->cols;
+ uint32_t rows = r->rows;
+ if (cols > 0 && rows > 0 &&
+ cols <= TERMSRV_MAX_COLS && rows <= TERMSRV_MAX_ROWS)
+ {
+ termpty_resize(sess->pty, (int)cols, (int)rows);
+ _flush_screen_to_shm(sess);
+ }
+ }
+ break;
+
+ case TSRV_MSG_DETACH:
+ free(payload);
+ _client_remove(sess, client);
+ return ECORE_CALLBACK_CANCEL;
+
+ default:
+ break;
+ }
+
+ free(payload);
+ return ECORE_CALLBACK_RENEW;
+}
+
+/* ── client lifecycle ────────────────────────────────────────────── */
+
+static void
+_client_remove(TermDSession *sess, TermDClient *client)
+{
+ sess->clients = eina_list_remove(sess->clients, client);
+ if (client->handler)
+ {
+ ecore_main_fd_handler_del(client->handler);
+ client->handler = NULL;
+ }
+ close(client->fd);
+ free(client);
+}
+
+/* ── public API ──────────────────────────────────────────────────── */
+
+TermDSession *
+termd_session_new(const char *name, const char *cmd)
+{
+ TermDSession *sess;
+ char memfd_name[128];
+
+ EINA_SAFETY_ON_NULL_RETURN_VAL(name, NULL);
+
+ if (_log_dom_session < 0)
+ {
+ _log_dom_session = eina_log_domain_register("tymux_session", NULL);
+ if (_log_dom_session < 0)
+ EINA_LOG_ERR("Could not create log domain 'tymux_session'");
+ }
+
+ sess = calloc(1, sizeof(*sess));
+ if (!sess)
+ return NULL;
+ sess->shm_fd = -1;
+
+ sess->name = strdup(name);
+ if (!sess->name)
+ goto err_name;
+
+ sess->config = _default_config_new();
+ if (!sess->config)
+ goto err_config;
+
+ /* memfd name is advisory; truncate safely */
+ snprintf(memfd_name, sizeof(memfd_name), "ty-session-%s", name);
+ sess->shm_fd = _memfd_create(memfd_name, MFD_CLOEXEC);
+ if (sess->shm_fd < 0)
+ {
+ ERR("memfd_create failed: %s", strerror(errno));
+ goto err_memfd;
+ }
+
+ if (ftruncate(sess->shm_fd, (off_t)TERMSRV_SHM_TOTAL_SIZE) < 0)
+ {
+ ERR("ftruncate failed: %s", strerror(errno));
+ goto err_ftrunc;
+ }
+
+ sess->shm = mmap(NULL, TERMSRV_SHM_TOTAL_SIZE,
+ PROT_READ | PROT_WRITE, MAP_SHARED,
+ sess->shm_fd, 0);
+ if (sess->shm == MAP_FAILED)
+ {
+ ERR("mmap failed: %s", strerror(errno));
+ sess->shm = NULL;
+ goto err_mmap;
+ }
+
+ sess->shm->magic = TERMSRV_SHM_MAGIC;
+ sess->shm->version = TERMSRV_SHM_VERSION;
+ atomic_store_explicit(&sess->shm->write_seq, 0, memory_order_relaxed);
+
+ /* Create PTY: 80×24 initial size, no login shell */
+ sess->pty = termpty_new(cmd, EINA_FALSE, NULL,
+ 80, 24,
+ sess->config, name, 0);
+ if (!sess->pty)
+ {
+ ERR("termpty_new failed");
+ goto err_pty;
+ }
+
+ sess->pty->cb.change.func = _pty_change_cb;
+ sess->pty->cb.change.data = ""
+ sess->pty->cb.set_title.func = _pty_title_cb;
+ sess->pty->cb.set_title.data = ""
+ sess->pty->cb.bell.func = _pty_bell_cb;
+ sess->pty->cb.bell.data = ""
+ sess->pty->cb.exited.func = _pty_exit_cb;
+ sess->pty->cb.exited.data = ""
+
+ _flush_screen_to_shm(sess);
+
+ return sess;
+
+err_pty:
+ munmap(sess->shm, TERMSRV_SHM_TOTAL_SIZE);
+ sess->shm = NULL;
+err_mmap:
+err_ftrunc:
+ close(sess->shm_fd);
+ sess->shm_fd = -1;
+err_memfd:
+ free(sess->config);
+ sess->config = NULL;
+err_config:
+ free(sess->name);
+ sess->name = NULL;
+err_name:
+ free(sess);
+ return NULL;
+}
+
+void
+termd_session_free(TermDSession *sess)
+{
+ TermDClient *c;
+
+ if (!sess)
+ return;
+
+ EINA_LIST_FREE(sess->clients, c)
+ {
+ if (c->handler)
+ ecore_main_fd_handler_del(c->handler);
+ close(c->fd);
+ free(c);
+ }
+
+ if (sess->pty)
+ {
+ termpty_free(sess->pty);
+ sess->pty = NULL;
+ }
+
+ if (sess->shm)
+ {
+ munmap(sess->shm, TERMSRV_SHM_TOTAL_SIZE);
+ sess->shm = NULL;
+ }
+
+ if (sess->shm_fd >= 0)
+ {
+ close(sess->shm_fd);
+ sess->shm_fd = -1;
+ }
+
+ free(sess->config);
+ free(sess->name);
+ free(sess);
+}
+
+void
+termd_session_client_add(TermDSession *sess, int cfd)
+{
+ TermDClient *client;
+
+ EINA_SAFETY_ON_NULL_RETURN(sess);
+ if (cfd < 0)
+ return;
+
+ if (termsrv_send_attached(cfd, sess->shm_fd,
+ (uint32_t)sess->pty->w,
+ (uint32_t)sess->pty->h) < 0)
+ {
+ ERR("termsrv_send_attached failed for fd %d", cfd);
+ close(cfd);
+ return;
+ }
+
+ client = calloc(1, sizeof(*client));
+ if (!client)
+ {
+ ERR("calloc failed for TermDClient");
+ close(cfd);
+ return;
+ }
+
+ client->fd = cfd;
+ client->sess = sess;
+ client->handler = ecore_main_fd_handler_add(cfd, ECORE_FD_READ,
+ _cb_client_data, client,
+ NULL, NULL);
+ if (!client->handler)
+ {
+ ERR("ecore_main_fd_handler_add failed for fd %d", cfd);
+ close(cfd);
+ free(client);
+ return;
+ }
+
+ sess->clients = eina_list_append(sess->clients, client);
+}
diff --git a/src/bin/tymux_session.h b/src/bin/tymux_session.h
index c283c658..682a4d7b 100644
--- a/src/bin/tymux_session.h
+++ b/src/bin/tymux_session.h
@@ -1,3 +1,23 @@
#ifndef TERMINOLOGY_TYMUX_SESSION_H_
#define TERMINOLOGY_TYMUX_SESSION_H_
-#endif
+
+#include <stdint.h>
+#include <Eina.h>
+#include "termpty.h"
+#include "termsrv.h"
+
+typedef struct _TermDSession TermDSession;
+
+/* Create the session: fork shell, set up memfd.
+ * cmd may be NULL (uses user's default shell).
+ * Returns NULL on failure. */
+TermDSession *termd_session_new(const char *name, const char *cmd);
+
+/* Free the session (kills shell, munmaps, closes memfd). */
+void termd_session_free(TermDSession *sess);
+
+/* Add a connected client fd; takes ownership, sets up Ecore fd handler.
+ * Sends ATTACHED immediately. */
+void termd_session_client_add(TermDSession *sess, int cfd);
+
+#endif /* TERMINOLOGY_TYMUX_SESSION_H_ */
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.