This recipe implements a simple lockfile to ensure that only one instance of an app is alive at any given time.
1.2 Added documentation and cleaned up a bit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | # flock.py
import os
import socket
class flock(object):
'''Class to handle creating and removing (pid) lockfiles'''
# custom exceptions
class FileLockAcquisitionError(Exception): pass
class FileLockReleaseError(Exception): pass
# convenience callables for formatting
addr = lambda self: '%d@%s' % (self.pid, self.host)
fddr = lambda self: '<%s %s>' % (self.path, self.addr())
pddr = lambda self, lock: '<%s %s@%s>' %\
(self.path, lock['pid'], lock['host'])
def __init__(self, path, debug=None):
self.pid = os.getpid()
self.host = socket.gethostname()
self.path = path
self.debug = debug # set this to get status messages
def acquire(self):
'''Acquire a lock, returning self if successful, False otherwise'''
if self.islocked():
if self.debug:
lock = self._readlock()
print 'Previous lock detected: %s' % self.pddr(lock)
return False
try:
fh = open(self.path, 'w')
fh.write(self.addr())
fh.close()
if self.debug:
print 'Acquired lock: %s' % self.fddr()
except:
if os.path.isfile(self.path):
try:
os.unlink(self.path)
except:
pass
raise (self.FileLockAcquisitionError,
'Error acquiring lock: %s' % self.fddr())
return self
def release(self):
'''Release lock, returning self'''
if self.ownlock():
try:
os.unlink(self.path)
if self.debug:
print 'Released lock: %s' % self.fddr()
except:
raise (self.FileLockReleaseError,
'Error releasing lock: %s' % self.fddr())
return self
def _readlock(self):
'''Internal method to read lock info'''
try:
lock = {}
fh = open(self.path)
data = fh.read().rstrip().split('@')
fh.close()
lock['pid'], lock['host'] = data
return lock
except:
return {'pid': 8**10, 'host': ''}
def islocked(self):
'''Check if we already have a lock'''
try:
lock = self._readlock()
os.kill(int(lock['pid']), 0)
return (lock['host'] == self.host)
except:
return False
def ownlock(self):
'''Check if we own the lock'''
lock = self._readlock()
return (self.fddr() == self.pddr(lock))
def __del__(self):
'''Magic method to clean up lock when program exits'''
self.release()
## ========
## Test programs: run test1.py then test2.py (in the same dir)
## from another teminal -- test2.py should print
## a message that there is a lock in place and exit.
# test1.py
from time import sleep
from flock import flock
lock = flock('tmp.lock', True).acquire()
if lock:
sleep(30)
else:
print 'locked!'
# test2.py
from flock import flock
lock = flock('tmp.lock', True).acquire()
if lock:
print 'doing stuff'
else:
print 'locked!'
|
Kudos to Frederick Lundh for the idea.
Ps. The lock file should be automatically cleaned up, even if your program excepts, due to the "magic" __del__ method.
Tags: programs
On UNIX systems mkdir is atomic which makes creating a lock very simple. I suspect that this also applies to Windows.
Good point. For a simple lock, creating/checking for the presence of a dir (or file for that matter) would work. But if you want to do something more complex like remote execution over ssh, then you need to also keep track of hostnames.
Race condition. I'm sorry I fail to see how this achieves atomicity. You've got a race condition in acquire() between the "if self.islocked()" check and the subsequent call to open().
In order to implement filesystem locks, you need to use some sort of atomic call that both creates a filesystem object or fails. open(..., 'w') is not good enough, i.e. two concurrent processes could succesfully run through the check in acquire(), and both open the file in write mode. You're not going to get errors (i.e. no locking will have occurred), and the later file will remain. You can use mkdir() instead, but I don't know if that'll work under Windows.
In any case, I thought it would be important to point out the flaw in this code, for the benefit of those who would cut-n-paste without looking.
True... No doubt. This code isn't so robust as to catch the codition you mention, but for 99% of the use cases, it should "just work". Please do correct and improve.
Even better, with no race condition: http://code.activestate.com/recipes/576572/
See also recipe 576891