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)`).
  • Loading branch information
dkubek committed Mar 6, 2024
1 parent a504470 commit 8290cfe
Show file tree
Hide file tree
Showing 4 changed files with 112 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/lib/leapp/leapp_lock.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):
pass
98 changes: 98 additions & 0 deletions leapp/utils/lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 _read_pid(fd):
return os.read(fd, 20)


def _write_pid(fd, pid):
os.write(fd, str(pid).encode('utf-8'))


def _validate_pid(old_pid):
try:
old_pid = int(old_pid)
except ValueError:
return False

return old_pid


def _is_old_process_running(old_pid):
return os.access('/proc/{}/stat'.format(old_pid), os.F_OK)


def _clear_lock(fd):
os.lseek(fd, 0, os.SEEK_SET)
os.ftruncate(fd, 0)


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')

def _try_lock(self, pid):
fd = os.open(self.lockfile, os.O_CREAT | os.O_RDWR, 0o644)

try:
if not _acquire_lock(fd):
msg = ('Leapp is currently locked by another process and cannot be started.\n'
'Please ensure all previous instances of the application are closed and try again.')
raise ProcessLockError(msg)

old_pid = _read_pid(fd)
if len(old_pid) == 0:
# No pid in lockfile
_write_pid(fd, pid)
return pid

old_pid = _validate_pid(old_pid)
if not old_pid:
msg = ('The lock file at {} appears to be corrupted.\n'
'Please ensure all previous instances of the application are closed and '
'remove the lock file manually before proceeding.').format(self.lockfile)
raise ProcessLockError(msg)

if old_pid == pid:
# already locked by this process
return pid

if not _is_old_process_running(old_pid):
_clear_lock(fd)
_write_pid(fd, pid)
return pid

return old_pid

finally:
os.close(fd)

def __enter__(self):
my_pid = os.getpid()
pid = self._try_lock(my_pid)
if pid != my_pid:
msg = ('Leapp is currently locked by process with PID {} and cannot be started.\n'
'Please ensure all previous instances of the application have finished and try again.').format(pid)
raise ProcessLockError(msg)

def __exit__(self, *exc_args):
os.unlink(self.lockfile)

0 comments on commit 8290cfe

Please sign in to comment.