Unix 3-phase poll: non-blocking fd check, FILE* buffer peek, blocking poll. * Returns: 1 = data ready, 0 = timeout (evicted idle stores), -1 = error/EOF. */
| 6235 | /* Unix 3-phase poll: non-blocking fd check, FILE* buffer peek, blocking poll. |
| 6236 | * Returns: 1 = data ready, 0 = timeout (evicted idle stores), -1 = error/EOF. */ |
| 6237 | static int poll_for_input_unix(cbm_mcp_server_t *srv, int fd, FILE *in) { |
| 6238 | struct pollfd pfd = {.fd = fd, .events = POLLIN}; |
| 6239 | int pr = poll(&pfd, SKIP_ONE, 0); /* Phase 1: non-blocking */ |
| 6240 | |
| 6241 | if (pr < 0) { |
| 6242 | return CBM_NOT_FOUND; |
| 6243 | } |
| 6244 | if (pr > 0) { |
| 6245 | return SKIP_ONE; |
| 6246 | } |
| 6247 | |
| 6248 | /* Phase 2: peek FILE* buffer */ |
| 6249 | int saved_flags = fcntl(fd, F_GETFL); |
| 6250 | if (saved_flags < 0) { |
| 6251 | /* fcntl failed — fall through to a short blocking poll (see the Phase-3 |
| 6252 | * note below on why the interval is bounded, not the full idle timeout) */ |
| 6253 | pr = poll(&pfd, SKIP_ONE, MCP_TIMEOUT_MS); |
| 6254 | if (pr < 0) { |
| 6255 | return CBM_NOT_FOUND; |
| 6256 | } |
| 6257 | if (pr == 0) { |
| 6258 | cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S); |
| 6259 | return 0; |
| 6260 | } |
| 6261 | return SKIP_ONE; |
| 6262 | } |
| 6263 | |
| 6264 | (void)fcntl(fd, F_SETFL, saved_flags | O_NONBLOCK); |
| 6265 | int c = fgetc(in); |
| 6266 | (void)fcntl(fd, F_SETFL, saved_flags); |
| 6267 | |
| 6268 | if (c == EOF) { |
| 6269 | if (feof(in)) { |
| 6270 | return CBM_NOT_FOUND; /* true EOF */ |
| 6271 | } |
| 6272 | clearerr(in); |
| 6273 | /* Phase 3: blocking poll, bounded to a SHORT interval (not the full idle |
| 6274 | * timeout). macOS poll()/select() do NOT report POLLIN/POLLHUP when a |
| 6275 | * FIFO's last writer closes — only read() returns 0 there (verified). A |
| 6276 | * 60s poll would therefore leave the server blocked up to a full idle |
| 6277 | * timeout after stdin EOF (a client that closes the pipe would appear to |
| 6278 | * hang). Waking every MCP_TIMEOUT_MS lets the Phase-2 read() above detect |
| 6279 | * the EOF within ~1s. Idle-store eviction (threshold STORE_IDLE_TIMEOUT_S) |
| 6280 | * is idempotent, so checking it on each short tick is harmless. */ |
| 6281 | pr = poll(&pfd, SKIP_ONE, MCP_TIMEOUT_MS); |
| 6282 | if (pr < 0) { |
| 6283 | return CBM_NOT_FOUND; |
| 6284 | } |
| 6285 | if (pr == 0) { |
| 6286 | cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S); |
| 6287 | return 0; |
| 6288 | } |
| 6289 | return SKIP_ONE; |
| 6290 | } |
| 6291 | |
| 6292 | (void)ungetc(c, in); |
| 6293 | return SKIP_ONE; |
| 6294 | } |
no test coverage detected