~widelands-dev/widelands-website/trunk

« back to all changes in this revision

Viewing changes to notification/lockfile.py

  • Committer: Holger Rapp
  • Date: 2019-06-21 18:34:42 UTC
  • mfrom: (540.1.3 update_ops_script)
  • Revision ID: sirver@gmx.de-20190621183442-y2ulybzr0rdvfefd
Adapt the update script for the new server.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
 
 
2
"""
 
3
lockfile.py - Platform-independent advisory file locks.
 
4
 
 
5
Requires Python 2.5 unless you apply 2.4.diff
 
6
Locking is done on a per-thread basis instead of a per-process basis.
 
7
 
 
8
Usage:
 
9
 
 
10
>>> lock = FileLock('somefile')
 
11
>>> try:
 
12
...     lock.acquire()
 
13
... except AlreadyLocked:
 
14
...     print 'somefile', 'is locked already.'
 
15
... except LockFailed:
 
16
...     print 'somefile', 'can\\'t be locked.'
 
17
... else:
 
18
...     print 'got lock'
 
19
got lock
 
20
>>> print lock.is_locked()
 
21
True
 
22
>>> lock.release()
 
23
 
 
24
>>> lock = FileLock('somefile')
 
25
>>> print lock.is_locked()
 
26
False
 
27
>>> with lock:
 
28
...    print lock.is_locked()
 
29
True
 
30
>>> print lock.is_locked()
 
31
False
 
32
>>> # It is okay to lock twice from the same thread...
 
33
>>> with lock:
 
34
...     lock.acquire()
 
35
...
 
36
>>> # Though no counter is kept, so you can't unlock multiple times...
 
37
>>> print lock.is_locked()
 
38
False
 
39
 
 
40
Exceptions:
 
41
 
 
42
    Error - base class for other exceptions
 
43
        LockError - base class for all locking exceptions
 
44
            AlreadyLocked - Another thread or process already holds the lock
 
45
            LockFailed - Lock failed for some other reason
 
46
        UnlockError - base class for all unlocking exceptions
 
47
            AlreadyUnlocked - File was not locked.
 
48
            NotMyLock - File was locked but not by the current thread/process
 
49
"""
 
50
 
 
51
 
 
52
 
 
53
import sys
 
54
import socket
 
55
import os
 
56
import threading
 
57
import time
 
58
import errno
 
59
 
 
60
# Work with PEP8 and non-PEP8 versions of threading module.
 
61
try:
 
62
    threading.current_thread
 
63
except AttributeError:
 
64
    threading.current_thread = threading.currentThread
 
65
try:
 
66
    # python 2.6 has threading.current_thread so we need to do this separately.
 
67
    threading.Thread.get_name
 
68
except AttributeError:
 
69
    threading.Thread.get_name = threading.Thread.getName
 
70
 
 
71
__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked',
 
72
           'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock',
 
73
           'LinkFileLock', 'MkdirFileLock', 'SQLiteFileLock']
 
74
 
 
75
 
 
76
class Error(Exception):
 
77
    """Base class for other exceptions.
 
78
 
 
79
    >>> try:
 
80
    ...   raise Error
 
81
    ... except Exception:
 
82
    ...   pass
 
83
 
 
84
    """
 
85
    pass
 
86
 
 
87
 
 
88
class LockError(Error):
 
89
    """Base class for error arising from attempts to acquire the lock.
 
90
 
 
91
    >>> try:
 
92
    ...   raise LockError
 
93
    ... except Error:
 
94
    ...   pass
 
95
 
 
96
    """
 
97
    pass
 
98
 
 
99
 
 
100
class LockTimeout(LockError):
 
101
    """Raised when lock creation fails within a user-defined period of time.
 
102
 
 
103
    >>> try:
 
104
    ...   raise LockTimeout
 
105
    ... except LockError:
 
106
    ...   pass
 
107
    """
 
108
    pass
 
109
 
 
110
 
 
111
class AlreadyLocked(LockError):
 
112
    """Some other thread/process is locking the file.
 
113
 
 
114
    >>> try:
 
115
    ...   raise AlreadyLocked
 
116
    ... except LockError:
 
117
    ...   pass
 
118
 
 
119
    """
 
120
    pass
 
121
 
 
122
 
 
123
class LockFailed(LockError):
 
124
    """Lock file creation failed for some other reason.
 
125
 
 
126
    >>> try:
 
127
    ...   raise LockFailed
 
128
    ... except LockError:
 
129
    ...   pass
 
130
 
 
131
    """
 
132
    pass
 
133
 
 
134
 
 
135
class UnlockError(Error):
 
