1# Copyright (C) 2005-2010 Canonical Ltd 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 16 17"""Locking using OS file locks or file existence. 18 19Note: This method of locking is generally deprecated in favour of LockDir, but 20is used to lock local WorkingTrees, and by some old formats. It's accessed 21through Transport.lock_read(), etc. 22 23This module causes two methods, lock() and unlock() to be defined in 24any way that works on the current platform. 25 26It is not specified whether these locks are reentrant (i.e. can be 27taken repeatedly by a single process) or whether they exclude 28different threads in a single process. That reentrancy is provided by 29LockableFiles. 30 31This defines two classes: ReadLock and WriteLock, which can be 32implemented in different ways on different platforms. Both have an 33unlock() method. 34""" 35 36import contextlib 37import errno 38import os 39import sys 40import warnings 41 42from . import ( 43 debug, 44 errors, 45 osutils, 46 trace, 47 ) 48from .hooks import Hooks 49from .i18n import gettext 50 51 52class LockHooks(Hooks): 53 54 def __init__(self): 55 Hooks.__init__(self, "breezy.lock", "Lock.hooks") 56 self.add_hook( 57 'lock_acquired', 58 "Called with a breezy.lock.LockResult when a physical lock is " 59 "acquired.", (1, 8)) 60 self.add_hook( 61 'lock_released', 62 "Called with a breezy.lock.LockResult when a physical lock is " 63 "released.", (1, 8)) 64 self.add_hook( 65 'lock_broken', 66 "Called with a breezy.lock.LockResult when a physical lock is " 67 "broken.", (1, 15)) 68 69 70class Lock(object): 71 """Base class for locks. 72 73 :cvar hooks: Hook dictionary for operations on locks. 74 """ 75 76 hooks = LockHooks() 77 78 79class LockResult(object): 80 """Result of an operation on a lock; passed to a hook""" 81 82 def __init__(self, lock_url, details=None): 83 """Create a lock result for lock with optional details about the lock.""" 84 self.lock_url = lock_url 85 self.details = details 86 87 def __eq__(self, other): 88 return self.lock_url == other.lock_url and self.details == other.details 89 90 def __repr__(self): 91 return '%s(%s, %s)' % (self.__class__.__name__, 92 self.lock_url, self.details) 93 94 95class LogicalLockResult(object): 96 """The result of a lock_read/lock_write/lock_tree_write call on lockables. 97 98 :ivar unlock: A callable which will unlock the lock. 99 """ 100 101 def __init__(self, unlock, token=None): 102 self.unlock = unlock 103 self.token = token 104 105 def __repr__(self): 106 return "LogicalLockResult(%s)" % (self.unlock) 107 108 def __enter__(self): 109 return self 110 111 def __exit__(self, exc_type, exc_val, exc_tb): 112 # If there was an error raised, prefer the original one 113 try: 114 self.unlock() 115 except BaseException: 116 if exc_type is None: 117 raise 118 return False 119 120 121def cant_unlock_not_held(locked_object): 122 """An attempt to unlock failed because the object was not locked. 123 124 This provides a policy point from which we can generate either a warning or 125 an exception. 126 """ 127 # This is typically masking some other error and called from a finally 128 # block, so it's useful to have the option not to generate a new error 129 # here. You can use -Werror to make it fatal. It should possibly also 130 # raise LockNotHeld. 131 if 'unlock' in debug.debug_flags: 132 warnings.warn("%r is already unlocked" % (locked_object,), 133 stacklevel=3) 134 else: 135 raise errors.LockNotHeld(locked_object) 136 137 138try: 139 import fcntl 140 have_fcntl = True 141except ImportError: 142 have_fcntl = False 143 144have_ctypes_win32 = False 145if sys.platform == 'win32': 146 import msvcrt 147 try: 148 import ctypes 149 have_ctypes_win32 = True 150 except ImportError: 151 pass 152 153 154class _OSLock(object): 155 156 def __init__(self): 157 self.f = None 158 self.filename = None 159 160 def _open(self, filename, filemode): 161 self.filename = osutils.realpath(filename) 162 try: 163 self.f = open(self.filename, filemode) 164 return self.f 165 except IOError as e: 166 if e.errno in (errno.EACCES, errno.EPERM): 167 raise errors.LockFailed(self.filename, str(e)) 168 if e.errno != errno.ENOENT: 169 raise 170 171 # maybe this is an old branch (before may 2005) 172 trace.mutter("trying to create missing lock %r", self.filename) 173 174 self.f = open(self.filename, 'wb+') 175 return self.f 176 177 def _clear_f(self): 178 """Clear the self.f attribute cleanly.""" 179 if self.f: 180 self.f.close() 181 self.f = None 182 183 def unlock(self): 184 raise NotImplementedError() 185 186 187_lock_classes = [] 188 189 190if have_fcntl: 191 192 class _fcntl_FileLock(_OSLock): 193 194 def _unlock(self): 195 fcntl.lockf(self.f, fcntl.LOCK_UN) 196 self._clear_f() 197 198 class _fcntl_WriteLock(_fcntl_FileLock): 199 200 _open_locks = set() 201 202 def __init__(self, filename): 203 super(_fcntl_WriteLock, self).__init__() 204 # Check we can grab a lock before we actually open the file. 205 self.filename = osutils.realpath(filename) 206 if self.filename in _fcntl_WriteLock._open_locks: 207 self._clear_f() 208 raise errors.LockContention(self.filename) 209 if self.filename in _fcntl_ReadLock._open_locks: 210 if 'strict_locks' in debug.debug_flags: 211 self._clear_f() 212 raise errors.LockContention(self.filename) 213 else: 214 trace.mutter('Write lock taken w/ an open read lock on: %s' 215 % (self.filename,)) 216 217 self._open(self.filename, 'rb+') 218 # reserve a slot for this lock - even if the lockf call fails, 219 # at this point unlock() will be called, because self.f is set. 220 # TODO: make this fully threadsafe, if we decide we care. 221 _fcntl_WriteLock._open_locks.add(self.filename) 222 try: 223 # LOCK_NB will cause IOError to be raised if we can't grab a 224 # lock right away. 225 fcntl.lockf(self.f, fcntl.LOCK_EX | fcntl.LOCK_NB) 226 except IOError as e: 227 if e.errno in (errno.EAGAIN, errno.EACCES): 228 # We couldn't grab the lock 229 self.unlock() 230 # we should be more precise about whats a locking 231 # error and whats a random-other error 232 raise errors.LockContention(self.filename, e) 233 234 def unlock(self): 235 _fcntl_WriteLock._open_locks.remove(self.filename) 236 self._unlock() 237 238 class _fcntl_ReadLock(_fcntl_FileLock): 239 240 _open_locks = {} 241 242 def __init__(self, filename): 243 super(_fcntl_ReadLock, self).__init__() 244 self.filename = osutils.realpath(filename) 245 if self.filename in _fcntl_WriteLock._open_locks: 246 if 'strict_locks' in debug.debug_flags: 247 # We raise before calling _open so we don't need to 248 # _clear_f 249 raise errors.LockContention(self.filename) 250 else: 251 trace.mutter('Read lock taken w/ an open write lock on: %s' 252 % (self.filename,)) 253 _fcntl_ReadLock._open_locks.setdefault(self.filename, 0) 254 _fcntl_ReadLock._open_locks[self.filename] += 1 255 self._open(filename, 'rb') 256 try: 257 # LOCK_NB will cause IOError to be raised if we can't grab a 258 # lock right away. 259 fcntl.lockf(self.f, fcntl.LOCK_SH | fcntl.LOCK_NB) 260 except IOError as e: 261 # we should be more precise about whats a locking 262 # error and whats a random-other error 263 raise errors.LockContention(self.filename, e) 264 265 def unlock(self): 266 count = _fcntl_ReadLock._open_locks[self.filename] 267 if count == 1: 268 del _fcntl_ReadLock._open_locks[self.filename] 269 else: 270 _fcntl_ReadLock._open_locks[self.filename] = count - 1 271 self._unlock() 272 273 def temporary_write_lock(self): 274 """Try to grab a write lock on the file. 275 276 On platforms that support it, this will upgrade to a write lock 277 without unlocking the file. 278 Otherwise, this will release the read lock, and try to acquire a 279 write lock. 280 281 :return: A token which can be used to switch back to a read lock. 282 """ 283 if self.filename in _fcntl_WriteLock._open_locks: 284 raise AssertionError('file already locked: %r' 285 % (self.filename,)) 286 try: 287 wlock = _fcntl_TemporaryWriteLock(self) 288 except errors.LockError: 289 # We didn't unlock, so we can just return 'self' 290 return False, self 291 return True, wlock 292 293 class _fcntl_TemporaryWriteLock(_OSLock): 294 """A token used when grabbing a temporary_write_lock. 295 296 Call restore_read_lock() when you are done with the write lock. 297 """ 298 299 def __init__(self, read_lock): 300 super(_fcntl_TemporaryWriteLock, self).__init__() 301 self._read_lock = read_lock 302 self.filename = read_lock.filename 303 304 count = _fcntl_ReadLock._open_locks[self.filename] 305 if count > 1: 306 # Something else also has a read-lock, so we cannot grab a 307 # write lock. 308 raise errors.LockContention(self.filename) 309 310 if self.filename in _fcntl_WriteLock._open_locks: 311 raise AssertionError('file already locked: %r' 312 % (self.filename,)) 313 314 # See if we can open the file for writing. Another process might 315 # have a read lock. We don't use self._open() because we don't want 316 # to create the file if it exists. That would have already been 317 # done by _fcntl_ReadLock 318 try: 319 new_f = open(self.filename, 'rb+') 320 except IOError as e: 321 if e.errno in (errno.EACCES, errno.EPERM): 322 raise errors.LockFailed(self.filename, str(e)) 323 raise 324 try: 325 # LOCK_NB will cause IOError to be raised if we can't grab a 326 # lock right away. 327 fcntl.lockf(new_f, fcntl.LOCK_EX | fcntl.LOCK_NB) 328 except IOError as e: 329 # TODO: Raise a more specific error based on the type of error 330 raise errors.LockContention(self.filename, e) 331 _fcntl_WriteLock._open_locks.add(self.filename) 332 333 self.f = new_f 334 335 def restore_read_lock(self): 336 """Restore the original ReadLock.""" 337 # For fcntl, since we never released the read lock, just release 338 # the write lock, and return the original lock. 339 fcntl.lockf(self.f, fcntl.LOCK_UN) 340 self._clear_f() 341 _fcntl_WriteLock._open_locks.remove(self.filename) 342 # Avoid reference cycles 343 read_lock = self._read_lock 344 self._read_lock = None 345 return read_lock 346 347 _lock_classes.append(('fcntl', _fcntl_WriteLock, _fcntl_ReadLock)) 348 349 350if have_ctypes_win32: 351 from ctypes.wintypes import DWORD, LPWSTR 352 LPSECURITY_ATTRIBUTES = ctypes.c_void_p # used as NULL no need to declare 353 HANDLE = ctypes.c_int # rather than unsigned as in ctypes.wintypes 354 _function_name = "CreateFileW" 355 356 # CreateFile <http://msdn.microsoft.com/en-us/library/aa363858.aspx> 357 _CreateFile = ctypes.WINFUNCTYPE( 358 HANDLE, # return value 359 LPWSTR, # lpFileName 360 DWORD, # dwDesiredAccess 361 DWORD, # dwShareMode 362 LPSECURITY_ATTRIBUTES, # lpSecurityAttributes 363 DWORD, # dwCreationDisposition 364 DWORD, # dwFlagsAndAttributes 365 HANDLE # hTemplateFile 366 )((_function_name, ctypes.windll.kernel32)) 367 368 INVALID_HANDLE_VALUE = -1 369 370 GENERIC_READ = 0x80000000 371 GENERIC_WRITE = 0x40000000 372 FILE_SHARE_READ = 1 373 OPEN_ALWAYS = 4 374 FILE_ATTRIBUTE_NORMAL = 128 375 376 ERROR_ACCESS_DENIED = 5 377 ERROR_SHARING_VIOLATION = 32 378 379 class _ctypes_FileLock(_OSLock): 380 381 def _open(self, filename, access, share, cflags, pymode): 382 self.filename = osutils.realpath(filename) 383 handle = _CreateFile(filename, access, share, None, OPEN_ALWAYS, 384 FILE_ATTRIBUTE_NORMAL, 0) 385 if handle in (INVALID_HANDLE_VALUE, 0): 386 e = ctypes.WinError() 387 if e.args[0] == ERROR_ACCESS_DENIED: 388 raise errors.LockFailed(filename, e) 389 if e.args[0] == ERROR_SHARING_VIOLATION: 390 raise errors.LockContention(filename, e) 391 raise e 392 fd = msvcrt.open_osfhandle(handle, cflags) 393 self.f = os.fdopen(fd, pymode) 394 return self.f 395 396 def unlock(self): 397 self._clear_f() 398 399 class _ctypes_ReadLock(_ctypes_FileLock): 400 def __init__(self, filename): 401 super(_ctypes_ReadLock, self).__init__() 402 self._open(filename, GENERIC_READ, FILE_SHARE_READ, os.O_RDONLY, 403 "rb") 404 405 def temporary_write_lock(self): 406 """Try to grab a write lock on the file. 407 408 On platforms that support it, this will upgrade to a write lock 409 without unlocking the file. 410 Otherwise, this will release the read lock, and try to acquire a 411 write lock. 412 413 :return: A token which can be used to switch back to a read lock. 414 """ 415 # I can't find a way to upgrade a read lock to a write lock without 416 # unlocking first. So here, we do just that. 417 self.unlock() 418 try: 419 wlock = _ctypes_WriteLock(self.filename) 420 except errors.LockError: 421 return False, _ctypes_ReadLock(self.filename) 422 return True, wlock 423 424 class _ctypes_WriteLock(_ctypes_FileLock): 425 def __init__(self, filename): 426 super(_ctypes_WriteLock, self).__init__() 427 self._open(filename, GENERIC_READ | GENERIC_WRITE, 0, os.O_RDWR, 428 "rb+") 429 430 def restore_read_lock(self): 431 """Restore the original ReadLock.""" 432 # For win32 we had to completely let go of the original lock, so we 433 # just unlock and create a new read lock. 434 self.unlock() 435 return _ctypes_ReadLock(self.filename) 436 437 _lock_classes.append(('ctypes', _ctypes_WriteLock, _ctypes_ReadLock)) 438 439 440if len(_lock_classes) == 0: 441 raise NotImplementedError( 442 "We must have one of fcntl or ctypes available" 443 " to support OS locking." 444 ) 445 446 447# We default to using the first available lock class. 448_lock_type, WriteLock, ReadLock = _lock_classes[0] 449 450 451class _RelockDebugMixin(object): 452 """Mixin support for -Drelock flag. 453 454 Add this as a base class then call self._note_lock with 'r' or 'w' when 455 acquiring a read- or write-lock. If this object was previously locked (and 456 locked the same way), and -Drelock is set, then this will trace.note a 457 message about it. 458 """ 459 460 _prev_lock = None 461 462 def _note_lock(self, lock_type): 463 if 'relock' in debug.debug_flags and self._prev_lock == lock_type: 464 if lock_type == 'r': 465 type_name = 'read' 466 else: 467 type_name = 'write' 468 trace.note(gettext('{0!r} was {1} locked again'), self, type_name) 469 self._prev_lock = lock_type 470 471 472@contextlib.contextmanager 473def write_locked(lockable): 474 lockable.lock_write() 475 try: 476 yield lockable 477 finally: 478 lockable.unlock() 479