#!/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)