diff options
author | Lorry <lorry@roadtrain.codethink.co.uk> | 2012-08-22 15:47:16 +0100 |
---|---|---|
committer | Lorry <lorry@roadtrain.codethink.co.uk> | 2012-08-22 15:47:16 +0100 |
commit | 25335618bf8755ce6b116ee14f47f5a1f2c821e9 (patch) | |
tree | d889d7ab3f9f985d0c54c534cb8052bd2e6d7163 /bzrlib/lock.py | |
download | bzr-tarball-25335618bf8755ce6b116ee14f47f5a1f2c821e9.tar.gz |
Tarball conversion
Diffstat (limited to 'bzrlib/lock.py')
-rw-r--r-- | bzrlib/lock.py | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/bzrlib/lock.py b/bzrlib/lock.py new file mode 100644 index 0000000..daaac3f --- /dev/null +++ b/bzrlib/lock.py @@ -0,0 +1,550 @@ +# Copyright (C) 2005-2010 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 + +"""Locking using OS file locks or file existence. + +Note: This method of locking is generally deprecated in favour of LockDir, but +is used to lock local WorkingTrees, and by some old formats. It's accessed +through Transport.lock_read(), etc. + +This module causes two methods, lock() and unlock() to be defined in +any way that works on the current platform. + +It is not specified whether these locks are reentrant (i.e. can be +taken repeatedly by a single process) or whether they exclude +different threads in a single process. That reentrancy is provided by +LockableFiles. + +This defines two classes: ReadLock and WriteLock, which can be +implemented in different ways on different platforms. Both have an +unlock() method. +""" + +from __future__ import absolute_import + +import errno +import os +import sys +import warnings + +from bzrlib import ( + debug, + errors, + osutils, + trace, + ) +from bzrlib.hooks import Hooks +from bzrlib.i18n import gettext + +class LockHooks(Hooks): + + def __init__(self): + Hooks.__init__(self, "bzrlib.lock", "Lock.hooks") + self.add_hook('lock_acquired', + "Called with a bzrlib.lock.LockResult when a physical lock is " + "acquired.", (1, 8)) + self.add_hook('lock_released', + "Called with a bzrlib.lock.LockResult when a physical lock is " + "released.", (1, 8)) + self.add_hook('lock_broken', + "Called with a bzrlib.lock.LockResult when a physical lock is " + "broken.", (1, 15)) + + +class Lock(object): + """Base class for locks. + + :cvar hooks: Hook dictionary for operations on locks. + """ + + hooks = LockHooks() + + +class LockResult(object): + """Result of an operation on a lock; passed to a hook""" + + def __init__(self, lock_url, details=None): + """Create a lock result for lock with optional details about the lock.""" + self.lock_url = lock_url + self.details = details + + def __eq__(self, other): + return self.lock_url == other.lock_url and self.details == other.details + + def __repr__(self): + return '%s(%s, %s)' % (self.__class__.__name__, + self.lock_url, self.details) + + +class LogicalLockResult(object): + """The result of a lock_read/lock_write/lock_tree_write call on lockables. + + :ivar unlock: A callable which will unlock the lock. + """ + + def __init__(self, unlock): + self.unlock = unlock + + def __repr__(self): + return "LogicalLockResult(%s)" % (self.unlock) + + + +def cant_unlock_not_held(locked_object): + """An attempt to unlock failed because the object was not locked. + + This provides a policy point from which we can generate either a warning + or an exception. + """ + # This is typically masking some other error and called from a finally + # block, so it's useful to have the option not to generate a new error + # here. You can use -Werror to make it fatal. It should possibly also + # raise LockNotHeld. + if 'unlock' in debug.debug_flags: + warnings.warn("%r is already unlocked" % (locked_object,), + stacklevel=3) + else: + raise errors.LockNotHeld(locked_object) + + +try: + import fcntl + have_fcntl = True +except ImportError: + have_fcntl = False + +have_pywin32 = False +have_ctypes_win32 = False +if sys.platform == 'win32': + import msvcrt + try: + import win32file, pywintypes, winerror + have_pywin32 = True + except ImportError: + pass + + try: + import ctypes + have_ctypes_win32 = True + except ImportError: + pass + + +class _OSLock(object): + + def __init__(self): + self.f = None + self.filename = None + + def _open(self, filename, filemode): + self.filename = osutils.realpath(filename) + try: + self.f = open(self.filename, filemode) + return self.f + except IOError, e: + if e.errno in (errno.EACCES, errno.EPERM): + raise errors.LockFailed(self.filename, str(e)) + if e.errno != errno.ENOENT: + raise + + # maybe this is an old branch (before may 2005) + trace.mutter("trying to create missing lock %r", self.filename) + + self.f = open(self.filename, 'wb+') + return self.f + + def _clear_f(self): + """Clear the self.f attribute cleanly.""" + if self.f: + self.f.close() + self.f = None + + def unlock(self): + raise NotImplementedError() + + +_lock_classes = [] + + +if have_fcntl: + + class _fcntl_FileLock(_OSLock): + + def _unlock(self): + fcntl.lockf(self.f, fcntl.LOCK_UN) + self._clear_f() + + + class _fcntl_WriteLock(_fcntl_FileLock): + + _open_locks = set() + + def __init__(self, filename): + super(_fcntl_WriteLock, self).__init__() + # Check we can grab a lock before we actually open the file. + self.filename = osutils.realpath(filename) + if self.filename in _fcntl_WriteLock._open_locks: + self._clear_f() + raise errors.LockContention(self.filename) + if self.filename in _fcntl_ReadLock._open_locks: + if 'strict_locks' in debug.debug_flags: + self._clear_f() + raise errors.LockContention(self.filename) + else: + trace.mutter('Write lock taken w/ an open read lock on: %s' + % (self.filename,)) + + self._open(self.filename, 'rb+') + # reserve a slot for this lock - even if the lockf call fails, + # at this point unlock() will be called, because self.f is set. + # TODO: make this fully threadsafe, if we decide we care. + _fcntl_WriteLock._open_locks.add(self.filename) + try: + # LOCK_NB will cause IOError to be raised if we can't grab a + # lock right away. + fcntl.lockf(self.f, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError, e: + if e.errno in (errno.EAGAIN, errno.EACCES): + # We couldn't grab the lock + self.unlock() + # we should be more precise about whats a locking + # error and whats a random-other error + raise errors.LockContention(self.filename, e) + + def unlock(self): + _fcntl_WriteLock._open_locks.remove(self.filename) + self._unlock() + + + class _fcntl_ReadLock(_fcntl_FileLock): + + _open_locks = {} + + def __init__(self, filename): + super(_fcntl_ReadLock, self).__init__() + self.filename = osutils.realpath(filename) + if self.filename in _fcntl_WriteLock._open_locks: + if 'strict_locks' in debug.debug_flags: + # We raise before calling _open so we don't need to + # _clear_f + raise errors.LockContention(self.filename) + else: + trace.mutter('Read lock taken w/ an open write lock on: %s' + % (self.filename,)) + _fcntl_ReadLock._open_locks.setdefault(self.filename, 0) + _fcntl_ReadLock._open_locks[self.filename] += 1 + self._open(filename, 'rb') + try: + # LOCK_NB will cause IOError to be raised if we can't grab a + # lock right away. + fcntl.lockf(self.f, fcntl.LOCK_SH | fcntl.LOCK_NB) + except IOError, e: + # we should be more precise about whats a locking + # error and whats a random-other error + raise errors.LockContention(self.filename, e) + + def unlock(self): + count = _fcntl_ReadLock._open_locks[self.filename] + if count == 1: + del _fcntl_ReadLock._open_locks[self.filename] + else: + _fcntl_ReadLock._open_locks[self.filename] = count - 1 + self._unlock() + + def temporary_write_lock(self): + """Try to grab a write lock on the file. + + On platforms that support it, this will upgrade to a write lock + without unlocking the file. + Otherwise, this will release the read lock, and try to acquire a + write lock. + + :return: A token which can be used to switch back to a read lock. + """ + if self.filename in _fcntl_WriteLock._open_locks: + raise AssertionError('file already locked: %r' + % (self.filename,)) + try: + wlock = _fcntl_TemporaryWriteLock(self) + except errors.LockError: + # We didn't unlock, so we can just return 'self' + return False, self + return True, wlock + + + class _fcntl_TemporaryWriteLock(_OSLock): + """A token used when grabbing a temporary_write_lock. + + Call restore_read_lock() when you are done with the write lock. + """ + + def __init__(self, read_lock): + super(_fcntl_TemporaryWriteLock, self).__init__() + self._read_lock = read_lock + self.filename = read_lock.filename + + count = _fcntl_ReadLock._open_locks[self.filename] + if count > 1: + # Something else also has a read-lock, so we cannot grab a + # write lock. + raise errors.LockContention(self.filename) + + if self.filename in _fcntl_WriteLock._open_locks: + raise AssertionError('file already locked: %r' + % (self.filename,)) + + # See if we can open the file for writing. Another process might + # have a read lock. We don't use self._open() because we don't want + # to create the file if it exists. That would have already been + # done by _fcntl_ReadLock + try: + new_f = open(self.filename, 'rb+') + except IOError, e: + if e.errno in (errno.EACCES, errno.EPERM): + raise errors.LockFailed(self.filename, str(e)) + raise + try: + # LOCK_NB will cause IOError to be raised if we can't grab a + # lock right away. + fcntl.lockf(new_f, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError, e: + # TODO: Raise a more specific error based on the type of error + raise errors.LockContention(self.filename, e) + _fcntl_WriteLock._open_locks.add(self.filename) + + self.f = new_f + + def restore_read_lock(self): + """Restore the original ReadLock.""" + # For fcntl, since we never released the read lock, just release the + # write lock, and return the original lock. + fcntl.lockf(self.f, fcntl.LOCK_UN) + self._clear_f() + _fcntl_WriteLock._open_locks.remove(self.filename) + # Avoid reference cycles + read_lock = self._read_lock + self._read_lock = None + return read_lock + + + _lock_classes.append(('fcntl', _fcntl_WriteLock, _fcntl_ReadLock)) + + +if have_pywin32 and sys.platform == 'win32': + if os.path.supports_unicode_filenames: + # for Windows NT/2K/XP/etc + win32file_CreateFile = win32file.CreateFileW + else: + # for Windows 98 + win32file_CreateFile = win32file.CreateFile + + class _w32c_FileLock(_OSLock): + + def _open(self, filename, access, share, cflags, pymode): + self.filename = osutils.realpath(filename) + try: + self._handle = win32file_CreateFile(filename, access, share, + None, win32file.OPEN_ALWAYS, + win32file.FILE_ATTRIBUTE_NORMAL, None) + except pywintypes.error, e: + if e.args[0] == winerror.ERROR_ACCESS_DENIED: + raise errors.LockFailed(filename, e) + if e.args[0] == winerror.ERROR_SHARING_VIOLATION: + raise errors.LockContention(filename, e) + raise + fd = win32file._open_osfhandle(self._handle, cflags) + self.f = os.fdopen(fd, pymode) + return self.f + + def unlock(self): + self._clear_f() + self._handle = None + + + class _w32c_ReadLock(_w32c_FileLock): + def __init__(self, filename): + super(_w32c_ReadLock, self).__init__() + self._open(filename, win32file.GENERIC_READ, + win32file.FILE_SHARE_READ, os.O_RDONLY, "rb") + + def temporary_write_lock(self): + """Try to grab a write lock on the file. + + On platforms that support it, this will upgrade to a write lock + without unlocking the file. + Otherwise, this will release the read lock, and try to acquire a + write lock. + + :return: A token which can be used to switch back to a read lock. + """ + # I can't find a way to upgrade a read lock to a write lock without + # unlocking first. So here, we do just that. + self.unlock() + try: + wlock = _w32c_WriteLock(self.filename) + except errors.LockError: + return False, _w32c_ReadLock(self.filename) + return True, wlock + + + class _w32c_WriteLock(_w32c_FileLock): + def __init__(self, filename): + super(_w32c_WriteLock, self).__init__() + self._open(filename, + win32file.GENERIC_READ | win32file.GENERIC_WRITE, 0, + os.O_RDWR, "rb+") + + def restore_read_lock(self): + """Restore the original ReadLock.""" + # For win32 we had to completely let go of the original lock, so we + # just unlock and create a new read lock. + self.unlock() + return _w32c_ReadLock(self.filename) + + + _lock_classes.append(('pywin32', _w32c_WriteLock, _w32c_ReadLock)) + + +if have_ctypes_win32: + from ctypes.wintypes import DWORD, LPCSTR, LPCWSTR + LPSECURITY_ATTRIBUTES = ctypes.c_void_p # used as NULL no need to declare + HANDLE = ctypes.c_int # rather than unsigned as in ctypes.wintypes + if os.path.supports_unicode_filenames: + _function_name = "CreateFileW" + LPTSTR = LPCWSTR + else: + _function_name = "CreateFileA" + class LPTSTR(LPCSTR): + def __new__(cls, obj): + return LPCSTR.__new__(cls, obj.encode("mbcs")) + + # CreateFile <http://msdn.microsoft.com/en-us/library/aa363858.aspx> + _CreateFile = ctypes.WINFUNCTYPE( + HANDLE, # return value + LPTSTR, # lpFileName + DWORD, # dwDesiredAccess + DWORD, # dwShareMode + LPSECURITY_ATTRIBUTES, # lpSecurityAttributes + DWORD, # dwCreationDisposition + DWORD, # dwFlagsAndAttributes + HANDLE # hTemplateFile + )((_function_name, ctypes.windll.kernel32)) + + INVALID_HANDLE_VALUE = -1 + + GENERIC_READ = 0x80000000 + GENERIC_WRITE = 0x40000000 + FILE_SHARE_READ = 1 + OPEN_ALWAYS = 4 + FILE_ATTRIBUTE_NORMAL = 128 + + ERROR_ACCESS_DENIED = 5 + ERROR_SHARING_VIOLATION = 32 + + class _ctypes_FileLock(_OSLock): + + def _open(self, filename, access, share, cflags, pymode): + self.filename = osutils.realpath(filename) + handle = _CreateFile(filename, access, share, None, OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, 0) + if handle in (INVALID_HANDLE_VALUE, 0): + e = ctypes.WinError() + if e.args[0] == ERROR_ACCESS_DENIED: + raise errors.LockFailed(filename, e) + if e.args[0] == ERROR_SHARING_VIOLATION: + raise errors.LockContention(filename, e) + raise e + fd = msvcrt.open_osfhandle(handle, cflags) + self.f = os.fdopen(fd, pymode) + return self.f + + def unlock(self): + self._clear_f() + + + class _ctypes_ReadLock(_ctypes_FileLock): + def __init__(self, filename): + super(_ctypes_ReadLock, self).__init__() + self._open(filename, GENERIC_READ, FILE_SHARE_READ, os.O_RDONLY, + "rb") + + def temporary_write_lock(self): + """Try to grab a write lock on the file. + + On platforms that support it, this will upgrade to a write lock + without unlocking the file. + Otherwise, this will release the read lock, and try to acquire a + write lock. + + :return: A token which can be used to switch back to a read lock. + """ + # I can't find a way to upgrade a read lock to a write lock without + # unlocking first. So here, we do just that. + self.unlock() + try: + wlock = _ctypes_WriteLock(self.filename) + except errors.LockError: + return False, _ctypes_ReadLock(self.filename) + return True, wlock + + class _ctypes_WriteLock(_ctypes_FileLock): + def __init__(self, filename): + super(_ctypes_WriteLock, self).__init__() + self._open(filename, GENERIC_READ | GENERIC_WRITE, 0, os.O_RDWR, + "rb+") + + def restore_read_lock(self): + """Restore the original ReadLock.""" + # For win32 we had to completely let go of the original lock, so we + # just unlock and create a new read lock. + self.unlock() + return _ctypes_ReadLock(self.filename) + + + _lock_classes.append(('ctypes', _ctypes_WriteLock, _ctypes_ReadLock)) + + +if len(_lock_classes) == 0: + raise NotImplementedError( + "We must have one of fcntl, pywin32, or ctypes available" + " to support OS locking." + ) + + +# We default to using the first available lock class. +_lock_type, WriteLock, ReadLock = _lock_classes[0] + + +class _RelockDebugMixin(object): + """Mixin support for -Drelock flag. + + Add this as a base class then call self._note_lock with 'r' or 'w' when + acquiring a read- or write-lock. If this object was previously locked (and + locked the same way), and -Drelock is set, then this will trace.note a + message about it. + """ + + _prev_lock = None + + def _note_lock(self, lock_type): + if 'relock' in debug.debug_flags and self._prev_lock == lock_type: + if lock_type == 'r': + type_name = 'read' + else: + type_name = 'write' + trace.note(gettext('{0!r} was {1} locked again'), self, type_name) + self._prev_lock = lock_type + |