136
    """Base class for errors arising from attempts to release the lock.
 
137
 
 
138
    >>> try:
 
139
    ...   raise UnlockError
 
140
    ... except Error:
 
141
    ...   pass
 
142
 
 
143
    """
 
144
    pass
 
145
 
 
146
 
 
147
class NotLocked(UnlockError):
 
148
    """Raised when an attempt is made to unlock an unlocked file.
 
149
 
 
150
    >>> try:
 
151
    ...   raise NotLocked
 
152
    ... except UnlockError:
 
153
    ...   pass
 
154
 
 
155
    """
 
156
    pass
 
157
 
 
158
 
 
159
class NotMyLock(UnlockError):
 
160
    """Raised when an attempt is made to unlock a file someone else locked.
 
161
 
 
162
    >>> try:
 
163
    ...   raise NotMyLock
 
164
    ... except UnlockError:
 
165
    ...   pass
 
166
 
 
167
    """
 
168
    pass
 
169
 
 
170
 
 
171
class LockBase:
 
172
    """Base class for platform-specific lock classes."""
 
173
 
 
174
    def __init__(self, path, threaded=True):
 
175
        """
 
176
        >>> lock = LockBase('somefile')
 
177
        >>> lock = LockBase('somefile', threaded=False)
 
178
        """
 
179
        self.path = path
 
180
        self.lock_file = os.path.abspath(path) + '.lock'
 
181
        self.hostname = socket.gethostname()
 
182
        self.pid = os.getpid()
 
183
        if threaded:
 
184
            tname = '%s-' % threading.current_thread().get_name()
 
185
        else:
 
186
            tname = ''
 
187
        dirname = os.path.dirname(self.lock_file)
 
188
        self.unique_name = os.path.join(dirname,
 
189
                                        '%s.%s%s' % (self.hostname,
 
190
                                                     tname,
 
191
                                                     self.pid))
 
192
 
 
193
    def acquire(self, timeout=None):
 
194
        """Acquire the lock.
 
195
 
 
196
        * If timeout is omitted (or None), wait forever trying to lock the
 
197
          file.
 
198
 
 
199
        * If timeout > 0, try to acquire the lock for that many seconds.  If
 
200
          the lock period expires and the file is still locked, raise
 
201
          LockTimeout.
 
202
 
 
203
        * If timeout <= 0, raise AlreadyLocked immediately if the file is
 
204
          already locked.
 
205
 
 
206
        """
 
207
        raise NotImplemented('implement in subclass')
 
208
 
 
209
    def release(self):
 
210
        """Release the lock.
 
211
 
 
212
        If the file is not locked, raise NotLocked.
 
213
 
 
214
        """
 
215
        raise NotImplemented('implement in subclass')
 
216
 
 
217
    def is_locked(self):
 
218
        """Tell whether or not the file is locked."""
 
219
        raise NotImplemented('implement in subclass')
 
220
 
 
221
    def i_am_locking(self):
 
222
        """Return True if this object is locking the file."""
 
223
        raise NotImplemented('implement in subclass')
 
224
 
 
225
    def break_lock(self):
 
226
        """Remove a lock.
 
227
 
 
228
        Useful if a locking thread failed to unlock.
 
229
 
 
230
        """
 
231
        raise NotImplemented('implement in subclass')
 
232
 
 
233
    def __enter__(self):
 
234
        """Context manager support."""
 
235
        self.acquire()
 
236
        return self
 
237
 
 
238
    def __exit__(self, *_exc):
 
239
        """Context manager support."""
 
240
        self.release()
 
241
 
 
242
 
 
243
class LinkFileLock(LockBase):
 
244
    """Lock access to a file using atomic property of link(2)."""
 
245
 
 
246
    def acquire(self, timeout=None):
 
247
        try:
 
248
            open(self.unique_name, 'wb').close()
 
249
        except IOError:
 
250
            raise LockFailed
 
251
 
 
252
        end_time = time.time()
 
253
        if timeout is not None and timeout > 0:
 
254
            end_time += timeout
 
255
 
 
256
        while True:
 
257
            # Try and create a hard link to it.
 
258
            try:
 
259
                os.link(self.unique_name, self.lock_file)
 
260
            except OSError:
 
261
                # Link creation failed.  Maybe we've double-locked?
 
262
                nlinks = os.stat(self.unique_name).st_nlink
 
263
                if nlinks == 2:
 
264
                    # The original link plus the one I created == 2.  We're
 
265
                    # good to go.
 
266
                    return
 
267
                else:
 
268
                    # Otherwise the lock creation failed.
 
269
                    if timeout is not None and time.time() > end_time:
 
270
                        os.unlink(self.unique_name)
 
