Skip to content

fix(locks): cross-platform file locking — fix Windows fcntl crash - #99

Merged
KylinMountain merged 2 commits into
mainfrom
fix/windows-fcntl-compat
Jun 15, 2026
Merged

fix(locks): cross-platform file locking — fix Windows fcntl crash#99
KylinMountain merged 2 commits into
mainfrom
fix/windows-fcntl-compat

Conversation

@KylinMountain

Copy link
Copy Markdown
Collaborator

Summary

openkb/locks.py and openkb/config.py hard-imported fcntl and called os.fchmod / directory os.fsync unconditionally — all Unix-only — so OpenKB crashed at import time on Windows:

ModuleNotFoundError: No module named 'fcntl'

This is a Windows-only regression from the KB-locks feature (#86). It surfaced in #93 once the Copilot extra_headers fix (#98) let that (Windows) user get past the header error.

Changes

  • locks.flock / locks.funlock — advisory-lock helpers. POSIX uses fcntl.flock; Windows falls back to msvcrt.locking byte-range locks (exclusive-only — shared requests degrade to exclusive, which is safe — and fcntl.flock's blocking acquire is emulated via a non-blocking retry).
  • Guard os.fchmod with hasattr (absent on Windows).
  • Skip the parent-directory fsync on Windows (can't fsync a directory handle; os.replace is already atomic on NTFS).
  • config.py drops its direct fcntl import and uses locks.flock/funlock.

Testing

  • New tests/test_cross_platform_locks.py simulates the no-fcntl (Windows) path on POSIX — a subprocess reproduces the import crash, and a faked msvcrt exercises the fallback dispatch.
  • Full suite: 750 passed locally (macOS).

⚠️ The Windows path is simulated on macOS/Linux — it has not been run on a real Windows host. The msvcrt logic follows the working patch from the #93 reporter; a confirmation on Windows (or Windows CI) would be ideal before relying on it.

Refs #93

openkb/locks.py and openkb/config.py hard-imported fcntl and called
os.fchmod / directory os.fsync unconditionally — all Unix-only — so OpenKB
crashed at import on Windows (ModuleNotFoundError: No module named 'fcntl'),
surfaced in #93 once the Copilot extra_headers fix (#98) unblocked that user.

- locks.flock/funlock: advisory-lock helpers — fcntl on POSIX, msvcrt
  byte-range locks on Windows (exclusive-only; shared degrades to exclusive,
  fcntl's blocking acquire emulated via non-blocking retry).
- guard os.fchmod with hasattr; skip directory fsync on Windows
  (os.replace is already atomic on NTFS).
- config.py drops its direct fcntl import and uses locks.flock/funlock.

Adds tests/test_cross_platform_locks.py: simulates the no-fcntl (Windows)
path on POSIX via subprocess + a faked msvcrt.

Refs #93
Comment thread tests/test_cross_platform_locks.py Fixed
Addresses review findings on the msvcrt fallback:

- flock's Windows retry loop no longer spins forever: bound the wait by
  _WINDOWS_LOCK_TIMEOUT (default 3600s, OPENKB_LOCK_TIMEOUT override) so a
  stuck/never-released lock or a non-contention OSError surfaces as an error
  instead of an infinite, silent busy-loop. Add exponential backoff (was a
  fixed 100ms spin) and a one-time 'still waiting' warning.
- Document that shared locks degrade to exclusive on Windows (msvcrt has no
  shared mode), so concurrent in-process readers serialise there.
- Correct the _fsync_directory comment to not conflate NTFS atomicity with
  crash durability.

Tests: cover the retry-until-available and raise-after-timeout paths (the
previously-uncovered msvcrt contention logic), runnable on POSIX via a faked
msvcrt. Full suite 752 passed.
@KylinMountain
KylinMountain merged commit 934d246 into main Jun 15, 2026
1 check passed
KylinMountain added a commit that referenced this pull request Jun 15, 2026
…100)

* refactor(locks): delegate file locking to portalocker

Replace the hand-rolled fcntl/msvcrt flock/funlock (merged in #99) with
portalocker, which is fcntl-backed on POSIX and msvcrt/Win32-backed on Windows
with maintained, cross-platform-tested behaviour. Removes the hand-written
Windows retry/timeout loop that could not be exercised on POSIX.

- flock/funlock now delegate to portalocker.lock/unlock.
- Drop the guarded 'import fcntl', the msvcrt fallback, and _WINDOWS_LOCK_TIMEOUT.
- Keep the os.fchmod guard and Windows directory-fsync skip (atomic writes,
  which portalocker does not cover).
- Pin portalocker==3.2.0 (BSD-3) to match the exact-pin dependency policy.

Note: true shared (reader) locks on Windows would still need pywin32; without
it portalocker uses msvcrt (exclusive). Not added — in-process concurrent KB
reads are rare. Refs #93.

Tests: swap the msvcrt-internals tests for a cross-process exclusion test that
verifies flock takes a real OS lock, plus the retained atomic-write/fsync-skip
guards. Full suite 749 passed.

* fix(locks): review fixes — accurate docs, fcntl import guard, test hardening

Addresses /code-review findings on the portalocker refactor:

- flock docstring corrected: portalocker uses Win32 LockFileEx (pywin32, pulled
  in automatically on Windows) for SHARED locks, so concurrent readers ARE
  honoured; EXCLUSIVE uses msvcrt (retries ~10s then raises, not an infinite
  block); failures raise portalocker.LockException, not OSError.
- Re-add the issue #93 regression guard: assert no openkb module hard-imports
  the Unix-only fcntl at module level (replaces the dropped import-without-fcntl
  test without depending on portalocker internals).
- Strengthen the cross-process lock test: assert both BLOCKED (while held) and
  ACQUIRED (after release), and check the probe's exit code so an ImportError
  surfaces clearly instead of an empty-stdout false failure.
- Drop the now-dead 'import pytest' / 'import portalocker' from the test module.

* test(locks): resolve code-quality bot nits — single openkb import style

Drop the unused 'import openkb' and derive the package dir from
locks.__file__ instead, so the test module uses one import style for openkb
(github-code-quality bot). The unused 'import portalocker' was already removed
in the prior commit.
@KylinMountain
KylinMountain deleted the fix/windows-fcntl-compat branch June 22, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant