Skip to content

fix(wiki-lock): fall back to mkdir mutex when flock(1) is unavailable - #144

Open
JesseLeeStringer wants to merge 1 commit into
AgriciDaniel:mainfrom
JesseLeeStringer:fix/wiki-lock-flock-portability
Open

fix(wiki-lock): fall back to mkdir mutex when flock(1) is unavailable#144
JesseLeeStringer wants to merge 1 commit into
AgriciDaniel:mainfrom
JesseLeeStringer:fix/wiki-lock-flock-portability

Conversation

@JesseLeeStringer

Copy link
Copy Markdown

Problem

Every wiki-lock.sh command routes through with_meta_lock(), which called flock(1) unconditionally. flock ships with util-linux and is absent on Git Bash / MSYS2 (Windows) and on a default macOS install, so on those platforms every invocation aborts before doing any work.

The effect is quiet rather than loud: v1.7's per-file advisory locking — the fix for the v1.6 multi-writer corruption hole — is a no-op for every non-Linux user. Skills that follow the documented wiki-lock acquire / release protocol get a failure they're told to interpret as "locked by another writer", so they skip the write silently.

Repro on Git Bash (MINGW64_NT-10.0, bash 5.2.12):

$ command -v flock || echo "flock: MISSING"
flock: MISSING

$ bash scripts/wiki-lock.sh acquire wiki/concepts/Foo.md
ERR: could not acquire meta-lock within 5s
$ echo $?
1

I hit this for real: two concurrent sessions writing the same vault collided on hot.md, which is exactly the failure the lock exists to prevent.

Fix

with_meta_lock() keeps flock where available and falls back to an atomic mkdir(2) mutex otherwise — same 5s ceiling (25 × 0.2s), plus a 30s reap so a crashed holder can't deadlock the vault.

This preserves the existing design: per-path acquire is already race-safe via set -o noclobber (as the header's Design note says), so the meta-lock only serializes LOCK_DIR mutation between acquire/release/list/clear-stale.

Two details worth your attention

1. EXIT trap. The fallback runs the payload in the current shell rather than a subshell, so die()'s exit() unwinds past the cleanup and leaks the mutex. The flock branch never had this problem — exit only left the ( … ) 9> subshell and the lock released on fd close. Symptom before I caught it: a leaked mutex made the next invocation block 5s and report exit 1, masking a path-validation error that should have been exit 4.

2. mtime_of() helper. BSD/macOS stat needs -f %m where GNU needs -c %Y — and the fallback runs on precisely the platforms where that differs. A GNU-only stat returns empty on macOS, so the mutex age computes as 0, every mutex looks infinitely stale, and it gets reaped instantly — defeating the mutual exclusion it's meant to provide. Mirrors the existing sha1sum/shasum fallback in sha1_of().

Verification

On Git Bash (MINGW64, bash 5.2.12), against an isolated vault via WIKI_LOCK_VAULT:

Case Expected Result
bash -n syntax check clean pass
peek unheld unheld, exit 0 pass
acquire exit 0 pass
acquire while fresh exit 75 pass
acquire --stale-after-sec 0 reap, exit 0 pass
list one record with age pass
releasepeek exit 0 → unheld pass
absolute path exit 4 pass
.. traversal exit 4 pass
residue after failures no lockfiles, no mutex dir pass
12 simultaneous acquire of one path exactly 1 winner pass

The flock branch is untouched, so Linux behaviour is unchanged.

Note on a related issue (not in this PR)

skills/save/SKILL.md declares allowed-tools: Read Write Edit Glob Grep — no Bash — while the skill body instructs bash scripts/wiki-lock.sh acquire "$NOTE_PATH". The skill therefore cannot acquire the lock its own documentation mandates, independently of this bug. skills/wiki-ingest/SKILL.md may want the same check. Happy to send that as a separate PR if you'd like it.

🤖 Generated with Claude Code

Every wiki-lock.sh command routes through with_meta_lock(), which called
flock(1) unconditionally. flock ships with util-linux and is absent on
Git Bash / MSYS2 (Windows) and on a default macOS install, so on those
platforms every invocation aborted before doing any work — silently
disabling vault locking for all non-Linux users.

Repro on Git Bash (MINGW64_NT, bash 5.2.12):

    $ bash scripts/wiki-lock.sh acquire wiki/concepts/Foo.md
    ERR: could not acquire meta-lock within 5s
    $ echo $?
    1

with_meta_lock() now uses flock where available and falls back to an
atomic mkdir(2) mutex otherwise, with the same 5s ceiling and a 30s reap
for a mutex abandoned by a crashed holder. Per-path acquire is already
race-safe via `set -o noclobber` (per the Design note in the header), so
the meta-lock only serializes LOCK_DIR mutation between commands.

Two details worth reviewer attention:

- The fallback runs the payload in the current shell rather than a
  subshell, so die()'s exit() would skip cleanup and leak the mutex —
  which then masked later path-validation failures as exit 1 instead of
  exit 4. Handled with an EXIT trap around the payload.

- Added mtime_of() because BSD/macOS stat needs `-f %m` where GNU needs
  `-c %Y`, and the fallback runs on exactly the platforms where that
  differs. A GNU-only stat would yield 0 there, making every mutex look
  infinitely stale and reaping it immediately.

Verified on Git Bash: acquire / contend(75) / stale-reap / list /
release / peek all behave per spec; path validation returns 4 for both
absolute and `..` paths; no lockfile or mutex residue after failures;
and 12 simultaneous acquires of one path yield exactly one winner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant