Source code for univention.updater.locking

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Copyright 2008-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
"""
Univention Updater locking
"""

from __future__ import absolute_import
from __future__ import print_function

import os
import sys
from contextlib import contextmanager
from time import sleep
try:
    from time import monotonic  # type: ignore
except ImportError:
    from monotonic import monotonic  # type: ignore
from errno import EEXIST, ESRCH, ENOENT
try:
    from typing import Optional, Type  # noqa: F401
    from types import TracebackType  # noqa: F401
except ImportError:
    pass
from .errors import UpdaterException


FN_LOCK_UP = '/var/lock/univention-updater'
FN_LOCK_APT = "/var/run/apt-get.lock"


[docs]class LockingError(UpdaterException): """ Signal other updater process running. >>> raise LockingError(1, "Invalid PID") # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... univention.updater.locking.LockingError: Another updater process 1 is currently running according to ...: Invalid PID """ def __str__(self): # type: () -> str return "Another updater process %s is currently running according to %s: %s" % ( self.args[0], FN_LOCK_UP, self.args[1], )
[docs]class UpdaterLock(object): """ Context wrapper for updater-lock :file:`/var/lock/univention-updater`. """ def __init__(self, timeout=0): # type: (int) -> None self.timeout = timeout self.lock = 0 def __enter__(self): # type: () -> UpdaterLock try: self.lock = self.updater_lock_acquire() return self except LockingError as ex: print(ex, file=sys.stderr) sys.exit(5) def __exit__(self, exc_type, exc_value, traceback): # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None if not self.updater_lock_release(): print('WARNING: updater-lock already released!', file=sys.stderr)
[docs] def updater_lock_acquire(self): # type: () -> int ''' Acquire the updater-lock. :returns: 0 if it could be acquired within <timeout> seconds, >= 1 if locked by parent. :rtype: int :raises EnvironmentError: on file system access errors. :raises LockingError: on invalid PID or timeout. ''' deadline = monotonic() + self.timeout lock_pid = 0 while True: try: lock_fd = os.open(FN_LOCK_UP, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) my_pid = b"%d\n" % os.getpid() bytes_written = os.write(lock_fd, my_pid) assert bytes_written == len(my_pid) os.close(lock_fd) return 0 except EnvironmentError as ex: if ex.errno != EEXIST: raise try: lock_fd = os.open(FN_LOCK_UP, os.O_RDONLY | os.O_EXCL) try: lock_pid_b = os.read(lock_fd, 11) # sizeof(s32) + len('\n') finally: os.close(lock_fd) except EnvironmentError as ex: if ex.errno != ENOENT: raise else: try: lock_pid_s = lock_pid_b.decode('ASCII').strip() except UnicodeDecodeError: raise LockingError(lock_pid_b, "Invalid PID") if not lock_pid_s: print('Empty lockfile %s, removing.' % (FN_LOCK_UP,), file=sys.stderr) os.remove(FN_LOCK_UP) continue # redo acquire try: lock_pid = int(lock_pid_s) except ValueError: raise LockingError(lock_pid_s, "Invalid PID") if lock_pid == os.getpid(): return 0 if lock_pid == os.getppid(): # u-repository-* called from u-updater return 1 try: os.kill(lock_pid, 0) except EnvironmentError as ex: if ex.errno == ESRCH: print('Stale PID %d in lockfile %s, removing.' % (lock_pid, FN_LOCK_UP), file=sys.stderr) os.remove(FN_LOCK_UP) continue # redo acquire # PID is valid and process is still alive... if monotonic() > deadline: raise LockingError(lock_pid, "Check lockfile") else: sleep(1)
[docs] def updater_lock_release(self): # type: () -> bool ''' Release the updater-lock. :returns: True if it has been unlocked (or decremented when nested), False if it was already unlocked. :rtype: bool ''' if self.lock > 0: # parent process still owns the lock, do nothing and just return success return True try: os.remove(FN_LOCK_UP) return True except EnvironmentError as error: if error.errno == ENOENT: return False else: raise
[docs]@contextmanager def apt_lock(timeout=300, out=sys.stdout): """ Acquire and release lock for APT. :param timeout: Time to wait. :param out: Output stream for progress and error messages. """ for count in range(timeout, 0, -1): if not os.path.exists(FN_LOCK_APT): break print("\r%3d Waiting for updater lock %s ..." % (count, FN_LOCK_APT), end="", file=out) sleep(1) else: print("Updater is still locked: %s" % (FN_LOCK_APT,), file=out) # FIXME: Abort? open(FN_LOCK_APT, "w").close() yield None if os.path.exists(FN_LOCK_APT): os.unlink(FN_LOCK_APT)