lock.py
441 lines
| 13.5 KiB
| text/x-python
|
PythonLexer
/ mercurial / lock.py
|
r9309 | # lock.py - simple advisory locking scheme for mercurial | ||
|
r161 | # | ||
|
r2859 | # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com> | ||
|
r161 | # | ||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r161 | |||
|
r25956 | from __future__ import absolute_import | ||
|
r26473 | import contextlib | ||
|
r25956 | import errno | ||
|
r30921 | import os | ||
|
r36717 | import signal | ||
|
r25956 | import socket | ||
import time | ||||
|
r8113 | import warnings | ||
|
r161 | |||
|
r35209 | from .i18n import _ | ||
|
r43359 | from .pycompat import getattr | ||
|
r35209 | |||
|
r25956 | from . import ( | ||
|
r31375 | encoding, | ||
|
r25956 | error, | ||
|
r30921 | pycompat, | ||
|
r38157 | util, | ||
|
r37138 | ) | ||
|
r43346 | from .utils import procutil | ||
|
r25956 | |||
|
r30920 | def _getlockprefix(): | ||
"""Return a string which is used to differentiate pid namespaces | ||||
It's useful to detect "dead" processes and remove stale locks with | ||||
|
r30921 | confidence. Typically it's just hostname. On modern linux, we include an | ||
extra Linux-specific pid namespace identifier. | ||||
|
r30920 | """ | ||
|
r35915 | result = encoding.strtolocal(socket.gethostname()) | ||
|
r43347 | if pycompat.sysplatform.startswith(b'linux'): | ||
|
r30921 | try: | ||
|
r43347 | result += b'/%x' % os.stat(b'/proc/self/ns/pid').st_ino | ||
|
r30921 | except OSError as ex: | ||
if ex.errno not in (errno.ENOENT, errno.EACCES, errno.ENOTDIR): | ||||
raise | ||||
return result | ||||
|
r30920 | |||
|
r43346 | |||
|
r36717 | @contextlib.contextmanager | ||
def _delayedinterrupt(): | ||||
"""Block signal interrupt while doing something critical | ||||
This makes sure that the code block wrapped by this context manager won't | ||||
be interrupted. | ||||
For Windows developers: It appears not possible to guard time.sleep() | ||||
from CTRL_C_EVENT, so please don't use time.sleep() to test if this is | ||||
working. | ||||
""" | ||||
assertedsigs = [] | ||||
blocked = False | ||||
orighandlers = {} | ||||
def raiseinterrupt(num): | ||||
|
r43346 | if num == getattr(signal, 'SIGINT', None) or num == getattr( | ||
signal, 'CTRL_C_EVENT', None | ||||
): | ||||
|
r36717 | raise KeyboardInterrupt | ||
else: | ||||
raise error.SignalInterrupt | ||||
|
r43346 | |||
|
r36717 | def catchterm(num, frame): | ||
if blocked: | ||||
assertedsigs.append(num) | ||||
else: | ||||
raiseinterrupt(num) | ||||
try: | ||||
# save handlers first so they can be restored even if a setup is | ||||
# interrupted between signal.signal() and orighandlers[] =. | ||||
|
r43347 | for name in [ | ||
b'CTRL_C_EVENT', | ||||
b'SIGINT', | ||||
b'SIGBREAK', | ||||
b'SIGHUP', | ||||
b'SIGTERM', | ||||
]: | ||||
|
r36717 | num = getattr(signal, name, None) | ||
if num and num not in orighandlers: | ||||
orighandlers[num] = signal.getsignal(num) | ||||
try: | ||||
for num in orighandlers: | ||||
signal.signal(num, catchterm) | ||||
except ValueError: | ||||
|
r43346 | pass # in a thread? no luck | ||
|
r36717 | |||
blocked = True | ||||
yield | ||||
finally: | ||||
# no simple way to reliably restore all signal handlers because | ||||
# any loops, recursive function calls, except blocks, etc. can be | ||||
# interrupted. so instead, make catchterm() raise interrupt. | ||||
blocked = False | ||||
try: | ||||
for num, handler in orighandlers.items(): | ||||
signal.signal(num, handler) | ||||
except ValueError: | ||||
|
r43346 | pass # in a thread? | ||
|
r36717 | |||
# re-raise interrupt exception if any, which may be shadowed by a new | ||||
# interrupt occurred while re-raising the first one | ||||
if assertedsigs: | ||||
raiseinterrupt(assertedsigs[0]) | ||||
|
r43346 | |||
|
r35210 | def trylock(ui, vfs, lockname, timeout, warntimeout, *args, **kwargs): | ||
|
r35209 | """return an acquired lock or raise an a LockHeld exception | ||
|
r35210 | This function is responsible to issue warnings and or debug messages about | ||
the held lock while trying to acquires it.""" | ||||
|
r35209 | |||
def printwarning(printer, locker): | ||||
"""issue the usual "waiting on lock" message through any channel""" | ||||
# show more details for new-style locks | ||||
|
r43347 | if b':' in locker: | ||
host, pid = locker.split(b":", 1) | ||||
|
r43346 | msg = _( | ||
|
r43347 | b"waiting for lock on %s held by process %r on host %r\n" | ||
|
r43346 | ) % ( | ||
pycompat.bytestr(l.desc), | ||||
pycompat.bytestr(pid), | ||||
pycompat.bytestr(host), | ||||
) | ||||
|
r35209 | else: | ||
|
r43347 | msg = _(b"waiting for lock on %s held by %r\n") % ( | ||
|
r43346 | l.desc, | ||
pycompat.bytestr(locker), | ||||
) | ||||
|
r35209 | printer(msg) | ||
l = lock(vfs, lockname, 0, *args, dolock=False, **kwargs) | ||||
|
r35210 | debugidx = 0 if (warntimeout and timeout) else -1 | ||
|
r35209 | warningidx = 0 | ||
if not timeout: | ||||
warningidx = -1 | ||||
|
r35210 | elif warntimeout: | ||
warningidx = warntimeout | ||||
|
r35209 | |||
delay = 0 | ||||
while True: | ||||
try: | ||||
l._trylock() | ||||
break | ||||
except error.LockHeld as inst: | ||||
|
r35210 | if delay == debugidx: | ||
printwarning(ui.debug, inst.locker) | ||||
|
r35209 | if delay == warningidx: | ||
printwarning(ui.warn, inst.locker) | ||||
if timeout <= delay: | ||||
|
r43346 | raise error.LockHeld( | ||
errno.ETIMEDOUT, inst.filename, l.desc, inst.locker | ||||
) | ||||
|
r35209 | time.sleep(1) | ||
delay += 1 | ||||
l.delay = delay | ||||
if l.delay: | ||||
|
r35210 | if 0 <= warningidx <= l.delay: | ||
|
r43347 | ui.warn(_(b"got lock after %d seconds\n") % l.delay) | ||
|
r35210 | else: | ||
|
r43347 | ui.debug(b"got lock after %d seconds\n" % l.delay) | ||
|
r35209 | if l.acquirefn: | ||
l.acquirefn() | ||||
return l | ||||
|
r43346 | |||
|
r1559 | class lock(object): | ||
|
r9309 | '''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/.''' | ||||
|
r1877 | # 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 | ||||
|
r4947 | _host = None | ||
|
r43346 | def __init__( | ||
self, | ||||
vfs, | ||||
fname, | ||||
timeout=-1, | ||||
releasefn=None, | ||||
acquirefn=None, | ||||
desc=None, | ||||
inheritchecker=None, | ||||
parentlock=None, | ||||
signalsafe=True, | ||||
dolock=True, | ||||
): | ||||
|
r20091 | self.vfs = vfs | ||
|
r37677 | self.f = fname | ||
|
r161 | self.held = 0 | ||
|
r1787 | self.timeout = timeout | ||
|
r1530 | self.releasefn = releasefn | ||
|
r26321 | self.acquirefn = acquirefn | ||
|
r2016 | self.desc = desc | ||
|
r26498 | self._inheritchecker = inheritchecker | ||
|
r26356 | self.parentlock = parentlock | ||
self._parentheld = False | ||||
self._inherited = False | ||||
|
r38157 | if signalsafe: | ||
self._maybedelayedinterrupt = _delayedinterrupt | ||||
else: | ||||
self._maybedelayedinterrupt = util.nullcontextmanager | ||||
|
r43346 | self.postrelease = [] | ||
|
r26383 | self.pid = self._getpid() | ||
|
r35209 | if dolock: | ||
self.delay = self.lock() | ||||
if self.acquirefn: | ||||
self.acquirefn() | ||||
|
r161 | |||
|
r27797 | def __enter__(self): | ||
return self | ||||
def __exit__(self, exc_type, exc_value, exc_tb): | ||||
self.release() | ||||
|
r161 | def __del__(self): | ||
|
r8108 | if self.held: | ||
|
r43346 | warnings.warn( | ||
|
r43809 | "use lock.release instead of del lock", | ||
|
r43346 | category=DeprecationWarning, | ||
stacklevel=2, | ||||
) | ||||
|
r8113 | |||
|
r8108 | # ensure the lock will be removed | ||
# even if recursive locking did occur | ||||
self.held = 1 | ||||
|
r161 | self.release() | ||
|
r26383 | def _getpid(self): | ||
|
r37138 | # wrapper around procutil.getpid() to make testing easier | ||
return procutil.getpid() | ||||
|
r26383 | |||
|
r161 | def lock(self): | ||
|
r1787 | timeout = self.timeout | ||
|
r14494 | while True: | ||
|
r161 | try: | ||
|
r26082 | self._trylock() | ||
|
r20380 | return self.timeout - timeout | ||
|
r25660 | except error.LockHeld as inst: | ||
|
r1787 | if timeout != 0: | ||
|
r161 | time.sleep(1) | ||
|
r1787 | if timeout > 0: | ||
timeout -= 1 | ||||
|
r161 | continue | ||
|
r43346 | raise error.LockHeld( | ||
errno.ETIMEDOUT, inst.filename, self.desc, inst.locker | ||||
) | ||||
|
r515 | |||
|
r26082 | def _trylock(self): | ||
|
r8108 | if self.held: | ||
self.held += 1 | ||||
return | ||||
|
r4947 | if lock._host is None: | ||
|
r30920 | lock._host = _getlockprefix() | ||
|
r43347 | lockname = b'%s:%d' % (lock._host, self.pid) | ||
|
r26081 | retry = 5 | ||
while not self.held and retry: | ||||
retry -= 1 | ||||
|
r1877 | try: | ||
|
r38157 | with self._maybedelayedinterrupt(): | ||
|
r36717 | self.vfs.makelock(lockname, self.f) | ||
self.held = 1 | ||||
|
r25660 | except (OSError, IOError) as why: | ||
|
r1877 | if why.errno == errno.EEXIST: | ||
|
r26387 | locker = self._readlock() | ||
|
r32088 | if locker is None: | ||
continue | ||||
|
r26387 | # special case where a parent process holds the lock -- this | ||
# is different from the pid being different because we do | ||||
# want the unlock and postrelease functions to be called, | ||||
# but the lockfile to not be removed. | ||||
if locker == self.parentlock: | ||||
self._parentheld = True | ||||
self.held = 1 | ||||
return | ||||
locker = self._testlock(locker) | ||||
|
r3686 | if locker is not None: | ||
|
r43346 | raise error.LockHeld( | ||
errno.EAGAIN, | ||||
self.vfs.join(self.f), | ||||
self.desc, | ||||
locker, | ||||
) | ||||
|
r1877 | else: | ||
|
r43346 | raise error.LockUnavailable( | ||
why.errno, why.strerror, why.filename, self.desc | ||||
) | ||||
|
r1877 | |||
|
r32089 | if not self.held: | ||
# use empty locker to mean "busy for frequent lock/unlock | ||||
# by many processes" | ||||
|
r43346 | raise error.LockHeld( | ||
|
r43347 | errno.EAGAIN, self.vfs.join(self.f), self.desc, b"" | ||
|
r43346 | ) | ||
|
r32089 | |||
|
r26290 | def _readlock(self): | ||
"""read lock and return its value | ||||
Returns None if no lock exists, pid for old-style locks, and host:pid | ||||
for new-style locks. | ||||
""" | ||||
try: | ||||
return self.vfs.readlock(self.f) | ||||
except (OSError, IOError) as why: | ||||
if why.errno == errno.ENOENT: | ||||
return None | ||||
raise | ||||
|
r44107 | def _lockshouldbebroken(self, locker): | ||
|
r26290 | if locker is None: | ||
|
r44107 | return False | ||
|
r2579 | try: | ||
|
r43347 | host, pid = locker.split(b":", 1) | ||
|
r2579 | except ValueError: | ||
|
r44107 | return False | ||
|
r4947 | if host != lock._host: | ||
|
r44107 | return False | ||
|
r161 | try: | ||
|
r2579 | pid = int(pid) | ||
|
r9685 | except ValueError: | ||
|
r44107 | return False | ||
|
r37138 | if procutil.testpid(pid): | ||
|
r44107 | return False | ||
return True | ||||
def _testlock(self, locker): | ||||
if not self._lockshouldbebroken(locker): | ||||
|
r1877 | return locker | ||
|
r44107 | |||
|
r1877 | # if locker dead, break lock. must do this with another lock | ||
# held, or can race and break valid lock. | ||||
try: | ||||
|
r44107 | with lock(self.vfs, self.f + b'.break', timeout=0): | ||
self.vfs.unlink(self.f) | ||||
|
r7640 | except error.LockError: | ||
|
r1877 | return locker | ||
|
r161 | |||
|
r26291 | 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. | ||||
""" | ||||
locker = self._readlock() | ||||
return self._testlock(locker) | ||||
|
r26473 | @contextlib.contextmanager | ||
def inherit(self): | ||||
"""context for the lock to be inherited by a Mercurial subprocess. | ||||
|
r26357 | |||
|
r26473 | Yields a string that will be recognized by the lock in the subprocess. | ||
Communicating this string to the subprocess needs to be done separately | ||||
-- typically by an environment variable. | ||||
|
r26357 | """ | ||
if not self.held: | ||||
raise error.LockInheritanceContractViolation( | ||||
|
r43347 | b'inherit can only be called while lock is held' | ||
|
r43346 | ) | ||
|
r26357 | if self._inherited: | ||
raise error.LockInheritanceContractViolation( | ||||
|
r43347 | b'inherit cannot be called while lock is already inherited' | ||
|
r43346 | ) | ||
|
r26498 | if self._inheritchecker is not None: | ||
self._inheritchecker() | ||||
|
r26357 | if self.releasefn: | ||
self.releasefn() | ||||
if self._parentheld: | ||||
lockname = self.parentlock | ||||
else: | ||||
|
r37676 | lockname = b'%s:%d' % (lock._host, self.pid) | ||
|
r26357 | self._inherited = True | ||
|
r26473 | try: | ||
yield lockname | ||||
finally: | ||||
if self.acquirefn: | ||||
self.acquirefn() | ||||
self._inherited = False | ||||
|
r26358 | |||
|
r161 | def release(self): | ||
|
r15583 | """release the lock and execute callback function if any | ||
|
r17537 | If the lock has been acquired multiple times, the actual release is | ||
|
r17510 | delayed to the last release call.""" | ||
|
r8108 | if self.held > 1: | ||
self.held -= 1 | ||||
|
r9680 | elif self.held == 1: | ||
|
r161 | self.held = 0 | ||
|
r26383 | if self._getpid() != self.pid: | ||
|
r18907 | # we forked, and are not the parent | ||
return | ||||
|
r503 | try: | ||
|
r23032 | if self.releasefn: | ||
self.releasefn() | ||||
finally: | ||||
|
r26359 | if not self._parentheld: | ||
try: | ||||
self.vfs.unlink(self.f) | ||||
except OSError: | ||||
pass | ||||
|
r26474 | # The postrelease functions typically assume the lock is not held | ||
# at all. | ||||
if not self._parentheld: | ||||
for callback in self.postrelease: | ||||
callback() | ||||
|
r28959 | # Prevent double usage and help clear cycles. | ||
self.postrelease = None | ||||
|
r161 | |||
|
r43346 | |||
|
r8108 | def release(*locks): | ||
for lock in locks: | ||||
if lock is not None: | ||||
lock.release() | ||||