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, posixpath
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, logger=None):
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.
115
@param logger: an optional logger object.
119
if posixpath.exists(fpath):
120
self.previous_lockfile_present = True
122
self.previous_lockfile_present = False
124
self.name = string.replace(fpath, '\\', '_')
125
self.mutex = win32event.CreateMutex(None, lockInitially, self.name)
128
self.flock = open(fpath, 'w')
129
self.fdlock = self.flock.fileno()
130
self.threadLock = threading.RLock()
135
#print '__del__ called' ##
139
win32api.CloseHandle(self.mutex)
141
try: self.flock.close()
145
return '<Global lock @ %s>' % self.name
147
def acquire(self, blocking=False):
148
""" Locks. Attemps to acquire a lock.
150
@param blocking: If True, suspends caller until done. Otherwise,
151
LockAlreadyAcquired is raised if the lock cannot be acquired immediately.
153
On windows an IOError is always raised after ~10 sec if the lock
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.
160
self.logger.info('creating lockfile')
163
timeout = win32event.INFINITE
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)
173
# First, acquire the global (inter-process) lock:
175
options = fcntl.LOCK_EX
177
options = fcntl.LOCK_EX|fcntl.LOCK_NB
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)
186
raise GlobalLockError('Cannot acquire lock on "file" '
187
'%s: %s\n' % (self.name, message))
188
#print 'got file lock.' ##
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)
195
if self.previous_lockfile_present and self.logger:
196
self.logger.warn("Stale lockfile detected and claimed.")
197
#print 'got thread lock.' ##
199
self.is_locked = True
201
def release(self, skip_delete=False):
202
''' Unlocks. (caller must own the lock!)
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.
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.
213
if not self.is_locked:
217
self.logger.debug('Removing lock file: %s', self.fpath)
218
os.unlink(self.fpath)
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)
226
result = ctypes.windll.kernel32.ReleaseMutex(self.mutex.handle)
228
raise NotOwner("Attempt to release somebody else's lock")
231
win32event.ReleaseMutex(self.mutex)
232
#print "released mutex"
233
except pywintypes.error, e:
234
errCode, fctName, errMsg = e.args
236
raise NotOwner("Attempt to release somebody else's lock")
238
raise GlobalLockError('%s: err#%d: %s' % (fctName, errCode,
241
# First release the local (inter-thread) lock:
243
self.threadLock.release()
244
except AssertionError:
245
raise NotOwner("Attempt to release somebody else's lock")
247
# Then release the global (inter-process) lock:
249
fcntl.flock(self.fdlock, fcntl.LOCK_UN)
250
except IOError: # (errno 13: permission denied)
251
raise GlobalLockError('Unlock of file "%s" failed\n' %
253
self.is_locked = False
255
def _errnoOf (self, message):
256
match = self.RE_ERROR_MSG.search(str(message))
258
return int(match.group(1))
260
raise Exception ('Malformed error message "%s"' % message)
262
#----------------------------------------------------------------------------
264
#----------------------------------------------------------------------------
265
##TODO: a more serious test with distinct processes !
267
print 'Testing glock.py...'
269
# unfortunately can't test inter-process lock here!
270
lockName = 'myFirstLock'
271
l = GlobalLock(lockName)
273
assert posixpath.exists(lockName)
275
l.acquire() # reentrant lock, must not block
280
except NotOwner: pass
281
else: raise Exception('should have raised a NotOwner exception')
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...',
291
print '\nmain: unlocking...',
296
print '=> Test of glock.py passed.'
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)...',
306
print 'thread: locking done.'
307
print 'thread: unlocking...',
310
print 'thread ended.'
312
#----------------------------------------------------------------------------
314
#----------------------------------------------------------------------------
315
if __name__ == "__main__":