2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
1 |
#!/usr/bin/env python
|
2 |
# -*- coding: latin1 -*-
|
|
3 |
#----------------------------------------------------------------------------
|
|
4 |
# glock.py: Global mutex
|
|
5 |
#
|
|
6 |
# See __doc__ string below.
|
|
7 |
#
|
|
8 |
# Requires:
|
|
9 |
# - Python 1.5.2 or newer (www.python.org)
|
|
10 |
# - On windows: win32 extensions installed
|
|
11 |
# (http://www.python.org/windows/win32all/win32all.exe)
|
|
12 |
# - OS: Unix, Windows.
|
|
13 |
#
|
|
14 |
# $Id: //depot/rgutils/rgutils/glock.py#5 $
|
|
15 |
#----------------------------------------------------------------------------
|
|
16 |
'''
|
|
17 |
This module defines the class GlobalLock that implements a global
|
|
18 |
(inter-process) mutex on Windows and Unix, using file-locking on
|
|
19 |
Unix.
|
|
20 |
||
21 |
@see: class L{GlobalLock} for more details.
|
|
22 |
'''
|
|
23 |
__version__ = '0.2.' + '$Revision: #5 $'[12:-2] |
|
24 |
__author__ = 'Richard Gruet', 'rjgruet@yahoo.com' |
|
25 |
__date__ = '$Date: 2005/06/19 $'[7:-2], '$Author: rgruet $'[9:-2] |
|
26 |
__since__ = '2000-01-22' |
|
27 |
__doc__ += '\n@author: %s (U{%s})\n@version: %s' % (__author__[0], |
|
28 |
__author__[1], __version__) |
|
29 |
__all__ = ['GlobalLock', 'GlobalLockError', 'LockAlreadyAcquired', 'NotOwner'] |
|
30 |
||
31 |
# Imports:
|
|
32 |
import sys, string, os, errno, re |
|
33 |
||
34 |
# System-dependent imports for locking implementation:
|
|
35 |
_windows = (sys.platform == 'win32') |
|
36 |
||
37 |
if _windows: |
|
38 |
try: |
|
39 |
import win32event, win32api, pywintypes |
|
40 |
except ImportError: |
|
41 |
sys.stderr.write('The win32 extensions need to be installed!') |
|
42 |
try: |
|
43 |
import ctypes |
|
44 |
except ImportError: |
|
45 |
ctypes = None |
|
46 |
else: # assume Unix |
|
47 |
try: |
|
48 |
import fcntl |
|
49 |
except ImportError: |
|
50 |
sys.stderr.write("On what kind of OS am I ? (Mac?) I should be on " |
|
51 |
"Unix but can't import fcntl.\n") |
|
52 |
raise
|
|
53 |
import threading |
|
54 |
||
55 |
# Exceptions :
|
|
56 |
# ----------
|
|
57 |
class GlobalLockError(Exception): |
|
58 |
''' Error raised by the glock module.
|
|
59 |
'''
|
|
60 |
pass
|
|
61 |
||
62 |
class NotOwner(GlobalLockError): |
|
63 |
''' Attempt to release somebody else's lock.
|
|
64 |
'''
|
|
65 |
pass
|
|
66 |
||
67 |
class LockAlreadyAcquired(GlobalLockError): |
|
68 |
''' Non-blocking acquire but lock already seized.
|
|
69 |
'''
|
|
70 |
pass
|
|
71 |
||
72 |
||
73 |
# Constants
|
|
74 |
# ---------:
|
|
75 |
if sys.version[:3] < '2.2': |
|
76 |
True, False = 1, 0 # built-in in Python 2.2+ |
|
77 |
||
78 |
#----------------------------------------------------------------------------
|
|
79 |
class GlobalLock: |
|
80 |
#----------------------------------------------------------------------------
|
|
81 |
''' A global mutex.
|
|
82 |
||
83 |
B{Specification}
|
|
84 |
||
85 |
- The lock must act as a global mutex, ie block between different
|
|
86 |
candidate processus, but ALSO between different candidate
|
|
87 |
threads of the same process.
|
|
88 |
||
89 |
- It must NOT block in case of reentrant lock request issued by
|
|
90 |
the SAME thread.
|
|
91 |
- Extraneous unlocks should be ideally harmless.
|
|
92 |
||
93 |
B{Implementation}
|
|
94 |
||
95 |
In Python there is no portable global lock AFAIK. There is only a
|
|
96 |
LOCAL/ in-process Lock mechanism (threading.RLock), so we have to
|
|
97 |
implement our own solution:
|
|
98 |
||
99 |
- Unix: use fcntl.flock(). Recursive calls OK. Different process OK.
|
|
100 |
But <> threads, same process don't block so we have to use an extra
|
|
101 |
threading.RLock to fix that point.
|
|
102 |
- Windows: We use WIN32 mutex from Python Win32 extensions. Can't use
|
|
103 |
std module msvcrt.locking(), because global lock is OK, but
|
|
104 |
blocks also for 2 calls from the same thread!
|
|
105 |
'''
|
|
106 |
RE_ERROR_MSG = re.compile ("^\[Errno ([0-9]+)\]") |
|
107 |
||
3691.348.1
by kiko
Remove the original lockfile class and use our contributed GlobalLock everywhere to avoid stale locks making our scripts stop to run. Update a bunch of scripts to use it. Hopefully backwards-compatible enough to survive tests. |
108 |
def __init__(self, fpath, lockInitially=False, logger=None): |
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
109 |
''' Creates (or opens) a global lock.
|
110 |
||
111 |
@param fpath: Path of the file used as lock target. This is also
|
|
3691.348.11
by kiko
Nail remaining test failures: delete locks unconditionally except for the poppy use case, and control this via an option in GlobalLock.release(). |
112 |
the global id of the lock. The file will be created
|
113 |
if non existent.
|
|
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
114 |
@param lockInitially: if True locks initially.
|
3691.348.11
by kiko
Nail remaining test failures: delete locks unconditionally except for the poppy use case, and control this via an option in GlobalLock.release(). |
115 |
@param logger: an optional logger object.
|
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
116 |
'''
|
3691.348.1
by kiko
Remove the original lockfile class and use our contributed GlobalLock everywhere to avoid stale locks making our scripts stop to run. Update a bunch of scripts to use it. Hopefully backwards-compatible enough to survive tests. |
117 |
self.logger = logger |
118 |
self.fpath = fpath |
|
119 |
if os.path.exists(fpath): |
|
120 |
self.previous_lockfile_present = True |
|
121 |
else: |
|
122 |
self.previous_lockfile_present = False |
|
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
123 |
if _windows: |
124 |
self.name = string.replace(fpath, '\\', '_') |
|
125 |
self.mutex = win32event.CreateMutex(None, lockInitially, self.name) |
|
126 |
else: # Unix |
|
127 |
self.name = fpath |
|
128 |
self.flock = open(fpath, 'w') |
|
129 |
self.fdlock = self.flock.fileno() |
|
130 |
self.threadLock = threading.RLock() |
|
131 |
if lockInitially: |
|
132 |
self.acquire() |
|
133 |
||
134 |
def __del__(self): |
|
135 |
#print '__del__ called' ##
|
|
136 |
try: self.release() |
|
137 |
except: pass |
|
138 |
if _windows: |
|
139 |
win32api.CloseHandle(self.mutex) |
|
140 |
else: |
|
141 |
try: self.flock.close() |
|
142 |
except: pass |
|
143 |
||
144 |
def __repr__(self): |
|
145 |
return '<Global lock @ %s>' % self.name |
|
146 |
||
3691.348.1
by kiko
Remove the original lockfile class and use our contributed GlobalLock everywhere to avoid stale locks making our scripts stop to run. Update a bunch of scripts to use it. Hopefully backwards-compatible enough to survive tests. |
147 |
def acquire(self, blocking=False): |
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
148 |
""" Locks. Attemps to acquire a lock.
|
149 |
||
150 |
@param blocking: If True, suspends caller until done. Otherwise,
|
|
151 |
LockAlreadyAcquired is raised if the lock cannot be acquired immediately.
|
|
152 |
||
153 |
On windows an IOError is always raised after ~10 sec if the lock
|
|
154 |
can't be acquired.
|
|
155 |
@exception GlobalLockError: if lock can't be acquired (timeout)
|
|
156 |
@exception LockAlreadyAcquired: someone already has the lock and
|
|
157 |
the caller decided not to block.
|
|
158 |
"""
|
|
3691.348.1
by kiko
Remove the original lockfile class and use our contributed GlobalLock everywhere to avoid stale locks making our scripts stop to run. Update a bunch of scripts to use it. Hopefully backwards-compatible enough to survive tests. |
159 |
if self.logger: |
7675.624.11
by Tim Penhey
Make the capitalisation consistent with other logging messages, and stay where the lock file is that is being created. |
160 |
self.logger.info('Creating lockfile: %s', self.fpath) |
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
161 |
if _windows: |
162 |
if blocking: |
|
163 |
timeout = win32event.INFINITE |
|
164 |
else: |
|
165 |
timeout = 0 |
|
166 |
r = win32event.WaitForSingleObject(self.mutex, timeout) |
|
167 |
if r == win32event.WAIT_FAILED: |
|
168 |
raise GlobalLockError("Can't acquire mutex: error") |
|
169 |
if not blocking and r == win32event.WAIT_TIMEOUT: |
|
170 |
raise LockAlreadyAcquired('Lock %s already acquired by ' |
|
171 |
'someone else' % self.name) |
|
172 |
else: |
|
173 |
# First, acquire the global (inter-process) lock:
|
|
174 |
if blocking: |
|
175 |
options = fcntl.LOCK_EX |
|
176 |
else: |
|
177 |
options = fcntl.LOCK_EX|fcntl.LOCK_NB |
|
178 |
try: |
|
179 |
fcntl.flock(self.fdlock, options) |
|
180 |
except IOError, message: #(errno 13: perm. denied, |
|
181 |
# 36: Resource deadlock avoided)
|
|
182 |
if not blocking and self._errnoOf (message) == errno.EWOULDBLOCK: |
|
183 |
raise LockAlreadyAcquired('Lock %s already acquired by ' |
|
184 |
'someone else' % self.name) |
|
185 |
else: |
|
186 |
raise GlobalLockError('Cannot acquire lock on "file" ' |
|
187 |
'%s: %s\n' % (self.name, message)) |
|
188 |
#print 'got file lock.' ##
|
|
189 |
||
190 |
# Then acquire the local (inter-thread) lock:
|
|
191 |
if not self.threadLock.acquire(blocking): |
|
192 |
fcntl.flock(self.fdlock, fcntl.LOCK_UN) # release global lock |
|
193 |
raise LockAlreadyAcquired('Lock %s already acquired by ' |
|
194 |
'someone else' % self.name) |
|
3691.348.1
by kiko
Remove the original lockfile class and use our contributed GlobalLock everywhere to avoid stale locks making our scripts stop to run. Update a bunch of scripts to use it. Hopefully backwards-compatible enough to survive tests. |
195 |
if self.previous_lockfile_present and self.logger: |
196 |
self.logger.warn("Stale lockfile detected and claimed.") |
|
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
197 |
#print 'got thread lock.' ##
|
198 |
||
3691.348.3
by kiko
Fix a leftover script (sorry\!) and change the place we set is_locked to true in glock.py |
199 |
self.is_locked = True |
3691.348.1
by kiko
Remove the original lockfile class and use our contributed GlobalLock everywhere to avoid stale locks making our scripts stop to run. Update a bunch of scripts to use it. Hopefully backwards-compatible enough to survive tests. |
200 |
|
3691.348.11
by kiko
Nail remaining test failures: delete locks unconditionally except for the poppy use case, and control this via an option in GlobalLock.release(). |
201 |
def release(self, skip_delete=False): |
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
202 |
''' Unlocks. (caller must own the lock!)
|
203 |
||
3691.348.11
by kiko
Nail remaining test failures: delete locks unconditionally except for the poppy use case, and control this via an option in GlobalLock.release(). |
204 |
@param skip_delete: don't try to delete the file. This can
|
205 |
be used when the original filename has changed; for
|
|
206 |
instance, if the lockfile is erased out-of-band, or if
|
|
207 |
the directory it contains has been renamed.
|
|
208 |
||
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
209 |
@return: The lock count.
|
210 |
@exception IOError: if file lock can't be released
|
|
211 |
@exception NotOwner: Attempt to release somebody else's lock.
|
|
212 |
'''
|
|
3691.348.1
by kiko
Remove the original lockfile class and use our contributed GlobalLock everywhere to avoid stale locks making our scripts stop to run. Update a bunch of scripts to use it. Hopefully backwards-compatible enough to survive tests. |
213 |
if not self.is_locked: |
214 |
return
|
|
3691.348.11
by kiko
Nail remaining test failures: delete locks unconditionally except for the poppy use case, and control this via an option in GlobalLock.release(). |
215 |
if not skip_delete: |
3691.348.9
by kiko
Make the removal of the lockfile conditional on it existing, since it may have been moved away and it is impossible to find out how in a portable manner. |
216 |
if self.logger: |
217 |
self.logger.debug('Removing lock file: %s', self.fpath) |
|
218 |
os.unlink(self.fpath) |
|
219 |
elif self.logger: |
|
220 |
# At certain times the lockfile will have been removed or
|
|
221 |
# moved away before we call release(); log a message because
|
|
222 |
# this is unusual and could be an error.
|
|
223 |
self.logger.debug('Oops, my lock file disappeared: %s', self.fpath) |
|
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
224 |
if _windows: |
225 |
if ctypes: |
|
226 |
result = ctypes.windll.kernel32.ReleaseMutex(self.mutex.handle) |
|
227 |
if not result: |
|
228 |
raise NotOwner("Attempt to release somebody else's lock") |
|
229 |
else: |
|
230 |
try: |
|
231 |
win32event.ReleaseMutex(self.mutex) |
|
232 |
#print "released mutex"
|
|
233 |
except pywintypes.error, e: |
|
234 |
errCode, fctName, errMsg = e.args |
|
235 |
if errCode == 288: |
|
236 |
raise NotOwner("Attempt to release somebody else's lock") |
|
237 |
else: |
|
238 |
raise GlobalLockError('%s: err#%d: %s' % (fctName, errCode, |
|
239 |
errMsg)) |
|
240 |
else: |
|
241 |
# First release the local (inter-thread) lock:
|
|
242 |
try: |
|
243 |
self.threadLock.release() |
|
244 |
except AssertionError: |
|
245 |
raise NotOwner("Attempt to release somebody else's lock") |
|
246 |
||
247 |
# Then release the global (inter-process) lock:
|
|
248 |
try: |
|
249 |
fcntl.flock(self.fdlock, fcntl.LOCK_UN) |
|
250 |
except IOError: # (errno 13: permission denied) |
|
251 |
raise GlobalLockError('Unlock of file "%s" failed\n' % |
|
252 |
self.name) |
|
3691.348.1
by kiko
Remove the original lockfile class and use our contributed GlobalLock everywhere to avoid stale locks making our scripts stop to run. Update a bunch of scripts to use it. Hopefully backwards-compatible enough to survive tests. |
253 |
self.is_locked = False |
2155
by Canonical.com Patch Queue Manager
Many fixes and tests for the poimport script r=salgado |
254 |
|
255 |
def _errnoOf (self, message): |
|
256 |
match = self.RE_ERROR_MSG.search(str(message)) |
|
257 |
if match: |
|
258 |
return int(match.group(1)) |
|
259 |
else: |
|
260 |
raise Exception ('Malformed error message "%s"' % message) |
|
261 |
||
262 |
#----------------------------------------------------------------------------
|
|
263 |
def test(): |
|
264 |
#----------------------------------------------------------------------------
|
|
265 |
##TODO: a more serious test with distinct processes !
|
|
266 |
||
267 |
print 'Testing glock.py...' |
|
268 |
||
269 |
# unfortunately can't test inter-process lock here!
|
|
270 |
lockName = 'myFirstLock' |
|
271 |
l = GlobalLock(lockName) |
|
272 |
if not _windows: |
|
273 |
assert os.path.exists(lockName) |
|
274 |
l.acquire() |
|
275 |
l.acquire() # reentrant lock, must not block |
|
276 |
l.release() |
|
277 |
l.release() |
|
278 |
||
279 |
try: l.release() |
|
280 |
except NotOwner: pass |
|
281 |
else: raise Exception('should have raised a NotOwner exception') |
|
282 |
||
283 |
# Check that <> threads of same process do block:
|
|
284 |
import threading, time |
|
285 |
thread = threading.Thread(target=threadMain, args=(l,)) |
|
286 |
print 'main: locking...', |
|
287 |
l.acquire() |
|
288 |
print ' done.' |
|
289 |
thread.start() |
|
290 |
time.sleep(3) |
|
291 |
print '\nmain: unlocking...', |
|
292 |
l.release() |
|
293 |
print ' done.' |
|
294 |
time.sleep(0.1) |
|
295 |
||
296 |
print '=> Test of glock.py passed.' |
|
297 |
return l |
|
298 |
||
299 |
def threadMain(lock): |
|
300 |
print 'thread started(%s).' % lock |
|
301 |
try: lock.acquire(blocking=False) |
|
302 |
except LockAlreadyAcquired: pass |
|
303 |
else: raise Exception('should have raised LockAlreadyAcquired') |
|
304 |
print 'thread: locking (should stay blocked for ~ 3 sec)...', |
|
305 |
lock.acquire() |
|
306 |
print 'thread: locking done.' |
|
307 |
print 'thread: unlocking...', |
|
308 |
lock.release() |
|
309 |
print ' done.' |
|
310 |
print 'thread ended.' |
|
311 |
||
312 |
#----------------------------------------------------------------------------
|
|
313 |
# M A I N
|
|
314 |
#----------------------------------------------------------------------------
|
|
315 |
if __name__ == "__main__": |
|
316 |
l = test() |