Skip to content

Commit b8b2dd3

Browse files
fix(mcp): resolve poll/getline FILE* buffering mismatch causing tools/list hang
1 parent 2704fdf commit b8b2dd3

1 file changed

Lines changed: 41 additions & 6 deletions

File tree

src/mcp/mcp.c

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2375,8 +2375,19 @@ int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out) {
23752375

23762376
for (;;) {
23772377
/* Poll with idle timeout so we can evict unused stores between requests.
2378-
* MCP is request-response (one line at a time), so mixing poll() on the
2379-
* raw fd with getline() on the buffered FILE* is safe in practice. */
2378+
*
2379+
* IMPORTANT: poll() operates on the raw fd, but getline() reads from a
2380+
* buffered FILE*. When a client sends multiple messages in rapid
2381+
* succession, the first getline() call may drain ALL kernel data into
2382+
* libc's internal FILE* buffer. Subsequent poll() calls then see an
2383+
* empty kernel fd and block for STORE_IDLE_TIMEOUT_S seconds even
2384+
* though the next messages are already in the FILE* buffer.
2385+
*
2386+
* Fix (Unix): use a two-phase approach —
2387+
* Phase 1: non-blocking poll (timeout=0) to check the kernel fd.
2388+
* Phase 2: if Phase 1 returns 0, peek the FILE* buffer via fgetc/
2389+
* ungetc to detect data buffered by a prior getline() call.
2390+
* Phase 3: only if both phases confirm no data, do blocking poll. */
23802391
#ifdef _WIN32
23812392
/* Windows: WaitForSingleObject on stdin handle */
23822393
HANDLE hStdin = (HANDLE)_get_osfhandle(fd);
@@ -2389,16 +2400,40 @@ int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out) {
23892400
continue;
23902401
}
23912402
#else
2403+
/* Phase 1: non-blocking poll — catches data already in the kernel fd
2404+
* AND handles the case where a prior getline() drained the kernel fd
2405+
* into libc's FILE* buffer (raw fd appears empty but data is buffered).
2406+
* We always try a zero-timeout poll first; if it misses buffered data,
2407+
* phase 2 below catches it via an explicit FILE* peek. */
23922408
struct pollfd pfd = {.fd = fd, .events = POLLIN};
2393-
int pr = poll(&pfd, 1, STORE_IDLE_TIMEOUT_S * 1000);
2409+
int pr = poll(&pfd, 1, 0); /* non-blocking */
23942410

23952411
if (pr < 0) {
23962412
break; /* error or signal */
23972413
}
23982414
if (pr == 0) {
2399-
/* Timeout — evict idle store to free resources */
2400-
cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S);
2401-
continue;
2415+
/* Raw fd appears empty. Check whether libc has already buffered
2416+
* data from a previous over-read by peeking one byte via fgetc.
2417+
* If successful, push it back and proceed to getline without
2418+
* blocking. If not (EOF or EAGAIN), do the blocking poll. */
2419+
int c = fgetc(in);
2420+
if (c == EOF) {
2421+
if (feof(in)) {
2422+
break; /* true EOF */
2423+
}
2424+
/* No buffered data and fd is empty — do blocking poll */
2425+
pr = poll(&pfd, 1, STORE_IDLE_TIMEOUT_S * 1000);
2426+
if (pr < 0) {
2427+
break;
2428+
}
2429+
if (pr == 0) {
2430+
cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S);
2431+
continue;
2432+
}
2433+
} else {
2434+
/* Buffered data found — push back and fall through to getline */
2435+
(void)ungetc(c, in);
2436+
}
24022437
}
24032438
#endif
24042439

0 commit comments

Comments
 (0)