From 7d378e5514e36c729a156142fc6fe02340d58da3 Mon Sep 17 00:00:00 2001 From: David Kubek Date: Thu, 22 Feb 2024 12:51:04 +0100 Subject: [PATCH] Add process lock This commit addresses the potential risk of running multiple instances of Leapp simultaneously on a single system. It implements a simple lock mechanism to prevent concurrent executions on a single system using a simple BSD lock (`flock(2)`). Lock is acquired at the start of the execution and a PID number is stored in lockfile. The PID in lockfile currently has purely informational character. --- leapp/cli/__init__.py | 10 ++++-- leapp/config.py | 3 ++ leapp/exceptions.py | 4 +++ leapp/utils/lock.py | 83 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 leapp/utils/lock.py diff --git a/leapp/cli/__init__.py b/leapp/cli/__init__.py index fe997d4cf..ab16198cc 100644 --- a/leapp/cli/__init__.py +++ b/leapp/cli/__init__.py @@ -2,12 +2,12 @@ import pkgutil import socket import sys -import textwrap from leapp import VERSION from leapp.cli import commands -from leapp.exceptions import UnknownCommandError +from leapp.exceptions import UnknownCommandError, ProcessLockError from leapp.utils.clicmd import command +from leapp.utils.lock import leapp_lock @command('') @@ -42,7 +42,8 @@ def main(): os.environ['LEAPP_HOSTNAME'] = socket.getfqdn() _load_commands(cli.command) try: - cli.command.execute('leapp version {}'.format(VERSION)) + with leapp_lock(): + cli.command.execute('leapp version {}'.format(VERSION)) except UnknownCommandError as e: bad_cmd = ( "Command \"{CMD}\" is unknown.\nMost likely there is a typo in the command or particular " @@ -54,3 +55,6 @@ def main(): bad_cmd = "No such argument {CMD}" print(bad_cmd.format(CMD=e.requested)) sys.exit(1) + except ProcessLockError as e: + sys.stderr.write('{}\nAborting.\n'.format(e.message)) + sys.exit(1) diff --git a/leapp/config.py b/leapp/config.py index 18c357d15..2487e0f09 100644 --- a/leapp/config.py +++ b/leapp/config.py @@ -40,6 +40,9 @@ 'dir': '/var/log/leapp/', 'files': ','.join(_FILES_TO_ARCHIVE), }, + 'lock': { + 'path': '/var/run/leapp.pid' + }, 'logs': { 'dir': '/var/log/leapp/', 'files': ','.join(_LOGS), diff --git a/leapp/exceptions.py b/leapp/exceptions.py index 43c59051c..9463a5c10 100644 --- a/leapp/exceptions.py +++ b/leapp/exceptions.py @@ -148,3 +148,7 @@ class RequestStopAfterPhase(LeappError): def __init__(self): super(RequestStopAfterPhase, self).__init__('Stop after phase has been requested.') + + +class ProcessLockError(LeappError): + """ This exception is used to represent an error within the process locking mechanism. """ diff --git a/leapp/utils/lock.py b/leapp/utils/lock.py new file mode 100644 index 000000000..33e825c89 --- /dev/null +++ b/leapp/utils/lock.py @@ -0,0 +1,83 @@ +import os +import fcntl +import logging + +from leapp.config import get_config +from leapp.exceptions import ProcessLockError + + +def leapp_lock(lockfile=None): + return ProcessLock(lockfile=lockfile) + + +def _acquire_lock(fd): + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + return True + except OSError: + return False + + +def _clear_lock(fd): + os.lseek(fd, 0, os.SEEK_SET) + os.ftruncate(fd, 0) + + +def _read_pid(fd): + return os.read(fd, 20) + + +def _write_pid(fd, pid): + _clear_lock(fd) + os.write(fd, str(pid).encode('utf-8')) + + +class ProcessLock(object): + + def __init__(self, lockfile=None): + self.log = logging.getLogger('leapp.utils.lock') + self.lockfile = lockfile if lockfile else get_config().get('lock', 'path') + + self.fd = None + + def _get_pid_from_lockfile(self): + running_pid = _read_pid(self.fd) + self.log.debug("_get_pid_from_lockfile: running_pid=%s", running_pid) + running_pid = int(running_pid) + + return running_pid + + def _try_lock(self, pid): + if not _acquire_lock(self.fd): + try: + running_pid = self._get_pid_from_lockfile() + except ValueError: + process_msg = '' + else: + process_msg = ' by process with PID {}'.format(running_pid) + + msg = ( + 'Leapp is currently locked{} and cannot be started.\n' + 'Please ensure no other instance of leapp is running and then delete the lockfile at {} and try again.' + ).format(process_msg, self.lockfile) + raise ProcessLockError(msg) + + try: + _write_pid(self.fd, pid) + except OSError: + raise ProcessLockError('Could not write PID to lockfile.') + + def __enter__(self): + my_pid = os.getpid() + + self.fd = os.open(self.lockfile, os.O_CREAT | os.O_RDWR, 0o600) + try: + self._try_lock(my_pid) + except ProcessLockError: + os.close(self.fd) + raise + + def __exit__(self, *exc_args): + _clear_lock(self.fd) + os.close(self.fd) + os.unlink(self.lockfile)