fluffos/.github/workflows/ci.yml
Claude 3bdbe9676e www: real ws backpressure fix, teardown leak fix, src/www docs, websocket smoke test in CI
The lws output wedge addressed in the previous commit was incompletely
understood: under genuine backpressure (peer slow or paused, kernel
send buffer full) a connection still froze permanently. Root cause,
established by tracing the writeable-request plumbing end to end:
lws_send_pipe_choked() is true not only when lws holds a truncated
send (lws re-arms the writeable callback itself then) but also when a
zero-timeout poll(POLLOUT) reports the socket simply full -- and in
that case every lws_write() has fully succeeded, lws has nothing
pending, and nobody re-arms anything. Fix, per lws README.coding.md:
whenever the drain loop exits with data still queued in pss->buffer,
request the next writeable callback. Queued data always has a callback
requested, so no exit path can strand output.

Also:
- LWS_CALLBACK_CLOSED frees the session evbuffer unconditionally: on
  driver-initiated closes (e.g. the mudlib destructing the interactive)
  close_user_websocket() nulls pss->user first, and the old early
  return leaked the buffer every time.
- src/www/README.md + src/www/AGENTS.md: architecture doc and agent
  checklist for the web terminal pages (xterm.js/telnet.js layering,
  vendor policy, packaging, testing, the wedge mechanism).
- tools/ws-smoke.js, wired into CI on the Clang Debug matrix entries
  (with and without sanitizers): a dependency-free node websocket
  client that boots the real driver and exercises the http mount,
  telnet + ascii subprotocols through the shared src/www/telnet.js,
  SGA char-mode switching, live TUI streaming, TLS, and -- the actual
  regression gate -- forced-backpressure bursts (paused socket, ~4.8MB)
  on the plain and TLS ports plus a destruct-while-choked teardown
  check. All three backpressure checks fail on the unfixed driver;
  neither GTest nor the LPC suite exercises any websocket client
  traffic.
- Fix stale src/www/wasm/vendor/ path references left from the vendor
  directory move (src/wasm/README.md, docs/build-wasm.md, release.yml).

Validated on the ASan Debug build: forced-backpressure repros recover
the full burst on both subprotocols, destruct-while-choked clean under
ASan, ws-smoke 17/17, GTest 312/312, LPC testsuite clean.
2026-07-14 09:53:26 -05:00

215 lines
8.2 KiB
YAML

