~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lockable_files.py

  • Committer: Robert Collins
  • Date: 2009-04-24 05:08:51 UTC
  • mto: This revision was merged to the branch mainline in revision 4304.
  • Revision ID: robertc@robertcollins.net-20090424050851-sdfonaqerfs386t0
Reduce round trips pushing new branches substantially.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2008, 2009 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
from cStringIO import StringIO
 
18
 
 
19
from bzrlib.lazy_import import lazy_import
 
20
lazy_import(globals(), """
 
21
import codecs
 
22
import warnings
 
23
 
 
24
from bzrlib import (
 
25
    counted_lock,
 
26
    errors,
 
27
    osutils,
 
28
    transactions,
 
29
    urlutils,
 
30
    )
 
31
""")
 
32
 
 
33
from bzrlib.decorators import (
 
34
    needs_read_lock,
 
35
    needs_write_lock,
 
36
    )
 
37
from bzrlib.symbol_versioning import (
 
38
    deprecated_in,
 
39
    deprecated_method,
 
40
    )
 
41
 
 
42
 
 
43
# XXX: The tracking here of lock counts and whether the lock is held is
 
44
# somewhat redundant with what's done in LockDir; the main difference is that
 
45
# LockableFiles permits reentrancy.
 
46
 
 
47
class _LockWarner(object):
 
48
    """Hold a counter for a lock and warn if GCed while the count is >= 1.
 
49
 
 
50
    This is separate from LockableFiles because putting a __del__ on
 
51
    LockableFiles can result in uncollectable cycles.
 
52
    """
 
53
 
 
54
    def __init__(self, repr):
 
55
        self.lock_count = 0
 
56
        self.repr = repr
 
57
 
 
58
    def __del__(self):
 
59
        if self.lock_count >= 1:
 
60
            # There should have been a try/finally to unlock this.
 
61
            warnings.warn("%r was gc'd while locked" % self.repr)
 
62
 
 
63
 
 
64
class LockableFiles(object):
 
65
    """Object representing a set of related files locked within the same scope.
 
66
 
 
67
    These files are used by a WorkingTree, Repository or Branch, and should
 
68
    generally only be touched by that object.
 
69
 
 
70
    LockableFiles also provides some policy on top of Transport for encoding
 
71
    control files as utf-8.
 
72
 
 
73
    LockableFiles manage a lock count and can be locked repeatedly by
 
74
    a single caller.  (The underlying lock implementation generally does not
 
75
    support this.)
 
76
 
 
77
    Instances of this class are often called control_files.
 
78
 
 
79
    This object builds on top of a Transport, which is used to actually write
 
80
    the files to disk, and an OSLock or LockDir, which controls how access to
 
81
    the files is controlled.  The particular type of locking used is set when
 
82
    the object is constructed.  In older formats OSLocks are used everywhere.
 
83
    in newer formats a LockDir is used for Repositories and Branches, and
 
84
    OSLocks for the local filesystem.
 
85
 
 
86
    This class is now deprecated; code should move to using the Transport
 
87
    directly for file operations and using the lock or CountedLock for
 
88
    locking.
 
89
    
 
90
    :ivar _lock: The real underlying lock (e.g. a LockDir)
 
91
    :ivar _counted_lock: A lock decorated with a semaphore, so that it 
 
92
        can be re-entered.
 
93
    """
 
94
 
 
95
    # _lock_mode: None, or 'r' or 'w'
 
96
 
 
97
    # _lock_count: If _lock_mode is true, a positive count of the number of
 
98
    # times the lock has been taken *by this process*.
 
99
 
 
100
    def __init__(self, transport, lock_name, lock_class):
 
101
        """Create a LockableFiles group
 
102
 
 
103
        :param transport: Transport pointing to the directory holding the
 
104
            control files and lock.
 
105
        :param lock_name: Name of the lock guarding these files.
 
106
        :param lock_class: Class of lock strategy to use: typically
 
107
            either LockDir or TransportLock.
 
108
        """
 
109
        self._transport = transport
 
110
        self.lock_name = lock_name
 
111
        self._transaction = None
 
112
        self._lock_mode = None
 
113
        self._lock_warner = _LockWarner(repr(self))
 
114
        self._find_modes()
 
115
        esc_name = self._escape(lock_name)
 
