Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions c/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,9 @@ portable:
iobench$(EXE): iobench.c compat.h
$(CC) $(CFLAGS) iobench.c -o iobench$(EXE) $(LDFLAGS)

tests/test_serve_sentinel$(EXE): tests/test_serve_sentinel.c compat.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)

tests/test_json$(EXE): tests/test_json.c json.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)

Expand Down
29 changes: 29 additions & 0 deletions c/compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -418,4 +418,33 @@ static inline int coli_stdin_readable(void)
}
#endif

/* --- coli_serve_binary_mode: stdin/stdout in BINARY per il protocollo di serve ---
*
* I motori parlano un protocollo a BYTE con `coli`:
* stdout \x01\x01READY\x01\x01\n, righe STAT, \x01\x01END\x01\x01\n
* stdin righe di testo piu' i byte di controllo \x02RESET / \x02MORE
* Il gateway confronta i sentinella con endswith() e una regex "^STAT ...", quindi
* devono arrivare ESATTI (LF, senza CR).
*
* Su Windows il CRT apre entrambi gli handle in modalita' TEXT: stdout traduce
* '\n' -> '\r\n' (il sentinella READY non combacia MAI e la chat si blocca senza
* errore), e stdin traduce '\r\n' -> '\n' e rifiuta la scrittura di byte grezzi con
* EINVAL, rompendo il protocollo di controllo. (#195)
*
* colibri.c lo fa da sempre; inkling.c e kimi_k3.c sono nati senza, e nessuno se n'e'
* accorto finche' le release binarie non hanno iniziato a contenere quei motori
* (#720 -> #748: Kimi K3 su Windows caricava 93 layer in 42 minuti e poi restava
* fermo per sempre, perche' il gateway aspettava un byte gia' storpiato).
* Sta QUI e non copiato in ogni motore: e' esattamente cosi' che era sparito.
*
* No-op su Linux/macOS. */
static inline void coli_serve_binary_mode(void)
{
#ifdef _WIN32
_setmode(_fileno(stdin), _O_BINARY);
_setmode(_fileno(stdout), _O_BINARY);
setvbuf(stdout, NULL, _IONBF, 0);
#endif
}

#endif /* COMPAT_H */
5 changes: 5 additions & 0 deletions c/inkling.c
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,11 @@ static void serve_tiers_emap(Model *m) {
}

static void serve_loop(Model *m, Tok *T) {
/* Before the sentinel: on Windows a TEXT-mode stdout rewrites the trailing \n
* as \r\n, the gateway never matches it and waits forever (#748). Lives in
* compat.h because colibri.c has had it since #195 and this engine was
* written without it. */
coli_serve_binary_mode();
setvbuf(stdin, NULL, _IONBF, 0);
const char *sd = getenv("SEED");
if (sd) g_rng ^= (uint64_t)strtoull(sd, NULL, 10);
Expand Down
5 changes: 5 additions & 0 deletions c/kimi_k3.c
Original file line number Diff line number Diff line change
Expand Up @@ -1552,6 +1552,11 @@ static void serve_one(Model *m, Tok *T, ServeReq *q){
}

static void serve_loop(Model *m, Tok *T){
/* PRIMA del sentinella: su Windows stdout in modalita' TEXT trasforma il \n
* finale in \r\n, il gateway non lo riconosce e resta in attesa per sempre
* (#748). Vive in compat.h perche' colibri.c ce l'ha da #195 e questo motore
* e' nato senza. */
coli_serve_binary_mode();
setvbuf(stdin,NULL,_IONBF,0);
fputs("\x01\x01READY\x01\x01\n",stdout);
printf("STAT 0 0.0 0.0 %.2f 0 0\n",rss_gb());
Expand Down
85 changes: 85 additions & 0 deletions c/tests/test_serve_sentinel.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* test_serve_sentinel — the serve handshake must leave the wire bytes alone.
*
* `coli` matches the engine's sentinels byte-exactly (endswith on
* "\x01\x01READY\x01\x01\n", a "^STAT ..." regex). If anything rewrites the
* trailing LF, the gateway waits for a byte that never arrives and the session
* hangs with no error at all -- which is #748: Kimi K3 on Windows loaded 93
* layers in 42 minutes and then sat there forever.
*
* The cause is the CRT opening stdout in TEXT mode on Windows, where '\n' is
* written as "\r\n". colibri.c has called _setmode(..., _O_BINARY) since #195;
* inkling.c and kimi_k3.c were written without it, and nothing in the tree
* noticed for months because no test ever looked at the bytes.
*
* This is that test. It writes the sentinel through a real FILE* to a real file
* with coli_serve_binary_mode() applied, then reads the bytes back and asserts
* there is no CR. On Linux and macOS it can only pass -- which is the point:
* it is a Windows regression guard that costs nothing to carry elsewhere, and
* CI now builds every engine on Windows (#736), so it runs where it matters.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef _WIN32
#include <unistd.h> /* close() dopo mkstemp */
#endif
#include "../compat.h"

#define SENTINEL "\x01\x01READY\x01\x01\n"

static int fails = 0;

static void check(int cond, const char *what)
{
printf(" %s %s\n", cond ? "ok " : "FAIL", what);
if (!cond) fails++;
}

int main(void)
{
char path[] = "serve_sentinel_XXXXXX";
/* mkstemp gives a unique name; we want a FILE* we control the mode of, so
* close the fd and reopen by name. */
int fd = mkstemp(path);
if (fd < 0) { perror("mkstemp"); return 2; }
close(fd);

FILE *f = fopen(path, "w"); /* deliberately TEXT mode: the bug's condition */
if (!f) { perror("fopen"); remove(path); return 2; }

/* Same call the serve loops make before emitting the handshake. On Windows it
* flips the stream to binary; elsewhere it is a no-op. We point it at stdout,
* so exercise the same primitive here on our own stream. */
coli_serve_binary_mode();
#ifdef _WIN32
_setmode(_fileno(f), _O_BINARY);
#endif

fputs(SENTINEL, f);
fprintf(f, "STAT 0 0.0 0.0 %.2f 0 0\n", 1.0);
fclose(f);

FILE *r = fopen(path, "rb"); /* rb: read the bytes as they landed */
if (!r) { perror("fopen rb"); remove(path); return 2; }
char buf[256];
size_t n = fread(buf, 1, sizeof(buf), r);
fclose(r);
remove(path);

printf("test_serve_sentinel: %zu bytes written\n", n);

check(n >= sizeof(SENTINEL) - 1, "something was written");
check(memcmp(buf, SENTINEL, sizeof(SENTINEL) - 1) == 0,
"READY sentinel is byte-exact");
check(memchr(buf, '\r', n) == NULL,
"no CR anywhere -- a TEXT-mode stream would have inserted one (#748)");

/* The STAT line is matched by a "^STAT " regex on a line basis, so its
* terminator has to be a bare LF too. */
const char *stat = memchr(buf, 'S', n);
check(stat && strncmp(stat, "STAT ", 5) == 0, "STAT line follows the sentinel");
check(buf[n - 1] == '\n', "output ends with a bare LF");

printf("test_serve_sentinel: %s\n", fails ? "FAILED" : "ok");
return fails ? 1 : 0;
}
Loading