LibraryLock::acquire wrote catalog.lock.owner with std::fs::write,
which follows a symlink at that name. A library folder someone else
prepared (a shared drive, an unpacked archive, a synced folder) could
carry catalog.lock.owner -> ~/.bashrc or any other file the user can
write, and opening the library replaced that file's content with the
owner JSON. The catalog's staging files had the same problem and were
fixed in #1502; the owner note was missed.
The note is now written through store::create_staging (made
pub(crate)): whatever is at the name is removed (a link itself, never
its target) and the file is created with create_new. Drop already
removed the note with remove_file, which never follows a link.
Test (Unix): a link at catalog.lock.owner pointing at a file outside
the library leaves that file's bytes unchanged through acquire and
drop, and the note lands in the library as a regular file naming this
process.
Reported-by: BS.ehlas.cz
desktop_exec treats a freedesktop Exec path as absolute when it starts
with '/' on every platform (Path::is_absolute wants a drive letter on
Windows); fill_box uses as_chunks (clippy::chunks_exact_to_as_chunks);
LibraryLock::descriptor_copy is cfg(all(test, unix)) like its only test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LibraryLock freed the library by letting its File close. On Unix the
flock lock belongs to the open file description and lasts while any
descriptor on it is open. When another thread spawns a process,
posix_spawn copies the whole descriptor table into the child and
O_CLOEXEC only closes the copies at execve, so a LibraryLock dropped in
that window stayed locked until the child exec'd. Re-acquiring the
library then failed with LockError::InUse(None) ("already open in
another LightCraft program") although nobody had it open. That is the
flake in tests_lock::second_opener_is_refused_until_the_first_lets_go,
whose neighbour lock_held_by_another_process_is_released_when_it_dies
spawns the test binary on a parallel libtest thread.
Drop now removes the owner note while still holding the lock (so it
can't delete the next holder's), then calls File::unlock(): LOCK_UN
through our descriptor frees the lock whatever copies a child still
holds. An unlock error is logged, never a panic; the handle still closes
right after, which releases the lock once every copy is gone, as before.
Tests: the_library_is_free_once_dropped_even_if_a_child_holds_a_copy
holds a try_clone of the lock's descriptor across the drop (the same
open file description a child gets between fork and exec) and
re-acquires; dropping_the_lock_frees_the_library_while_another_thread_
spawns_processes runs a thread spawning /bin/true against two seconds of
acquire/drop cycles. Both fail every run without the fix (the first
deterministically) and pass with it; the flaky pair went from 3/100 to
0/100 failed runs here. Both are cfg(unix): Windows handles aren't
inherited by CreateProcess.
Two processes on one library each kept their own catalog and seq counter;
the last snapshot silently dropped the other's edits.
- lightcraft_catalog:🔒 LibraryLock holds an exclusive OS lock
(std File::try_lock: flock / LockFileEx) on <library>/catalog.lock for
the session; who holds it goes to catalog.lock.owner (separate file: a
Windows lock also blocks reads). Released on drop and by the OS when the
process dies, so leftover files from a crash never lock anyone out. File
systems without lock support open unprotected with a warning.
- Session::open_library takes the lock before reading anything and fails
with EngineError::LibraryInUse ("this library is already open in
LightCraft 0.x (process N on host)"); reopening the session's own
library hands its lock over.
- lightcraft-cli (mcp/run/snapshot --library) adds how to drive the
running app instead (--control + mcp --connect).
- Tests: in-process, leftover files, a killed child process, two engine
sessions, CLI e2e (mcp --library holds it, run --library is refused).
Existing tests that reopened a library while it was open now drop the
first session.
- docs: parity row LRC-LIB-CATALOG-LOCK, mcp.md, web.md (no guard across
browser tabs yet), whats-new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>