serve mux: fix Windows request dispatch — BINARY-mode stdin/stdout + PeekNamedPipe-only polling (fixes #195)

Two Windows-only bugs in run_serve_mux left the gateway hanging after READY:
1. No _setmode(_O_BINARY): the CRT collapsed CRLF inside fread() payloads (waits
   forever for missing bytes) and expanded LF in the READY/STAT sentinels.
2. WaitForSingleObject on an anonymous pipe is undefined (always-signaled or
   WAIT_FAILED) and PeekNamedPipe fails on file/console handles, so the dispatch
   gate never opened. New rule: idle -> block in getline (POSIX select(NULL)
   semantics); active -> PeekNamedPipe poll.

Reproduced and verified on real Windows via MinGW cross-compile + WSL interop:
old binary writes READY (with CRLF corruption) then hangs forever on a crafted
SUBMIT frame; fixed binary answers DONE + STAT and exits cleanly. Linux path
untouched (0 warnings, oracle-exact).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
JustVugg
2026-07-14 18:15:29 +02:00
parent e12a4295cc
commit 2ead86a27f
+18 -10
View File
@@ -3418,6 +3418,14 @@ static void run_serve_mux(Model *m, const char *snap){
KVState *initial=m->kv; free(initial->kv_start); free(initial);
ServeCtx *ctx=calloc(nctx,sizeof(*ctx)); ServeReq *req=calloc(nctx,sizeof(*req));
for(int i=0;i<nctx;i++) serve_ctx_init(m,&ctx[i],snap,i,maxctx);
#ifdef _WIN32
/* Same byte-exact protocol as run_serve: in TEXT mode the CRT collapses CRLF in
* fread() payloads (waits forever for the missing bytes) and expands LF on the
* way out (corrupting the READY/STAT sentinels). BINARY on both ends. (#195) */
_setmode(_fileno(stdin), _O_BINARY);
_setmode(_fileno(stdout), _O_BINARY);
setvbuf(stdout, NULL, _IONBF, 0);
#endif
setvbuf(stdin,NULL,_IONBF,0);
printf("\x01\x01READY\x01\x01\nSTAT 0 0.00 0.0 %.2f\n",rss_gb()); fflush(stdout);
hwinfo_emit(m);
@@ -3441,16 +3449,16 @@ static void run_serve_mux(Model *m, const char *snap){
#elif defined(_WIN32)
HANDLE ih=(HANDLE)_get_osfhandle(_fileno(stdin));
DWORD avail=0;
/* WaitForSingleObject(handle, 0) is non-blocking and works on both
* pipe and console handles; for pipes PeekNamedPipe gives the byte
* count. Either returns "data is there right now". When a decode
* is active we poll (timeout 0); when idle we block until input. */
if(active){
ready=(WaitForSingleObject(ih,0)==WAIT_OBJECT_0)?1:0;
} else {
ready=(WaitForSingleObject(ih,INFINITE)==WAIT_OBJECT_0)?1:0;
}
if(ready && PeekNamedPipe(ih,NULL,0,NULL,&avail,NULL) && avail>0)
/* Anonymous pipes are NOT waitable objects: WaitForSingleObject on them is
* undefined (always-signaled or WAIT_FAILED), and PeekNamedPipe fails on
* file/console handles — the old gate never dispatched (#195). New rule:
* idle -> block in getline() inside mux_submit (same semantics as the
* POSIX select(NULL)); active -> poll the pipe with PeekNamedPipe, and on
* non-pipe stdin just defer submits until the batch finishes. */
if(eof) ready=0;
else if(!active) ready=1;
else ready=(PeekNamedPipe(ih,NULL,0,NULL,&avail,NULL) && avail>0)?1:0;
if(ready)
#endif
if(mux_submit(m,&T,ctx,req,nctx,maxctx,eos)<0) eof=1;
}