~ubuntu-branches/ubuntu/precise/checkbox/precise

« back to all changes in this revision

Viewing changes to checkbox/contrib/glock.py

  • Committer: Bazaar Package Importer
  • Author(s): Marc Tardif
  • Date: 2009-01-20 16:46:15 UTC
  • Revision ID: james.westby@ubuntu.com-20090120164615-7iz6nmlef41h4vx2
Tags: 0.4
* Setup bzr-builddeb in native mode.
* Removed LGPL notice from the copyright file.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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, posixpath
 
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
 
 
108
    def __init__(self, fpath, lockInitially=False, logger=None):
 
109
        ''' Creates (or opens) a global lock.
 
110
 
 
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.
 
115
            @param logger: an optional logger object.
 
116
        '''
 
117
        self.logger = logger
 
118
        self.fpath = fpath
 
119
        if posixpath.exists(fpath):
 
120
            self.previous_lockfile_present = True
 
121
        else:
 
122
            self.previous_lockfile_present = False
 
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
 
 
147
    def acquire(self, blocking=False):
 
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
        """
 
159
        if self.logger:
 
160
            self.logger.info('creating lockfile')
 
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)
 
195
            if self.previous_lockfile_present and self.logger:
 
196
                self.logger.warn("Stale lockfile detected and claimed.")
 
197
            #print 'got thread lock.' ##
 
198
 
 
199
        self.is_locked = True
 
200
 
 
201
    def release(self, skip_delete=False):
 
202
        ''' Unlocks. (caller must own the lock!)
 
203
 
 
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
 
 
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
        '''
 
213
        if not self.is_locked:
 
214
            return
 
215
        if not skip_delete:
 
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)
 
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)
 
253
        self.is_locked = False
 
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 posixpath.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()