• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 Google Inc. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Locked file interface that should work on Unix and Windows pythons.
16
17This module first tries to use fcntl locking to ensure serialized access
18to a file, then falls back on a lock file if that is unavialable.
19
20Usage::
21
22    f = LockedFile('filename', 'r+b', 'rb')
23    f.open_and_lock()
24    if f.is_locked():
25      print('Acquired filename with r+b mode')
26      f.file_handle().write('locked data')
27    else:
28      print('Acquired filename with rb mode')
29    f.unlock_and_close()
30
31"""
32
33from __future__ import print_function
34
35import errno
36import logging
37import os
38import time
39
40from oauth2client import util
41
42
43__author__ = 'cache@google.com (David T McWherter)'
44
45logger = logging.getLogger(__name__)
46
47
48class CredentialsFileSymbolicLinkError(Exception):
49    """Credentials files must not be symbolic links."""
50
51
52class AlreadyLockedException(Exception):
53    """Trying to lock a file that has already been locked by the LockedFile."""
54    pass
55
56
57def validate_file(filename):
58    if os.path.islink(filename):
59        raise CredentialsFileSymbolicLinkError(
60            'File: %s is a symbolic link.' % filename)
61
62
63class _Opener(object):
64    """Base class for different locking primitives."""
65
66    def __init__(self, filename, mode, fallback_mode):
67        """Create an Opener.
68
69        Args:
70            filename: string, The pathname of the file.
71            mode: string, The preferred mode to access the file with.
72            fallback_mode: string, The mode to use if locking fails.
73        """
74        self._locked = False
75        self._filename = filename
76        self._mode = mode
77        self._fallback_mode = fallback_mode
78        self._fh = None
79        self._lock_fd = None
80
81    def is_locked(self):
82        """Was the file locked."""
83        return self._locked
84
85    def file_handle(self):
86        """The file handle to the file. Valid only after opened."""
87        return self._fh
88
89    def filename(self):
90        """The filename that is being locked."""
91        return self._filename
92
93    def open_and_lock(self, timeout, delay):
94        """Open the file and lock it.
95
96        Args:
97            timeout: float, How long to try to lock for.
98            delay: float, How long to wait between retries.
99        """
100        pass
101
102    def unlock_and_close(self):
103        """Unlock and close the file."""
104        pass
105
106
107class _PosixOpener(_Opener):
108    """Lock files using Posix advisory lock files."""
109
110    def open_and_lock(self, timeout, delay):
111        """Open the file and lock it.
112
113        Tries to create a .lock file next to the file we're trying to open.
114
115        Args:
116            timeout: float, How long to try to lock for.
117            delay: float, How long to wait between retries.
118
119        Raises:
120            AlreadyLockedException: if the lock is already acquired.
121            IOError: if the open fails.
122            CredentialsFileSymbolicLinkError if the file is a symbolic link.
123        """
124        if self._locked:
125            raise AlreadyLockedException('File %s is already locked' %
126                                         self._filename)
127        self._locked = False
128
129        validate_file(self._filename)
130        try:
131            self._fh = open(self._filename, self._mode)
132        except IOError as e:
133            # If we can't access with _mode, try _fallback_mode and don't lock.
134            if e.errno == errno.EACCES:
135                self._fh = open(self._filename, self._fallback_mode)
136                return
137
138        lock_filename = self._posix_lockfile(self._filename)
139        start_time = time.time()
140        while True:
141            try:
142                self._lock_fd = os.open(lock_filename,
143                                        os.O_CREAT | os.O_EXCL | os.O_RDWR)
144                self._locked = True
145                break
146
147            except OSError as e:
148                if e.errno != errno.EEXIST:
149                    raise
150                if (time.time() - start_time) >= timeout:
151                    logger.warn('Could not acquire lock %s in %s seconds',
152                                lock_filename, timeout)
153                    # Close the file and open in fallback_mode.
154                    if self._fh:
155                        self._fh.close()
156                    self._fh = open(self._filename, self._fallback_mode)
157                    return
158                time.sleep(delay)
159
160    def unlock_and_close(self):
161        """Unlock a file by removing the .lock file, and close the handle."""
162        if self._locked:
163            lock_filename = self._posix_lockfile(self._filename)
164            os.close(self._lock_fd)
165            os.unlink(lock_filename)
166            self._locked = False
167            self._lock_fd = None
168        if self._fh:
169            self._fh.close()
170
171    def _posix_lockfile(self, filename):
172        """The name of the lock file to use for posix locking."""
173        return '%s.lock' % filename
174
175
176try:
177    import fcntl
178
179    class _FcntlOpener(_Opener):
180        """Open, lock, and unlock a file using fcntl.lockf."""
181
182        def open_and_lock(self, timeout, delay):
183            """Open the file and lock it.
184
185            Args:
186                timeout: float, How long to try to lock for.
187                delay: float, How long to wait between retries
188
189            Raises:
190                AlreadyLockedException: if the lock is already acquired.
191                IOError: if the open fails.
192                CredentialsFileSymbolicLinkError: if the file is a symbolic
193                                                  link.
194            """
195            if self._locked:
196                raise AlreadyLockedException('File %s is already locked' %
197                                             self._filename)
198            start_time = time.time()
199
200            validate_file(self._filename)
201            try:
202                self._fh = open(self._filename, self._mode)
203            except IOError as e:
204                # If we can't access with _mode, try _fallback_mode and
205                # don't lock.
206                if e.errno in (errno.EPERM, errno.EACCES):
207                    self._fh = open(self._filename, self._fallback_mode)
208                    return
209
210            # We opened in _mode, try to lock the file.
211            while True:
212                try:
213                    fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
214                    self._locked = True
215                    return
216                except IOError as e:
217                    # If not retrying, then just pass on the error.
218                    if timeout == 0:
219                        raise
220                    if e.errno != errno.EACCES:
221                        raise
222                    # We could not acquire the lock. Try again.
223                    if (time.time() - start_time) >= timeout:
224                        logger.warn('Could not lock %s in %s seconds',
225                                    self._filename, timeout)
226                        if self._fh:
227                            self._fh.close()
228                        self._fh = open(self._filename, self._fallback_mode)
229                        return
230                    time.sleep(delay)
231
232        def unlock_and_close(self):
233            """Close and unlock the file using the fcntl.lockf primitive."""
234            if self._locked:
235                fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
236            self._locked = False
237            if self._fh:
238                self._fh.close()
239except ImportError:
240    _FcntlOpener = None
241
242
243try:
244    import pywintypes
245    import win32con
246    import win32file
247
248    class _Win32Opener(_Opener):
249        """Open, lock, and unlock a file using windows primitives."""
250
251        # Error #33:
252        #  'The process cannot access the file because another process'
253        FILE_IN_USE_ERROR = 33
254
255        # Error #158:
256        #  'The segment is already unlocked.'
257        FILE_ALREADY_UNLOCKED_ERROR = 158
258
259        def open_and_lock(self, timeout, delay):
260            """Open the file and lock it.
261
262            Args:
263                timeout: float, How long to try to lock for.
264                delay: float, How long to wait between retries
265
266            Raises:
267                AlreadyLockedException: if the lock is already acquired.
268                IOError: if the open fails.
269                CredentialsFileSymbolicLinkError: if the file is a symbolic
270                                                  link.
271            """
272            if self._locked:
273                raise AlreadyLockedException('File %s is already locked' %
274                                             self._filename)
275            start_time = time.time()
276
277            validate_file(self._filename)
278            try:
279                self._fh = open(self._filename, self._mode)
280            except IOError as e:
281                # If we can't access with _mode, try _fallback_mode
282                # and don't lock.
283                if e.errno == errno.EACCES:
284                    self._fh = open(self._filename, self._fallback_mode)
285                    return
286
287            # We opened in _mode, try to lock the file.
288            while True:
289                try:
290                    hfile = win32file._get_osfhandle(self._fh.fileno())
291                    win32file.LockFileEx(
292                        hfile,
293                        (win32con.LOCKFILE_FAIL_IMMEDIATELY |
294                         win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
295                        pywintypes.OVERLAPPED())
296                    self._locked = True
297                    return
298                except pywintypes.error as e:
299                    if timeout == 0:
300                        raise
301
302                    # If the error is not that the file is already
303                    # in use, raise.
304                    if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
305                        raise
306
307                    # We could not acquire the lock. Try again.
308                    if (time.time() - start_time) >= timeout:
309                        logger.warn('Could not lock %s in %s seconds' % (
310                            self._filename, timeout))
311                        if self._fh:
312                            self._fh.close()
313                        self._fh = open(self._filename, self._fallback_mode)
314                        return
315                    time.sleep(delay)
316
317        def unlock_and_close(self):
318            """Close and unlock the file using the win32 primitive."""
319            if self._locked:
320                try:
321                    hfile = win32file._get_osfhandle(self._fh.fileno())
322                    win32file.UnlockFileEx(hfile, 0, -0x10000,
323                                           pywintypes.OVERLAPPED())
324                except pywintypes.error as e:
325                    if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
326                        raise
327            self._locked = False
328            if self._fh:
329                self._fh.close()
330except ImportError:
331    _Win32Opener = None
332
333
334class LockedFile(object):
335    """Represent a file that has exclusive access."""
336
337    @util.positional(4)
338    def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
339        """Construct a LockedFile.
340
341        Args:
342            filename: string, The path of the file to open.
343            mode: string, The mode to try to open the file with.
344            fallback_mode: string, The mode to use if locking fails.
345            use_native_locking: bool, Whether or not fcntl/win32 locking is
346                                used.
347        """
348        opener = None
349        if not opener and use_native_locking:
350            if _Win32Opener:
351                opener = _Win32Opener(filename, mode, fallback_mode)
352            if _FcntlOpener:
353                opener = _FcntlOpener(filename, mode, fallback_mode)
354
355        if not opener:
356            opener = _PosixOpener(filename, mode, fallback_mode)
357
358        self._opener = opener
359
360    def filename(self):
361        """Return the filename we were constructed with."""
362        return self._opener._filename
363
364    def file_handle(self):
365        """Return the file_handle to the opened file."""
366        return self._opener.file_handle()
367
368    def is_locked(self):
369        """Return whether we successfully locked the file."""
370        return self._opener.is_locked()
371
372    def open_and_lock(self, timeout=0, delay=0.05):
373        """Open the file, trying to lock it.
374
375        Args:
376            timeout: float, The number of seconds to try to acquire the lock.
377            delay: float, The number of seconds to wait between retry attempts.
378
379        Raises:
380            AlreadyLockedException: if the lock is already acquired.
381            IOError: if the open fails.
382        """
383        self._opener.open_and_lock(timeout, delay)
384
385    def unlock_and_close(self):
386        """Unlock and close a file."""
387        self._opener.unlock_and_close()
388