# This module is taken from https://gist.github.com/ionrock/3015700 # A file lock implementation that tries to avoid platform specific # issues. It is inspired by a whole bunch of different implementations # listed below. # - https://bitbucket.org/jaraco/yg.lockfile/src/6c448dcbf6e5/yg/lockfile/__init__.py # - http://svn.zope.org/zc.lockfile/trunk/src/zc/lockfile/__init__.py?rev=121133&view=markup # - http://stackoverflow.com/questions/489861/locking-a-file-in-python # - http://www.evanfosmark.com/2009/01/cross-platform-file-locking-support-in-python/ # - http://packages.python.org/lockfile/lockfile.html # There are some tests below and a blog posting conceptually the # problems I wanted to try and solve. The tests reflect these ideas. # - http://ionrock.wordpress.com/2012/06/28/file-locking-in-python/ # I'm not advocating using this package. But if you do happen to try it # out and have suggestions please let me know. import os import time class FileLocked(Exception): pass class FileLock(object): def __init__(self, lock_filename, timeout=None, force=False): self.lock_filename = '%s.lock' % lock_filename self.timeout = timeout self.force = force self._pid = str(os.getpid()) # Store pid in a file in the same directory as desired lockname self.pid_filename = os.path.join( os.path.dirname(self.lock_filename), self._pid, ) + '.lock' def get_lock_pid(self): try: return int(open(self.lock_filename).read()) except IOError: # If we can't read symbolic link, there are two possibilities: # 1. The symbolic link is dead (point to non existing file) # 2. Symbolic link is not there # In either case, we can safely release the lock self.release() def valid_lock(self): """ See if the lock exists and is left over from an old process. """ lock_pid = self.get_lock_pid() # If we're unable to get lock_pid if lock_pid is None: return False # this is our process if self._pid == lock_pid: return True # it is/was another process # see if it is running try: os.kill(lock_pid, 0) except OSError: self.release() return False # it is running return True def is_locked(self, force=False): # We aren't locked if not self.valid_lock(): return False # We are locked, but we want to force it without waiting if not self.timeout: if self.force: self.release() return False else: # We're not waiting or forcing the lock raise FileLocked() # Locked, but want to wait for an unlock interval = .1 intervals = int(self.timeout / interval) while intervals: if self.valid_lock(): intervals -= 1 time.sleep(interval) #print('stopping %s' % intervals) else: return True # check one last time if self.valid_lock(): if self.force: self.release() else: # still locked :( raise FileLocked() def acquire(self): """Create a pid filename and create a symlink (the actual lock file) across platforms that points to it. Symlink is used because it's an atomic operation across platforms. """ pid_file = os.open(self.pid_filename, os.O_CREAT | os.O_EXCL | os.O_RDWR) os.write(pid_file, str(os.getpid()).encode('utf-8')) os.close(pid_file) if hasattr(os, 'symlink'): os.symlink(self.pid_filename, self.lock_filename) else: # Windows platforms doesn't support symlinks, at least not through the os API self.lock_filename = self.pid_filename def release(self): """Try to delete the lock files. Doesn't matter if we fail""" if self.lock_filename != self.pid_filename: try: os.unlink(self.lock_filename) except OSError: pass try: os.remove(self.pid_filename) except OSError: pass def __enter__(self): if not self.is_locked(): self.acquire() return self def __exit__(self, type, value, traceback): self.release()