Skip to content

Commit

Permalink
Add process lock
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dkubek committed Mar 27, 2024
1 parent a504470 commit 7d378e5
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 3 deletions.
10 changes: 7 additions & 3 deletions leapp/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('')
Expand Down Expand Up @@ -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 "
Expand All @@ -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)
3 changes: 3 additions & 0 deletions leapp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions leapp/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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. """
83 changes: 83 additions & 0 deletions leapp/utils/lock.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 7d378e5

Please sign in to comment.