blob: 8a7aff5c440e5273ece9b649c13cdcca19762bd0 [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
26logger = logging.getLogger(__name__)
27
28
29class AlreadyLockedException(Exception):
30 """Trying to lock a file that has already been locked by the LockedFile."""
31 pass
32
33
34class _Opener(object):
35 """Base class for different locking primitives."""
36
37 def __init__(self, filename, mode, fallback_mode):
38 """Create an Opener.
39
40 Args:
41 filename: string, The pathname of the file.
42 mode: string, The preferred mode to access the file with.
43 fallback_mode: string, The mode to use if locking fails.
44 """
45 self._locked = False
46 self._filename = filename
47 self._mode = mode
48 self._fallback_mode = fallback_mode
49 self._fh = None
50
51 def is_locked(self):
52 """Was the file locked."""
53 return self._locked
54
55 def file_handle(self):
56 """The file handle to the file. Valid only after opened."""
57 return self._fh
58
59 def filename(self):
60 """The filename that is being locked."""
61 return self._filename
62
63 def open_and_lock(self, timeout, delay):
64 """Open the file and lock it.
65
66 Args:
67 timeout: float, How long to try to lock for.
68 delay: float, How long to wait between retries.
69 """
70 pass
71
72 def unlock_and_close(self):
73 """Unlock and close the file."""
74 pass
75
76
77class _PosixOpener(_Opener):
78 """Lock files using Posix advisory lock files."""
79
80 def open_and_lock(self, timeout, delay):
81 """Open the file and lock it.
82
83 Tries to create a .lock file next to the file we're trying to open.
84
85 Args:
86 timeout: float, How long to try to lock for.
87 delay: float, How long to wait between retries.
88
89 Raises:
90 AlreadyLockedException: if the lock is already acquired.
91 IOError: if the open fails.
92 """
93 if self._locked:
94 raise AlreadyLockedException('File %s is already locked' %
95 self._filename)
96 self._locked = False
97
98 try:
99 self._fh = open(self._filename, self._mode)
100 except IOError, e:
101 # If we can't access with _mode, try _fallback_mode and don't lock.
102 if e.errno == errno.EACCES:
103 self._fh = open(self._filename, self._fallback_mode)
104 return
105
106 lock_filename = self._posix_lockfile(self._filename)
107 start_time = time.time()
108 while True:
109 try:
110 self._lock_fd = os.open(lock_filename,
111 os.O_CREAT|os.O_EXCL|os.O_RDWR)
112 self._locked = True
113 break
114
115 except OSError, e:
116 if e.errno != errno.EEXIST:
117 raise
118 if (time.time() - start_time) >= timeout:
119 logger.warn('Could not acquire lock %s in %s seconds' % (
120 lock_filename, timeout))
121 # Close the file and open in fallback_mode.
122 if self._fh:
123 self._fh.close()
124 self._fh = open(self._filename, self._fallback_mode)
125 return
126 time.sleep(delay)
127
128 def unlock_and_close(self):
129 """Unlock a file by removing the .lock file, and close the handle."""
130 if self._locked:
131 lock_filename = self._posix_lockfile(self._filename)
132 os.unlink(lock_filename)
133 os.close(self._lock_fd)
134 self._locked = False
135 self._lock_fd = None
136 if self._fh:
137 self._fh.close()
138
139 def _posix_lockfile(self, filename):
140 """The name of the lock file to use for posix locking."""
141 return '%s.lock' % filename
142
143
144try:
145 import fcntl
Joe Gregoriof46ea552012-06-20 09:49:26 -0400146
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400147 class _FcntlOpener(_Opener):
148 """Open, lock, and unlock a file using fcntl.lockf."""
149
150 def open_and_lock(self, timeout, delay):
151 """Open the file and lock it.
152
153 Args:
154 timeout: float, How long to try to lock for.
155 delay: float, How long to wait between retries
156
157 Raises:
158 AlreadyLockedException: if the lock is already acquired.
159 IOError: if the open fails.
160 """
161 if self._locked:
162 raise AlreadyLockedException('File %s is already locked' %
163 self._filename)
164 start_time = time.time()
165
166 try:
167 self._fh = open(self._filename, self._mode)
168 except IOError, e:
169 # If we can't access with _mode, try _fallback_mode and don't lock.
170 if e.errno == errno.EACCES:
171 self._fh = open(self._filename, self._fallback_mode)
172 return
173
174 # We opened in _mode, try to lock the file.
175 while True:
176 try:
177 fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
178 self._locked = True
179 return
180 except IOError, e:
181 # If not retrying, then just pass on the error.
182 if timeout == 0:
183 raise e
184 if e.errno != errno.EACCES:
185 raise e
186 # We could not acquire the lock. Try again.
187 if (time.time() - start_time) >= timeout:
188 logger.warn('Could not lock %s in %s seconds' % (
189 self._filename, timeout))
190 if self._fh:
191 self._fh.close()
192 self._fh = open(self._filename, self._fallback_mode)
193 return
194 time.sleep(delay)
195
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400196 def unlock_and_close(self):
197 """Close and unlock the file using the fcntl.lockf primitive."""
198 if self._locked:
199 fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
200 self._locked = False
201 if self._fh:
202 self._fh.close()
203except ImportError:
204 _FcntlOpener = None
205
206
Joe Gregoriof46ea552012-06-20 09:49:26 -0400207try:
208 import pywintypes
209 import win32con
210 import win32file
211
212 class _Win32Opener(_Opener):
213 """Open, lock, and unlock a file using windows primitives."""
214
215 # Error #33:
216 # 'The process cannot access the file because another process'
217 FILE_IN_USE_ERROR = 33
218
219 # Error #158:
220 # 'The segment is already unlocked.'
221 FILE_ALREADY_UNLOCKED_ERROR = 158
222
223 def open_and_lock(self, timeout, delay):
224 """Open the file and lock it.
225
226 Args:
227 timeout: float, How long to try to lock for.
228 delay: float, How long to wait between retries
229
230 Raises:
231 AlreadyLockedException: if the lock is already acquired.
232 IOError: if the open fails.
233 """
234 if self._locked:
235 raise AlreadyLockedException('File %s is already locked' %
236 self._filename)
237 start_time = time.time()
238
239 try:
240 self._fh = open(self._filename, self._mode)
241 except IOError, e:
242 # If we can't access with _mode, try _fallback_mode and don't lock.
243 if e.errno == errno.EACCES:
244 self._fh = open(self._filename, self._fallback_mode)
245 return
246
247 # We opened in _mode, try to lock the file.
248 while True:
249 try:
250 hfile = win32file._get_osfhandle(self._fh.fileno())
251 win32file.LockFileEx(
252 hfile,
253 (win32con.LOCKFILE_FAIL_IMMEDIATELY|
254 win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
255 pywintypes.OVERLAPPED())
256 self._locked = True
257 return
258 except pywintypes.error, e:
259 if timeout == 0:
260 raise e
261
262 # If the error is not that the file is already in use, raise.
263 if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
264 raise
265
266 # We could not acquire the lock. Try again.
267 if (time.time() - start_time) >= timeout:
268 logger.warn('Could not lock %s in %s seconds' % (
269 self._filename, timeout))
270 if self._fh:
271 self._fh.close()
272 self._fh = open(self._filename, self._fallback_mode)
273 return
274 time.sleep(delay)
275
276 def unlock_and_close(self):
277 """Close and unlock the file using the win32 primitive."""
278 if self._locked:
279 try:
280 hfile = win32file._get_osfhandle(self._fh.fileno())
281 win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED())
282 except pywintypes.error, e:
283 if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
284 raise
285 self._locked = False
286 if self._fh:
287 self._fh.close()
288except ImportError:
289 _Win32Opener = None
290
291
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400292class LockedFile(object):
293 """Represent a file that has exclusive access."""
294
Joe Gregoriof46ea552012-06-20 09:49:26 -0400295 def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400296 """Construct a LockedFile.
297
298 Args:
299 filename: string, The path of the file to open.
300 mode: string, The mode to try to open the file with.
301 fallback_mode: string, The mode to use if locking fails.
Joe Gregoriof46ea552012-06-20 09:49:26 -0400302 use_native_locking: bool, Whether or not fcntl/win32 locking is used.
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400303 """
Joe Gregoriof46ea552012-06-20 09:49:26 -0400304 opener = None
305 if not opener and use_native_locking:
306 if _Win32Opener:
307 opener = _Win32Opener(filename, mode, fallback_mode)
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400308 if _FcntlOpener:
Joe Gregoriof46ea552012-06-20 09:49:26 -0400309 opener = _FcntlOpener(filename, mode, fallback_mode)
310
311 if not opener:
312 opener = _PosixOpener(filename, mode, fallback_mode)
313
314 self._opener = opener
Joe Gregorio2dfd74e2012-06-13 12:18:30 -0400315
316 def filename(self):
317 """Return the filename we were constructed with."""
318 return self._opener._filename
319
320 def file_handle(self):
321 """Return the file_handle to the opened file."""
322 return self._opener.file_handle()
323
324 def is_locked(self):
325 """Return whether we successfully locked the file."""
326 return self._opener.is_locked()
327
328 def open_and_lock(self, timeout=0, delay=0.05):
329 """Open the file, trying to lock it.
330
331 Args:
332 timeout: float, The number of seconds to try to acquire the lock.
333 delay: float, The number of seconds to wait between retry attempts.
334
335 Raises:
336 AlreadyLockedException: if the lock is already acquired.
337 IOError: if the open fails.
338 """
339 self._opener.open_and_lock(timeout, delay)
340
341 def unlock_and_close(self):
342 """Unlock and close a file."""
343 self._opener.unlock_and_close()