~launchpad-pqm/launchpad/devel

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()