name: CI
on:
push:
branches: [master]
paths-ignore: ['docs/**']
pull_request:
branches: [master]
paths-ignore: ['docs/**']
env:
CTEST_OUTPUT_ON_FAILURE: 1
VERBOSE: 1
jobs:
build-and-test:
name: ${{ matrix.platform }} (${{ matrix.compiler || 'default' }}${{ matrix.sanitizer && '+sanitizer' || '' }}, ${{ matrix.build }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Ubuntu - GCC (also builds the optional dwlib package so it can't
# silently bit-rot again)
- platform: Ubuntu
os: ubuntu-24.04
compiler: gcc
cc: gcc
cxx: g++
build: Debug
cmake_flags: -DPACKAGE_DWLIB=ON
packages: build-essential autoconf automake bison flex expect libmysqlclient-dev libpcre3-dev libpq-dev libsqlite3-dev libssl-dev libtool libz-dev telnet libgtest-dev libjemalloc-dev pkg-config libffi-dev
- platform: Ubuntu
os: ubuntu-24.04
compiler: gcc
cc: gcc
cxx: g++
build: RelWithDebInfo
cmake_flags: -DPACKAGE_DWLIB=ON
packages: build-essential autoconf automake bison flex expect libmysqlclient-dev libpcre3-dev libpq-dev libsqlite3-dev libssl-dev libtool libz-dev telnet libgtest-dev libjemalloc-dev pkg-config libffi-dev
# Ubuntu - Clang
- platform: Ubuntu
os: ubuntu-24.04
compiler: clang
cc: clang
cxx: clang++
build: Debug
packages: build-essential autoconf automake bison flex expect libmysqlclient-dev libpcre3-dev libpq-dev libsqlite3-dev libssl-dev libtool libz-dev telnet libgtest-dev libjemalloc-dev pkg-config libffi-dev
- platform: Ubuntu
os: ubuntu-24.04
compiler: clang
cc: clang
cxx: clang++
build: RelWithDebInfo
packages: build-essential autoconf automake bison flex expect libmysqlclient-dev libpcre3-dev libpq-dev libsqlite3-dev libssl-dev libtool libz-dev telnet libgtest-dev libjemalloc-dev pkg-config libffi-dev
# Ubuntu - Clang with Sanitizer
- platform: Ubuntu
os: ubuntu-24.04
compiler: clang
cc: clang
cxx: clang++
build: Debug
sanitizer: true
cmake_flags: -DENABLE_SANITIZER=ON
packages: build-essential autoconf automake bison flex expect libmysqlclient-dev libpcre3-dev libpq-dev libsqlite3-dev libssl-dev libtool libz-dev telnet libgtest-dev libjemalloc-dev pkg-config libdw-dev libbz2-dev libffi-dev
- platform: Ubuntu
os: ubuntu-24.04
compiler: clang
cc: clang
cxx: clang++
build: RelWithDebInfo
sanitizer: true
cmake_flags: -DENABLE_SANITIZER=ON
packages: build-essential autoconf automake bison flex expect libmysqlclient-dev libpcre3-dev libpq-dev libsqlite3-dev libssl-dev libtool libz-dev telnet libgtest-dev libjemalloc-dev pkg-config libdw-dev libbz2-dev libffi-dev
# macOS
- platform: macOS
os: macos-14
build: Debug
packages: cmake pkg-config pcre libgcrypt openssl jemalloc icu4c mysql sqlite3 googletest flex bison libffi
openssl_root: /usr/local/opt/openssl
icu_root: /opt/homebrew/opt/icu4c
- platform: macOS
os: macos-14
build: RelWithDebInfo
packages: cmake pkg-config pcre libgcrypt openssl jemalloc icu4c mysql sqlite3 googletest flex bison libffi
openssl_root: /usr/local/opt/openssl
icu_root: /opt/homebrew/opt/icu4c
# Windows (MSYS2), the static Alpine build, and WASM are separate jobs
# below -- each wraps a composite action in .github/actions/ that the
# release workflow reuses, so a release-only build can't silently break.
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Dependencies (Ubuntu)
if: matrix.platform == 'Ubuntu'
run: |
sudo apt update
sudo apt install -y ${{ matrix.packages }}
- name: Install Dependencies (macOS)
if: matrix.platform == 'macOS'
env:
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
run: |
brew install ${{ matrix.packages }}
# Homebrew flex/bison are keg-only (they'd shadow the too-old
# system copies); put their bin dirs on PATH so CMake's
# find_package(FLEX 2.6)/(BISON 3.8) picks them up and the
# scanner/parser are regenerated from lex.l/grammar.y instead of
# falling back to the checked-in pre-generated sources.
echo "$(brew --prefix flex)/bin" >> "$GITHUB_PATH"
echo "$(brew --prefix bison)/bin" >> "$GITHUB_PATH"
# bison also needs its lib dir for -ly at link time on some setups.
echo "LDFLAGS=-L$(brew --prefix bison)/lib" >> "$GITHUB_ENV"
# libffi is keg-only on macOS; expose its pkg-config so
# pkg_check_modules(libffi) in package_ffi resolves it.
echo "PKG_CONFIG_PATH=$(brew --prefix libffi)/lib/pkgconfig:$PKG_CONFIG_PATH" >> "$GITHUB_ENV"
- name: Configure
run: |
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=${{ matrix.build }} \
${{ matrix.cmake_flags }} \
-DPACKAGE_DB_SQLITE=2 \
..
env:
CC: ${{ matrix.cc }}
CXX: ${{ matrix.cxx }}
OPENSSL_ROOT_DIR: ${{ matrix.openssl_root }}
ICU_ROOT: ${{ matrix.icu_root }}
- name: Build
run: cd build && make -j 2 install
env:
OPENSSL_ROOT_DIR: ${{ matrix.openssl_root }}
ICU_ROOT: ${{ matrix.icu_root }}
- name: Run Unit Tests
run: cd build && ctest --output-on-failure -LE testsuite
- name: Run LPC Testsuite
run: cd build && ctest --output-on-failure -L testsuite
# Only on Clang Debug (with and without the sanitizer) -- it's the
# only test that drives real websocket client traffic against the
# native driver (see src/www/AGENTS.md); the sanitizer variant also
# catches memory-safety bugs in the ws_telnet.cc/ws_ascii.cc nudge
# timer that ctest's unit/LPC suites can't reach.
- name: Websocket smoke test
if: matrix.platform == 'Ubuntu' && matrix.compiler == 'clang' && matrix.build == 'Debug'
run: node tools/ws-smoke.js build/src/driver testsuite
# Windows (MSYS2/MinGW64). Uses the same build-windows composite as the
# release workflow, so the release build can't break in a way CI misses.
windows:
name: Windows (${{ matrix.build }})
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
build: [Debug, RelWithDebInfo]
defaults:
run:
shell: msys2 {0}
steps:
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
update: true
- name: Checkout
uses: actions/checkout@v4
- name: Build & test
uses: ./.github/actions/build-windows
with:
build-type: ${{ matrix.build }}
db-sqlite: '2'
# flex regenerates the lexer from lexer.l instead of using the
# checked-in pre-generated source.
extra-packages: flex
# Statically-linked Alpine build. Uses the same build-alpine-static
# composite the release workflow ships as
# fluffos-<version>-linux-x86_64-static.tar.gz, so a missing/renamed Alpine
# package is caught on every PR instead of at release time.
alpine-static:
name: Alpine (static)
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build & test
uses: ./.github/actions/build-alpine-static
# WebAssembly build: cross-compile the driver with Emscripten and run the
# full LPC testsuite inside the wasm driver under node, via the same
# build-wasm composite the release workflow uses. See docs/build-wasm.md
# for the workflow and src/wasm/README.md for the architecture.
wasm:
name: WASM (Emscripten)
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build & test
uses: ./.github/actions/build-wasm