The mediapreview error-handling refactor dropped the outer
asyncio.wait_for around preview generation. Pool-internal timeouts only
bound individual stages (idle-worker wait, worker request, OO HTTP
calls); queueing on top of them let requests run far past 10s and
eventually return 200 instead of 503. Wrap both generate paths in
wait_for(PREVIEW_TIMEOUT) again — cancellation propagates correctly:
the pool dispatcher drops cancelled futures, and the OnlyOffice manager
cancels orphaned conversion tasks.
Version comes from importlib.metadata (the package has no version
constant), making it obvious at a glance which mediapreview build the
running server resolved.
The mediapreview setup_docker() return value (the JWT secret) was passed
through to main() and sys.exit(), printing it a second time on stderr
with a failure exit code. The secret is already printed to stdout in the
finally block; return 0 on success.
OnlyOffice setup is done entirely by mediapreview (cista --oosetup ->
mediapreview.office.setup_docker: image build, oonet network creation,
docker run). Nothing referenced this compose file; it was a leftover
from before the preview functionality moved to the mediapreview package.
- docker-compose: attach the OnlyOffice container to the internal-only
oonet network (fixed name matching mediapreview's OO_NETWORK, fixed
container IP 172.30.0.2); no published ports, no outbound internet.
- --oosetup prints ONLYOFFICE_JWT_SECRET=<key> at the end regardless of
build outcome (the secret is deterministically derived from config).
- Successful SSO /auth/api/validate responses are cached per credential
and perm/renew URL for 10s, so watch websocket re-checks do not hammer
the auth backend. A POST to the logout endpoint purges all cached
entries for the request's credentials immediately, so logout/login
flows are not served stale successes.
- The watch websocket now re-validates auth before each forwarded
message and every 10s when idle (SSO and built-in sessions alike).
When the session is gone the client gets an auth error message and
the socket is closed, instead of streaming updates forever.
- Token-authenticated (API/share token) sockets are exempt from
re-validation; they are checked once at handshake.
- Force sanic.root/error/server back to INFO from before_server_start, after
dev mode's runtime setLevel(DEBUG) — kills the useless 'Error Page:' noise
- Silence mediapreview.pool WARNINGs (timeouts already in access log extra);
they previously fell to logging.lastResort with no level prefix
- Handle CancelledError with 499 (client disconnect / RequestCancelled) or
503 (server shutdown) instead of Sanic's default 500 error page
- 422 preview failures now log 'backend: reason' in the access log, with
upstream error text shortened (first line, no [Errno] prefix, cut at ': ');
dev mode prints the full original error to console
- Preview workers ignore SIGINT (pool mode only) and load tracerite, so
Ctrl-C no longer dumps a KeyboardInterrupt traceback per worker.
- Spawn workers in their own process group and kill with killpg, so a
SIGKILLed worker cannot orphan an in-flight ffmpeg grandchild.
- Fail all pending and in-flight preview futures when the pool closes
instead of orphaning them until timeout; never restart the pool once
shut down (mid-shutdown requests get a quiet 'preview cancelled' 503).
- Re-raise CancelledError in the preview route instead of responding on
a torn-down connection ('NoneType' is_closing crash).
- Log preview failures with logger.exception where they occur (in the
worker, whose stderr is inherited) instead of re-logging a traceback-
less error string in the parent.
- Cache docker bridge IP auto-detection (it cannot change at runtime),
so the debug message is logged once instead of per preview.
- Log availability transitions only (unreachable/back), re-probing every
30s via the existing TTL cache.
- Skip office conversion attempts entirely while OnlyOffice is known to
be down; fail with a quiet 'onlyoffice error' 503.
- Video rotation (0/90/180/270) now stays fully in planar YUV420,
preserving 10-bit HDR (yuv420p10le via PyAV uint16 planes) and the
source colorspace; no more RGB round-trip.
- AVIF images now go through the ffmpeg CLI path like HEIC: pyvips drops
CICP colour metadata, turning HDR sources into washed-out SDR previews.
- Fix pyvips "out of order read" on JPEGs needing EXIF rotation by
reopening with random access only when autorot actually rotates.
A corrupt TIFF in production produced a wall of ffmpeg error output:
pyvips could not decode it, the generic ffmpeg fallback was tried, and
ffmpeg's TIFF decoder failed just the same — with banner, configuration
and stream-mapping spam included.
- Non-HEIC images are now decoded by pyvips only; a pyvips failure
raises a clean one-line ValueError ("cannot decode image: ...", a 422
like any other undecodable file) instead of invoking ffmpeg. The
ffmpeg path is kept for HEIC/HEIF, where pyvips genuinely falls short
(tile assembly, HDR metadata).
- ffmpeg runs with -hide_banner -loglevel error -nostats: error output
is still shown on failure, without the version/configuration/progress
noise. The -s insertion index no longer depends on fixed positions.
- AVIF saves: replace deprecated strip=True with keep="none" (libvips
8.15+; production already runs a version that deprecates strip).
- Set the pyvips logger to WARNING in the worker: its INFO messages
("threadpool completed ...") are pure spam on every operation.
Worker stderr is now inherited by the parent, so its log lines land in the
server log — but they arrived with the default logging format and a noisy
"preview-worker config=..." line at every spawn.
- Extract the emoji level-prefix formatting from cista.sanic_logging into
cista.util.logformat, which has no Sanic dependency (the worker must not
import Sanic: import-time prints could corrupt the stdout protocol).
- Worker configures its stderr handler with the same emoji prefixes plus a
worker[pid] tag, and the config-loaded info message is removed.
Production symptom: previews of all types (pdf/pyvips/onlyoffice) start
hitting the 10s timeout and never recover until server restart, while the
rest of the server stays healthy.
Root cause (reproduced on Python 3.12): workers were spawned with
stderr=PIPE that nothing drained after startup. Once the OS pipe buffer
filled from accumulated worker tracebacks and library warnings, asyncio
flow control stopped the parent reading it and the worker blocked forever
mid-request on a stderr write. The 10s timeout then fired, but
_replace_worker hung forever in proc.wait() even after kill() — the
flow-control-paused pipe transport never sees EOF — permanently wedging
one dispatcher per stuck worker. Once all dispatchers were stuck, every
preview request timed out. Restart cleared it.
Fixes:
- Spawn workers with inherited stderr (stderr=None) so worker diagnostics
go straight to the server log and no undrained pipe can exist.
- Bound proc.wait() in worker kill() with a 5s grace timeout so a wedged
transport can never hang a dispatcher; log the worker pid instead.
- Guard the dispatch loop with an outer exception handler so a dispatcher
can never die silently and shrink pool capacity.
- Retry failed worker replacement spawns with 1s-30s backoff instead of
silently shrinking the pool.
- Fix latent crash: except-tuple referenced msgspec.json.DecodeError,
which does not exist in the installed msgspec; any protocol failure
would itself raise AttributeError. Use msgspec.DecodeError.
- Worker: redirect Python-level sys.stdout to stderr in persistent mode
and keep the raw buffer solely for the binary protocol, so a library
print() can never corrupt the command channel again (cf. the pymupdf
deprecation warning that crashed workers at startup).
- Worker: close the pymupdf document explicitly in process_pdf.
- Log worker pid on timeout/protocol/checksum failures, and log failed
kills and replacement retries, for future production diagnostics.
Add tests/test_preview_pool.py with an end-to-end regression recreating
the wedged-worker setup (piped, undrained stderr) plus kill-grace,
respawn-retry and dispatcher-survival tests.
Newer ruff flagged CPY001 across the codebase (copyright notices are not
wanted here, rule disabled) and PLR0917 on format_access_log. duration_ms
and extra are now keyword-only at the single call site.
vitest, @vue/test-utils, jsdom and @types/jsdom were installed but no
frontend tests exist or are planned. Removing them also drops the
deprecated glob@10 dependency chain (js-beautify). type-check now uses
tsconfig.app.json.
Deactivated FileExplorer/Gallery instances stay alive in KeepAlive with
frozen, potentially empty document props. Their empty-folder watcher
cleared store.cursor and yanked focus to the breadcrumb on every cursor
change, breaking rename via gallery pen and keyboard entry into the
file list, and hiding the explorer rename button.
- Guard cursor watchers in FileExplorer/Gallery with an isActive flag
(set on activated, cleared on deactivated)
- Declare emits in GalleryFigure (rename/menu fell through to the root
anchor as native listeners)
- Show the explorer rename button on row hover with a delayed fade-in
instead of only on the keyboard-focused row
The ffmpeg fallback in the preview worker inherited the worker's stdin
pipe (the framed request protocol). When a slow conversion was killed
at the 10s timeout, the orphaned ffmpeg grandchild kept that pipe open,
so the parent's proc.wait() blocked forever waiting for pipe EOF —
permanently sticking one dispatcher per event until the whole pool
starved and every preview request (pdf, image, office) returned 503.
- Run ffmpeg with stdin=DEVNULL (also stops it eating protocol bytes)
- Drop start_new_session (only needed for group kills, POSIX-only)
- Stop logging the master secret at worker startup
- Replace circular watchEffects in FileExplorer/Gallery with explicit watchers
to stop recursive Vue updates when creating items in empty folders.
- Move EmptyFolder rendering inside FileExplorer/Gallery so empty/list swaps
no longer trigger folder slide transitions.
- Keep EmptyFolder text size consistent across list and gallery views.
- Detect navigation direction (forward/backward) via router beforeEach guard
- Store transition direction in Pinia for cross-component access
- Wrap ExplorerView content in a CSS grid transition wrapper so old and new
views overlap in the same grid cell during animation
- Add slide-forward/slide-backward transition classes with translate3d
for GPU-accelerated, simultaneous enter/leave without gaps
- Keep fixed-position search loader outside the transition