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 0424d174fbded204e022e60acce48a941643c962
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 22 13:50:17 2026 -0600
feat: async backlog buffer prefetching for tymux scrollback
Replace blocking synchronous buffer fetches (which failed when
NOTIFY messages interleaved with BACKLOG_BUF responses) with fully
asynchronous prefetching. termsrv_msg_recv now uses recvmsg to
extract SCM_RIGHTS fds, enabling BACKLOG_BUF handling in the normal
fd handler. On attach, placeholder entries are created for all
older buffers and a cascading prefetch chain fetches them one by
one. Proximity-based prefetch triggers when scrolling within 20%
of a buffer boundary. This fixes the 256-line scrollback limit
and eliminates UI freezes during buffer fetches.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
src/bin/termd_tymux.c | 2 +-
src/bin/termsrv.c | 159 ++++++++----------
src/bin/termsrv.h | 15 +-
src/bin/termtymux.c | 426 ++++++++++++++++++++++++++++++-----------------
src/bin/termtymux.h | 14 +-
src/tests/test_termsrv.c | 4 +-
6 files changed, 355 insertions(+), 265 deletions(-)
diff --git a/src/bin/termd_tymux.c b/src/bin/termd_tymux.c
index d83c4e88..0bd0314a 100644
--- a/src/bin/termd_tymux.c
+++ b/src/bin/termd_tymux.c
@@ -425,7 +425,7 @@ _cb_client_data(void *data, Ecore_Fd_Handler *fd_handler EINA_UNUSED)
uint32_t size;
int type;
- type = termsrv_msg_recv(client->fd, &payload, &size);
+ type = termsrv_msg_recv(client->fd, &payload, &size, NULL);
if (type < 0)
{
free(payload);
diff --git a/src/bin/termsrv.c b/src/bin/termsrv.c
index 6f299690..9180b35d 100644
--- a/src/bin/termsrv.c
+++ b/src/bin/termsrv.c
@@ -75,48 +75,6 @@ termsrv_msg_send(int fd, TermSrvMsgType type,
return (n == expected) ? 0 : -1;
}
-/*
- * Read exactly @need bytes from @fd into @buf, tolerating short reads
- * that can occur on non-blocking sockets. Spins with EAGAIN up to a
- * tight retry limit — on AF_UNIX the peer always sends header+payload
- * atomically via sendmsg/iovec, so the bytes arrive together; any
- * EAGAIN after a partial read is an extremely transient condition.
- *
- * Returns 0 on success, -1 on EOF or unrecoverable error.
- */
-static int
-_recv_exact(int fd, void *buf, size_t need)
-{
- size_t got = 0;
- int retries = 0;
-
- while (got < need)
- {
- ssize_t r = recv(fd, (char *)buf + got, need - got, 0);
- if (r > 0)
- {
- got += (size_t)r;
- retries = 0;
- }
- else if (r == 0)
- {
- return -1; /* EOF */
- }
- else
- {
- if (errno == EINTR) continue;
- if ((errno == EAGAIN || errno == EWOULDBLOCK) && retries < 100)
- {
- /* Payload bytes haven't arrived yet — yield briefly */
- retries++;
- usleep(1000); /* 1 ms */
- continue;
- }
- return -1;
- }
- }
- return 0;
-}
/*
* Receive one tymux message from @fd.
@@ -128,15 +86,26 @@ _recv_exact(int fd, void *buf, size_t need)
* (EAGAIN/EWOULDBLOCK on the header read) — caller should
* return ECORE_CALLBACK_RENEW and wait for the next wakeup.
* Returns -1 on EOF or protocol error.
+ *
+ * If fd_out is non-NULL and the message carries an SCM_RIGHTS fd,
+ * *fd_out is set to the received fd; otherwise it is set to -1.
+ * Pass NULL for fd_out when the caller does not expect an ancillary fd
+ * (e.g. the daemon side); in that case any received fd is closed immediately
+ * to prevent leaks.
*/
int
-termsrv_msg_recv(int fd, void **payload_out, uint32_t *size_out)
+termsrv_msg_recv(int fd, void **payload_out, uint32_t *size_out, int *fd_out)
{
TermSrvMsgHdr hdr;
*payload_out = NULL;
*size_out = 0;
+ if (fd_out)
+ *fd_out = -1;
- ssize_t r = recv(fd, &hdr, sizeof(hdr), MSG_DONTWAIT);
+ /* Peek-check: use MSG_DONTWAIT so we can return 0 (no data yet) when the
+ * socket is non-blocking and empty, without consuming the data.
+ * We only read the header here; the payload is read with recvmsg below. */
+ ssize_t r = recv(fd, &hdr, sizeof(hdr), MSG_DONTWAIT | MSG_PEEK);
if (r == 0) return -1; /* EOF */
if (r < 0)
{
@@ -146,10 +115,9 @@ termsrv_msg_recv(int fd, void **payload_out, uint32_t *size_out)
}
if (r != (ssize_t)sizeof(hdr))
{
- /* Partial header: the rest must be in the kernel buffer already
- * (same sendmsg atomicity guarantee). Retrieve the remainder. */
- if (_recv_exact(fd, (char *)&hdr + r, sizeof(hdr) - (size_t)r) < 0)
- return -1;
+ /* Partial header in peek: the rest hasn't arrived yet.
+ * Return 0 to retry on the next fd-ready event. */
+ return 0;
}
/* Reject unknown message types before allocating — valid range is
@@ -158,15 +126,64 @@ termsrv_msg_recv(int fd, void **payload_out, uint32_t *size_out)
if (hdr.type == 0 || hdr.type > 11) return -1;
if (hdr.size > TERMSRV_MAX_MSG_PAYLOAD) return -1;
+ /* Now do the real read. Use recvmsg so that we can extract an optional
+ * SCM_RIGHTS fd (present in BACKLOG_BUF responses). The control buffer
+ * is sized for exactly one fd; MSG_WAITALL ensures we get the full
+ * header + payload in one call (AF_UNIX sends atomically). */
+ void *buf = NULL;
+ size_t total = sizeof(hdr) + (size_t)hdr.size;
+ char *databuf = malloc(total);
+ if (!databuf) return -1;
+
+ char cmsgbuf[CMSG_SPACE(sizeof(int))];
+ memset(cmsgbuf, 0, sizeof(cmsgbuf));
+
+ struct iovec iov = { .iov_base = databuf, .iov_len = total };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = cmsgbuf,
+ .msg_controllen = sizeof(cmsgbuf),
+ };
+
+ ssize_t n = recvmsg(fd, &msg, MSG_WAITALL);
+ if (n != (ssize_t)total)
+ {
+ free(databuf);
+ return (n == 0) ? -1 : -1;
+ }
+
+ /* Re-read the header from the data buffer (consumed by recvmsg) */
+ memcpy(&hdr, databuf, sizeof(hdr));
+
+ /* Extract ancillary fd if present */
+ struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
+ if (cm && cm->cmsg_level == SOL_SOCKET &&
+ cm->cmsg_type == SCM_RIGHTS &&
+ cm->cmsg_len >= CMSG_LEN(sizeof(int)))
+ {
+ int recv_fd;
+ memcpy(&recv_fd, CMSG_DATA(cm), sizeof(int));
+ if (fd_out)
+ *fd_out = recv_fd;
+ else
+ close(recv_fd); /* caller did not want it — prevent leak */
+ }
+
if (hdr.size > 0)
{
- void *buf = malloc(hdr.size);
- if (!buf) return -1;
- if (_recv_exact(fd, buf, hdr.size) < 0)
- { free(buf); return -1; }
+ buf = malloc(hdr.size);
+ if (!buf)
+ {
+ free(databuf);
+ if (fd_out && *fd_out >= 0) { close(*fd_out); *fd_out = -1; }
+ return -1;
+ }
+ memcpy(buf, databuf + sizeof(hdr), hdr.size);
*payload_out = buf;
}
+ free(databuf);
*size_out = hdr.size;
return (int)hdr.type;
}
@@ -297,43 +314,5 @@ termsrv_send_backlog_buf(int sock_fd, int buf_fd,
return (n == (ssize_t)(sizeof(hdr) + sizeof(pl))) ? 0 : -1;
}
-int
-termsrv_recv_backlog_buf(int sock_fd, int *buf_fd_out,
- uint32_t *buf_id_out, uint32_t *cols_out,
- uint32_t *num_lines_out, uint32_t *max_lines_out)
-{
- TermSrvMsgHdr hdr;
- TermSrvMsgBacklogBuf 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)
- };
-
- *buf_fd_out = -1;
- ssize_t n = recvmsg(sock_fd, &msg, MSG_WAITALL);
- if (n < (ssize_t)(sizeof(hdr) + sizeof(pl))) return -1;
- if (msg.msg_flags & MSG_CTRUNC) return -1;
- if (hdr.type != TSRV_MSG_BACKLOG_BUF) return -1;
- if (hdr.size < sizeof(TermSrvMsgBacklogBuf)) return -1;
-
- struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
- if (!cm || cm->cmsg_type != SCM_RIGHTS ||
- cm->cmsg_len < CMSG_LEN(sizeof(int))) return -1;
- memcpy(buf_fd_out, CMSG_DATA(cm), sizeof(int));
-
- *buf_id_out = pl.buf_id;
- *cols_out = pl.cols;
- *num_lines_out = pl.num_lines;
- *max_lines_out = pl.max_lines;
- return 0;
-}
#endif /* HAVE_TYMUX */
diff --git a/src/bin/termsrv.h b/src/bin/termsrv.h
index 035db6c7..41050d3c 100644
--- a/src/bin/termsrv.h
+++ b/src/bin/termsrv.h
@@ -188,8 +188,13 @@ int termsrv_msg_send(int fd, TermSrvMsgType type,
* Returns 0 if the fd is non-blocking and no data is available yet
* (EAGAIN/EWOULDBLOCK) — caller should retry on the next fd-ready event.
* Returns -1 on EOF or protocol error.
- * *payload_out is NULL and *size_out is 0 when there is no payload. */
-int termsrv_msg_recv(int fd, void **payload_out, uint32_t *size_out);
+ * *payload_out is NULL and *size_out is 0 when there is no payload.
+ *
+ * If fd_out is non-NULL and the message carries an SCM_RIGHTS fd,
+ * *fd_out receives it; otherwise *fd_out is set to -1.
+ * Pass NULL for fd_out if the caller is not expecting an fd (e.g. daemon). */
+int termsrv_msg_recv(int fd, void **payload_out, uint32_t *size_out,
+ int *fd_out);
/* Send ATTACHED with shm memfd (and optional backlog memfd) via SCM_RIGHTS.
* Pass backlog_fd >= 0 to include the head backlog buffer fd.
@@ -215,12 +220,6 @@ int termsrv_send_backlog_buf(int sock_fd, int buf_fd,
uint32_t buf_id, uint32_t cols,
uint32_t num_lines, uint32_t max_lines);
-/* Receive BACKLOG_BUF and extract the buffer memfd.
- * Blocking receive — caller must ensure no concurrent reads on sock_fd.
- * Returns 0/-1. */
-int termsrv_recv_backlog_buf(int sock_fd, int *buf_fd_out,
- uint32_t *buf_id_out, uint32_t *cols_out,
- uint32_t *num_lines_out, uint32_t *max_lines_out);
#endif /* HAVE_TYMUX */
#endif /* TERMINOLOGY_TERMSRV_H_ */
diff --git a/src/bin/termtymux.c b/src/bin/termtymux.c
index 72203836..aa0a3668 100644
--- a/src/bin/termtymux.c
+++ b/src/bin/termtymux.c
@@ -19,6 +19,11 @@
#include <stdatomic.h>
#include <time.h>
+/* ── forward declarations ───────────────────────────────────────────── */
+
+static void _backlog_update_backsize(TermTymux *ts);
+static void _backlog_request_async(TermTymux *ts, uint32_t buf_id);
+
/* ── backlog buffer helpers ─────────────────────────────────────────── */
/*
@@ -103,78 +108,132 @@ _backlog_bufs_evict_old(TermTymux *ts, uint32_t new_oldest_id)
}
/*
- * Request and mmap a backlog buffer from the daemon.
- *
- * If slot >= 0, the fetched buffer fills the existing NULL entry at
- * ts->backlog_bufs[slot] (called from _backlog_get_cb for a known-position
- * unmapped buffer). If slot < 0, the buffer is appended at the tail
- * (called from the NOTIFY handler for new head buffers).
- *
- * Returns EINA_TRUE on success.
- *
- * NOTE: this temporarily clears O_NONBLOCK on the socket (via
- * termtymux_request_backlog_buf) and must only be called from the main
- * loop (not from an Ecore fd-handler that is itself running inside a
- * non-blocking recv).
+ * Send an async BACKLOG_BUF_REQ to the daemon for buf_id.
+ * No-op if a request is already in flight (one at a time) or the socket
+ * is not open. On success ts->pending_fetch_id is set to buf_id.
*/
-static Eina_Bool
-_backlog_buf_fetch(TermTymux *ts, uint32_t buf_id, int slot)
+static void
+_backlog_request_async(TermTymux *ts, uint32_t buf_id)
{
- int fd = -1;
- uint32_t cols = 0, num_lines = 0, max_lines = 0;
- size_t map_size;
- void *ptr;
- TermBacklogBufHeader *hdr;
+ TermSrvMsgBacklogBufReq req;
- if (termtymux_request_backlog_buf(ts, buf_id,
- &fd, &cols,
- &num_lines, &max_lines) < 0)
- return EINA_FALSE;
+ if (ts->pending_fetch_id >= 0)
+ return; /* one request in flight at a time */
+ if (ts->sock_fd < 0)
+ return;
- if (cols == 0 || cols > TERMSRV_MAX_COLS)
+ req.buf_id = buf_id;
+ if (termsrv_msg_send(ts->sock_fd, TSRV_MSG_BACKLOG_BUF_REQ,
+ &req, sizeof(req)) < 0)
{
- ERR("termtymux: backlog buf %u has invalid cols %u", buf_id, cols);
- close(fd);
- return EINA_FALSE;
+ ERR("termtymux: send BACKLOG_BUF_REQ buf_id=%u: %s",
+ buf_id, strerror(errno));
+ return;
+ }
+ ts->pending_fetch_id = (int)buf_id;
+}
+
+/*
+ * Handle an incoming BACKLOG_BUF response: mmap the fd, validate the
+ * buffer header, install it in the tracking array, then chain-prefetch
+ * the next older buffer if one is still unmapped.
+ *
+ * Called from _cb_tymux_data when TSRV_MSG_BACKLOG_BUF is received.
+ * Takes ownership of recv_fd (closes it after mmap or on any error path).
+ */
+static void
+_backlog_handle_buf_response(TermTymux *ts,
+ int recv_fd,
+ const TermSrvMsgBacklogBuf *bp)
+{
+ size_t map_size;
+ void *ptr;
+ TermBacklogBufHeader *hdr;
+ int slot = -1;
+ int j;
+
+ if (bp->cols == 0 || bp->cols > TERMSRV_MAX_COLS)
+ {
+ WRN("termtymux: BACKLOG_BUF buf_id=%u invalid cols %u",
+ bp->buf_id, bp->cols);
+ close(recv_fd);
+ return;
}
- map_size = TERMSRV_BACKLOG_TOTAL_SIZE(cols);
- ptr = mmap(NULL, map_size, PROT_READ, MAP_SHARED, fd, 0);
- close(fd);
+ map_size = TERMSRV_BACKLOG_TOTAL_SIZE(bp->cols);
+ ptr = mmap(NULL, map_size, PROT_READ, MAP_SHARED, recv_fd, 0);
+ close(recv_fd); /* fd no longer needed after mmap */
if (ptr == MAP_FAILED)
{
- ERR("termtymux: mmap backlog buf %u: %s", buf_id, strerror(errno));
- return EINA_FALSE;
+ ERR("termtymux: mmap BACKLOG_BUF buf_id=%u: %s",
+ bp->buf_id, strerror(errno));
+ return;
}
hdr = ptr;
if (hdr->magic != TERMSRV_BACKLOG_MAGIC ||
hdr->version != TERMSRV_BACKLOG_VERSION)
{
- WRN("termtymux: backlog buf %u magic/version mismatch", buf_id);
+ WRN("termtymux: BACKLOG_BUF buf_id=%u magic/version mismatch",
+ bp->buf_id);
munmap(ptr, map_size);
- return EINA_FALSE;
+ return;
+ }
+
+ /* Find the slot in the tracking array that matches this buf_id */
+ for (j = 0; j < ts->backlog_buf_count; j++)
+ {
+ if (ts->backlog_bufs[j].buf_id == bp->buf_id)
+ {
+ slot = j;
+ break;
+ }
}
if (slot >= 0)
{
- /* Fill the pre-existing NULL slot directly — no array growth needed */
- TermBacklogBuf *entry = &ts->backlog_bufs[slot];
- entry->hdr = hdr;
- entry->size = map_size;
- /* entry->buf_id is already set */
+ if (ts->backlog_bufs[slot].hdr != NULL)
+ {
+ /* Already mapped — daemon sent a duplicate, discard */
+ WRN("termtymux: BACKLOG_BUF buf_id=%u already mapped, ignoring",
+ bp->buf_id);
+ munmap(ptr, map_size);
+ return;
+ }
+ ts->backlog_bufs[slot].hdr = hdr;
+ ts->backlog_bufs[slot].size = map_size;
}
else
{
- /* Append new head buffer */
- if (!_backlog_buf_push(ts, hdr, map_size, buf_id))
+ /* buf_id not in array — treat as a new head buffer */
+ if (!_backlog_buf_push(ts, hdr, map_size, bp->buf_id))
{
munmap(ptr, map_size);
- return EINA_FALSE;
+ return;
}
+ slot = ts->backlog_buf_count - 1;
}
- return EINA_TRUE;
+ /* Clear in-flight flag if this is the response we were waiting for */
+ if (ts->pending_fetch_id == (int)bp->buf_id)
+ ts->pending_fetch_id = -1;
+
+ /* Update scrollback size so the renderer sees the new lines */
+ _backlog_update_backsize(ts);
+
+ /* Notify renderer that new backlog data is available */
+ if (ts->cb_change.func)
+ ts->cb_change.func(ts->cb_change.data);
+
+ /* Chain-prefetch: request the next older unmapped buffer */
+ for (j = slot - 1; j >= 0; j--)
+ {
+ if (!ts->backlog_bufs[j].hdr)
+ {
+ _backlog_request_async(ts, ts->backlog_bufs[j].buf_id);
+ break;
+ }
+ }
}
/*
@@ -228,10 +287,11 @@ _backlog_update_backsize(TermTymux *ts)
*
* We walk the buffer chain newest-to-oldest, counting lines until we find
* the buffer that owns offset = -y. For lines that land in a buffer that
- * has not been mmap'd yet we request it on the fly.
+ * has not been mmap'd yet, an async request is sent and NULL is returned;
+ * the response handler triggers a re-render once the data arrives.
*
* Returns a pointer into the mmap'd cells on success; NULL if y is out
- * of range or a fetch fails.
+ * of range or the buffer is not yet fetched.
*/
static Termcell *
_backlog_get_cb(void *data, int y, ssize_t *wret)
@@ -281,11 +341,12 @@ _backlog_get_cb(void *data, int y, ssize_t *wret)
/* Ensure buffer is mmap'd */
if (!b->hdr)
{
- /* Pass slot=i so _backlog_buf_fetch fills this existing entry
- * directly rather than appending a duplicate at the tail. */
- if (!_backlog_buf_fetch(ts, b->buf_id, i))
- return NULL;
- b = &ts->backlog_bufs[i]; /* re-look-up in case array was reallocated */
+ /* Not yet fetched — fire an async request if one is not already
+ * in flight. Return NULL for now; the response handler will
+ * trigger a re-render once the data arrives. */
+ if (ts->pending_fetch_id < 0)
+ _backlog_request_async(ts, b->buf_id);
+ return NULL;
}
hdr = b->hdr;
if (!hdr)
@@ -303,6 +364,18 @@ _backlog_get_cb(void *data, int y, ssize_t *wret)
}
*wret = (ssize_t)hdr->line_widths[line_idx];
+
+ /* Proximity prefetch: if we are accessing a line in the older 20%
+ * of this buffer, speculatively request the next older unmapped buffer
+ * so it is ready before the user scrolls past the boundary. */
+ if (i > 0 && local_offset > (n_lines * 4u / 5u))
+ {
+ TermBacklogBuf *prev = &ts->backlog_bufs[i - 1];
+
+ if (!prev->hdr && ts->pending_fetch_id < 0)
+ _backlog_request_async(ts, prev->buf_id);
+ }
+
return TERMSRV_BACKLOG_LINE(hdr, line_idx);
}
@@ -496,8 +569,9 @@ _cb_tymux_data(void *data, Ecore_Fd_Handler *handler EINA_UNUSED)
void *payload = NULL;
uint32_t size = 0;
int type;
+ int recv_fd = -1;
- type = termsrv_msg_recv(ts->sock_fd, &payload, &size);
+ type = termsrv_msg_recv(ts->sock_fd, &payload, &size, &recv_fd);
if (type == 0)
{
/* EAGAIN — no complete message available yet, wait for next wakeup */
@@ -542,17 +616,21 @@ _cb_tymux_data(void *data, Ecore_Fd_Handler *handler EINA_UNUSED)
if (next_new_id < new_oldest)
next_new_id = new_oldest;
- /* Fetch new head buffer(s) produced since the last NOTIFY.
+ /* Register new head buffer(s) as placeholder entries and
+ * request the newest one asynchronously. The response
+ * handler will chain-fetch older buffers automatically.
* new_head = oldest + count - 1; we want every id from
* next_new_id through new_head. */
if (new_count > 0)
{
uint32_t new_head = new_oldest + new_count - 1;
uint32_t id;
+ uint32_t newest_untracked = 0;
+ Eina_Bool have_untracked = EINA_FALSE;
for (id = next_new_id; id <= new_head; id++)
{
- /* Don't re-fetch an id already tracked */
+ /* Don't duplicate an id already tracked */
Eina_Bool already = EINA_FALSE;
int k;
@@ -565,8 +643,28 @@ _cb_tymux_data(void *data, Ecore_Fd_Handler *handler EINA_UNUSED)
}
}
if (!already)
- _backlog_buf_fetch(ts, id, -1);
+ {
+ /* Add a NULL placeholder — will be filled when
+ * the BACKLOG_BUF response arrives */
+ if (_backlog_bufs_grow(ts))
+ {
+ TermBacklogBuf *slot;
+
+ slot = &ts->backlog_bufs[ts->backlog_buf_count];
+ slot->hdr = NULL;
+ slot->size = 0;
+ slot->buf_id = id;
+ ts->backlog_buf_count++;
+ newest_untracked = id;
+ have_untracked = EINA_TRUE;
+ }
+ }
}
+
+ /* Request the newest untracked buffer; the response
+ * handler will cascade through older unmapped ones */
+ if (have_untracked)
+ _backlog_request_async(ts, newest_untracked);
}
ts->last_backlog_buf_count = new_count;
@@ -606,16 +704,36 @@ _cb_tymux_data(void *data, Ecore_Fd_Handler *handler EINA_UNUSED)
break;
case TSRV_MSG_EXIT:
+ if (recv_fd >= 0) { close(recv_fd); recv_fd = -1; }
if (ts->cb_exit.func)
ts->cb_exit.func(ts->cb_exit.data);
free(payload);
return ECORE_CALLBACK_CANCEL;
+ case TSRV_MSG_BACKLOG_BUF:
+ if (recv_fd >= 0 && payload &&
+ size >= sizeof(TermSrvMsgBacklogBuf))
+ {
+ _backlog_handle_buf_response(ts, recv_fd,
+ (TermSrvMsgBacklogBuf *)payload);
+ recv_fd = -1; /* ownership transferred to response handler */
+ }
+ else
+ {
+ WRN("termtymux: BACKLOG_BUF missing fd or payload");
+ /* recv_fd closed below */
+ }
+ break;
+
default:
WRN("termtymux: unexpected message type %d", type);
break;
}
+ /* Close any ancillary fd that was not consumed by a case handler */
+ if (recv_fd >= 0)
+ close(recv_fd);
+
free(payload);
return ECORE_CALLBACK_RENEW;
}
@@ -865,71 +983,125 @@ termtymux_attach(const char *tymux_name,
ts->oldest_buf_id = oldest_buf_id;
ts->last_backlog_buf_count = backlog_buf_count;
+ ts->pending_fetch_id = -1;
- /* mmap the head backlog buffer received in ATTACHED.
- * Use a two-stage map: first map just the header page to read the actual
- * cols width, then re-map at the correct total size. Mapping at
- * TERMSRV_BACKLOG_TOTAL_SIZE(TERMSRV_MAX_COLS) when the file is smaller
- * (because the session was started with fewer columns) would cause SIGBUS
- * on any access past the file's actual end. */
- if (backlog_fd >= 0 && backlog_buf_count > 0)
+ /* Build the backlog tracking array.
+ *
+ * The daemon told us there are backlog_buf_count buffers with ids
+ * [oldest_buf_id .. head_id]. It sent us the head buffer's memfd in
+ * the ATTACHED message. All older buffers are registered as NULL
+ * placeholders here so that _backlog_get_cb knows they exist and can
+ * trigger async fetches; they will be filled as BACKLOG_BUF responses
+ * arrive.
+ *
+ * Array order: oldest (index 0) → newest (index backlog_buf_count-1).
+ *
+ * Use a two-stage mmap for the head: first map just the header page to
+ * read the actual cols width, then re-map at the correct total size.
+ * Mapping at TERMSRV_BACKLOG_TOTAL_SIZE(TERMSRV_MAX_COLS) when the file
+ * is smaller would cause SIGBUS on access past the file's actual end. */
+ if (backlog_buf_count > 0)
{
- TermBacklogBufHeader *hdr_tmp;
- void *hdr_page;
- uint32_t head_cols = 0;
uint32_t head_id = oldest_buf_id + backlog_buf_count - 1;
+ uint32_t id;
+ TermBacklogBufHeader *head_hdr = NULL;
+ size_t head_size = 0;
- hdr_page = mmap(NULL, TERMSRV_BACKLOG_HEADER_SIZE,
- PROT_READ, MAP_SHARED, backlog_fd, 0);
- if (hdr_page != MAP_FAILED)
+ /* Step 1: mmap the head buffer if its fd was provided */
+ if (backlog_fd >= 0)
{
- hdr_tmp = hdr_page;
- if (hdr_tmp->magic == TERMSRV_BACKLOG_MAGIC &&
- hdr_tmp->version == TERMSRV_BACKLOG_VERSION &&
- hdr_tmp->cols > 0 &&
- hdr_tmp->cols <= TERMSRV_MAX_COLS)
- head_cols = hdr_tmp->cols;
- munmap(hdr_page, TERMSRV_BACKLOG_HEADER_SIZE);
- }
- else
- WRN("termtymux: mmap head backlog buf header: %s", strerror(errno));
+ TermBacklogBufHeader *hdr_tmp;
+ void *hdr_page;
+ uint32_t head_cols = 0;
- if (head_cols > 0)
- {
- size_t map_size = TERMSRV_BACKLOG_TOTAL_SIZE(head_cols);
- void *ptr;
-
- ptr = mmap(NULL, map_size, PROT_READ, MAP_SHARED, backlog_fd, 0);
- close(backlog_fd);
- backlog_fd = -1;
-
- if (ptr != MAP_FAILED)
+ hdr_page = mmap(NULL, TERMSRV_BACKLOG_HEADER_SIZE,
+ PROT_READ, MAP_SHARED, backlog_fd, 0);
+ if (hdr_page != MAP_FAILED)
{
- TermBacklogBufHeader *hdr = ptr;
+ hdr_tmp = hdr_page;
+ if (hdr_tmp->magic == TERMSRV_BACKLOG_MAGIC &&
+ hdr_tmp->version == TERMSRV_BACKLOG_VERSION &&
+ hdr_tmp->cols > 0 &&
+ hdr_tmp->cols <= TERMSRV_MAX_COLS)
+ head_cols = hdr_tmp->cols;
+ munmap(hdr_page, TERMSRV_BACKLOG_HEADER_SIZE);
+ }
+ else
+ WRN("termtymux: mmap head backlog buf header: %s",
+ strerror(errno));
- if (hdr->magic == TERMSRV_BACKLOG_MAGIC &&
- hdr->version == TERMSRV_BACKLOG_VERSION)
+ if (head_cols > 0)
+ {
+ void *ptr;
+
+ head_size = TERMSRV_BACKLOG_TOTAL_SIZE(head_cols);
+ ptr = mmap(NULL, head_size, PROT_READ, MAP_SHARED,
+ backlog_fd, 0);
+ if (ptr != MAP_FAILED)
{
- if (!_backlog_buf_push(ts, hdr, map_size, head_id))
+ TermBacklogBufHeader *h = ptr;
+
+ if (h->magic == TERMSRV_BACKLOG_MAGIC &&
+ h->version == TERMSRV_BACKLOG_VERSION)
+ head_hdr = h;
+ else
{
- WRN("termtymux: failed to push head backlog buf");
- munmap(ptr, map_size);
+ WRN("termtymux: head backlog buf magic/version mismatch");
+ munmap(ptr, head_size);
+ head_size = 0;
}
}
else
- {
- WRN("termtymux: head backlog buf magic/version mismatch");
- munmap(ptr, map_size);
- }
+ WRN("termtymux: mmap head backlog buf: %s", strerror(errno));
}
- else
- WRN("termtymux: mmap head backlog buf: %s", strerror(errno));
- }
- else
- {
+
close(backlog_fd);
backlog_fd = -1;
}
+
+ /* Step 2: add NULL placeholder entries for all older buffers */
+ for (id = oldest_buf_id; id < head_id; id++)
+ {
+ if (!_backlog_bufs_grow(ts))
+ break;
+ {
+ TermBacklogBuf *slot = &ts->backlog_bufs[ts->backlog_buf_count];
+
+ slot->hdr = NULL;
+ slot->size = 0;
+ slot->buf_id = id;
+ ts->backlog_buf_count++;
+ }
+ }
+
+ /* Step 3: push the head buffer (with real mapping, or NULL if mmap
+ * failed — the async path will fetch it later) */
+ if (!_backlog_buf_push(ts, head_hdr, head_size, head_id))
+ {
+ WRN("termtymux: failed to push head backlog buf");
+ if (head_hdr)
+ {
+ munmap((void *)head_hdr, head_size);
+ head_hdr = NULL;
+ head_size = 0;
+ }
+ /* Still add a NULL placeholder for the head so backlog_get
+ * can kick an async fetch */
+ if (_backlog_bufs_grow(ts))
+ {
+ TermBacklogBuf *slot = &ts->backlog_bufs[ts->backlog_buf_count];
+
+ slot->hdr = NULL;
+ slot->size = 0;
+ slot->buf_id = head_id;
+ ts->backlog_buf_count++;
+ }
+ }
+
+ /* Step 4: if there are older unmapped buffers, request the newest
+ * one; the response handler will chain through the rest */
+ if (backlog_buf_count > 1)
+ _backlog_request_async(ts, head_id - 1);
}
else if (backlog_fd >= 0)
{
@@ -1077,53 +1249,5 @@ termtymux_resize(TermTymux *ts, int cols, int rows)
ERR("termtymux: send RESIZE: %s", strerror(errno));
}
-int
-termtymux_request_backlog_buf(TermTymux *ts, uint32_t buf_id,
- int *fd_out, uint32_t *cols_out,
- uint32_t *num_lines_out,
- uint32_t *max_lines_out)
-{
- TermSrvMsgBacklogBufReq req;
- int flags;
- int ret;
-
- EINA_SAFETY_ON_NULL_RETURN_VAL(ts, -1);
- EINA_SAFETY_ON_NULL_RETURN_VAL(fd_out, -1);
- EINA_SAFETY_ON_NULL_RETURN_VAL(cols_out, -1);
- EINA_SAFETY_ON_NULL_RETURN_VAL(num_lines_out, -1);
- EINA_SAFETY_ON_NULL_RETURN_VAL(max_lines_out, -1);
-
- if (ts->sock_fd < 0)
- return -1;
-
- req.buf_id = buf_id;
- if (termsrv_msg_send(ts->sock_fd, TSRV_MSG_BACKLOG_BUF_REQ,
- &req, sizeof(req)) < 0)
- {
- ERR("termtymux: send BACKLOG_BUF_REQ: %s", strerror(errno));
- return -1;
- }
-
- /* Temporarily clear O_NONBLOCK so termsrv_recv_backlog_buf can use
- * MSG_WAITALL to receive header + payload + cmsg atomically.
- * The socket was set non-blocking after the attach handshake; restore
- * it unconditionally after the recv so the Ecore fd handler is not
- * affected regardless of the recv outcome. */
- flags = fcntl(ts->sock_fd, F_GETFL, 0);
- if (flags >= 0)
- fcntl(ts->sock_fd, F_SETFL, flags & ~O_NONBLOCK);
-
- ret = termsrv_recv_backlog_buf(ts->sock_fd, fd_out,
- &buf_id, cols_out,
- num_lines_out, max_lines_out);
- if (ret < 0)
- ERR("termtymux: recv BACKLOG_BUF failed");
-
- /* Restore non-blocking mode regardless of recv outcome */
- if (flags >= 0)
- fcntl(ts->sock_fd, F_SETFL, flags);
-
- return ret;
-}
#endif /* HAVE_TYMUX */
diff --git a/src/bin/termtymux.h b/src/bin/termtymux.h
index 04f4c904..fbcb8152 100644
--- a/src/bin/termtymux.h
+++ b/src/bin/termtymux.h
@@ -36,6 +36,7 @@ struct _TermTymux {
int backlog_buf_alloc; /* allocated slots */
uint32_t oldest_buf_id; /* first valid buffer id from daemon */
uint32_t last_backlog_buf_count; /* daemon's buf_count at last NOTIFY */
+ int pending_fetch_id; /* buf_id of in-flight BACKLOG_BUF_REQ, or -1 */
/* Callbacks — set these after termtymux_attach() returns,
* before the Ecore main loop fires. */
@@ -63,19 +64,6 @@ void termtymux_free(TermTymux *ts);
/* Send a resize event to the daemon. */
void termtymux_resize(TermTymux *ts, int cols, int rows);
-/*
- * Request a specific backlog buffer from the daemon and receive it
- * synchronously. The socket is briefly set to blocking mode for the
- * recv, then restored to O_NONBLOCK.
- *
- * On success *fd_out is a new memfd (caller must close it when done);
- * *cols_out, *num_lines_out and *max_lines_out describe the buffer.
- * Returns 0 on success, -1 on failure.
- */
-int termtymux_request_backlog_buf(TermTymux *ts, uint32_t buf_id,
- int *fd_out, uint32_t *cols_out,
- uint32_t *num_lines_out,
- uint32_t *max_lines_out);
#endif /* HAVE_TYMUX */
#endif /* TERMINOLOGY_TERMTYMUX_H_ */
diff --git a/src/tests/test_termsrv.c b/src/tests/test_termsrv.c
index 9d8639e6..dde4a527 100644
--- a/src/tests/test_termsrv.c
+++ b/src/tests/test_termsrv.c
@@ -56,7 +56,7 @@ test_msg_round_trip(void)
void *payload = NULL;
uint32_t size = 0;
- int type = termsrv_msg_recv(sv[1], &payload, &size);
+ int type = termsrv_msg_recv(sv[1], &payload, &size, NULL);
assert(type == TSRV_MSG_RESIZE);
assert(size == sizeof(TermSrvMsgResize));
TermSrvMsgResize *r = payload;
@@ -65,7 +65,7 @@ test_msg_round_trip(void)
/* zero-payload message */
assert(termsrv_msg_send(sv[0], TSRV_MSG_BELL, NULL, 0) == 0);
- type = termsrv_msg_recv(sv[1], &payload, &size);
+ type = termsrv_msg_recv(sv[1], &payload, &size, NULL);
assert(type == TSRV_MSG_BELL && size == 0 && payload == NULL);
close(sv[0]); close(sv[1]);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.