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 e3b31aac3bd1c9860e9c704a6dceb349ecddaae3
Author: [email protected] <[email protected]>
AuthorDate: Mon Mar 23 22:01:02 2026 -0600
feat: client roles and hijack state machine in tymuxd
Add TermDClientRole enum (PRIMARY/SHARED/READONLY/PAUSED) and a
paused_stack LIFO to TermDTymux. termd_tymux_client_add() now reads
the ATTACH message to extract the mode byte before sending ATTACHED:
HIJACK finds the current PRIMARY, sends it TSRV_MSG_PAUSE, and pushes
it onto the stack while the new client becomes PRIMARY; SHARED and
READONLY are assigned directly. INPUT is now gated so only PRIMARY and
SHARED clients can write to the PTY. _client_remove() pops the most
recent paused client and sends it TSRV_MSG_RESUME when the PRIMARY
disconnects, restoring the previous session.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
src/bin/termd_tymux.c | 129 ++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 126 insertions(+), 3 deletions(-)
diff --git a/src/bin/termd_tymux.c b/src/bin/termd_tymux.c
index 31d8b5c2..d08d6381 100644
--- a/src/bin/termd_tymux.c
+++ b/src/bin/termd_tymux.c
@@ -8,7 +8,9 @@
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
+#include <sys/socket.h>
#include <sys/syscall.h>
+#include <sys/time.h>
#include <linux/memfd.h>
#include <Eina.h>
@@ -46,11 +48,19 @@ _memfd_create(const char *name, unsigned int flags)
/* ── per-client state ────────────────────────────────────────────── */
+typedef enum {
+ CLIENT_PRIMARY,
+ CLIENT_SHARED,
+ CLIENT_READONLY,
+ CLIENT_PAUSED
+} TermDClientRole;
+
typedef struct _TermDClient
{
int fd;
Ecore_Fd_Handler *handler;
TermDTymux *sess;
+ TermDClientRole role;
} TermDClient;
/* ── tymux state ───────────────────────────────────────────────── */
@@ -78,6 +88,7 @@ struct _TermDTymux
Ecore_Animator *notify_anim; /* batches flush + NOTIFY at frame rate */
Eina_Bool dirty; /* screen changed since last flush */
+ Eina_List *paused_stack; /* LIFO of paused TermDClient pointers */
};
/* ── synthetic Config for headless use ───────────────────────────── */
@@ -481,12 +492,14 @@ _cb_client_data(void *data, Ecore_Fd_Handler *fd_handler EINA_UNUSED)
switch ((TermSrvMsgType)type)
{
case TSRV_MSG_INPUT:
- if (payload && size > 0)
+ if (payload && size > 0 &&
+ (client->role == CLIENT_PRIMARY || client->role == CLIENT_SHARED))
termpty_write(sess->pty, (const char *)payload, (int)size);
break;
case TSRV_MSG_RESIZE:
- if (payload && size >= sizeof(TermSrvMsgResize))
+ if (payload && size >= sizeof(TermSrvMsgResize) &&
+ client->role == CLIENT_PRIMARY)
{
TermSrvMsgResize *r = payload;
uint32_t cols = r->cols;
@@ -540,6 +553,28 @@ static void
_client_remove(TermDTymux *sess, TermDClient *client)
{
sess->clients = eina_list_remove(sess->clients, client);
+
+ /* If a PAUSED client disconnects, remove from stack */
+ if (client->role == CLIENT_PAUSED)
+ sess->paused_stack = eina_list_remove(sess->paused_stack, client);
+
+ /* If PRIMARY disconnects, resume most recent paused client */
+ if (client->role == CLIENT_PRIMARY && sess->paused_stack)
+ {
+ TermDClient *resumed = eina_list_data_get(sess->paused_stack);
+ TermSrvMsgResume rr;
+
+ /* eina_list_remove_list(list, list) pops the head node */
+ sess->paused_stack = eina_list_remove_list(sess->paused_stack,
+ sess->paused_stack);
+ resumed->role = CLIENT_PRIMARY;
+
+ rr.cols = (uint32_t)sess->pty->w;
+ rr.rows = (uint32_t)sess->pty->h;
+ if (termsrv_msg_send(resumed->fd, TSRV_MSG_RESUME, &rr, sizeof(rr)) < 0)
+ WRN("failed to send RESUME to fd %d", resumed->fd);
+ }
+
if (client->handler)
{
ecore_main_fd_handler_del(client->handler);
@@ -679,6 +714,8 @@ termd_tymux_free(TermDTymux *sess)
close(c->fd);
free(c);
}
+ eina_list_free(sess->paused_stack);
+ sess->paused_stack = NULL;
{
int b;
@@ -717,12 +754,57 @@ termd_tymux_free(TermDTymux *sess)
void
termd_tymux_client_add(TermDTymux *sess, int cfd)
{
- TermDClient *client;
+ TermDClient *client;
+ void *payload = NULL;
+ uint32_t size = 0;
+ int type;
+ TermSrvAttachMode mode = TSRV_ATTACH_HIJACK;
EINA_SAFETY_ON_NULL_RETURN(sess);
if (cfd < 0)
return;
+ /* Read the ATTACH message the client sends immediately on connect.
+ * Set a 1-second receive timeout so a client that connects but
+ * never sends cannot block the event loop indefinitely. */
+ {
+ struct timeval tv = { .tv_sec = 1, .tv_usec = 0 };
+ setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+ }
+ type = termsrv_msg_recv(cfd, &payload, &size, NULL);
+ if (type == 0)
+ {
+ /* EAGAIN — retry once */
+ free(payload); payload = NULL; size = 0;
+ type = termsrv_msg_recv(cfd, &payload, &size, NULL);
+ }
+ /* Clear the timeout so normal event-loop I/O is non-blocking again */
+ {
+ struct timeval tv = { .tv_sec = 0, .tv_usec = 0 };
+ setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+ }
+ if (type < 0)
+ {
+ ERR("failed to read ATTACH message from fd %d", cfd);
+ free(payload);
+ close(cfd);
+ return;
+ }
+ if (type != TSRV_MSG_ATTACH)
+ {
+ ERR("expected ATTACH (%d) from fd %d, got %d", TSRV_MSG_ATTACH, cfd, type);
+ free(payload);
+ close(cfd);
+ return;
+ }
+
+ /* Extract mode from first payload byte; default HIJACK if absent */
+ if (payload && size >= 1)
+ mode = (TermSrvAttachMode)((const uint8_t *)payload)[0];
+ free(payload);
+ payload = NULL;
+
+ /* Send ATTACHED */
{
int head_fd = (sess->backlog_buf_count > 0)
? sess->backlog_bufs[sess->backlog_buf_count - 1].fd
@@ -763,4 +845,45 @@ termd_tymux_client_add(TermDTymux *sess, int cfd)
}
sess->clients = eina_list_append(sess->clients, client);
+
+ /* Assign role and apply hijack logic */
+ switch (mode)
+ {
+ case TSRV_ATTACH_HIJACK:
+ {
+ /* Find the current PRIMARY client and pause it */
+ Eina_List *l;
+ TermDClient *c;
+
+ EINA_LIST_FOREACH(sess->clients, l, c)
+ {
+ if (c != client && c->role == CLIENT_PRIMARY)
+ {
+ TermSrvMsgPause pp;
+
+ pp.cols = (uint32_t)sess->pty->w;
+ pp.rows = (uint32_t)sess->pty->h;
+ termsrv_msg_send(c->fd, TSRV_MSG_PAUSE, &pp, sizeof(pp));
+ c->role = CLIENT_PAUSED;
+ sess->paused_stack = eina_list_prepend(sess->paused_stack, c);
+ break;
+ }
+ }
+ client->role = CLIENT_PRIMARY;
+ break;
+ }
+
+ case TSRV_ATTACH_SHARED:
+ client->role = CLIENT_SHARED;
+ break;
+
+ case TSRV_ATTACH_READONLY:
+ client->role = CLIENT_READONLY;
+ break;
+
+ default:
+ WRN("unknown attach mode %d from fd %d, defaulting to READONLY", mode, cfd);
+ client->role = CLIENT_READONLY;
+ break;
+ }
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.