Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 110 lines
  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.

6 comments

Rod Hyde 14 years, 9 months ago  # | flag

On UNIX systems mkdir is atomic which makes creating a lock very simple. I suspect that this also applies to Windows.

Jordan Callicoat (author) 14 years, 9 months ago  # | flag

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.

Martin Blais 14 years, 7 months ago  # | flag

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.

Jordan Callicoat (author) 14 years, 6 months ago  # | flag

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.

Aaron Gallagher 13 years ago  # | flag

Even better, with no race condition: http://code.activestate.com/recipes/576572/

Max Polk 12 years, 3 months ago  # | flag

See also recipe 576891