Skip to content

Commit 8290cfe

Browse files
committed
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)`).
1 parent a504470 commit 8290cfe

4 files changed

Lines changed: 112 additions & 3 deletions

File tree

leapp/cli/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
import pkgutil
33
import socket
44
import sys
5-
import textwrap
65

76
from leapp import VERSION
87
from leapp.cli import commands
9-
from leapp.exceptions import UnknownCommandError
8+
from leapp.exceptions import UnknownCommandError, ProcessLockError
109
from leapp.utils.clicmd import command
10+
from leapp.utils.lock import leapp_lock
1111

1212

1313
@command('')
@@ -42,7 +42,8 @@ def main():
4242
os.environ['LEAPP_HOSTNAME'] = socket.getfqdn()
4343
_load_commands(cli.command)
4444
try:
45-
cli.command.execute('leapp version {}'.format(VERSION))
45+
with leapp_lock():
46+
cli.command.execute('leapp version {}'.format(VERSION))
4647
except UnknownCommandError as e:
4748
bad_cmd = (
4849
"Command \"{CMD}\" is unknown.\nMost likely there is a typo in the command or particular "
@@ -54,3 +55,6 @@ def main():
5455
bad_cmd = "No such argument {CMD}"
5556
print(bad_cmd.format(CMD=e.requested))
5657
sys.exit(1)
58+
except ProcessLockError as e:
59+
sys.stderr.write('{}\nAborting.\n'.format(e.message))
60+
sys.exit(1)

leapp/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
'dir': '/var/log/leapp/',
4141
'files': ','.join(_FILES_TO_ARCHIVE),
4242
},
43+
'lock': {
44+
'path': '/var/lib/leapp/leapp_lock.pid'
45+
},
4346
'logs': {
4447
'dir': '/var/log/leapp/',
4548
'files': ','.join(_LOGS),

leapp/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,7 @@ class RequestStopAfterPhase(LeappError):
148148

149149
def __init__(self):
150150
super(RequestStopAfterPhase, self).__init__('Stop after phase has been requested.')
151+
152+
153+
class ProcessLockError(LeappError):
154+
pass

leapp/utils/lock.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import os
2+
import fcntl
3+
import logging
4+
5+
from leapp.config import get_config
6+
from leapp.exceptions import ProcessLockError
7+
8+
9+
def leapp_lock(lockfile=None):
10+
return ProcessLock(lockfile=lockfile)
11+
12+
13+
def _acquire_lock(fd):
14+
try:
15+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
16+
return True
17+
except OSError:
18+
return False
19+
20+
21+
def _read_pid(fd):
22+
return os.read(fd, 20)
23+
24+
25+
def _write_pid(fd, pid):
26+
os.write(fd, str(pid).encode('utf-8'))
27+
28+
29+
def _validate_pid(old_pid):
30+
try:
31+
old_pid = int(old_pid)
32+
except ValueError:
33+
return False
34+
35+
return old_pid
36+
37+
38+
def _is_old_process_running(old_pid):
39+
return os.access('/proc/{}/stat'.format(old_pid), os.F_OK)
40+
41+
42+
def _clear_lock(fd):
43+
os.lseek(fd, 0, os.SEEK_SET)
44+
os.ftruncate(fd, 0)
45+
46+
47+
class ProcessLock(object):
48+
49+
def __init__(self, lockfile=None):
50+
self.log = logging.getLogger('leapp.utils.lock')
51+
self.lockfile = lockfile if lockfile else get_config().get('lock', 'path')
52+
53+
def _try_lock(self, pid):
54+
fd = os.open(self.lockfile, os.O_CREAT | os.O_RDWR, 0o644)
55+
56+
try:
57+
if not _acquire_lock(fd):
58+
msg = ('Leapp is currently locked by another process and cannot be started.\n'
59+
'Please ensure all previous instances of the application are closed and try again.')
60+
raise ProcessLockError(msg)
61+
62+
old_pid = _read_pid(fd)
63+
if len(old_pid) == 0:
64+
# No pid in lockfile
65+
_write_pid(fd, pid)
66+
return pid
67+
68+
old_pid = _validate_pid(old_pid)
69+
if not old_pid:
70+
msg = ('The lock file at {} appears to be corrupted.\n'
71+
'Please ensure all previous instances of the application are closed and '
72+
'remove the lock file manually before proceeding.').format(self.lockfile)
73+
raise ProcessLockError(msg)
74+
75+
if old_pid == pid:
76+
# already locked by this process
77+
return pid
78+
79+
if not _is_old_process_running(old_pid):
80+
_clear_lock(fd)
81+
_write_pid(fd, pid)
82+
return pid
83+
84+
return old_pid
85+
86+
finally:
87+
os.close(fd)
88+
89+
def __enter__(self):
90+
my_pid = os.getpid()
91+
pid = self._try_lock(my_pid)
92+
if pid != my_pid:
93+
msg = ('Leapp is currently locked by process with PID {} and cannot be started.\n'
94+
'Please ensure all previous instances of the application have finished and try again.').format(pid)
95+
raise ProcessLockError(msg)
96+
97+
def __exit__(self, *exc_args):
98+
os.unlink(self.lockfile)

0 commit comments

Comments
 (0)