• src/doors/termgfx/door32.c door32.hexec/load/syncretro_lobby.js src/do

    From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sat Jul 11 18:22:41 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/19901be145ac554e92b4b388
    Added Files:
    src/doors/termgfx/door32.c door32.h
    Modified Files:
    exec/load/syncretro_lobby.js src/doors/syncconquer/door/door_io.c src/doors/syncdoom/syncdoom.c src/doors/syncduke/syncduke.h syncduke_config.c syncduke_door.c syncduke_io.c src/doors/syncmoo1/hw_sbbs.c syncmoo1.h syncmoo1_door.c syncmoo1_io.c syncmoo1_plat.c syncmoo1_plat.h src/doors/syncretro/README.md main.c syncretro.h syncretro_door.c syncretro_input.c syncretro_io.c syncretro_plat.c syncretro_plat.h src/doors/termgfx/CMakeLists.txt xtrn/syncnes/lobby.js
    Log Message:
    doors: one DOOR32.SYS parser, and every door can be a stdio door (*nix)

    Synchronet does not hand a native door the client socket. It opens a
    loopback socketpair, gives the door one end, and pumps between it and the
    BBS's own ring buffers -- so the BBS does the telnet negotiation and the
    SSH crypto, and a door has never seen an IAC byte in its life. That is why these doors work over SSH without knowing what SSH is.

    Mystic on *nix does the same job the other way: it forks the door and
    redirects stdin/stdout, doing the telnet and SSH itself. The stream that arrives is just as clean. So the doors can run there -- but only if they
    can read ONE descriptor and write ANOTHER.

    * SyncDOOM and SyncDuke already could (g_rfd/g_wfd, syncduke_input_fd).
    * SyncRetro, SyncMOO1 and SyncConquer read the fd they WRITE. That is
    right for a socket, right at a dev console (a tty descriptor is
    bidirectional), and stone deaf down a pipe: the door draws a picture
    and ignores every keystroke. Now split.

    THE DROP FILE ALREADY SAID SO, AND NOBODY WAS LISTENING. DOOR32.SYS line 1
    comm type 0 means LOCAL -- there is no socket because the BBS redirected
    our stdio. Synchronet writes exactly that (comm type 0, INVALID_SOCKET)
    when an external program is registered XTRN_STDIO; Mystic does the same.
    Five doors carried five copies of that parser and not one of them knew.

    So the parser moves to termgfx/door32.c. The copies had drifted -- four different ways to copy the alias, one door that never read it -- and each
    had learned something the others hadn't: the 0-based-vs-1-based line
    numbering, and why the alias is memcpy'd rather than snprintf'd (the
    truncation is deliberate; snprintf earns a -Wformat-truncation that
    -Werror turns into a build failure on some targets).

    It also fixes a hole the copies shared: atoi("") and atoi("garbage") both return 0, and 0 is now a MEANINGFUL comm type. A truncated drop file made
    a door believe the BBS had redirected its stdio and ignore the perfectly
    good socket handle on the next line. Line 1 is parsed strictly now; no
    number means UNKNOWN, never local. And a comm type we cannot use -- serial, undefined, or a telnet line with a dead handle -- is REPORTED rather than
    left to fail as a door that starts, shows nothing and answers nothing.

    A STDIO DOOR MUST NOT WRITE TO STDERR. The BBS dup2()s stderr onto the same stream as stdout (xtrn.cpp's EX_STDIO path), so on a stdio door stderr IS
    the player's screen -- and these doors are chatty on it. SyncDOOM stamps timestamped lines there; 1oom prints config errors there; SyncDuke
    DELIBERATELY routes Chocolate Duke's printf/Error() there, which is correct when stderr is the BBS log and exactly wrong when it is the terminal. termgfx_stderr_capture() sends it to data/<door>/<door>_n<node>.log instead.

    Two bugs that only a live stdio run could find:

    * SyncDuke's dup2(STDERR, STDOUT) -- right for a socket door, where fd 1
    is unused -- DESTROYS the connection of a stdio door, where fd 1 IS the
    player. It wrote every sixel frame into the BBS log. It now keeps a
    duplicate of the real stdout, taken before the engine takes fd 1.
    * A blocking stdin parks the whole game inside read() until the player
    happens to press a key: no tick, no render, apparently hung. Both new
    input descriptors are set non-blocking.

    Verified on all five doors, socket and stdio, byte-for-byte comparable
    output on each path, stdin actually read (strace), and zero stderr leaked
    onto the player's stream. SyncRetro was also live-tested through
    Synchronet's own EX_STDIO path -- an NES cartridge played on a pty with no socket at all -- which is what turned the stderr leak up.

    What this does NOT add is telnet. A BBS that hands over the RAW client
    socket would need the door to negotiate options, unescape IAC IAC, and
    survive a NAWS window-resize whose payload bytes arrive looking like
    keystrokes -- one of which, 0x11, is the quit key. Measured, not assumed:
    fed a real negotiation and a resize, the door read the NAWS column count
    (17 = 0x11) as Ctrl-Q and exited. The output side needs no escaping and is
    now measured too: 461 KB of live wire capture -- sixel, base64 audio APCs, overlays -- contains not one byte >= 0x80. (The CP437 text tiers of the
    sibling doors are NOT 7-bit, and would.)

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net