116
        self._lock = lock_class(transport, esc_name,
 
117
                                file_modebits=self._file_mode,
 
118
                                dir_modebits=self._dir_mode)
 
119
        self._counted_lock = counted_lock.CountedLock(self._lock)
 
120
 
 
121
    def create_lock(self):
 
122
        """Create the lock.
 
123
 
 
124
        This should normally be called only when the LockableFiles directory
 
125
        is first created on disk.
 
126
        """
 
127
        self._lock.create(mode=self._dir_mode)
 
128
 
 
129
    def __repr__(self):
 
130
        return '%s(%r)' % (self.__class__.__name__,
 
131
                           self._transport)
 
132
    def __str__(self):
 
133
        return 'LockableFiles(%s, %s)' % (self.lock_name, self._transport.base)
 
134
 
 
135
    def break_lock(self):
 
136
        """Break the lock of this lockable files group if it is held.
 
137
 
 
138
        The current ui factory will be used to prompt for user conformation.
 
139
        """
 
140
        self._lock.break_lock()
 
141
 
 
142
    def _escape(self, file_or_path):
 
143
        """DEPRECATED: Do not use outside this class"""
 
144
        if not isinstance(file_or_path, basestring):
 
145
            file_or_path = '/'.join(file_or_path)
 
146
        if file_or_path == '':
 
147
            return u''
 
148
        return urlutils.escape(osutils.safe_unicode(file_or_path))
 
149
 
 
150
    def _find_modes(self):
 
151
        """Determine the appropriate modes for files and directories.
 
152
 
 
153
        :deprecated: Replaced by BzrDir._find_modes.
 
154
        """
 
155
        # XXX: The properties created by this can be removed or deprecated
 
156
        # once all the _get_text_store methods etc no longer use them.
 
157
        # -- mbp 20080512
 
158
        try:
 
159
            st = self._transport.stat('.')
 
160
        except errors.TransportNotPossible:
 
161
            self._dir_mode = 0755
 
162
            self._file_mode = 0644
 
163
        else:
 
164
            # Check the directory mode, but also make sure the created
 
165
            # directories and files are read-write for this user. This is
 
166
            # mostly a workaround for filesystems which lie about being able to
 
167
            # write to a directory (cygwin & win32)
 
168
            self._dir_mode = (st.st_mode & 07777) | 00700
 
169
            # Remove the sticky and execute bits for files
 
170
            self._file_mode = self._dir_mode & ~07111
 
171
 
 
172
    @deprecated_method(deprecated_in((1, 6, 0)))
 
173
    def controlfilename(self, file_or_path):
 
174
        """Return location relative to branch.
 
175
 
 
176
        :deprecated: Use Transport methods instead.
 
177
        """
 
178
        return self._transport.abspath(self._escape(file_or_path))
 
179
 
 
180
    @needs_read_lock
 
181
    @deprecated_method(deprecated_in((1, 5, 0)))
 
182
    def get(self, relpath):
 
183
        """Get a file as a bytestream.
 
184
 
 
185
        :deprecated: Use a Transport instead of LockableFiles.
 
186
        """
 
187
        relpath = self._escape(relpath)
 
188
        return self._transport.get(relpath)
 
189
 
 
190
    @needs_read_lock
 
191
    @deprecated_method(deprecated_in((1, 5, 0)))
 
192
    def get_utf8(self, relpath):
 
193
        """Get a file as a unicode stream.
 
194
 
 
195
        :deprecated: Use a Transport instead of LockableFiles.
 
196
        """
 
197
        relpath = self._escape(relpath)
 
198
        # DO NOT introduce an errors=replace here.
 
199
        return codecs.getreader('utf-8')(self._transport.get(relpath))
 
200
 
 
201
    @needs_write_lock
 
202
    @deprecated_method(deprecated_in((1, 6, 0)))
 
203
    def put(self, path, file):
 
204
        """Write a file.
 
205
 
 
206
        :param path: The path to put the file, relative to the .bzr control
 
207
                     directory
 
208
        :param file: A file-like or string object whose contents should be copied.
 
209
 
 
210
        :deprecated: Use Transport methods instead.
 
211
        """
 
212
        self._transport.put_file(self._escape(path), file, mode=self._file_mode)
 
213
 
 
214
    @needs_write_lock
 
