File: //opt/saltstack/salt/lib/python3.10/site-packages/virtualenv/util/lock.py
"""holds locking functionality that works across processes."""
from __future__ import annotations
import logging
import os
from abc import ABC, abstractmethod
from contextlib import contextmanager, suppress
from pathlib import Path
from threading import Lock, RLock
from typing import TYPE_CHECKING
from filelock import FileLock, Timeout
if TYPE_CHECKING:
from collections.abc import Iterator
from types import TracebackType
LOGGER = logging.getLogger(__name__)
class _CountedFileLock(FileLock):
def __init__(self, lock_file: str) -> None:
parent = os.path.dirname(lock_file)
with suppress(OSError):
os.makedirs(parent, exist_ok=True)
super().__init__(lock_file)
self.count = 0
self.thread_safe = RLock()
def acquire( # ty: ignore[invalid-method-override]
self,
timeout: float | None = None,
poll_interval: float = 0.05,
) -> None:
if not self.thread_safe.acquire(timeout=-1 if timeout is None else timeout):
raise Timeout(self.lock_file)
if self.count == 0:
try:
super().acquire(timeout, poll_interval)
except BaseException:
self.thread_safe.release()
raise
self.count += 1
def release(self, force: bool = False) -> None: # noqa: FBT002
with self.thread_safe:
if self.count > 0:
if self.count == 1:
super().release(force=force)
self.count -= 1
if self.count == 0:
# if we have no more users of this lock, release the thread lock
self.thread_safe.release()
_lock_store = {}
_store_lock = Lock()
class PathLockBase(ABC):
def __init__(self, folder: str | Path) -> None:
path = Path(folder)
self.path = path.resolve() if path.exists() else path
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.path})"
def __truediv__(self, other: str) -> PathLockBase:
return type(self)(self.path / other)
@abstractmethod
def __enter__(self) -> None:
raise NotImplementedError
@abstractmethod
def __exit__(
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None:
raise NotImplementedError
@abstractmethod
@contextmanager
def lock_for_key(self, name: str, no_block: bool = False) -> Iterator[None]: # noqa: FBT002
raise NotImplementedError
@abstractmethod
@contextmanager
def non_reentrant_lock_for_key(self, name: str) -> Iterator[None]:
raise NotImplementedError
class ReentrantFileLock(PathLockBase):
def __init__(self, folder: str | Path) -> None:
super().__init__(folder)
self._lock = None
def _create_lock(self, name: str = "") -> _CountedFileLock:
lock_file = str(self.path / f"{name}.lock")
with _store_lock:
if lock_file not in _lock_store:
_lock_store[lock_file] = _CountedFileLock(lock_file)
return _lock_store[lock_file]
@staticmethod
def _del_lock(lock: _CountedFileLock | None) -> None:
if lock is not None:
with _store_lock, lock.thread_safe:
if lock.count == 0:
_lock_store.pop(lock.lock_file, None)
def __del__(self) -> None:
self._del_lock(self._lock)
def __enter__(self) -> None:
self._lock = self._create_lock()
self._lock_file(self._lock)
def __exit__(
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None:
self._release(self._lock) # ty: ignore[invalid-argument-type]
self._del_lock(self._lock)
self._lock = None
def _lock_file(self, lock: _CountedFileLock, no_block: bool = False) -> None: # noqa: FBT002
# multiple processes might be trying to get a first lock... so we cannot check if this directory exist without
# a lock, but that lock might then become expensive, and it's not clear where that lock should live.
# Instead here we just ignore if we fail to create the directory.
with suppress(OSError):
os.makedirs(str(self.path), exist_ok=True)
try:
lock.acquire(0.0001)
except Timeout:
if no_block:
raise
LOGGER.debug("lock file %s present, will block until released", lock.lock_file)
lock.release() # release the acquire try from above
lock.acquire()
@staticmethod
def _release(lock: _CountedFileLock) -> None:
lock.release()
@contextmanager
def lock_for_key(self, name: str, no_block: bool = False) -> Iterator[None]: # noqa: FBT002
lock = self._create_lock(name)
try:
with self._lock_and_yield(lock, no_block):
yield
finally:
self._del_lock(lock)
lock = None
@contextmanager
def _lock_and_yield(self, lock: _CountedFileLock, no_block: bool) -> Iterator[None]:
self._lock_file(lock, no_block)
try:
yield
finally:
self._release(lock)
@contextmanager
def non_reentrant_lock_for_key(self, name: str) -> Iterator[None]:
with _CountedFileLock(str(self.path / f"{name}.lock")):
yield
class NoOpFileLock(PathLockBase):
def __enter__(self) -> None:
raise NotImplementedError
def __exit__(
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None:
raise NotImplementedError
@contextmanager
def lock_for_key(self, name: str, no_block: bool = False) -> Iterator[None]: # noqa: ARG002, FBT002
yield
@contextmanager
def non_reentrant_lock_for_key(self, name: str) -> Iterator[None]: # noqa: ARG002
yield
__all__ = [
"NoOpFileLock",
"ReentrantFileLock",
"Timeout",
]