271
                        if timeout > 0:
 
272
                            raise LockTimeout
 
273
                        else:
 
274
                            raise AlreadyLocked
 
275
                    time.sleep(timeout is not None and timeout / 10 or 0.1)
 
276
            else:
 
277
                # Link creation succeeded.  We're good to go.
 
278
                return
 
279
 
 
280
    def release(self):
 
281
        if not self.is_locked():
 
282
            raise NotLocked
 
283
        elif not os.path.exists(self.unique_name):
 
284
            raise NotMyLock
 
285
        os.unlink(self.unique_name)
 
286
        os.unlink(self.lock_file)
 
287
 
 
288
    def is_locked(self):
 
289
        return os.path.exists(self.lock_file)
 
290
 
 
291
    def i_am_locking(self):
 
292
        return (self.is_locked() and
 
293
                os.path.exists(self.unique_name) and
 
294
                os.stat(self.unique_name).st_nlink == 2)
 
295
 
 
296
    def break_lock(self):
 
297
        if os.path.exists(self.lock_file):
 
298
            os.unlink(self.lock_file)
 
299
 
 
300
 
 
301
class MkdirFileLock(LockBase):
 
302
    """Lock file by creating a directory."""
 
303
 
 
304
    def __init__(self, path, threaded=True):
 
305
        """
 
306
        >>> lock = MkdirFileLock('somefile')
 
307
        >>> lock = MkdirFileLock('somefile', threaded=False)
 
308
        """
 
309
        LockBase.__init__(self, path, threaded)
 
310
        if threaded:
 
311
            tname = '%x-' % thread.get_ident()
 
312
        else:
 
313
            tname = ''
 
314
        # Lock file itself is a directory.  Place the unique file name into
 
315
        # it.
 
316
        self.unique_name = os.path.join(self.lock_file,
 
317
                                        '%s.%s%s' % (self.hostname,
 
318
                                                     tname,
 
319
                                                     self.pid))
 
320
 
 
321
    def acquire(self, timeout=None):
 
322
        end_time = time.time()
 
323
        if timeout is not None and timeout > 0:
 
324
            end_time += timeout
 
325
 
 
326
        if timeout is None:
 
327
            wait = 0.1
 
328
        else:
 
329
            wait = max(0, timeout / 10)
 
330
 
 
331
        while True:
 
332
            try:
 
333
                os.mkdir(self.lock_file)
 
334
            except OSError:
 
335
                err = sys.exc_info()[1]
 
336
                if err.errno == errno.EEXIST:
 
337
                    # Already locked.
 
338
                    if os.path.exists(self.unique_name):
 
339
                        # Already locked by me.
 
340
                        return
 
341
                    if timeout is not None and time.time() > end_time:
 
342
                        if timeout > 0:
 
343
                            raise LockTimeout
 
344
                        else:
 
345
                            # Someone else has the lock.
 
346
                            raise AlreadyLocked
 
347
                    time.sleep(wait)
 
348
                else:
 
349
                    # Couldn't create the lock for some other reason
 
350
                    raise LockFailed
 
351
            else:
 
352
                open(self.unique_name, 'wb').close()
 
353
                return
 
354
 
 
355
    def release(self):
 
356
        if not self.is_locked():
 
357
            raise NotLocked
 
358
        elif not os.path.exists(self.unique_name):
 
359
            raise NotMyLock
 
360
        os.unlink(self.unique_name)
 
361
        os.rmdir(self.lock_file)
 
362
 
 
363
    def is_locked(self):
 
364
        return os.path.exists(self.lock_file)
 
365
 
 
366
    def i_am_locking(self):
 
367
        return (self.is_locked() and
 
368
                os.path.exists(self.unique_name))
 
369
 
 
370
    def break_lock(self):
 
371
        if os.path.exists(self.lock_file):
 
372
            for name in os.listdir(self.lock_file):
 
373
                os.unlink(os.path.join(self.lock_file, name))
 
374
            os.rmdir(self.lock_file)
 
375
 
 
376
 
 
377
class SQLiteFileLock(LockBase):
 
378
    'Demonstration of using same SQL-based locking.'
 
379
 
 
380
    import tempfile
 
381
    _fd, testdb = tempfile.mkstemp()
 
382
    os.close(_fd)
 
383
    os.unlink(testdb)
 
384
    del _fd, tempfile
 
385
 
 
386
    def __init__(self, path, threaded=True):
 
387
        LockBase.__init__(self, path, threaded)
 
388
        self.lock_file = str(self.lock_file)
 
389
        self.unique_name = str(self.unique_name)
 
390
 
 
391
        import sqlite3
 
392
        self.connection = sqlite3.connect(SQLiteFileLock.testdb)
 
