• 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: {0} is a symbolic link.'.format(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(
126                'File {0} is already locked'.format(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 '{0}.lock'.format(filename)
174
175
176class LockedFile(object):
177    """Represent a file that has exclusive access."""
178
179    @util.positional(4)
180    def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
181        """Construct a LockedFile.
182
183        Args:
184            filename: string, The path of the file to open.
185            mode: string, The mode to try to open the file with.
186            fallback_mode: string, The mode to use if locking fails.
187            use_native_locking: bool, Whether or not fcntl/win32 locking is
188                                used.
189        """
190        opener = None
191        if not opener and use_native_locking:
192            try:
193                from oauth2client.contrib._win32_opener import _Win32Opener
194                opener = _Win32Opener(filename, mode, fallback_mode)
195            except ImportError:
196                try:
197                    from oauth2client.contrib._fcntl_opener import _FcntlOpener
198                    opener = _FcntlOpener(filename, mode, fallback_mode)
199                except ImportError:
200                    pass
201
202        if not opener:
203            opener = _PosixOpener(filename, mode, fallback_mode)
204
205        self._opener = opener
206
207    def filename(self):
208        """Return the filename we were constructed with."""
209        return self._opener._filename
210
211    def file_handle(self):
212        """Return the file_handle to the opened file."""
213        return self._opener.file_handle()
214
215    def is_locked(self):
216        """Return whether we successfully locked the file."""
217        return self._opener.is_locked()
218
219    def open_and_lock(self, timeout=0, delay=0.05):
220        """Open the file, trying to lock it.
221
222        Args:
223            timeout: float, The number of seconds to try to acquire the lock.
224            delay: float, The number of seconds to wait between retry attempts.
225
226        Raises:
227            AlreadyLockedException: if the lock is already acquired.
228            IOError: if the open fails.
229        """
230        self._opener.open_and_lock(timeout, delay)
231
232    def unlock_and_close(self):
233        """Unlock and close a file."""
234        self._opener.unlock_and_close()
235