mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-09-15 13:19:56 +00:00
It has been found that there are two cases in which pkgbuild was not parsed correctly 1. Major case in which there is quotation mark inside comment line, which would cause ValueError: No closing quotation error 2. Minor case, if there are utf symbols in pkgbuild file (e.g. hieroglyphs, see ttf-google-fonts-git), it will case incorrect reading in `_is_escaped` method
232 lines
7.9 KiB
Python
232 lines
7.9 KiB
Python
#
|
|
# Copyright (c) 2021-2024 ahriman team.
|
|
#
|
|
# This file is part of ahriman
|
|
# (see https://github.com/arcan1s/ahriman).
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
import argparse
|
|
import fcntl
|
|
import os
|
|
|
|
from io import TextIOWrapper
|
|
from pathlib import Path
|
|
from types import TracebackType
|
|
from typing import Literal, Self
|
|
|
|
from ahriman import __version__
|
|
from ahriman.core.configuration import Configuration
|
|
from ahriman.core.exceptions import DuplicateRunError
|
|
from ahriman.core.log import LazyLogging
|
|
from ahriman.core.status import Client
|
|
from ahriman.core.utils import check_user
|
|
from ahriman.models.build_status import BuildStatusEnum
|
|
from ahriman.models.repository_id import RepositoryId
|
|
from ahriman.models.waiter import Waiter
|
|
|
|
|
|
class Lock(LazyLogging):
|
|
"""
|
|
wrapper for application lock file. Credits for idea to https://github.com/bmhatfield/python-pidfile.git
|
|
|
|
Attributes:
|
|
force(bool): remove lock file on start if any
|
|
path(Path): path to lock file if any
|
|
reporter(Client): build status reporter instance
|
|
paths(RepositoryPaths): repository paths instance
|
|
unsafe(bool): skip user check
|
|
wait_timeout(int): wait in seconds until lock will free
|
|
|
|
Examples:
|
|
Instance of this class except for controlling file-based lock is also required for basic applications checks.
|
|
The common flow is to create instance in ``with`` block and handle exceptions after all::
|
|
|
|
>>> from ahriman.core.configuration import Configuration
|
|
>>> from ahriman.models.repository_id import RepositoryId
|
|
>>>
|
|
>>> configuration = Configuration()
|
|
>>> try:
|
|
>>> with Lock(args, RepositoryId("x86_64", "aur"), configuration):
|
|
>>> do_something()
|
|
>>> except Exception as exception:
|
|
>>> handle_exceptions(exception)
|
|
"""
|
|
|
|
def __init__(self, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration) -> None:
|
|
"""
|
|
Args:
|
|
args(argparse.Namespace): command line args
|
|
repository_id(RepositoryId): repository unique identifier
|
|
configuration(Configuration): configuration instance
|
|
"""
|
|
self.path: Path | None = None
|
|
if args.lock is not None:
|
|
self.path = args.lock
|
|
if not repository_id.is_empty:
|
|
self.path = self.path.with_stem(f"{args.lock.stem}_{repository_id.id}")
|
|
if not self.path.is_absolute():
|
|
# prepend full path to the lock file
|
|
self.path = Path("/") / "run" / "ahriman" / self.path
|
|
self._pid_file: TextIOWrapper | None = None
|
|
|
|
self.force: bool = args.force
|
|
self.unsafe: bool = args.unsafe
|
|
self.wait_timeout: int = args.wait_timeout
|
|
|
|
self.paths = configuration.repository_paths
|
|
self.reporter = Client.load(repository_id, configuration, report=args.report)
|
|
|
|
@staticmethod
|
|
def perform_lock(fd: int) -> bool:
|
|
"""
|
|
perform file lock
|
|
|
|
Args:
|
|
fd(int): file descriptor:
|
|
|
|
Returns:
|
|
bool: ``True`` in case if file is locked and ``False`` otherwise
|
|
"""
|
|
try:
|
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except OSError:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _open(self) -> None:
|
|
"""
|
|
create lock file
|
|
"""
|
|
if self.path is None:
|
|
return
|
|
self._pid_file = self.path.open("a+", encoding="utf8")
|
|
|
|
def _watch(self) -> bool:
|
|
"""
|
|
watch until lock disappear
|
|
|
|
Returns:
|
|
bool: ``True`` in case if file is locked and ``False`` otherwise
|
|
"""
|
|
# there are reasons why we are not using inotify here. First of all, if we would use it, it would bring to
|
|
# race conditions because multiple processes will be notified at the same time. Secondly, it is good library,
|
|
# but platform-specific, and we only need to check if file exists
|
|
if self._pid_file is None:
|
|
return False
|
|
|
|
waiter = Waiter(self.wait_timeout)
|
|
return bool(waiter.wait(lambda fd: not self.perform_lock(fd), self._pid_file.fileno()))
|
|
|
|
def _write(self, *, is_locked: bool = False) -> None:
|
|
"""
|
|
write pid to the lock file
|
|
|
|
Args:
|
|
is_locked(bool, optional): indicates if file was already locked or not (Default value = False)
|
|
|
|
Raises:
|
|
DuplicateRunError: if it cannot lock PID file
|
|
"""
|
|
if self._pid_file is None:
|
|
return
|
|
if not is_locked:
|
|
if not self.perform_lock(self._pid_file.fileno()):
|
|
raise DuplicateRunError
|
|
|
|
self._pid_file.seek(0) # reset position and remove file content if any
|
|
self._pid_file.truncate()
|
|
|
|
self._pid_file.write(str(os.getpid())) # write current pid
|
|
self._pid_file.flush() # flush data to disk
|
|
|
|
self._pid_file.seek(0) # reset position again
|
|
|
|
def check_user(self) -> None:
|
|
"""
|
|
check if current user is actually owner of ahriman root
|
|
"""
|
|
check_user(self.paths, unsafe=self.unsafe)
|
|
self.paths.tree_create()
|
|
|
|
def check_version(self) -> None:
|
|
"""
|
|
check web server version
|
|
"""
|
|
status = self.reporter.status_get()
|
|
if status.version is not None and status.version != __version__:
|
|
self.logger.warning("status watcher version mismatch, our %s, their %s",
|
|
__version__, status.version)
|
|
|
|
def clear(self) -> None:
|
|
"""
|
|
remove lock file
|
|
"""
|
|
if self._pid_file is not None: # close file descriptor
|
|
try:
|
|
self._pid_file.close()
|
|
except IOError:
|
|
pass # suppress any IO errors occur
|
|
if self.path is not None: # remove lock file
|
|
self.path.unlink(missing_ok=True)
|
|
|
|
def lock(self) -> None:
|
|
"""
|
|
create pid file
|
|
"""
|
|
if self.force: # remove lock if force flag is set
|
|
self.clear()
|
|
self._open()
|
|
is_locked = self._watch()
|
|
self._write(is_locked=is_locked)
|
|
|
|
def __enter__(self) -> Self:
|
|
"""
|
|
default workflow is the following:
|
|
|
|
#. Check user UID
|
|
#. Check web status watcher status
|
|
#. Open lock file
|
|
#. Wait for lock file to be free
|
|
#. Write current PID to the lock file
|
|
#. Report to status page if enabled
|
|
|
|
Returns:
|
|
Self: always instance of self
|
|
"""
|
|
self.check_user()
|
|
self.check_version()
|
|
self.lock()
|
|
self.reporter.status_update(BuildStatusEnum.Building)
|
|
return self
|
|
|
|
def __exit__(self, exc_type: type[Exception] | None, exc_val: Exception | None,
|
|
exc_tb: TracebackType) -> Literal[False]:
|
|
"""
|
|
remove lock file when done
|
|
|
|
Args:
|
|
exc_type(type[Exception] | None): exception type name if any
|
|
exc_val(Exception | None): exception raised if any
|
|
exc_tb(TracebackType): exception traceback if any
|
|
|
|
Returns:
|
|
Literal[False]: always ``False`` (do not suppress any exception)
|
|
"""
|
|
self.clear()
|
|
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
|
|
self.reporter.status_update(status)
|
|
return False
|