2 # -*- coding: latin1 -*-
3 #----------------------------------------------------------------------------
4 # glock.py: Global mutex
6 # See __doc__ string below.
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 #----------------------------------------------------------------------------
17 This module defines the class GlobalLock that implements a global
18 (inter-process) mutex on Windows and Unix, using file-locking on
21 @see: class L{GlobalLock} for more details.
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']
32 import sys
, string
, os
, errno
, re
34 # System-dependent imports for locking implementation:
35 _windows
= (sys
.platform
== 'win32')
39 import win32event
, win32api
, pywintypes
41 sys
.stderr
.write('The win32 extensions need to be installed!')
50 sys
.stderr
.write("On what kind of OS am I ? (Mac?) I should be on "
51 "Unix but can't import fcntl.\n")
57 class GlobalLockError(Exception):
58 ''' Error raised by the glock module.
62 class NotOwner(GlobalLockError
):
63 ''' Attempt to release somebody else's lock.
67 class LockAlreadyAcquired(GlobalLockError
):
68 ''' Non-blocking acquire but lock already seized.
75 if sys
.version
[:3] < '2.2':
76 True, False = 1, 0 # built-in in Python 2.2+
78 #----------------------------------------------------------------------------
80 #----------------------------------------------------------------------------
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
91 - Extraneous unlocks should be ideally harmless.
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
114 @param lockInitially: if True locks initially.
117 self
.name
= string
.replace(fpath
, '\\', '_')
118 self
.mutex
= win32event
.CreateMutex(None, lockInitially
, self
.name
)
121 self
.flock
= open(fpath
, 'w')
122 self
.fdlock
= self
.flock
.fileno()
123 self
.threadLock
= threading
.RLock()
128 #print '__del__ called' ##
132 win32api
.CloseHandle(self
.mutex
)
134 try: self
.flock
.close()
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
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.
154 timeout
= win32event
.INFINITE
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
)
164 # First, acquire the global (inter-process) lock:
166 options
= fcntl
.LOCK_EX
168 options
= fcntl
.LOCK_EX|fcntl
.LOCK_NB
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
)
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.' ##
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.
197 result
= ctypes
.windll
.kernel32
.ReleaseMutex(self
.mutex
.handle
)
199 raise NotOwner("Attempt to release somebody else's lock")
202 win32event
.ReleaseMutex(self
.mutex
)
203 #print "released mutex"
204 except pywintypes
.error
, e
:
205 errCode
, fctName
, errMsg
= e
.args
207 raise NotOwner("Attempt to release somebody else's lock")
209 raise GlobalLockError('%s: err#%d: %s' % (fctName
, errCode
,
212 # First release the local (inter-thread) lock:
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:
220 fcntl
.flock(self
.fdlock
, fcntl
.LOCK_UN
)
221 except IOError: # (errno 13: permission denied)
222 raise GlobalLockError('Unlock of file "%s" failed\n' %
225 def _errnoOf (self
, message
):
226 match
= self
.RE_ERROR_MSG
.search(str(message
))
228 return int(match
.group(1))
230 raise Exception ('Malformed error message "%s"' % message
)
232 #----------------------------------------------------------------------------
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
)
243 assert os
.path
.exists(lockName
)
245 l
.acquire() # reentrant lock, must not block
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...',
261 print '\nmain: unlocking...',
266 print '=> Test of glock.py passed.'
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)...',
276 print 'thread: locking done.'
277 print 'thread: unlocking...',
280 print 'thread ended.'
282 #----------------------------------------------------------------------------
284 #----------------------------------------------------------------------------
285 if __name__
== "__main__":