summaryrefslogtreecommitdiff
path: root/bzrlib/lockdir.py
diff options
context:
space:
mode:
Diffstat (limited to 'bzrlib/lockdir.py')
-rw-r--r--bzrlib/lockdir.py863
1 files changed, 863 insertions, 0 deletions
diff --git a/bzrlib/lockdir.py b/bzrlib/lockdir.py
new file mode 100644
index 0000000..5821e4e
--- /dev/null
+++ b/bzrlib/lockdir.py
@@ -0,0 +1,863 @@
+# Copyright (C) 2006-2011 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""On-disk mutex protecting a resource
+
+bzr on-disk objects are locked by the existence of a directory with a
+particular name within the control directory. We use this rather than OS
+internal locks (such as flock etc) because they can be seen across all
+transports, including http.
+
+Objects can be read if there is only physical read access; therefore
+readers can never be required to create a lock, though they will
+check whether a writer is using the lock. Writers can't detect
+whether anyone else is reading from the resource as they write.
+This works because of ordering constraints that make sure readers
+see a consistent view of existing data.
+
+Waiting for a lock must be done by polling; this can be aborted after
+a timeout.
+
+Locks must always be explicitly released, typically from a try/finally
+block -- they are not released from a finalizer or when Python
+exits.
+
+Locks may fail to be released if the process is abruptly terminated
+(machine stop, SIGKILL) or if a remote transport becomes permanently
+disconnected. There is therefore a method to break an existing lock.
+This should rarely be used, and generally only with user approval.
+Locks contain some information on when the lock was taken and by who
+which may guide in deciding whether it can safely be broken. (This is
+similar to the messages displayed by emacs and vim.) Note that if the
+lock holder is still alive they will get no notification that the lock
+has been broken and will continue their work -- so it is important to be
+sure they are actually dead.
+
+A lock is represented on disk by a directory of a particular name,
+containing an information file. Taking a lock is done by renaming a
+temporary directory into place. We use temporary directories because
+for all known transports and filesystems we believe that exactly one
+attempt to claim the lock will succeed and the others will fail. (Files
+won't do because some filesystems or transports only have
+rename-and-overwrite, making it hard to tell who won.)
+
+The desired characteristics are:
+
+* Locks are not reentrant. (That is, a client that tries to take a
+ lock it already holds may deadlock or fail.)
+* Stale locks can be guessed at by a heuristic
+* Lost locks can be broken by any client
+* Failed lock operations leave little or no mess
+* Deadlocks are avoided by having a timeout always in use, clients
+ desiring indefinite waits can retry or set a silly big timeout.
+
+Storage formats use the locks, and also need to consider concurrency
+issues underneath the lock. A format may choose not to use a lock
+at all for some operations.
+
+LockDirs always operate over a Transport. The transport may be readonly, in
+which case the lock can be queried but not acquired.
+
+Locks are identified by a path name, relative to a base transport.
+
+Calling code will typically want to make sure there is exactly one LockDir
+object per actual lock on disk. This module does nothing to prevent aliasing
+and deadlocks will likely occur if the locks are aliased.
+
+In the future we may add a "freshen" method which can be called
+by a lock holder to check that their lock has not been broken, and to
+update the timestamp within it.
+
+Example usage:
+
+>>> from bzrlib.transport.memory import MemoryTransport
+>>> # typically will be obtained from a BzrDir, Branch, etc
+>>> t = MemoryTransport()
+>>> l = LockDir(t, 'sample-lock')
+>>> l.create()
+>>> token = l.wait_lock()
+>>> # do something here
+>>> l.unlock()
+
+Some classes of stale locks can be predicted by checking: the host name is the
+same as the local host name; the user name is the same as the local user; the
+process id no longer exists. The check on user name is not strictly necessary
+but helps protect against colliding host names.
+"""
+
+from __future__ import absolute_import
+
+
+# TODO: We sometimes have the problem that our attempt to rename '1234' to
+# 'held' fails because the transport server moves into an existing directory,
+# rather than failing the rename. If we made the info file name the same as
+# the locked directory name we would avoid this problem because moving into
+# the held directory would implicitly clash. However this would not mesh with
+# the existing locking code and needs a new format of the containing object.
+# -- robertc, mbp 20070628
+
+import os
+import time
+
+from bzrlib import (
+ config,
+ debug,
+ errors,
+ lock,
+ osutils,
+ ui,
+ urlutils,
+ )
+from bzrlib.decorators import only_raises
+from bzrlib.errors import (
+ DirectoryNotEmpty,
+ FileExists,
+ LockBreakMismatch,
+ LockBroken,
+ LockContention,
+ LockCorrupt,
+ LockFailed,
+ LockNotHeld,
+ NoSuchFile,
+ PathError,
+ ResourceBusy,
+ TransportError,
+ )
+from bzrlib.trace import mutter, note
+from bzrlib.osutils import format_delta, rand_chars, get_host_name
+from bzrlib.i18n import gettext
+
+from bzrlib.lazy_import import lazy_import
+lazy_import(globals(), """
+from bzrlib import rio
+""")
+
+# XXX: At the moment there is no consideration of thread safety on LockDir
+# objects. This should perhaps be updated - e.g. if two threads try to take a
+# lock at the same time they should *both* get it. But then that's unlikely
+# to be a good idea.
+
+# TODO: Perhaps store some kind of note like the bzr command line in the lock
+# info?
+
+# TODO: Some kind of callback run while polling a lock to show progress
+# indicators.
+
+# TODO: Make sure to pass the right file and directory mode bits to all
+# files/dirs created.
+
+
+_DEFAULT_TIMEOUT_SECONDS = 30
+_DEFAULT_POLL_SECONDS = 1.0
+
+
+class LockDir(lock.Lock):
+ """Write-lock guarding access to data.
+ """
+
+ __INFO_NAME = '/info'
+
+ def __init__(self, transport, path, file_modebits=0644, dir_modebits=0755,
+ extra_holder_info=None):
+ """Create a new LockDir object.
+
+ The LockDir is initially unlocked - this just creates the object.
+
+ :param transport: Transport which will contain the lock
+
+ :param path: Path to the lock within the base directory of the
+ transport.
+
+ :param extra_holder_info: If passed, {str:str} dict of extra or
+ updated information to insert into the info file when the lock is
+ taken.
+ """
+ self.transport = transport
+ self.path = path
+ self._lock_held = False
+ self._locked_via_token = False
+ self._fake_read_lock = False
+ self._held_dir = path + '/held'
+ self._held_info_path = self._held_dir + self.__INFO_NAME
+ self._file_modebits = file_modebits
+ self._dir_modebits = dir_modebits
+ self._report_function = note
+ self.extra_holder_info = extra_holder_info
+ self._warned_about_lock_holder = None
+
+ def __repr__(self):
+ return '%s(%s%s)' % (self.__class__.__name__,
+ self.transport.base,
+ self.path)
+
+ is_held = property(lambda self: self._lock_held)
+
+ def create(self, mode=None):
+ """Create the on-disk lock.
+
+ This is typically only called when the object/directory containing the
+ directory is first created. The lock is not held when it's created.
+ """
+ self._trace("create lock directory")
+ try:
+ self.transport.mkdir(self.path, mode=mode)
+ except (TransportError, PathError), e:
+ raise LockFailed(self, e)
+
+ def _attempt_lock(self):
+ """Make the pending directory and attempt to rename into place.
+
+ If the rename succeeds, we read back the info file to check that we
+ really got the lock.
+
+ If we fail to acquire the lock, this method is responsible for
+ cleaning up the pending directory if possible. (But it doesn't do
+ that yet.)
+
+ :returns: The nonce of the lock, if it was successfully acquired.
+
+ :raises LockContention: If the lock is held by someone else. The
+ exception contains the info of the current holder of the lock.
+ """
+ self._trace("lock_write...")
+ start_time = time.time()
+ try:
+ tmpname = self._create_pending_dir()
+ except (errors.TransportError, PathError), e:
+ self._trace("... failed to create pending dir, %s", e)
+ raise LockFailed(self, e)
+ while True:
+ try:
+ self.transport.rename(tmpname, self._held_dir)
+ break
+ except (errors.TransportError, PathError, DirectoryNotEmpty,
+ FileExists, ResourceBusy), e:
+ self._trace("... contention, %s", e)
+ other_holder = self.peek()
+ self._trace("other holder is %r" % other_holder)
+ try:
+ self._handle_lock_contention(other_holder)
+ except:
+ self._remove_pending_dir(tmpname)
+ raise
+ except Exception, e:
+ self._trace("... lock failed, %s", e)
+ self._remove_pending_dir(tmpname)
+ raise
+ # We must check we really got the lock, because Launchpad's sftp
+ # server at one time had a bug were the rename would successfully
+ # move the new directory into the existing directory, which was
+ # incorrect. It's possible some other servers or filesystems will
+ # have a similar bug allowing someone to think they got the lock
+ # when it's already held.
+ #
+ # See <https://bugs.launchpad.net/bzr/+bug/498378> for one case.
+ #
+ # Strictly the check is unnecessary and a waste of time for most
+ # people, but probably worth trapping if something is wrong.
+ info = self.peek()
+ self._trace("after locking, info=%r", info)
+ if info is None:
+ raise LockFailed(self, "lock was renamed into place, but "
+ "now is missing!")
+ if info.get('nonce') != self.nonce:
+ self._trace("rename succeeded, "
+ "but lock is still held by someone else")
+ raise LockContention(self)
+ self._lock_held = True
+ self._trace("... lock succeeded after %dms",
+ (time.time() - start_time) * 1000)
+ return self.nonce
+
+ def _handle_lock_contention(self, other_holder):
+ """A lock we want to take is held by someone else.
+
+ This function can: tell the user about it; possibly detect that it's
+ safe or appropriate to steal the lock, or just raise an exception.
+
+ If this function returns (without raising an exception) the lock will
+ be attempted again.
+
+ :param other_holder: A LockHeldInfo for the current holder; note that
+ it might be None if the lock can be seen to be held but the info
+ can't be read.
+ """
+ if (other_holder is not None):
+ if other_holder.is_lock_holder_known_dead():
+ if self.get_config().get('locks.steal_dead'):
+ ui.ui_factory.show_user_warning(
+ 'locks_steal_dead',
+ lock_url=urlutils.join(self.transport.base, self.path),
+ other_holder_info=unicode(other_holder))
+ self.force_break(other_holder)
+ self._trace("stole lock from dead holder")
+ return
+ raise LockContention(self)
+
+ def _remove_pending_dir(self, tmpname):
+ """Remove the pending directory
+
+ This is called if we failed to rename into place, so that the pending
+ dirs don't clutter up the lockdir.
+ """
+ self._trace("remove %s", tmpname)
+ try:
+ self.transport.delete(tmpname + self.__INFO_NAME)
+ self.transport.rmdir(tmpname)
+ except PathError, e:
+ note(gettext("error removing pending lock: %s"), e)
+
+ def _create_pending_dir(self):
+ tmpname = '%s/%s.tmp' % (self.path, rand_chars(10))
+ try:
+ self.transport.mkdir(tmpname)
+ except NoSuchFile:
+ # This may raise a FileExists exception
+ # which is okay, it will be caught later and determined
+ # to be a LockContention.
+ self._trace("lock directory does not exist, creating it")
+ self.create(mode=self._dir_modebits)
+ # After creating the lock directory, try again
+ self.transport.mkdir(tmpname)
+ info = LockHeldInfo.for_this_process(self.extra_holder_info)
+ self.nonce = info.get('nonce')
+ # We use put_file_non_atomic because we just created a new unique
+ # directory so we don't have to worry about files existing there.
+ # We'll rename the whole directory into place to get atomic
+ # properties
+ self.transport.put_bytes_non_atomic(tmpname + self.__INFO_NAME,
+ info.to_bytes())
+ return tmpname
+
+ @only_raises(LockNotHeld, LockBroken)
+ def unlock(self):
+ """Release a held lock
+ """
+ if self._fake_read_lock:
+ self._fake_read_lock = False
+ return
+ if not self._lock_held:
+ return lock.cant_unlock_not_held(self)
+ if self._locked_via_token:
+ self._locked_via_token = False
+ self._lock_held = False
+ else:
+ old_nonce = self.nonce
+ # rename before deleting, because we can't atomically remove the
+ # whole tree
+ start_time = time.time()
+ self._trace("unlocking")
+ tmpname = '%s/releasing.%s.tmp' % (self.path, rand_chars(20))
+ # gotta own it to unlock
+ self.confirm()
+ self.transport.rename(self._held_dir, tmpname)
+ self._lock_held = False
+ self.transport.delete(tmpname + self.__INFO_NAME)
+ try:
+ self.transport.rmdir(tmpname)
+ except DirectoryNotEmpty, e:
+ # There might have been junk left over by a rename that moved
+ # another locker within the 'held' directory. do a slower
+ # deletion where we list the directory and remove everything
+ # within it.
+ #
+ # Maybe this should be broader to allow for ftp servers with
+ # non-specific error messages?
+ self._trace("doing recursive deletion of non-empty directory "
+ "%s", tmpname)
+ self.transport.delete_tree(tmpname)
+ self._trace("... unlock succeeded after %dms",
+ (time.time() - start_time) * 1000)
+ result = lock.LockResult(self.transport.abspath(self.path),
+ old_nonce)
+ for hook in self.hooks['lock_released']:
+ hook(result)
+
+ def break_lock(self):
+ """Break a lock not held by this instance of LockDir.
+
+ This is a UI centric function: it uses the ui.ui_factory to
+ prompt for input if a lock is detected and there is any doubt about
+ it possibly being still active. force_break is the non-interactive
+ version.
+
+ :returns: LockResult for the broken lock.
+ """
+ self._check_not_locked()
+ try:
+ holder_info = self.peek()
+ except LockCorrupt, e:
+ # The lock info is corrupt.
+ if ui.ui_factory.get_boolean(u"Break (corrupt %r)" % (self,)):
+ self.force_break_corrupt(e.file_data)
+ return
+ if holder_info is not None:
+ if ui.ui_factory.confirm_action(
+ u"Break %(lock_info)s",
+ 'bzrlib.lockdir.break',
+ dict(lock_info=unicode(holder_info))):
+ result = self.force_break(holder_info)
+ ui.ui_factory.show_message(
+ "Broke lock %s" % result.lock_url)
+
+ def force_break(self, dead_holder_info):
+ """Release a lock held by another process.
+
+ WARNING: This should only be used when the other process is dead; if
+ it still thinks it has the lock there will be two concurrent writers.
+ In general the user's approval should be sought for lock breaks.
+
+ After the lock is broken it will not be held by any process.
+ It is possible that another process may sneak in and take the
+ lock before the breaking process acquires it.
+
+ :param dead_holder_info:
+ Must be the result of a previous LockDir.peek() call; this is used
+ to check that it's still held by the same process that the user
+ decided was dead. If this is not the current holder,
+ LockBreakMismatch is raised.
+
+ :returns: LockResult for the broken lock.
+ """
+ if not isinstance(dead_holder_info, LockHeldInfo):
+ raise ValueError("dead_holder_info: %r" % dead_holder_info)
+ self._check_not_locked()
+ current_info = self.peek()
+ if current_info is None:
+ # must have been recently released
+ return
+ if current_info != dead_holder_info:
+ raise LockBreakMismatch(self, current_info, dead_holder_info)
+ tmpname = '%s/broken.%s.tmp' % (self.path, rand_chars(20))
+ self.transport.rename(self._held_dir, tmpname)
+ # check that we actually broke the right lock, not someone else;
+ # there's a small race window between checking it and doing the
+ # rename.
+ broken_info_path = tmpname + self.__INFO_NAME
+ broken_info = self._read_info_file(broken_info_path)
+ if broken_info != dead_holder_info:
+ raise LockBreakMismatch(self, broken_info, dead_holder_info)
+ self.transport.delete(broken_info_path)
+ self.transport.rmdir(tmpname)
+ result = lock.LockResult(self.transport.abspath(self.path),
+ current_info.get('nonce'))
+ for hook in self.hooks['lock_broken']:
+ hook(result)
+ return result
+
+ def force_break_corrupt(self, corrupt_info_lines):
+ """Release a lock that has been corrupted.
+
+ This is very similar to force_break, it except it doesn't assume that
+ self.peek() can work.
+
+ :param corrupt_info_lines: the lines of the corrupted info file, used
+ to check that the lock hasn't changed between reading the (corrupt)
+ info file and calling force_break_corrupt.
+ """
+ # XXX: this copes with unparseable info files, but what about missing
+ # info files? Or missing lock dirs?
+ self._check_not_locked()
+ tmpname = '%s/broken.%s.tmp' % (self.path, rand_chars(20))
+ self.transport.rename(self._held_dir, tmpname)
+ # check that we actually broke the right lock, not someone else;
+ # there's a small race window between checking it and doing the
+ # rename.
+ broken_info_path = tmpname + self.__INFO_NAME
+ broken_content = self.transport.get_bytes(broken_info_path)
+ broken_lines = osutils.split_lines(broken_content)
+ if broken_lines != corrupt_info_lines:
+ raise LockBreakMismatch(self, broken_lines, corrupt_info_lines)
+ self.transport.delete(broken_info_path)
+ self.transport.rmdir(tmpname)
+ result = lock.LockResult(self.transport.abspath(self.path))
+ for hook in self.hooks['lock_broken']:
+ hook(result)
+
+ def _check_not_locked(self):
+ """If the lock is held by this instance, raise an error."""
+ if self._lock_held:
+ raise AssertionError("can't break own lock: %r" % self)
+
+ def confirm(self):
+ """Make sure that the lock is still held by this locker.
+
+ This should only fail if the lock was broken by user intervention,
+ or if the lock has been affected by a bug.
+
+ If the lock is not thought to be held, raises LockNotHeld. If
+ the lock is thought to be held but has been broken, raises
+ LockBroken.
+ """
+ if not self._lock_held:
+ raise LockNotHeld(self)
+ info = self.peek()
+ if info is None:
+ # no lock there anymore!
+ raise LockBroken(self)
+ if info.get('nonce') != self.nonce:
+ # there is a lock, but not ours
+ raise LockBroken(self)
+
+ def _read_info_file(self, path):
+ """Read one given info file.
+
+ peek() reads the info file of the lock holder, if any.
+ """
+ return LockHeldInfo.from_info_file_bytes(
+ self.transport.get_bytes(path))
+
+ def peek(self):
+ """Check if the lock is held by anyone.
+
+ If it is held, this returns the lock info structure as a dict
+ which contains some information about the current lock holder.
+ Otherwise returns None.
+ """
+ try:
+ info = self._read_info_file(self._held_info_path)
+ self._trace("peek -> held")
+ return info
+ except NoSuchFile, e:
+ self._trace("peek -> not held")
+
+ def _prepare_info(self):
+ """Write information about a pending lock to a temporary file.
+ """
+
+ def attempt_lock(self):
+ """Take the lock; fail if it's already held.
+
+ If you wish to block until the lock can be obtained, call wait_lock()
+ instead.
+
+ :return: The lock token.
+ :raises LockContention: if the lock is held by someone else.
+ """
+ if self._fake_read_lock:
+ raise LockContention(self)
+ result = self._attempt_lock()
+ hook_result = lock.LockResult(self.transport.abspath(self.path),
+ self.nonce)
+ for hook in self.hooks['lock_acquired']:
+ hook(hook_result)
+ return result
+
+ def lock_url_for_display(self):
+ """Give a nicely-printable representation of the URL of this lock."""
+ # As local lock urls are correct we display them.
+ # We avoid displaying remote lock urls.
+ lock_url = self.transport.abspath(self.path)
+ if lock_url.startswith('file://'):
+ lock_url = lock_url.split('.bzr/')[0]
+ else:
+ lock_url = ''
+ return lock_url
+
+ def wait_lock(self, timeout=None, poll=None, max_attempts=None):
+ """Wait a certain period for a lock.
+
+ If the lock can be acquired within the bounded time, it
+ is taken and this returns. Otherwise, LockContention
+ is raised. Either way, this function should return within
+ approximately `timeout` seconds. (It may be a bit more if
+ a transport operation takes a long time to complete.)
+
+ :param timeout: Approximate maximum amount of time to wait for the
+ lock, in seconds.
+
+ :param poll: Delay in seconds between retrying the lock.
+
+ :param max_attempts: Maximum number of times to try to lock.
+
+ :return: The lock token.
+ """
+ if timeout is None:
+ timeout = _DEFAULT_TIMEOUT_SECONDS
+ if poll is None:
+ poll = _DEFAULT_POLL_SECONDS
+ # XXX: the transport interface doesn't let us guard against operations
+ # there taking a long time, so the total elapsed time or poll interval
+ # may be more than was requested.
+ deadline = time.time() + timeout
+ deadline_str = None
+ last_info = None
+ attempt_count = 0
+ lock_url = self.lock_url_for_display()
+ while True:
+ attempt_count += 1
+ try:
+ return self.attempt_lock()
+ except LockContention:
+ # possibly report the blockage, then try again
+ pass
+ # TODO: In a few cases, we find out that there's contention by
+ # reading the held info and observing that it's not ours. In
+ # those cases it's a bit redundant to read it again. However,
+ # the normal case (??) is that the rename fails and so we
+ # don't know who holds the lock. For simplicity we peek
+ # always.
+ new_info = self.peek()
+ if new_info is not None and new_info != last_info:
+ if last_info is None:
+ start = gettext('Unable to obtain')
+ else:
+ start = gettext('Lock owner changed for')
+ last_info = new_info
+ msg = gettext('{0} lock {1} {2}.').format(start, lock_url,
+ new_info)
+ if deadline_str is None:
+ deadline_str = time.strftime('%H:%M:%S',
+ time.localtime(deadline))
+ if timeout > 0:
+ msg += '\n' + gettext(
+ 'Will continue to try until %s, unless '
+ 'you press Ctrl-C.') % deadline_str
+ msg += '\n' + gettext('See "bzr help break-lock" for more.')
+ self._report_function(msg)
+ if (max_attempts is not None) and (attempt_count >= max_attempts):
+ self._trace("exceeded %d attempts")
+ raise LockContention(self)
+ if time.time() + poll < deadline:
+ self._trace("waiting %ss", poll)
+ time.sleep(poll)
+ else:
+ # As timeout is always 0 for remote locks
+ # this block is applicable only for local
+ # lock contention
+ self._trace("timeout after waiting %ss", timeout)
+ raise LockContention('(local)', lock_url)
+
+ def leave_in_place(self):
+ self._locked_via_token = True
+
+ def dont_leave_in_place(self):
+ self._locked_via_token = False
+
+ def lock_write(self, token=None):
+ """Wait for and acquire the lock.
+
+ :param token: if this is already locked, then lock_write will fail
+ unless the token matches the existing lock.
+ :returns: a token if this instance supports tokens, otherwise None.
+ :raises TokenLockingNotSupported: when a token is given but this
+ instance doesn't support using token locks.
+ :raises MismatchedToken: if the specified token doesn't match the token
+ of the existing lock.
+
+ A token should be passed in if you know that you have locked the object
+ some other way, and need to synchronise this object's state with that
+ fact.
+
+ XXX: docstring duplicated from LockableFiles.lock_write.
+ """
+ if token is not None:
+ self.validate_token(token)
+ self.nonce = token
+ self._lock_held = True
+ self._locked_via_token = True
+ return token
+ else:
+ return self.wait_lock()
+
+ def lock_read(self):
+ """Compatibility-mode shared lock.
+
+ LockDir doesn't support shared read-only locks, so this
+ just pretends that the lock is taken but really does nothing.
+ """
+ # At the moment Branches are commonly locked for read, but
+ # we can't rely on that remotely. Once this is cleaned up,
+ # reenable this warning to prevent it coming back in
+ # -- mbp 20060303
+ ## warn("LockDir.lock_read falls back to write lock")
+ if self._lock_held or self._fake_read_lock:
+ raise LockContention(self)
+ self._fake_read_lock = True
+
+ def validate_token(self, token):
+ if token is not None:
+ info = self.peek()
+ if info is None:
+ # Lock isn't held
+ lock_token = None
+ else:
+ lock_token = info.get('nonce')
+ if token != lock_token:
+ raise errors.TokenMismatch(token, lock_token)
+ else:
+ self._trace("revalidated by token %r", token)
+
+ def _trace(self, format, *args):
+ if 'lock' not in debug.debug_flags:
+ return
+ mutter(str(self) + ": " + (format % args))
+
+ def get_config(self):
+ """Get the configuration that governs this lockdir."""
+ # XXX: This really should also use the locationconfig at least, but
+ # that seems a bit hard to hook up at the moment. -- mbp 20110329
+ # FIXME: The above is still true ;) -- vila 20110811
+ return config.GlobalStack()
+
+
+class LockHeldInfo(object):
+ """The information recorded about a held lock.
+
+ This information is recorded into the lock when it's taken, and it can be
+ read back by any process with access to the lockdir. It can be used, for
+ example, to tell the user who holds the lock, or to try to detect whether
+ the lock holder is still alive.
+
+ Prior to bzr 2.4 a simple dict was used instead of an object.
+ """
+
+ def __init__(self, info_dict):
+ self.info_dict = info_dict
+
+ def __repr__(self):
+ """Return a debugging representation of this object."""
+ return "%s(%r)" % (self.__class__.__name__, self.info_dict)
+
+ def __unicode__(self):
+ """Return a user-oriented description of this object."""
+ d = self.to_readable_dict()
+ return ( gettext(
+ u'held by %(user)s on %(hostname)s (process #%(pid)s), '
+ u'acquired %(time_ago)s') % d)
+
+ def to_readable_dict(self):
+ """Turn the holder info into a dict of human-readable attributes.
+
+ For example, the start time is presented relative to the current time,
+ rather than as seconds since the epoch.
+
+ Returns a list of [user, hostname, pid, time_ago] all as readable
+ strings.
+ """
+ start_time = self.info_dict.get('start_time')
+ if start_time is None:
+ time_ago = '(unknown)'
+ else:
+ time_ago = format_delta(
+ time.time() - int(self.info_dict['start_time']))
+ user = self.info_dict.get('user', '<unknown>')
+ hostname = self.info_dict.get('hostname', '<unknown>')
+ pid = self.info_dict.get('pid', '<unknown>')
+ return dict(
+ user=user,
+ hostname=hostname,
+ pid=pid,
+ time_ago=time_ago)
+
+ def get(self, field_name):
+ """Return the contents of a field from the lock info, or None."""
+ return self.info_dict.get(field_name)
+
+ @classmethod
+ def for_this_process(cls, extra_holder_info):
+ """Return a new LockHeldInfo for a lock taken by this process.
+ """
+ info = dict(
+ hostname=get_host_name(),
+ pid=str(os.getpid()),
+ nonce=rand_chars(20),
+ start_time=str(int(time.time())),
+ user=get_username_for_lock_info(),
+ )
+ if extra_holder_info is not None:
+ info.update(extra_holder_info)
+ return cls(info)
+
+ def to_bytes(self):
+ s = rio.Stanza(**self.info_dict)
+ return s.to_string()
+
+ @classmethod
+ def from_info_file_bytes(cls, info_file_bytes):
+ """Construct from the contents of the held file."""
+ lines = osutils.split_lines(info_file_bytes)
+ try:
+ stanza = rio.read_stanza(lines)
+ except ValueError, e:
+ mutter('Corrupt lock info file: %r', lines)
+ raise LockCorrupt("could not parse lock info file: " + str(e),
+ lines)
+ if stanza is None:
+ # see bug 185013; we fairly often end up with the info file being
+ # empty after an interruption; we could log a message here but
+ # there may not be much we can say
+ return cls({})
+ else:
+ return cls(stanza.as_dict())
+
+ def __cmp__(self, other):
+ """Value comparison of lock holders."""
+ return (
+ cmp(type(self), type(other))
+ or cmp(self.info_dict, other.info_dict))
+
+ def is_locked_by_this_process(self):
+ """True if this process seems to be the current lock holder."""
+ return (
+ self.get('hostname') == get_host_name()
+ and self.get('pid') == str(os.getpid())
+ and self.get('user') == get_username_for_lock_info())
+
+ def is_lock_holder_known_dead(self):
+ """True if the lock holder process is known to be dead.
+
+ False if it's either known to be still alive, or if we just can't tell.
+
+ We can be fairly sure the lock holder is dead if it declared the same
+ hostname and there is no process with the given pid alive. If people
+ have multiple machines with the same hostname this may cause trouble.
+
+ This doesn't check whether the lock holder is in fact the same process
+ calling this method. (In that case it will return true.)
+ """
+ if self.get('hostname') != get_host_name():
+ return False
+ if self.get('hostname') == 'localhost':
+ # Too ambiguous.
+ return False
+ if self.get('user') != get_username_for_lock_info():
+ # Could well be another local process by a different user, but
+ # just to be safe we won't conclude about this either.
+ return False
+ pid_str = self.info_dict.get('pid', None)
+ if not pid_str:
+ mutter("no pid recorded in %r" % (self, ))
+ return False
+ try:
+ pid = int(pid_str)
+ except ValueError:
+ mutter("can't parse pid %r from %r"
+ % (pid_str, self))
+ return False
+ return osutils.is_local_pid_dead(pid)
+
+
+def get_username_for_lock_info():
+ """Get a username suitable for putting into a lock.
+
+ It's ok if what's written here is not a proper email address as long
+ as it gives some clue who the user is.
+ """
+ try:
+ return config.GlobalConfig().username()
+ except errors.NoWhoami:
+ return osutils.getuser_unicode()