• exec/load/door_deploy.js

    From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun Jul 12 05:11:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/8e5f359a05d0bfb7f6f0689b
    Modified Files:
    exec/load/door_deploy.js src/doors/syncconquer/build.bat build.sh src/doors/syncdoom/build.bat build.sh src/doors/syncduke/build.bat build.sh src/doors/syncmoo1/build.bat build.sh src/doors/syncretro/deploy.js
    Log Message:
    doors: stop deploy.js from truncating the binary it deploys

    The deploy step could copy a door binary onto itself and destroy it. The
    usual dev install has the door's live entry SYMLINKED to the build output (./build.sh and the change is live, no deploy step) -- and file_copy()
    opens the destination for writing, which through that symlink IS the
    source. It truncated the binary to zero bytes before reading a byte of
    it, leaving a 0-byte door and a 0-byte "copy". The build system then saw
    a target newer than its sources and declined to relink, so the next
    build.sh reported success over a corpse.

    Both guards that were supposed to prevent it are blind to a symlink:

    * fullpath(dst) == fullpath(exe) -- fullpath() NORMALIZES a path, it
    does not RESOLVE it, so a symlink never compares equal to its target.
    * size + mtime -- right on one filesystem, wrong across the SMB mount a
    live install is usually reached through: CIFS reports the same file's
    mtime a second off from the local view, the guard misses, the copy
    runs.

    The deploy.sh scripts this replaced got it right with `[ "$EXE" -ef
    "$DST" ]`, a device+inode identity test. Synchronet's JS has no stat(),
    so compare CONTENT instead: it answers a strictly stronger question, and answers it through symlinks, mounts and clock skew alike. If dst's bytes
    ARE exe's bytes there is nothing to copy -- and the destructive case (dst resolves to exe) is identical BY CONSTRUCTION, so it is always taken by
    that branch and the copy can never eat its own source.

    Also add a post-copy tripwire: if the source's size changed across the
    copy, say so loudly. It cannot undo the damage, but a 0-byte door dies at
    the player's first keystroke and the build that produced it looks clean.

    syncretro/deploy.js had grown a private copy of the same broken guard
    while its comment claimed it shared the common one; it now calls door_deploy_file(). It also only ever scanned this checkout's xtrn/, so
    the live install was never deployed to at all -- it now scans both, and
    keeps an existing flat binary in step with the <os>-<arch> one so a
    removed sub-dir cannot silently resurrect a stale door.

    The build scripts still pointed at the deploy.sh/deploy.bat pair that
    deploy.js replaced.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun Jul 12 05:11:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/f98e9fb9909bbee7a516aec9
    Modified Files:
    exec/load/door_deploy.js src/doors/syncretro/syncretro_io.c src/doors/termgfx/geometry.c geometry.h
    Log Message:
    syncretro: erase the stale first frame, center against the real canvas

    Two separate defects, both visible in Windows Terminal as a grey slab down
    the left of the picture and an image that is not quite centered.

    THE GREY SLAB is a stale frame. The door assumes an 80x25 terminal until the probe replies come back, and a reply costs a network round-trip -- so it was painting frames during that window, then re-fitting to the real canvas and drawing a bigger image at a different column. A sixel is PIXELS, not cells: nothing about drawing the new image erases the old one, so whatever the first image painted outside the new one's rect stayed on screen for the whole session. It is grey because it is the console's power-on frame, which is what the core was still showing when those frames went out.

    The clearing repaint already existed (a forced repaint emits ESC[2J ahead of the frame). sr_io_set_aspect() called it; sr_io_set_canvas() and sr_io_set_grid() -- the two that fire on EVERY session -- did not. Move the invalidate into sr_io_recompute_geom(), where the rect is actually decided
    and no caller can forget it, and hold the first frame until the grid reply lands (or 400 ms passes, for a terminal that never answers) so there is no wrong-sized flash to erase in the first place.

    THE OFF-CENTERING is an integer truncation. A cell is canvas/grid, and 1648
    px over 164 columns is 10.05, not 10. The page was then derived as cols *
    cell -- up to `cols` pixels NARROWER THAN THE SCREEN, ~100 px on a wide terminal -- and centering inside a short page lands the image half that error to the left. On a display at 125%/150% scaling the fraction is bigger still, which is why this showed up on Windows Terminal and not here.

    Keep the cell fractional (termgfx_geom_center_ex, with the old int entry
    point kept as a wrapper so the sibling doors are bit-identical), center
    against the true canvas, and quantize once at the end, where the text cursor genuinely forces a whole cell. Residual error is now under half a cell.

    SyncTERM is deliberately excluded from that: it never answers the ESC[14t canvas probe, so its canvas is our GUESS, and centering against a guess is
    how syncmoo1's image used to land on the last row. When the canvas is not
    known the geometry keeps deriving the page from the grid, which IS real, and comes out exactly as before.

    The door now logs the geometry it settled on, which is what this needed and
    did not have: the terminal's canvas, grid, fractional cell, page, image size and cell origin, in one line.

    The same two defects are latent in syncmoo1, syncdoom, syncduke and syncconquer; they are untouched here.

    deploy.js: a running door cannot be overwritten (ETXTBSY), which on a live
    BBS is the normal case rather than an edge one. Report it as what it is and change nothing. Do NOT reach for the package-manager trick of copying
    alongside and renaming over the top: the door dir is reached over an SMB
    mount, where rename() onto a file another process is executing deletes the target and THEN fails, leaving a deleted-but-still-open phantom where the
    door binary was.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sun Jul 12 15:06:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/ffe97f619b7eb324f2fde889
    Modified Files:
    exec/load/door_deploy.js src/doors/syncconquer/deploy.js
    Log Message:
    door_deploy: look for the binary where build.bat actually leaves it

    Deploy was broken on Windows for every door that uses the shared helper -- syncconquer, syncdoom, syncduke and syncmoo1. door_deploy() resolved the built binary as build/<name>, which is right for build.sh and wrong for build.bat: that configures a Visual Studio (multi-config) generator into build-msvc/, so the binary lands in build-msvc/Release/. A Windows sysop therefore got

    [deploy] ERROR: ...\build\syncduke.exe not found -- run build.bat first

    immediately after a build.bat that had just succeeded.

    Probe instead of assuming. door_find_built() walks the locations a build can actually leave a binary -- build-msvc/<Config>/ (build.bat), build/<Config>/
    (a hand-run VS generator, which COMPILING.md documents), and build/ (build.sh, or a single-config generator) -- and reports where it looked when it finds nothing, so the next person doesn't have to read the source. spec.built still pins the location and skips the probe.

    When several builds exist -- a stale build/Debug beside a fresh build-msvc/Release -- take the newest and say so. Silently preferring a fixed order would deploy a binary the sysop did not just build, which surfaces later as a change that "didn't take": a miserable thing to chase.

    The convention itself is not new. syncretro's deploy.js does not use the
    shared helper and already had build-msvc/Release right; only the shared path never learned it.

    syncconquer's own deploy.js repeated the assumption in its SyncDawn gate (file_exists("build/syncdawn")), so it would have gone on silently skipping SyncDawn on Windows even after the helper was fixed. It asks door_find_built() now.

    Verified under jsexec on Win32: both syncalert.exe and syncdawn.exe resolve to build-msvc/Release (previously: not found), a missing build still yields null rather than throwing, spec.built bypasses the probe, and the newest of two builds wins with the notice printed.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Mon Jul 13 21:37:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/d343993cf8abd5940d44f503
    Modified Files:
    exec/load/door_deploy.js src/doors/syncconquer/deploy.js src/doors/syncdoom/deploy.js src/doors/syncduke/deploy.js src/doors/syncmoo1/deploy.js src/doors/syncretro/deploy.js
    Log Message:
    doors: deploy the built binary to the live install only

    door_deploy() had two destinations: the in-tree bundle (<checkout>/xtrn/<door>/) and the live install (the xtrn/<door> dir beside system.ctrl_dir). On the recommended install those are ONE FILE -- install- sbbs.mk SYMLINK=1 does "ln -sf $(REPODIR)/xtrn $(SBBSDIR)", so the live xtrn
    is a symlink to the repo's -- and the guard meant to collapse them cannot see it: fullpath() normalizes a path but does not resolve it, so a symlink and its target never compare equal, and Synchronet's JS has no stat() to compare
    inodes with.

    The second pass was therefore harmless but baffling. It hashed the
    destination, found the bytes the first pass had just written, and reported

    [deploy] Deployed: /home/user/sbbs/repo/xtrn/syncdoom/syncdoom
    [deploy] /home/user/sbbs/ctrl/../xtrn/syncdoom/syncdoom is already this
    build -- nothing to copy

    which reads as a contradiction precisely when a sysop is asking "did my build land?" -- and cost a round of head-scratching on IRC. (Only one write ever happened: the content check that stopped the second is the same guard that keeps a copy onto a symlinked destination from truncating its own source.)

    Where the two ARE different directories -- a copy-style install, or a checkout that is not the install -- the in-tree copy was pointless: nothing launches from the checkout (xtrn.ini runs the door out of the live dir, and install- xtrn fetches the binary via get-binary.js), so it only littered the working tree with a gitignored multi-megabyte binary.

    So deploy to the live install, full stop. A bare checkout with no BBS
    installed (packaging, CI) still falls back to the in-tree bundle, and says so. No layout now addresses one file twice.

    Verified by running deploy.js under jsexec against a fake install for each layout: symlinked install (one copy, one line), copy-style install (live updated, no stray binary in the checkout), bare checkout (falls back to the bundle), and a second run with nothing rebuilt ("already this build -- nothing to copy").

    Reported by Accession.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Jul 14 21:04:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/9ba41ffc77430e157543bb0d
    Modified Files:
    exec/load/door_deploy.js src/doors/syncretro/deploy.js
    Log Message:
    syncretro: deploy to the live install only (no double pass)

    The "deploy to the live install only" fix that stopped the sibling doors double-addressing one directory landed in door_deploy()'s destination
    logic -- and SyncRetro does not use it. SyncRetro uses door_deploy.js for
    the COPY (door_deploy_file's content-hash guard) and the target token, but
    one binary serves N console dirs found by scanning, not one xtrn/<door>
    named by a spec, so it carries its own destination logic. That logic still scanned BOTH the in-tree bundle (<checkout>/xtrn/) and the live install (<ctrl>/../xtrn/).

    On the recommended install (install-sbbs.mk SYMLINK=1) the live xtrn IS a symlink to the repo's, so those two scans address ONE directory, and the
    guard meant to collapse them cannot see it: fullpath() normalizes a path
    but does not resolve it, so a symlink and its target never compare equal,
    and Synchronet's JS has no stat() to compare inodes with. The second pass
    then found the bytes the first had just written and printed "already this
    build -- nothing to copy" directly after "Deployed" -- a contradiction precisely when a sysop is checking whether their build landed. Reported by Accession.

    So scan the live install, full stop; fall back to the in-tree bundle only
    when no console is installed there (a bare checkout for packaging/CI), and
    say so. Same reasoning and same fix as door_deploy(). Where live and
    checkout are genuinely two directories (a copy install), the in-tree copy
    was pointless anyway -- nothing launches from the checkout.

    Also normalize the "already this build" line in door_deploy.js to print fullpath(dst): it was showing the raw <ctrl>/../xtrn/... form beside a normalized "Deployed:" path, which was half of why the output read as a contradiction. Benefits every door.

    Verified under jsexec against a fake install in all three layouts:
    symlinked install (each console visited once, no phantom second pass), copy-style install (live updated, checkout untouched), and bare checkout
    (falls back to the bundle and says why).

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun Jul 26 22:57:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/03a61c1973e94fd92dd1fd8b
    Modified Files:
    exec/load/door_deploy.js
    Log Message:
    doors: deploy the binary with its execute permission

    file_copy() creates its destination with fopen(dest, "wb"), so the copy
    gets mode 0666 & ~umask -- 0644 under the usual 0022 -- and the source's permission bits are not carried across (GitLab #1202). A door binary
    therefore arrived non-executable and the BBS refused to launch it:

    !ERROR 13 (Permission denied) executing: /sbbs/xtrn/syncalert/syncalert

    Add the execute bits after each copy, and only where read permission is
    already granted, so a deliberately restricted install is never widened.
    A destination that can already be executed is left alone entirely, which
    keeps deploys onto a file owned by the BBS user -- or reached over a
    mount that forbids chmod -- from warning about a mode that is correct.
    Windows has no execute bit to lose and is skipped.

    This runs on the "already this build" path too: a binary deployed before
    this fix is byte-identical and still non-executable, and re-running the
    deploy is exactly what a sysop hitting the permission error will try.

    Only a first deploy was ever affected. Copying over an existing binary truncates it in place, so that file keeps the mode it already had, and
    any install where the door had been deployed once -- or whose live entry
    is a symlink into the build tree -- looked correct.

    Reported by nelgin, installing SyncAlert on a fresh system.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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