Files
chemenu/tools/chemenu/filelock.py
T
torben d0f08d1fba
CI / verify (push) Successful in 2m42s
CI / pwsh (push) Successful in 1m55s
Release / release (push) Successful in 36s
feat: Windows portability - path separators, LF line endings, UTF-8 decoding and output, msvcrt lock fallback (#152)
Files changed:
- .gitattributes
- CHANGES.md
- VERSION
- raw/CONTRACT.md
- tools/README.md
- tools/chemenu/commands/_util.py
- tools/chemenu/commands/dist_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/eval_cmd.py
- tools/chemenu/commands/git_publish.py
- tools/chemenu/commands/index_build.py
- tools/chemenu/commands/lint.py
- tools/chemenu/commands/log_append.py
- tools/chemenu/commands/migrate_cmd.py
- tools/chemenu/commands/provenance_cmd.py
- tools/chemenu/commands/raw_cmd.py
- tools/chemenu/commands/run_budget.py
- tools/chemenu/commands/upstream_cmd.py
- tools/chemenu/commands/version_cmd.py
- tools/chemenu/commands/work_cmd.py
- tools/chemenu/config.py
- tools/chemenu/corpus_cache.py
- tools/chemenu/filelock.py
- tools/chemenu/frontmatter_io.py
- tools/chemenu/kb_scan.py
- tools/chemenu/kb_state.py
- tools/chemenu/lint_core.py
- tools/chemenu/prerequisites.py
- tools/chemenu/provenance.py
- tools/chemenu/search/base.py
- tools/chemenu/search/ripgrep.py
- tools/chemenu/telemetry/writer.py
- tools/chemenu/tests/test_dist_cmd.py
- tools/chemenu/tests/test_portability.py
- tools/chemenu/tests/test_search.py
- tools/chemenu/tests/test_trace_ingest.py
- tools/chemenu/type_resolver.py
- tools/chemenu/upload.py
- tools/chemenu/version.py
- tools/run_wikitool.py
- tools/trace_ingest.py
2026-10-01 21:15:03 +02:00

78 lines
2.7 KiB
Python

"""An exclusive lock on an open file that holds on POSIX and on Windows alike.
Two writers share files across processes: the budget state (`commands/run_budget.py`),
where a lost race loses an increment and lets a session run past its gate, and the
telemetry trace (`telemetry/writer.py`), where it lets one line land inside another.
Both used `fcntl.flock` and skipped the lock where `fcntl` does not exist - which is
every native Windows install, silently. This module is the one place that knows the
platform difference, and the only module allowed to import `fcntl`
(`tests/test_portability_guards.py` holds that).
Windows has no `flock`. `msvcrt.locking` locks a byte range instead, and the lock is
mandatory: a locked byte cannot be read or written by any other process. So the lock
does not go on the file's data, which a reader such as `trace_ingest.py` would then fail
on, but on one byte far past it. Windows allows a lock beyond the end of a file, and
there it excludes the other lockers and nobody else.
"""
from __future__ import annotations
import os
from contextlib import contextmanager
from typing import IO, Iterator
# Where the Windows lock sits: past any size a trace or budget file reaches, and
# still inside a signed 32-bit offset.
WINDOWS_LOCK_OFFSET = 2**31 - 2
@contextmanager
def exclusive(handle: IO) -> Iterator[None]:
"""Hold an exclusive lock on `handle` for the duration of the block.
Blocks until the lock is free, on both platforms. The handle's file position is
left where it was, so an append handle keeps appending.
"""
try:
import fcntl
except ImportError:
fcntl = None
if fcntl is not None:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
return
import msvcrt
handle.flush()
_windows_lock(handle.fileno(), msvcrt, msvcrt.LK_LOCK)
try:
yield
finally:
handle.flush()
_windows_lock(handle.fileno(), msvcrt, msvcrt.LK_UNLCK)
def _windows_lock(fd: int, msvcrt, mode: int) -> None:
"""Lock or unlock the one byte at WINDOWS_LOCK_OFFSET.
`msvcrt.locking` acts on the current position, so this seeks there and back.
`LK_LOCK` gives up after ten one-second retries with an OSError; it is retried
here until it succeeds, which is what `flock` does on POSIX.
"""
position = os.lseek(fd, 0, os.SEEK_CUR)
os.lseek(fd, WINDOWS_LOCK_OFFSET, os.SEEK_SET)
try:
while True:
try:
msvcrt.locking(fd, mode, 1)
return
except OSError:
if mode != msvcrt.LK_LOCK:
raise
finally:
os.lseek(fd, position, os.SEEK_SET)