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 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.

Reply via email to