blob: 8f35c90b9b14b498a6cedf228198c003b22d8a77 [file] [log] [blame]
Joe Gregorio2dfd74e2012-06-13 12:18:30 -04001# Copyright 2011 Google Inc. All Rights Reserved.
2
3"""Locked file interface that should work on Unix and Windows pythons.
4
5This module first tries to use fcntl locking to ensure serialized access
6to a file, then falls back on a lock file if that is unavialable.
7
8Usage:
9 f = LockedFile('filename', 'r+b', 'rb')
10 f.open_and_lock()
11 if f.is_locked():
12 print 'Acquired filename with r+b mode'
13 f.file_handle().write('locked data')
14 else:
15 print 'Aquired filename with rb mode'
16 f.unlock_and_close()
17"""
18
19__author__ = 'cache@google.com (David T McWherter)'
20
21import errno
22import logging
23import os
24import time
25
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040026from oauth2client import util
27
Joe Gregorio2dfd74e2012-06-13 12:18:30 -040028logger = logging.getLogger(__name__)
29
30
31class AlreadyLockedException(Exception):
32 """Trying to lock a file that has already been locked by the LockedFile."""
33 pass
34
35
36class _Opener(object):
37 """Base class for different locking primitives."""
38
39 def __init__(self, filename, mode, fallback_mode):
40 """Create an Opener.
41
42 Args:
43 filename: string, The pathname of the file.
44 mode: string, The preferred mode to access the file with.
45 fallback_mode: string, The mode to use if locking fails.
46 """
47 self._locked = False
48 self._filename = filename
49 self._mode = mode
50 self._fallback_mode = fallback_mode
51 self._fh = None
52
53 def is_locked(self):
54 """Was the file locked."""
55 return self._locked
56
57 def file_handle(self):
58 """The file handle to the file. Valid only after opened."""
59 return self._fh
60
61 def filename(self):
62 """The filename that is being locked."""
63 return self._filename
64
65 def open_and_lock(self, timeout, delay):
66 """Open the file and lock it.
67
68 Args:
69 timeout: float, How long to try to lock for.
70 delay: float, How long to wait between retries.
71 """
72 pass
73
74 def unlock_and_close(self):
75 """Unlock and close the file."""
76 pass
77
78
79class _PosixOpener(_Opener):
80 """Lock files using Posix advisory lock files."""
81
82 def open_and_lock(self, timeout, delay):
83 """Open the file and lock it.
84
85 Tries to create a .lock file next to the file we're trying to open.
86
87 Args:
88 timeout: float, How long to try to lock for.
89 delay: float, How long to wait between retries.
90
91 Raises:
92 AlreadyLockedException: if the lock is already acquired.
93 IOError: if the open fails.
94 """
95 if self._locked:
96 raise AlreadyLockedException('File %s is already locked' %
97 self._filename)
98 self._locked = False
99
100 try:
101 self._fh = open(self._filename, self._mode)
102 except IOError, e:
103 # If we can't access with _mode, try _fallback_mode and don't lock.
104 if e.errno == errno.EACCES:
105 self._fh = open(self._filename, self._fallback_mode)
106 return
107
108 lock_filename = self._posix_lockfile(self._filename)
109 start_time = time.time()
110 while True:
111 try:
112 self._lock_fd = os.open(lock_filename,
113 os.O_CREAT|os.O_EXCL|os.O_RDWR)
114 self._locked = True
115 break
116
117 except OSError, e:
118 if e.errno != errno.EEXIST:
119 raise
120 if (time.time() - start_time) >= timeout:
121 logger.warn('Could not acquire lock %s in %s seconds' % (
122 lock_filename, timeout))
123 # Close the file and open in fallback_mode.
124 if self._fh:
125 self._fh.close()
126 self._fh = open(self._filename, self._fallback_mode)
127 return
128 time.sleep(delay)
129
130 def unlock_and_close(self):
131 """Unlock a file by removing the .lock file, and close the handle."""
132 if self._locked:
133 lock_filename = self._posix_lockfile(self._filename)
134 os.unlink(lock_filename)
135 os.close(self._lock_fd)
136 self._locked = False
137 self._lock_fd = None
138 if self._fh:
139 self._fh.close()
140
141 def _posix_lockfile(self, filename):
142 """The name of the lock file to use for posix locking."""
143 return '%s.lock' % filename
144
145
146try:
147 import fcntl
Joe Gregoriof46ea552012-06-20 09:49:26 -0400148
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400149 class _FcntlOpener(_Opener):
150 """Open, lock, and unlock a file using fcntl.lockf."""
151
152 def open_and_lock(self, timeout, delay):
153 """Open the file and lock it.
154
155 Args:
156 timeout: float, How long to try to lock for.
157 delay: float, How long to wait between retries
158
159 Raises:
160 AlreadyLockedException: if the lock is already acquired.
161 IOError: if the open fails.
162 """
163 if self._locked:
164 raise AlreadyLockedException('File %s is already locked' %
165 self._filename)
166 start_time = time.time()
167
168 try:
169 self._fh = open(self._filename, self._mode)
170 except IOError, e:
171 # If we can't access with _mode, try _fallback_mode and don't lock.
172 if e.errno == errno.EACCES:
173 self._fh = open(self._filename, self._fallback_mode)
174 return
175
176 # We opened in _mode, try to lock the file.
177 while True:
178 try:
179 fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
180 self._locked = True
181 return
182 except IOError, e:
183 # If not retrying, then just pass on the error.
184 if timeout == 0:
185 raise e
186 if e.errno != errno.EACCES:
187 raise e
188 # We could not acquire the lock. Try again.
189 if (time.time() - start_time) >= timeout:
190 logger.warn('Could not lock %s in %s seconds' % (
191 self._filename, timeout))
192 if self._fh:
193 self._fh.close()
194 self._fh = open(self._filename, self._fallback_mode)
195 return
196 time.sleep(delay)
197
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400198 def unlock_and_close(self):
199 """Close and unlock the file using the fcntl.lockf primitive."""
200 if self._locked:
201 fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
202 self._locked = False
203 if self._fh:
204 self._fh.close()
205except ImportError:
206 _FcntlOpener = None
207
208
Joe Gregoriof46ea552012-06-20 09:49:26 -0400209try:
210 import pywintypes
211 import win32con
212 import win32file
213
214 class _Win32Opener(_Opener):
215 """Open, lock, and unlock a file using windows primitives."""
216
217 # Error #33:
218 # 'The process cannot access the file because another process'
219 FILE_IN_USE_ERROR = 33
220
221 # Error #158:
222 # 'The segment is already unlocked.'
223 FILE_ALREADY_UNLOCKED_ERROR = 158
224
225 def open_and_lock(self, timeout, delay):
226 """Open the file and lock it.
227
228 Args:
229 timeout: float, How long to try to lock for.
230 delay: float, How long to wait between retries
231
232 Raises:
233 AlreadyLockedException: if the lock is already acquired.
234 IOError: if the open fails.
235 """
236 if self._locked:
237 raise AlreadyLockedException('File %s is already locked' %
238 self._filename)
239 start_time = time.time()
240
241 try:
242 self._fh = open(self._filename, self._mode)
243 except IOError, e:
244 # If we can't access with _mode, try _fallback_mode and don't lock.
245 if e.errno == errno.EACCES:
246 self._fh = open(self._filename, self._fallback_mode)
247 return
248
249 # We opened in _mode, try to lock the file.
250 while True:
251 try:
252 hfile = win32file._get_osfhandle(self._fh.fileno())
253 win32file.LockFileEx(
254 hfile,
255 (win32con.LOCKFILE_FAIL_IMMEDIATELY|
256 win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
257 pywintypes.OVERLAPPED())
258 self._locked = True
259 return
260 except pywintypes.error, e:
261 if timeout == 0:
262 raise e
263
264 # If the error is not that the file is already in use, raise.
265 if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
266 raise
267
268 # We could not acquire the lock. Try again.
269 if (time.time() - start_time) >= timeout:
270 logger.warn('Could not lock %s in %s seconds' % (
271 self._filename, timeout))
272 if self._fh:
273 self._fh.close()
274 self._fh = open(self._filename, self._fallback_mode)
275 return
276 time.sleep(delay)
277
278 def unlock_and_close(self):
279 """Close and unlock the file using the win32 primitive."""
280 if self._locked:
281 try:
282 hfile = win32file._get_osfhandle(self._fh.fileno())
283 win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED())
284 except pywintypes.error, e:
285 if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
286 raise
287 self._locked = False
288 if self._fh:
289 self._fh.close()
290except ImportError:
291 _Win32Opener = None
292
293
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400294class LockedFile(object):
295 """Represent a file that has exclusive access."""
296
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400297 @util.positional(4)
Joe Gregoriof46ea552012-06-20 09:49:26 -0400298 def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400299 """Construct a LockedFile.
300
301 Args:
302 filename: string, The path of the file to open.
303 mode: string, The mode to try to open the file with.
304 fallback_mode: string, The mode to use if locking fails.
Joe Gregoriof46ea552012-06-20 09:49:26 -0400305 use_native_locking: bool, Whether or not fcntl/win32 locking is used.
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400306 """
Joe Gregoriof46ea552012-06-20 09:49:26 -0400307 opener = None
308 if not opener and use_native_locking:
309 if _Win32Opener:
310 opener = _Win32Opener(filename, mode, fallback_mode)
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400311 if _FcntlOpener:
Joe Gregoriof46ea552012-06-20 09:49:26 -0400312 opener = _FcntlOpener(filename, mode, fallback_mode)
313
314 if not opener:
315 opener = _PosixOpener(filename, mode, fallback_mode)
316
317 self._opener = opener
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400318
319 def filename(self):
320 """Return the filename we were constructed with."""
321 return self._opener._filename
322
323 def file_handle(self):
324 """Return the file_handle to the opened file."""
325 return self._opener.file_handle()
326
327 def is_locked(self):
328 """Return whether we successfully locked the file."""
329 return self._opener.is_locked()
330
331 def open_and_lock(self, timeout=0, delay=0.05):
332 """Open the file, trying to lock it.
333
334 Args:
335 timeout: float, The number of seconds to try to acquire the lock.
336 delay: float, The number of seconds to wait between retry attempts.
337
338 Raises:
339 AlreadyLockedException: if the lock is already acquired.
340 IOError: if the open fails.
341 """
342 self._opener.open_and_lock(timeout, delay)
343
344 def unlock_and_close(self):
345 """Unlock and close a file."""
346 self._opener.unlock_and_close()