Commit graph

10 commits

Author SHA1 Message Date
maziggy
28a6ca6f4d Add CAP_NET_BIND_SERVICE everywhere the service is defined (#2549)
The Virtual Printer binds 990 and 322, below 1024, which a service running
as a normal user may not do without CAP_NET_BIND_SERVICE. Without it the
rest of Bambuddy works and only the VP is dead -- sockets never open, the
slicer never finds the printer, and the sole trace is one journal line.

332a7c6ac added the line to install/install.sh in March under the heading
"Fix install.sh missing AmbientCapabilities". Three other places define the
same unit and none of them got it: the manual template, the combined
Bambuddy + SpoolBuddy installer, and the unit the wiki tells you to paste.
The wiki additionally claimed the capability was always included.

Also diagnose it. The VP diagnostic reported only that nothing was listening
on 990, which reads identically to a port conflict. It now checks CapEff for
the capability and names it as the cause -- but stays quiet when the port is
answering (an iptables REDIRECT is the documented alternative and that host
works) and when the capability is held (the port is down for another reason
and blaming this would misdirect). Skips where there is no procfs rather
than putting a systemd instruction in front of a macOS user.
2026-08-04 08:33:30 +02:00
maziggy
5bbfeefa65 fix(backup): diagnose an unwritable backup path instead of quoting errno 30 (#2544)
Nightly backups to a mounted NAS share ran from May and then stopped, failing
with [Errno 30] Read-only file system. The reporter checked folder permissions
-- correctly: the mount is gid=backup,dir_mode=0775, the service user is in that
group, and his own shell writes to the share fine.

Errno 30 is EROFS. A permission problem is errno 13. EROFS means the filesystem
refused the write, and it refused because we told it to: our systemd unit ships
ProtectSystem=strict, which mounts everything read-only inside the service's
mount namespace and carves back out only ReadWritePaths=<install> <data> <logs>.
A NAS share is not one of those three. Reads are unaffected -- which is why the
UI happily listed his existing backups from the share while being unable to
write a new one -- and his shell is outside the namespace entirely, so every
check he could think to run said the directory was fine.

Both installers write the unit file wholesale, so a ReadWritePaths line added by
hand disappeared on the next install, taking the backups with it. They now back
the old unit up (.bak-<timestamp>) and carry the operator's extra writable paths
forward, reporting which ones they kept. The unit template documents the
carve-out.

The output directory is probed with a real write when it is saved and when the
backup card loads, so an unwritable path is caught there rather than at 03:00
for a week. On failure the card names the cause and hands over the fix with the
operator's path already in it (systemctl edit bambuddy -> ReadWritePaths=...),
and a failed run reports the same diagnosis rather than the raw OSError. EROFS
outside systemd, permission-denied, out-of-space, not-a-directory and missing are
told apart, in all 11 locales.

Docker: a backup path that is not bind-mounted is writable -- the write lands in
the container's ephemeral layer and is lost on the next compose up. The probe
compares the directory's device against the container root and warns, with the
compose snippet that mounts it properly.
2026-07-12 08:44:53 +02:00
maziggy
ba1394db3e fix(shutdown): exec uvicorn as PID 1 in Docker, and bound the graceful-shutdown wait
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).
2026-07-11 14:44:45 +02:00
maziggy
7d4dfd5a7d fix(vp): stop uvloop from silently truncating VP FTP uploads (#1896)
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.
2026-07-05 10:32:13 +02:00
maziggy
fb1e9a917e fix(install): use ProtectHome=read-only for /home-rooted installs (#1685)
bambuddy.service shipped with ProtectHome=true, which makes /home/* invisible
  to the service namespace. Installing into /home/bambuddy/ (instead of the
  default /opt/bambuddy/) made ExecStart=/home/bambuddy/venv/bin/uvicorn fail
  with status=203/EXEC because systemd couldn't resolve the binary path.
  ReadWritePaths=$INSTALL_PATH does not reliably re-expose /home/* subpaths for
  exec resolution.

  install/install.sh now detects /home/* INSTALL_PATH and emits ProtectHome=read-only;
  default /opt/bambuddy installs keep ProtectHome=true. The manual deploy template
  defaults to read-only with a comment on when to tighten it.

  read-only keeps /home immutable to the service - no security regression, since
  ReadWritePaths still gates writes to the install/data/log dirs only.
2026-06-09 07:31:23 +02:00
David Dix
26f4dad832
Support for self-signed CA certificates (#1431) 2026-05-19 13:40:46 +02:00
maziggy
3407afc0c7 fix(docker): normalise data-volume ownership at startup via gosu entrypoint
Two related failure modes have been biting Docker users repeatedly,
  most recently in #1211:

    1. Docker named volumes are created by the daemon as root:root, and
       the previous `chmod 777 /app/data` Dockerfile workaround only
       covered the named-volume root — so subdirs Bambuddy creates at
       runtime (virtual_printer/uploads, virtual_printer/certs, etc.)
       inherited wrong ownership when the container ran as 1000:1000.

    2. The shipped docker-compose.yml ships
       `./virtual_printer:/app/data/virtual_printer` uncommented, and
       dockerd creates a missing bind-mount source on the host as root
       before the container starts — leaving the host directory
       unwritable by uid 1000 inside the container even though the named
       volume above it had the chmod-777 workaround.

  Symptom either way: [Errno 13] Permission denied:
  '/app/data/virtual_printer/uploads', no virtual printer ever starts,
  "VP doesn't work" support reports follow.

  Replace the chmod-777 hack with a proper entrypoint:

    - deploy/docker-entrypoint.sh runs as root, chowns /app/data and
      /app/logs (and /app/data/virtual_printer when bind-mounted) to
      PUID:PGID, then drops to that uid via gosu before exec'ing the
      app. The chown is gated behind a top-level ownership check so
      subsequent restarts skip the recursive traversal — no multi-
      second startup penalty on multi-GB archive directories.

    - A sentinel .bambuddy file in each data path prevents Docker from
      re-syncing image directory metadata on every mount (otherwise
      empty volumes have their ownership reverted from the image on
      each restart, defeating the idempotency).

    - When the container is started with an explicit `user:` directive
      or `--user` flag the entrypoint detects it isn't root and falls
      through to direct exec — preserving compatibility for users who
      pin a specific uid.

  Compose template changes:

    - Remove `user: "${PUID:-1000}:${PGID:-1000}"` (entrypoint owns
      privilege drop now).
    - Add PUID / PGID env vars with the same defaults.
    - Comment out the ./virtual_printer:/app/data/virtual_printer
      bind mount by default, with explicit "only needed if you also
      run a native install of Bambuddy on the same host and want both
      to share the VP CA cert" guidance. The entrypoint chowns the
      host-side dir through the bind mount the first time it sees
      wrong ownership, so existing uncomented installs continue to
      work and #1211 specifically gets fixed.
2026-05-05 17:22:08 +02:00
maziggy
196b7a93e9 Added one-shot install scripts 2026-01-31 14:31:10 +01:00
maziggy
bced6415e0 Updated systemd script 2026-01-30 10:08:34 +01:00
maziggy
a7319f0e70 Fix browser freeze on print completion with camera stream open
Root cause: When a print completed with the camera stream popup open,
spawning a second ffmpeg process for finish photo capture caused a
conflict that froze the browser tab and video window.

Solution: Buffer the last frame from active camera streams. When
capturing finish photo, use the buffered frame if a stream is active
instead of spawning a new ffmpeg process.

Backend changes:
- camera.py: Added _last_frames buffer and get_buffered_frame() helper
- main.py: Photo capture uses buffered frame when stream is active
- main.py: Moved slow operations to background tasks (energy calc,
  photo capture, smart plug, notifications, maintenance)
- archive.py: Fixed sync file write blocking event loop (asyncio.to_thread)
- printers.py: Added debug endpoint to simulate print completion

Frontend changes:
- useWebSocket.ts: Throttled printer status updates (100ms) to prevent
  UI overload from rapid WebSocket messages
- useWebSocket.ts: Debounced archive invalidations (3s) to prevent
  cascade of re-renders on print completion
- useWebSocket.test.ts: Updated tests for throttled/debounced handlers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 18:45:03 +01:00