1# Copyright (C) 2005-2011 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
17from .lazy_import import lazy_import
18lazy_import(globals(), """
19from breezy import (
20    counted_lock,
21    lock,
22    transactions,
23    urlutils,
24    )
25""")
26
27from . import (
28    errors,
29    )
30from .decorators import (
31    only_raises,
32    )
33
34
35class LockableFiles(object):
36    """Object representing a set of related files locked within the same scope.
37
38    This coordinates access to the lock along with providing a transaction.
39
40    LockableFiles manage a lock count and can be locked repeatedly by
41    a single caller.  (The underlying lock implementation generally does not
42    support this.)
43
44    Instances of this class are often called control_files.
45
46    This class is now deprecated; code should move to using the Transport
47    directly for file operations and using the lock or CountedLock for
48    locking.
49
50    :ivar _lock: The real underlying lock (e.g. a LockDir)
51    :ivar _lock_count: If _lock_mode is true, a positive count of the number
52        of times the lock has been taken (and not yet released) *by this
53        process*, through this particular object instance.
54    :ivar _lock_mode: None, or 'r' or 'w'
55    """
56
57    def __init__(self, transport, lock_name, lock_class):
58        """Create a LockableFiles group
59
60        :param transport: Transport pointing to the directory holding the
61            control files and lock.
62        :param lock_name: Name of the lock guarding these files.
63        :param lock_class: Class of lock strategy to use: typically
64            either LockDir or TransportLock.
65        """
66        self._transport = transport
67        self.lock_name = lock_name
68        self._transaction = None
69        self._lock_mode = None
70        self._lock_count = 0
71        self._find_modes()
72        esc_name = self._escape(lock_name)
73        self._lock = lock_class(transport, esc_name,
74                                file_modebits=self._file_mode,
75                                dir_modebits=self._dir_mode)
76        self._counted_lock = counted_lock.CountedLock(self._lock)
77
78    def create_lock(self):
79        """Create the lock.
80
81        This should normally be called only when the LockableFiles directory
82        is first created on disk.
83        """
84        self._lock.create(mode=self._dir_mode)
85
86    def __repr__(self):
87        return '%s(%r)' % (self.__class__.__name__,
88                           self._transport)
89
90    def __str__(self):
91        return 'LockableFiles(%s, %s)' % (self.lock_name, self._transport.base)
92
93    def break_lock(self):
94        """Break the lock of this lockable files group if it is held.
95
96        The current ui factory will be used to prompt for user conformation.
97        """
98        self._lock.break_lock()
99
100    def _escape(self, file_or_path):
101        """DEPRECATED: Do not use outside this class"""
102        if file_or_path == '':
103            return u''
104        return urlutils.escape(file_or_path)
105
106    def _find_modes(self):
107        """Determine the appropriate modes for files and directories.
108
109        :deprecated: Replaced by BzrDir._find_creation_modes.
110        """
111        # XXX: The properties created by this can be removed or deprecated
112        # once all the _get_text_store methods etc no longer use them.
113        # -- mbp 20080512
114        try:
115            st = self._transport.stat('.')
116        except errors.TransportNotPossible:
117            self._dir_mode = 0o755
118            self._file_mode = 0o644
119        else:
120            # Check the directory mode, but also make sure the created
121            # directories and files are read-write for this user. This is
122            # mostly a workaround for filesystems which lie about being able to
123            # write to a directory (cygwin & win32)
124            self._dir_mode = (st.st_mode & 0o7777) | 0o0700
125            # Remove the sticky and execute bits for files
126            self._file_mode = self._dir_mode & ~0o7111
127
128    def leave_in_place(self):
129        """Set this LockableFiles to not clear the physical lock on unlock."""
130        self._lock.leave_in_place()
131
132    def dont_leave_in_place(self):
133        """Set this LockableFiles to clear the physical lock on unlock."""
134        self._lock.dont_leave_in_place()
135
136    def lock_write(self, token=None):
137        """Lock this group of files for writing.
138
139        :param token: if this is already locked, then lock_write will fail
140            unless the token matches the existing lock.
141        :returns: a token if this instance supports tokens, otherwise None.
142        :raises TokenLockingNotSupported: when a token is given but this
143            instance doesn't support using token locks.
144        :raises MismatchedToken: if the specified token doesn't match the token
145            of the existing lock.
146
147        A token should be passed in if you know that you have locked the object
148        some other way, and need to synchronise this object's state with that
149        fact.
150        """
151        if self._lock_mode:
152            if (self._lock_mode != 'w'
153                    or not self.get_transaction().writeable()):
154                raise errors.ReadOnlyError(self)
155            self._lock.validate_token(token)
156            self._lock_count += 1
157            return self._token_from_lock
158        else:
159            token_from_lock = self._lock.lock_write(token=token)
160            # traceback.print_stack()
161            self._lock_mode = 'w'
162            self._lock_count = 1
163            self._set_write_transaction()
164            self._token_from_lock = token_from_lock
165            return token_from_lock
166
167    def lock_read(self):
168        if self._lock_mode:
169            if self._lock_mode not in ('r', 'w'):
170                raise ValueError("invalid lock mode %r" % (self._lock_mode,))
171            self._lock_count += 1
172        else:
173            self._lock.lock_read()
174            # traceback.print_stack()
175            self._lock_mode = 'r'
176            self._lock_count = 1
177            self._set_read_transaction()
178
179    def _set_read_transaction(self):
180        """Setup a read transaction."""
181        self._set_transaction(transactions.ReadOnlyTransaction())
182        # 5K may be excessive, but hey, its a knob.
183        self.get_transaction().set_cache_size(5000)
184
185    def _set_write_transaction(self):
186        """Setup a write transaction."""
187        self._set_transaction(transactions.WriteTransaction())
188
189    @only_raises(errors.LockNotHeld, errors.LockBroken)
190    def unlock(self):
191        if not self._lock_mode:
192            return lock.cant_unlock_not_held(self)
193        if self._lock_count > 1:
194            self._lock_count -= 1
195        else:
196            # traceback.print_stack()
197            self._finish_transaction()
198            try:
199                self._lock.unlock()
200            finally:
201                self._lock_count = 0
202                self._lock_mode = None
203
204    def is_locked(self):
205        """Return true if this LockableFiles group is locked"""
206        return self._lock_count >= 1
207
208    def get_physical_lock_status(self):
209        """Return physical lock status.
210
211        Returns true if a lock is held on the transport. If no lock is held, or
212        the underlying locking mechanism does not support querying lock
213        status, false is returned.
214        """
215        try:
216            return self._lock.peek() is not None
217        except NotImplementedError:
218            return False
219
220    def get_transaction(self):
221        """Return the current active transaction.
222
223        If no transaction is active, this returns a passthrough object
224        for which all data is immediately flushed and no caching happens.
225        """
226        if self._transaction is None:
227            return transactions.PassThroughTransaction()
228        else:
229            return self._transaction
230
231    def _set_transaction(self, new_transaction):
232        """Set a new active transaction."""
233        if self._transaction is not None:
234            raise errors.LockError('Branch %s is in a transaction already.' %
235                                   self)
236        self._transaction = new_transaction
237
238    def _finish_transaction(self):
239        """Exit the current transaction."""
240        if self._transaction is None:
241            raise errors.LockError('Branch %s is not in a transaction' %
242                                   self)
243        transaction = self._transaction
244        self._transaction = None
245        transaction.finish()
246
247
248class TransportLock(object):
249    """Locking method which uses transport-dependent locks.
250
251    On the local filesystem these transform into OS-managed locks.
252
253    These do not guard against concurrent access via different
254    transports.
255
256    This is suitable for use only in WorkingTrees (which are at present
257    always local).
258    """
259
260    def __init__(self, transport, escaped_name, file_modebits, dir_modebits):
261        self._transport = transport
262        self._escaped_name = escaped_name
263        self._file_modebits = file_modebits
264        self._dir_modebits = dir_modebits
265
266    def break_lock(self):
267        raise NotImplementedError(self.break_lock)
268
269    def leave_in_place(self):
270        raise NotImplementedError(self.leave_in_place)
271
272    def dont_leave_in_place(self):
273        raise NotImplementedError(self.dont_leave_in_place)
274
275    def lock_write(self, token=None):
276        if token is not None:
277            raise errors.TokenLockingNotSupported(self)
278        self._lock = self._transport.lock_write(self._escaped_name)
279
280    def lock_read(self):
281        self._lock = self._transport.lock_read(self._escaped_name)
282
283    def unlock(self):
284        self._lock.unlock()
285        self._lock = None
286
287    def peek(self):
288        raise NotImplementedError()
289
290    def create(self, mode=None):
291        """Create lock mechanism"""
292        # for old-style locks, create the file now
293        self._transport.put_bytes(self._escaped_name, b'',
294                                  mode=self._file_modebits)
295
296    def validate_token(self, token):
297        if token is not None:
298            raise errors.TokenLockingNotSupported(self)
299