removed main.cfg, not used anywhere
[limo.git] / glock.py
blob34eab5b8516cea3e46a74ccf34e4b9fddba4071a
1 #!/usr/bin/env python
2 # -*- coding: latin1 -*-
3 #----------------------------------------------------------------------------
4 # glock.py: Global mutex
6 # See __doc__ string below.
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.
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.
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']
31 # Imports:
32 import sys, string, os, errno, re
34 # System-dependent imports for locking implementation:
35 _windows = (sys.platform == 'win32')
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
55 # Exceptions :
56 # ----------
57 class GlobalLockError(Exception):
58 ''' Error raised by the glock module.
59 '''
60 pass
62 class NotOwner(GlobalLockError):
63 ''' Attempt to release somebody else's lock.
64 '''
65 pass
67 class LockAlreadyAcquired(GlobalLockError):
68 ''' Non-blocking acquire but lock already seized.
69 '''
70 pass
73 # Constants
74 # ---------:
75 if sys.version[:3] < '2.2':
76 True, False = 1, 0 # built-in in Python 2.2+
78 #----------------------------------------------------------------------------
79 class GlobalLock:
80 #----------------------------------------------------------------------------
81 ''' A global mutex.
83 B{Specification}
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.
89 - It must NOT block in case of reentrant lock request issued by
90 the SAME thread.
91 - Extraneous unlocks should be ideally harmless.
93 B{Implementation}
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:
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!
106 RE_ERROR_MSG = re.compile ("^\[Errno ([0-9]+)\]")
108 def __init__(self, fpath, lockInitially=False):
109 ''' Creates (or opens) a global lock.
111 @param fpath: Path of the file used as lock target. This is also
112 the global id of the lock. The file will be created
113 if non existent.
114 @param lockInitially: if True locks initially.
116 if _windows:
117 self.name = string.replace(fpath, '\\', '_')
118 self.mutex = win32event.CreateMutex(None, lockInitially, self.name)
119 else: # Unix
120 self.name = fpath
121 self.flock = open(fpath, 'w')
122 self.fdlock = self.flock.fileno()
123 self.threadLock = threading.RLock()
124 if lockInitially:
125 self.acquire()
127 def __del__(self):
128 #print '__del__ called' ##
129 try: self.release()
130 except: pass
131 if _windows:
132 win32api.CloseHandle(self.mutex)
133 else:
134 try: self.flock.close()
135 except: pass
137 def __repr__(self):
138 return '<Global lock @ %s>' % self.name
140 def acquire(self, blocking=True):
141 """ Locks. Attemps to acquire a lock.
143 @param blocking: If True, suspends caller until done. Otherwise,
144 LockAlreadyAcquired is raised if the lock cannot be acquired immediately.
146 On windows an IOError is always raised after ~10 sec if the lock
147 can't be acquired.
148 @exception GlobalLockError: if lock can't be acquired (timeout)
149 @exception LockAlreadyAcquired: someone already has the lock and
150 the caller decided not to block.
152 if _windows:
153 if blocking:
154 timeout = win32event.INFINITE
155 else:
156 timeout = 0
157 r = win32event.WaitForSingleObject(self.mutex, timeout)
158 if r == win32event.WAIT_FAILED:
159 raise GlobalLockError("Can't acquire mutex: error")
160 if not blocking and r == win32event.WAIT_TIMEOUT:
161 raise LockAlreadyAcquired('Lock %s already acquired by '
162 'someone else' % self.name)
163 else:
164 # First, acquire the global (inter-process) lock:
165 if blocking:
166 options = fcntl.LOCK_EX
167 else:
168 options = fcntl.LOCK_EX|fcntl.LOCK_NB
169 try:
170 fcntl.flock(self.fdlock, options)
171 except IOError, message: #(errno 13: perm. denied,
172 # 36: Resource deadlock avoided)
173 if not blocking and self._errnoOf (message) == errno.EWOULDBLOCK:
174 raise LockAlreadyAcquired('Lock %s already acquired by '
175 'someone else' % self.name)
176 else:
177 raise GlobalLockError('Cannot acquire lock on "file" '
178 '%s: %s\n' % (self.name, message))
179 #print 'got file lock.' ##
181 # Then acquire the local (inter-thread) lock:
182 if not self.threadLock.acquire(blocking):
183 fcntl.flock(self.fdlock, fcntl.LOCK_UN) # release global lock
184 raise LockAlreadyAcquired('Lock %s already acquired by '
185 'someone else' % self.name)
186 #print 'got thread lock.' ##
188 def release(self):
189 ''' Unlocks. (caller must own the lock!)
191 @return: The lock count.
192 @exception IOError: if file lock can't be released
193 @exception NotOwner: Attempt to release somebody else's lock.
195 if _windows:
196 if ctypes:
197 result = ctypes.windll.kernel32.ReleaseMutex(self.mutex.handle)
198 if not result:
199 raise NotOwner("Attempt to release somebody else's lock")
200 else:
201 try:
202 win32event.ReleaseMutex(self.mutex)
203 #print "released mutex"
204 except pywintypes.error, e:
205 errCode, fctName, errMsg = e.args
206 if errCode == 288:
207 raise NotOwner("Attempt to release somebody else's lock")
208 else:
209 raise GlobalLockError('%s: err#%d: %s' % (fctName, errCode,
210 errMsg))
211 else:
212 # First release the local (inter-thread) lock:
213 try:
214 self.threadLock.release()
215 except AssertionError:
216 raise NotOwner("Attempt to release somebody else's lock")
218 # Then release the global (inter-process) lock:
219 try:
220 fcntl.flock(self.fdlock, fcntl.LOCK_UN)
221 except IOError: # (errno 13: permission denied)
222 raise GlobalLockError('Unlock of file "%s" failed\n' %
223 self.name)
225 def _errnoOf (self, message):
226 match = self.RE_ERROR_MSG.search(str(message))
227 if match:
228 return int(match.group(1))
229 else:
230 raise Exception ('Malformed error message "%s"' % message)
232 #----------------------------------------------------------------------------
233 def test():
234 #----------------------------------------------------------------------------
235 ##TODO: a more serious test with distinct processes !
237 print 'Testing glock.py...'
239 # unfortunately can't test inter-process lock here!
240 lockName = 'myFirstLock'
241 l = GlobalLock(lockName)
242 if not _windows:
243 assert os.path.exists(lockName)
244 l.acquire()
245 l.acquire() # reentrant lock, must not block
246 l.release()
247 l.release()
249 try: l.release()
250 except NotOwner: pass
251 else: raise Exception('should have raised a NotOwner exception')
253 # Check that <> threads of same process do block:
254 import threading, time
255 thread = threading.Thread(target=threadMain, args=(l,))
256 print 'main: locking...',
257 l.acquire()
258 print ' done.'
259 thread.start()
260 time.sleep(3)
261 print '\nmain: unlocking...',
262 l.release()
263 print ' done.'
264 time.sleep(0.1)
266 print '=> Test of glock.py passed.'
267 return l
269 def threadMain(lock):
270 print 'thread started(%s).' % lock
271 try: lock.acquire(blocking=False)
272 except LockAlreadyAcquired: pass
273 else: raise Exception('should have raised LockAlreadyAcquired')
274 print 'thread: locking (should stay blocked for ~ 3 sec)...',
275 lock.acquire()
276 print 'thread: locking done.'
277 print 'thread: unlocking...',
278 lock.release()
279 print ' done.'
280 print 'thread ended.'
282 #----------------------------------------------------------------------------
283 # M A I N
284 #----------------------------------------------------------------------------
285 if __name__ == "__main__":
286 l = test()