~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lockable_files.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-02-20 04:39:19 UTC
  • mfrom: (1534.6.13 repository)
  • Revision ID: pqm@pqm.ubuntu.com-20060220043919-b01f8960604b339d
Merge push/pull and branching within repositories bugfix.

Show diffs side-by-side

added added

removed removed

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