215
    @deprecated_method(deprecated_in((1, 6, 0)))
 
216
    def put_bytes(self, path, a_string):
 
217
        """Write a string of bytes.
 
218
 
 
219
        :param path: The path to put the bytes, relative to the transport root.
 
220
        :param a_string: A string object, whose exact bytes are to be copied.
 
221
 
 
222
        :deprecated: Use Transport methods instead.
 
223
        """
 
224
        self._transport.put_bytes(self._escape(path), a_string,
 
225
                                  mode=self._file_mode)
 
226
 
 
227
    @needs_write_lock
 
228
    @deprecated_method(deprecated_in((1, 6, 0)))
 
229
    def put_utf8(self, path, a_string):
 
230
        """Write a string, encoding as utf-8.
 
231
 
 
232
        :param path: The path to put the string, relative to the transport root.
 
233
        :param string: A string or unicode object whose contents should be copied.
 
234
 
 
235
        :deprecated: Use Transport methods instead.
 
236
        """
 
237
        # IterableFile would not be needed if Transport.put took iterables
 
238
        # instead of files.  ADHB 2005-12-25
 
239
        # RBC 20060103 surely its not needed anyway, with codecs transcode
 
240
        # file support ?
 
241
        # JAM 20060103 We definitely don't want encode(..., 'replace')
 
242
        # these are valuable files which should have exact contents.
 
243
        if not isinstance(a_string, basestring):
 
244
            raise errors.BzrBadParameterNotString(a_string)
 
245
        self.put_bytes(path, a_string.encode('utf-8'))
 
246
 
 
247
    def leave_in_place(self):
 
248
        """Set this LockableFiles to not clear the physical lock on unlock."""
 
249
        self._lock.leave_in_place()
 
250
 
 
251
    def dont_leave_in_place(self):
 
252
        """Set this LockableFiles to clear the physical lock on unlock."""
 
253
        self._lock.dont_leave_in_place()
 
254
 
 
255
    def lock_write(self, token=None):
 
256
        """Lock this group of files for writing.
 
257
 
 
258
        :param token: if this is already locked, then lock_write will fail
 
259
            unless the token matches the existing lock.
 
260
        :returns: a token if this instance supports tokens, otherwise None.
 
261
        :raises TokenLockingNotSupported: when a token is given but this
 
262
            instance doesn't support using token locks.
 
263
        :raises MismatchedToken: if the specified token doesn't match the token
 
264
            of the existing lock.
 
265
 
 
266
        A token should be passed in if you know that you have locked the object
 
267
        some other way, and need to synchronise this object's state with that
 
268
        fact.
 
269
        """
 
270
        # TODO: Upgrade locking to support using a Transport,
 
271
        # and potentially a remote locking protocol
 
272
        if self._lock_mode:
 
273
            if self._lock_mode != 'w' or not self.get_transaction().writeable():
 
274
                raise errors.ReadOnlyError(self)
 
275
            self._lock.validate_token(token)
 
276
            self._lock_warner.lock_count += 1
 
277
            return self._token_from_lock
 
278
        else:
 
279
            token_from_lock = self._lock.lock_write(token=token)
 
280
            #traceback.print_stack()
 
281
            self._lock_mode = 'w'
 
282
            self._lock_warner.lock_count = 1
 
283
            self._set_write_transaction()
 
284
            self._token_from_lock = token_from_lock
 
285
            return token_from_lock
 
286
 
 
287
    def lock_read(self):
 
288
        if self._lock_mode:
 
289
            if self._lock_mode not in ('r', 'w'):
 
290
                raise ValueError("invalid lock mode %r" % (self._lock_mode,))
 
291
            self._lock_warner.lock_count += 1
 
292
        else:
 
293
            self._lock.lock_read()
 
294
            #traceback.print_stack()
 
295
            self._lock_mode = 'r'
 
296
            self._lock_warner.lock_count = 1
 
297
            self._set_read_transaction()
 
298
 
 
299
    def _set_read_transaction(self):
 
300
        """Setup a read transaction."""
 
301
        self._set_transaction(transactions.ReadOnlyTransaction())
 
302
        # 5K may be excessive, but hey, its a knob.
 
303
        self.get_transaction().set_cache_size(5000)
 
304
 
 
305
    def _set_write_transaction(self):
 
306
        """Setup a write transaction."""
 
