/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/lock.py

  • Committer: Jelmer Vernooij
  • Date: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

Show diffs side-by-side

added added

removed removed

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