#!/usr/bin/env python import os import string import time import FCNTL import errno import __builtin__ class LockFileError: name = "Some lockfile error" def __init__(self, name=None, **args): if name: self.name = name self.__args = args for attr, val in args.items(): setattr(self, attr, val) def __str__(self): if self.__args: argstr = str(self.__args) else: argstr = "" if self.name: if argstr: return self.name + ", " + argstr else: return self.name else: return argstr class LockNotAcquired(LockFileError): name = "Lock file is not yet acquired" pass class LockInUse(LockFileError): name = "Lock file is in use" pass class LockFile: """\ Class for manipulating lock files. Instantiation takes a path parameter, giving the name of the lock file. This only creates the data structure for manipulating the lock file; to actually acquire the lock, use the trylock() method. Deleting the instance was supposed to release the lock file, if it was acquired, but this does currently not work reliably, probably due to the lack of real garbage collection in Python. Methods: - haslock(): Returns true if the lock file has been acquired. - owner(): Returns id of the process owning the lock file. - owner_alive(): Returns true if the owning process is alive. - trylock(): Try to acquire the lock file. - unlock(): Release the lock file. Attributes: - path: The path of the lock file """ # Keep a reference to unlink, for unlock() to use, even if the # lockfile modules namespace has been cleaned at Python termination. # To remove any posibility that the function is converted to a method, # it is encapsulated in a tuple. unlink = (os.unlink,) def __init__(self, path): self.path = path self.__locked = 0 self.__pf = None def haslock(self): """Returns true if the lock file has been acquired. """ return self.__locked def assert_lock(self): """Check that the lock is acquired, and raise an exception if not.""" if not self.haslock(): raise LockNotAcquired() def owner(self): """Returns the id of the process currently owning the lock file. An exception is raised if no-one is holding the lock. """ try: pid = string.atoi(__builtin__.open(self.path).readline()) #except IOError, (err, msg): # raise LockFileError("Can't read existing lockfile", # errno=err, path=self.path) except ValueError: raise LockFileError("Contents of existing lockfile is broken") return pid def owner_alive(self): """Returns true if the process owning the lock file is presumed alive. """ try: otherpid = self.owner() except: return 0 try: os.kill(otherpid, 0) except os.error, (err, msg): if err == errno.ESRCH: return 0 return 1 def trylock(self, _tries=3): """Try to acquire the lock file. An exception is raised if it can't be acquired, e.g if the owning process is still alive, or if the lock file can't be created for other reasons. """ pidfile = self.path + "__" + str(os.getpid()) try: os.unlink(pidfile) except: pass try: from FCNTL import * fd = -1 fd = os.open(pidfile, O_WRONLY|O_APPEND|O_CREAT|O_TRUNC, 0444) os.write(fd, str(os.getpid()) + "\n") try: os.link(pidfile, self.path) self.__locked = 1 self.__pf = os.fdopen(fd, "a") fd = -1 except os.error, (err, msg): if err != errno.EEXIST: raise os.error, (err, msg) if self.owner_alive(): otherpid = self.owner() if otherpid != os.getpid(): raise LockInUse(path=self.path, pid=otherpid) # XXX: Race condition: another process could acquire the # lock between us checking if the owner is alive, and # removing the file. try: os.unlink(self.path) except: pass if _tries < 1: raise LockFileError("Can't acquire lock file") self.trylock(_tries=_tries-1) finally: if fd >= 0: os.close(fd) try: os.unlink(pidfile) except: pass def waitlock(self, timeout=None, pollintvl=1): """Acquire the lock file, waiting at most TIMEOUT seconds. If TIMEOUT is None (default), it waits indefinitely. """ if timeout != None and timeout < pollintvl: pollintvl = timeout / 8.0 while timeout == None or timeout > 0: try: self.trylock() return except LockFileError: time.sleep(pollintvl) if timeout != None: timeout = timeout - pollintvl self.trylock() def add_data(self, data): self.assert_lock() self.__pf.write(data) self.__pf.flush() def read_data(self): pf = open(self.path) pf.readline() # Skip the pid data = pf.read() pf.close() return data def unlock(self): """Release the lock file (by removing it). If the lock file has not been acquired, an exception is raised. """ self.assert_lock() self.unlink[0](self.path) self.__locked = 0 self.__pf.close() self.__pf = None def __del__(self): if self.haslock(): self.unlock()