307
        self._set_transaction(transactions.WriteTransaction())
 
308
 
 
309
    def unlock(self):
 
310
        if not self._lock_mode:
 
311
            raise errors.LockNotHeld(self)
 
312
        if self._lock_warner.lock_count > 1:
 
313
            self._lock_warner.lock_count -= 1
 
314
        else:
 
315
            #traceback.print_stack()
 
316
            self._finish_transaction()
 
317
            try:
 
318
                self._lock.unlock()
 
319
            finally:
 
320
                self._lock_mode = self._lock_warner.lock_count = None
 
321
 
 
322
    @property
 
323
    def _lock_count(self):
 
324
        return self._lock_warner.lock_count
 
325
 
 
326
    def is_locked(self):
 
327
        """Return true if this LockableFiles group is locked"""
 
328
        return self._lock_warner.lock_count >= 1
 
329
 
 
330
    def get_physical_lock_status(self):
 
331
        """Return physical lock status.
 
332
 
 
333
        Returns true if a lock is held on the transport. If no lock is held, or
 
334
        the underlying locking mechanism does not support querying lock
 
335
        status, false is returned.
 
336
        """
 
337
        try:
 
338
            return self._lock.peek() is not None
 
339
        except NotImplementedError:
 
340
            return False
 
341
 
 
342
    def get_transaction(self):
 
343
        """Return the current active transaction.
 
344
 
 
345
        If no transaction is active, this returns a passthrough object
 
346
        for which all data is immediately flushed and no caching happens.
 
347
        """
 
348
        if self._transaction is None:
 
349
            return transactions.PassThroughTransaction()
 
350
        else:
 
351
            return self._transaction
 
352
 
 
353
    def _set_transaction(self, new_transaction):
 
354
        """Set a new active transaction."""
 
355
        if self._transaction is not None:
 
356
            raise errors.LockError('Branch %s is in a transaction already.' %
 
357
                                   self)
 
358
        self._transaction = new_transaction
 
359
 
 
360
    def _finish_transaction(self):
 
361
        """Exit the current transaction."""
 
362
        if self._transaction is None:
 
363
            raise errors.LockError('Branch %s is not in a transaction' %
 
364
                                   self)
 
365
        transaction = self._transaction
 
366
        self._transaction = None
 
367
        transaction.finish()
 
368
 
 
369
 
 
370
class TransportLock(object):
 
371
    """Locking method which uses transport-dependent locks.
 
372
 
 
373
    On the local filesystem these transform into OS-managed locks.
 
374
 
 
375
    These do not guard against concurrent access via different
 
376
    transports.
 
377
 
 
378
    This is suitable for use only in WorkingTrees (which are at present
 
379
    always local).
 
380
    """
 
381
    def __init__(self, transport, escaped_name, file_modebits, dir_modebits):
 
382
        self._transport = transport
 
383
        self._escaped_name = escaped_name
 
384
        self._file_modebits = file_modebits
 
385
        self._dir_modebits = dir_modebits
 
386
 
 
387
    def break_lock(self):
 
388
        raise NotImplementedError(self.break_lock)
 
389
 
 
390
    def leave_in_place(self):
 
391
        raise NotImplementedError(self.leave_in_place)
 
392
 
 
393
    def dont_leave_in_place(self):
 
394
        raise NotImplementedError(self.dont_leave_in_place)
 
395
 
 
396
    def lock_write(self, token=None):
 
397
        if token is not None:
 
398
            raise errors.TokenLockingNotSupported(self)
 
399
        self._lock = self._transport.lock_write(self._escaped_name)
 
400
 
 
401
    def lock_read(self):
 
402
        self._lock = self._transport.lock_read(self._escaped_name)
 
403
 
 
404
    def unlock(self):
 
405
        self._lock.unlock()
 
406
        self._lock = None
 
407
 
 
408
    def peek(self):
 
409
        raise NotImplementedError()
 
410
 
 
411
    def create(self, mode=None):
 
412
        """Create lock mechanism"""
 
413
        # for old-style locks, create the file now
 
414
        self._transport.put_bytes(self._escaped_name, '',
 
415
                            mode=self._file_modebits)
 
416
 
 
417
    def validate_token(self, token):
 
418
        if token is not None:
 
419
            raise errors.TokenLockingNotSupported(self)
 
420