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
- Verify single queue: uploads append to upqueue and are processed
sequentially by one worker (parallel only within a single file).
- Before accepting a new batch, calculate total size minus existing
files that will be overwritten.
- Reject the whole batch if free space < net need + 512 MiB margin.
- Show a toast with human-readable needed vs available space.
- Add cista/util/diskspace.py with MIN_FREE_BYTES limit and cached
check_free_space() helper.
- Check available space in File.write() before ftruncate/write.
- Catch ENOSPC in File.write and re-raise as InsufficientStorageError.
- upload_file_chunk catches both proactive and ENOSPC errors and
returns HTTP 507 Insufficient Storage.
- Add tests for low-disk rejection and ENOSPC handling.