2025-02-09 21:33:11 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2025-02-09 21:10:22 +01:00
|
|
|
import contextlib
|
|
|
|
import errno
|
2025-02-09 21:23:50 +01:00
|
|
|
import sys
|
2025-02-09 21:46:04 +01:00
|
|
|
from collections.abc import Generator
|
2025-02-09 21:10:22 +01:00
|
|
|
from typing import Callable
|
|
|
|
|
|
|
|
|
2025-02-09 21:23:50 +01:00
|
|
|
if sys.platform == 'win32': # pragma: no cover (windows)
|
2025-02-09 21:10:22 +01:00
|
|
|
import msvcrt
|
|
|
|
|
|
|
|
# https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking
|
|
|
|
|
|
|
|
# on windows we lock "regions" of files, we don't care about the actual
|
|
|
|
# byte region so we'll just pick *some* number here.
|
|
|
|
_region = 0xffff
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def _locked(
|
|
|
|
fileno: int,
|
|
|
|
blocked_cb: Callable[[], None],
|
2025-02-09 21:51:43 +01:00
|
|
|
) -> Generator[None]:
|
2025-02-09 21:10:22 +01:00
|
|
|
try:
|
2025-02-09 21:21:56 +01:00
|
|
|
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region)
|
2025-02-09 21:10:22 +01:00
|
|
|
except OSError:
|
|
|
|
blocked_cb()
|
|
|
|
while True:
|
|
|
|
try:
|
2025-02-09 21:21:56 +01:00
|
|
|
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region)
|
2025-02-09 21:10:22 +01:00
|
|
|
except OSError as e:
|
|
|
|
# Locking violation. Returned when the _LK_LOCK or _LK_RLCK
|
|
|
|
# flag is specified and the file cannot be locked after 10
|
|
|
|
# attempts.
|
|
|
|
if e.errno != errno.EDEADLOCK:
|
|
|
|
raise
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
# From cursory testing, it seems to get unlocked when the file is
|
|
|
|
# closed so this may not be necessary.
|
|
|
|
# The documentation however states:
|
|
|
|
# "Regions should be locked only briefly and should be unlocked
|
|
|
|
# before closing a file or exiting the program."
|
2025-02-09 21:21:56 +01:00
|
|
|
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region)
|
2025-02-09 21:10:22 +01:00
|
|
|
else: # pragma: win32 no cover
|
|
|
|
import fcntl
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def _locked(
|
|
|
|
fileno: int,
|
|
|
|
blocked_cb: Callable[[], None],
|
2025-02-09 21:51:43 +01:00
|
|
|
) -> Generator[None]:
|
2025-02-09 21:10:22 +01:00
|
|
|
try:
|
|
|
|
fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
|
|
except OSError: # pragma: no cover (tests are single-threaded)
|
|
|
|
blocked_cb()
|
|
|
|
fcntl.flock(fileno, fcntl.LOCK_EX)
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
fcntl.flock(fileno, fcntl.LOCK_UN)
|
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def lock(
|
|
|
|
path: str,
|
|
|
|
blocked_cb: Callable[[], None],
|
2025-02-09 21:51:43 +01:00
|
|
|
) -> Generator[None]:
|
2025-02-09 21:10:22 +01:00
|
|
|
with open(path, 'a+') as f:
|
|
|
|
with _locked(f.fileno(), blocked_cb):
|
|
|
|
yield
|