/mercurial/lock.py
Python | 154 lines | 144 code | 2 blank | 8 comment | 1 complexity | aea6ea01c033fca134b3b2c1efb5c988 MD5 | raw file
Possible License(s): GPL-2.0
- # lock.py - simple advisory locking scheme for mercurial
- #
- # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
- #
- # This software may be used and distributed according to the terms of the
- # GNU General Public License version 2 or any later version.
- import util, error
- import errno, os, socket, time
- import warnings
- class lock(object):
- '''An advisory lock held by one process to control access to a set
- of files. Non-cooperating processes or incorrectly written scripts
- can ignore Mercurial's locking scheme and stomp all over the
- repository, so don't do that.
- Typically used via localrepository.lock() to lock the repository
- store (.hg/store/) or localrepository.wlock() to lock everything
- else under .hg/.'''
- # lock is symlink on platforms that support it, file on others.
- # symlink is used because create of directory entry and contents
- # are atomic even over nfs.
- # old-style lock: symlink to pid
- # new-style lock: symlink to hostname:pid
- _host = None
- def __init__(self, vfs, file, timeout=-1, releasefn=None, desc=None):
- self.vfs = vfs
- self.f = file
- self.held = 0
- self.timeout = timeout
- self.releasefn = releasefn
- self.desc = desc
- self.postrelease = []
- self.pid = os.getpid()
- self.delay = self.lock()
- def __del__(self):
- if self.held:
- warnings.warn("use lock.release instead of del lock",
- category=DeprecationWarning,
- stacklevel=2)
- # ensure the lock will be removed
- # even if recursive locking did occur
- self.held = 1
- self.release()
- def lock(self):
- timeout = self.timeout
- while True:
- try:
- self.trylock()
- return self.timeout - timeout
- except error.LockHeld, inst:
- if timeout != 0:
- time.sleep(1)
- if timeout > 0:
- timeout -= 1
- continue
- raise error.LockHeld(errno.ETIMEDOUT, inst.filename, self.desc,
- inst.locker)
- def trylock(self):
- if self.held:
- self.held += 1
- return
- if lock._host is None:
- lock._host = socket.gethostname()
- lockname = '%s:%s' % (lock._host, self.pid)
- while not self.held:
- try:
- self.vfs.makelock(lockname, self.f)
- self.held = 1
- except (OSError, IOError), why:
- if why.errno == errno.EEXIST:
- locker = self.testlock()
- if locker is not None:
- raise error.LockHeld(errno.EAGAIN,
- self.vfs.join(self.f), self.desc,
- locker)
- else:
- raise error.LockUnavailable(why.errno, why.strerror,
- why.filename, self.desc)
- def testlock(self):
- """return id of locker if lock is valid, else None.
- If old-style lock, we cannot tell what machine locker is on.
- with new-style lock, if locker is on this machine, we can
- see if locker is alive. If locker is on this machine but
- not alive, we can safely break lock.
- The lock file is only deleted when None is returned.
- """
- try:
- locker = self.vfs.readlock(self.f)
- except (OSError, IOError), why:
- if why.errno == errno.ENOENT:
- return None
- raise
- try:
- host, pid = locker.split(":", 1)
- except ValueError:
- return locker
- if host != lock._host:
- return locker
- try:
- pid = int(pid)
- except ValueError:
- return locker
- if util.testpid(pid):
- return locker
- # if locker dead, break lock. must do this with another lock
- # held, or can race and break valid lock.
- try:
- l = lock(self.vfs, self.f + '.break', timeout=0)
- self.vfs.unlink(self.f)
- l.release()
- except error.LockError:
- return locker
- def release(self):
- """release the lock and execute callback function if any
- If the lock has been acquired multiple times, the actual release is
- delayed to the last release call."""
- if self.held > 1:
- self.held -= 1
- elif self.held == 1:
- self.held = 0
- if os.getpid() != self.pid:
- # we forked, and are not the parent
- return
- if self.releasefn:
- self.releasefn()
- try:
- self.vfs.unlink(self.f)
- except OSError:
- pass
- for callback in self.postrelease:
- callback()
- def release(*locks):
- for lock in locks:
- if lock is not None:
- lock.release()