Two defects, both invisible until you ask the app to stop.
Docker never shut down gracefully at all. CMD ["sh","-c","uvicorn ..."] left
the shell as PID 1 with uvicorn as its child, and dash does not forward
signals, so docker stop SIGTERMed the shell and uvicorn never heard about it.
Measured on the shipped image: the full 10s grace period, exit 137, and no
"Shutting down" line in the log. Every stop, restart and image update was a
hard kill -- no WAL checkpoint, no MQTT disconnect, no virtual-printer
teardown. `exec` makes uvicorn PID 1; the rebuilt image now stops in 1s with
exit 0 and checkpoints the WAL.
Separately, uvicorn's timeout_graceful_shutdown defaults to None -- wait
forever for in-flight requests. An MJPEG camera stream is a response that
never completes (httptools' connection shutdown() only flips keep_alive on an
in-flight cycle, it never closes the transport), so one open camera tile
pinned the process until systemd SIGKILLed at 90s. The ordering makes it
unfixable from inside the app: uvicorn fires the lifespan shutdown -- the code
that tears the streams down -- only after connections drain.
All six launchers now pass --timeout-graceful-shutdown 5: Dockerfile,
deploy/bambuddy.service, the systemd unit and launchd plist from
install/install.sh, the SpoolBuddy installer's unit, and the Windows NSSM
registration. On timeout uvicorn cancels the request tasks; the camera
generators already unwind cleanly on CancelledError.
TimeoutStopSec raised to 30s on the units and stop_grace_period: 30s added to
compose, as backstops rather than the mechanism. On Windows NSSM's default
1500ms AppStopMethodConsole was force-killing uvicorn mid-teardown; raised to
15s, with the WM_CLOSE and thread-message stages skipped (uvicorn is a console
app with neither a window nor a message loop).
Native (non-Docker) installs launched uvicorn without --loop asyncio, so
uvicorn[standard] auto-selected uvloop. uvloop's SSL layer drops
already-received but still-buffered data when the client closes the data
connection without a TLS close_notify while the reader is flow-control
paused on slow storage. cmd_STOR writes each chunk to disk inside the read
loop, so a slow consumer falls behind, the tail is lost, read() returns a
clean EOF, and the loop exits with no exception -- the server acked 226 for
a file it truncated itself, then archived, queued, and forwarded the corrupt
3MF to the real printer.
Fix in two independent layers:
1. Remove the trigger: add --loop asyncio to every native launch path,
matching the Dockerfile -- deploy/bambuddy.service, install/install.sh
(systemd + launchd), spoolbuddy/install/install.sh, the Windows NSSM
service, README, CONTRIBUTING dev command.
2. Defense in depth (loop-independent): cmd_STOR now validates that a
received .3mf opens as a ZIP (reads the central directory, no
decompression) before replying 226. A truncated/corrupt file is dropped
and answered with 426, and on_file_received never runs -- so a broken
upload surfaces as an immediate slicer-side send error instead of being
archived and pushed to the printer. Scoped to .3mf; other filetypes pass
through unchanged.
Lays down the Inno Setup + embedded Python pipeline for producing a
self-contained Bambuddy Windows installer .exe. The installer ships
an embedded Python 3.13, the pre-built React bundle, NSSM (service
supervisor) and ffmpeg — no host Python or Node required on the
target machine.
Architecture:
- Install: C:\Program Files\Bambuddy (admin install, one-time UAC)
- Data: C:\ProgramData\Bambuddy\data (preserved on uninstall)
- Service: registered via NSSM, runs as LocalSystem, autostart on boot
- UI: browser at http://localhost:8000 (Start Menu shortcut)
Files:
- installers/windows/build.py stages embedded Python + deps,
frontend bundle, NSSM, ffmpeg
- installers/windows/bambuddy.iss Inno Setup compiler script
- installers/windows/service/*.bat NSSM register/deregister
- .github/workflows/windows-installer.yml CI build on tag push + manual
dispatch, uploads .exe artifact
build.py hard-fails on non-Windows hosts; Wine cross-build is an
unsupported escape hatch behind --allow-non-windows. v1 ships unsigned
(SmartScreen warns on first run) — production signing will be wired up
via SignPath OSS once the application is approved.
See installers/windows/README.md for build prerequisites and the
embedded-Python ._pth gotchas.