393
 
 
394
        c = self.connection.cursor()
 
395
        try:
 
396
            c.execute('create table locks'
 
397
                      '('
 
398
                      '   lock_file varchar(32),'
 
399
                      '   unique_name varchar(32)'
 
400
                      ')')
 
401
        except sqlite3.OperationalError:
 
402
            pass
 
403
        else:
 
404
            self.connection.commit()
 
405
            import atexit
 
406
            atexit.register(os.unlink, SQLiteFileLock.testdb)
 
407
 
 
408
    def acquire(self, timeout=None):
 
409
        end_time = time.time()
 
410
        if timeout is not None and timeout > 0:
 
411
            end_time += timeout
 
412
 
 
413
        if timeout is None:
 
414
            wait = 0.1
 
415
        elif timeout <= 0:
 
416
            wait = 0
 
417
        else:
 
418
            wait = timeout / 10
 
419
 
 
420
        cursor = self.connection.cursor()
 
421
 
 
422
        while True:
 
423
            if not self.is_locked():
 
424
                # Not locked.  Try to lock it.
 
425
                cursor.execute('insert into locks'
 
426
                               '  (lock_file, unique_name)'
 
427
                               '  values'
 
428
                               '  (?, ?)',
 
429
                               (self.lock_file, self.unique_name))
 
430
                self.connection.commit()
 
431
 
 
432
                # Check to see if we are the only lock holder.
 
433
                cursor.execute('select * from locks'
 
434
                               '  where unique_name = ?',
 
435
                               (self.unique_name,))
 
436
                rows = cursor.fetchall()
 
437
                if len(rows) > 1:
 
438
                    # Nope.  Someone else got there.  Remove our lock.
 
439
                    cursor.execute('delete from locks'
 
440
                                   '  where unique_name = ?',
 
441
                                   (self.unique_name,))
 
442
                    self.connection.commit()
 
443
                else:
 
444
                    # Yup.  We're done, so go home.
 
445
                    return
 
446
            else:
 
447
                # Check to see if we are the only lock holder.
 
448
                cursor.execute('select * from locks'
 
449
                               '  where unique_name = ?',
 
450
                               (self.unique_name,))
 
451
                rows = cursor.fetchall()
 
452
                if len(rows) == 1:
 
453
                    # We're the locker, so go home.
 
454
                    return
 
455
 
 
456
            # Maybe we should wait a bit longer.
 
457
            if timeout is not None and time.time() > end_time:
 
458
                if timeout > 0:
 
459
                    # No more waiting.
 
460
                    raise LockTimeout
 
461
                else:
 
462
                    # Someone else has the lock and we are impatient..
 
463
                    raise AlreadyLocked
 
464
 
 
465
            # Well, okay.  We'll give it a bit longer.
 
466
            time.sleep(wait)
 
467
 
 
468
    def release(self):
 
469
        if not self.is_locked():
 
470
            raise NotLocked
 
471
        if not self.i_am_locking():
 
472
            raise NotMyLock((self._who_is_locking(), self.unique_name))
 
473
        cursor = self.connection.cursor()
 
474
        cursor.execute('delete from locks'
 
475
                       '  where unique_name = ?',
 
476
                       (self.unique_name,))
 
477
        self.connection.commit()
 
478
 
 
479
    def _who_is_locking(self):
 
480
        cursor = self.connection.cursor()
 
481
        cursor.execute('select unique_name from locks'
 
482
                       '  where lock_file = ?',
 
483
                       (self.lock_file,))
 
484
        return cursor.fetchone()[0]
 
485
 
 
486
    def is_locked(self):
 
487
        cursor = self.connection.cursor()
 
488
        cursor.execute('select * from locks'
 
489
                       '  where lock_file = ?',
 
490
                       (self.lock_file,))
 
491
        rows = cursor.fetchall()
 
492
        return not not rows
 
493
 
 
494
    def i_am_locking(self):
 
495
        cursor = self.connection.cursor()
 
496
        cursor.execute('select * from locks'
 
497
                       '  where lock_file = ?'
 
498
                       '    and unique_name = ?',
 
499
                       (self.lock_file, self.unique_name))
 
500
        return not not cursor.fetchall()
 
501
 
 
502
    def break_lock(self):
 
503
        cursor = self.connection.cursor()
 
504
        cursor.execute('delete from locks'
 
505
                       '  where lock_file = ?',
 
506
                       (self.lock_file,))
 
507
        self.connection.commit()
 
508
 
 
509
if hasattr(os, 'link'):
 
510
    FileLock = LinkFileLock
 
511
else:
 
512
    FileLock = MkdirFileLock