Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/tools/filelock.py
4128 views
1
# This is free and unencumbered software released into the public domain.
2
#
3
# Anyone is free to copy, modify, publish, use, compile, sell, or
4
# distribute this software, either in source code form or as a compiled
5
# binary, for any purpose, commercial or non-commercial, and by any
6
# means.
7
#
8
# In jurisdictions that recognize copyright laws, the author or authors
9
# of this software dedicate any and all copyright interest in the
10
# software to the public domain. We make this dedication for the benefit
11
# of the public at large and to the detriment of our heirs and
12
# successors. We intend this dedication to be an overt act of
13
# relinquishment in perpetuity of all present and future rights to this
14
# software under copyright law.
15
#
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
# OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# For more information, please refer to <http://unlicense.org>
25
26
# This software package was obtained from
27
# https://github.com/benediktschmitt/py-filelock
28
#
29
# Local changes:
30
# - Changed logger.info to logger.warn to avoid displaying logging
31
# information under normal emscripten usage.
32
33
"""
34
A platform independent file lock that supports the with-statement.
35
"""
36
37
38
# Modules
39
# ------------------------------------------------
40
import logging
41
import os
42
import threading
43
import time
44
try:
45
import warnings
46
except ImportError:
47
warnings = None
48
49
try:
50
import msvcrt
51
except ImportError:
52
msvcrt = None
53
54
try:
55
import fcntl
56
except ImportError:
57
fcntl = None
58
59
60
# Backward compatibility
61
# ------------------------------------------------
62
try:
63
TimeoutError
64
except NameError:
65
TimeoutError = OSError
66
67
68
# Data
69
# ------------------------------------------------
70
__all__ = [
71
"Timeout",
72
"BaseFileLock",
73
"WindowsFileLock",
74
"UnixFileLock",
75
"SoftFileLock",
76
"FileLock"
77
]
78
79
__version__ = "3.0.12"
80
81
82
_logger = None
83
def logger():
84
"""Returns the logger instance used in this module."""
85
global _logger
86
_logger = _logger or logging.getLogger(__name__)
87
return _logger
88
89
90
# Exceptions
91
# ------------------------------------------------
92
class Timeout(TimeoutError):
93
"""
94
Raised when the lock could not be acquired in *timeout*
95
seconds.
96
"""
97
98
def __init__(self, lock_file):
99
"""
100
"""
101
#: The path of the file lock.
102
self.lock_file = lock_file
103
return None
104
105
def __str__(self):
106
temp = "The file lock '{}' could not be acquired."\
107
.format(self.lock_file)
108
return temp
109
110
111
# Classes
112
# ------------------------------------------------
113
114
# This is a helper class which is returned by :meth:`BaseFileLock.acquire`
115
# and wraps the lock to make sure __enter__ is not called twice when entering
116
# the with statement.
117
# If we would simply return *self*, the lock would be acquired again
118
# in the *__enter__* method of the BaseFileLock, but not released again
119
# automatically.
120
#
121
# :seealso: issue #37 (memory leak)
122
class _Acquire_ReturnProxy:
123
124
def __init__(self, lock):
125
self.lock = lock
126
return None
127
128
def __enter__(self):
129
return self.lock
130
131
def __exit__(self, exc_type, exc_value, traceback):
132
self.lock.release()
133
return None
134
135
136
class BaseFileLock:
137
"""
138
Implements the base class of a file lock.
139
"""
140
141
def __init__(self, lock_file, timeout = -1):
142
"""
143
"""
144
# The path to the lock file.
145
self._lock_file = lock_file
146
147
# The file descriptor for the *_lock_file* as it is returned by the
148
# os.open() function.
149
# This file lock is only NOT None, if the object currently holds the
150
# lock.
151
self._lock_file_fd = None
152
153
# The default timeout value.
154
self.timeout = timeout
155
156
# We use this lock primarily for the lock counter.
157
self._thread_lock = threading.Lock()
158
159
# The lock counter is used for implementing the nested locking
160
# mechanism. Whenever the lock is acquired, the counter is increased and
161
# the lock is only released, when this value is 0 again.
162
self._lock_counter = 0
163
return None
164
165
@property
166
def lock_file(self):
167
"""
168
The path to the lock file.
169
"""
170
return self._lock_file
171
172
@property
173
def timeout(self):
174
"""
175
You can set a default timeout for the filelock. It will be used as
176
fallback value in the acquire method, if no timeout value (*None*) is
177
given.
178
179
If you want to disable the timeout, set it to a negative value.
180
181
A timeout of 0 means, that there is exactly one attempt to acquire the
182
file lock.
183
184
.. versionadded:: 2.0.0
185
"""
186
return self._timeout
187
188
@timeout.setter
189
def timeout(self, value):
190
"""
191
"""
192
self._timeout = float(value)
193
return None
194
195
# Platform dependent locking
196
# --------------------------------------------
197
198
def _acquire(self):
199
"""
200
Platform dependent. If the file lock could be
201
acquired, self._lock_file_fd holds the file descriptor
202
of the lock file.
203
"""
204
raise NotImplementedError()
205
206
def _release(self):
207
"""
208
Releases the lock and sets self._lock_file_fd to None.
209
"""
210
raise NotImplementedError()
211
212
# Platform independent methods
213
# --------------------------------------------
214
215
@property
216
def is_locked(self):
217
"""
218
True, if the object holds the file lock.
219
220
.. versionchanged:: 2.0.0
221
222
This was previously a method and is now a property.
223
"""
224
return self._lock_file_fd is not None
225
226
def acquire(self, timeout=None, poll_intervall=0.05):
227
"""
228
Acquires the file lock or fails with a :exc:`Timeout` error.
229
230
.. code-block:: python
231
232
# You can use this method in the context manager (recommended)
233
with lock.acquire():
234
pass
235
236
# Or use an equivalent try-finally construct:
237
lock.acquire()
238
try:
239
pass
240
finally:
241
lock.release()
242
243
:arg float timeout:
244
The maximum time waited for the file lock.
245
If ``timeout < 0``, there is no timeout and this method will
246
block until the lock could be acquired.
247
If ``timeout`` is None, the default :attr:`~timeout` is used.
248
249
:arg float poll_intervall:
250
We check once in *poll_intervall* seconds if we can acquire the
251
file lock.
252
253
:raises Timeout:
254
if the lock could not be acquired in *timeout* seconds.
255
256
.. versionchanged:: 2.0.0
257
258
This method returns now a *proxy* object instead of *self*,
259
so that it can be used in a with statement without side effects.
260
"""
261
# Use the default timeout, if no timeout is provided.
262
if timeout is None:
263
timeout = self.timeout
264
265
# Increment the number right at the beginning.
266
# We can still undo it, if something fails.
267
with self._thread_lock:
268
self._lock_counter += 1
269
270
lock_id = id(self)
271
lock_filename = self._lock_file
272
start_time = time.time()
273
try:
274
while True:
275
with self._thread_lock:
276
if not self.is_locked:
277
logger().debug('Attempting to acquire lock %s on %s', lock_id, lock_filename)
278
self._acquire()
279
280
if self.is_locked:
281
logger().debug('Lock %s acquired on %s', lock_id, lock_filename)
282
break
283
elif timeout >= 0 and time.time() - start_time > timeout:
284
logger().debug('Timeout on acquiring lock %s on %s', lock_id, lock_filename)
285
raise Timeout(self._lock_file)
286
else:
287
logger().debug(
288
'Lock %s not acquired on %s, waiting %s seconds ...',
289
lock_id, lock_filename, poll_intervall
290
)
291
time.sleep(poll_intervall)
292
except:
293
# Something did go wrong, so decrement the counter.
294
with self._thread_lock:
295
self._lock_counter = max(0, self._lock_counter - 1)
296
297
raise
298
return _Acquire_ReturnProxy(lock = self)
299
300
def release(self, force = False):
301
"""
302
Releases the file lock.
303
304
Please note, that the lock is only completely released, if the lock
305
counter is 0.
306
307
Also note, that the lock file itself is not automatically deleted.
308
309
:arg bool force:
310
If true, the lock counter is ignored and the lock is released in
311
every case.
312
"""
313
with self._thread_lock:
314
315
if self.is_locked:
316
self._lock_counter -= 1
317
318
if self._lock_counter == 0 or force:
319
lock_id = id(self)
320
lock_filename = self._lock_file
321
322
logger().debug('Attempting to release lock %s on %s', lock_id, lock_filename)
323
self._release()
324
self._lock_counter = 0
325
logger().debug('Lock %s released on %s', lock_id, lock_filename)
326
327
return None
328
329
def __enter__(self):
330
self.acquire()
331
return self
332
333
def __exit__(self, exc_type, exc_value, traceback):
334
self.release()
335
return None
336
337
def __del__(self):
338
self.release(force = True)
339
return None
340
341
342
# Windows locking mechanism
343
# ~~~~~~~~~~~~~~~~~~~~~~~~~
344
345
class WindowsFileLock(BaseFileLock):
346
"""
347
Uses the :func:`msvcrt.locking` function to hard lock the lock file on
348
windows systems.
349
"""
350
351
def _acquire(self):
352
open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
353
354
try:
355
fd = os.open(self._lock_file, open_mode)
356
except OSError:
357
pass
358
else:
359
try:
360
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
361
except (IOError, OSError):
362
os.close(fd)
363
else:
364
self._lock_file_fd = fd
365
return None
366
367
def _release(self):
368
fd = self._lock_file_fd
369
self._lock_file_fd = None
370
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
371
os.close(fd)
372
373
try:
374
os.remove(self._lock_file)
375
# Probably another instance of the application
376
# that acquired the file lock.
377
except OSError:
378
pass
379
return None
380
381
# Unix locking mechanism
382
# ~~~~~~~~~~~~~~~~~~~~~~
383
384
class UnixFileLock(BaseFileLock):
385
"""
386
Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.
387
"""
388
389
def _acquire(self):
390
open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
391
fd = os.open(self._lock_file, open_mode)
392
393
try:
394
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
395
except (IOError, OSError):
396
os.close(fd)
397
else:
398
st = os.fstat(fd);
399
if st.st_nlink == 0:
400
# We raced with another process that deleted the lock file before
401
# we called fcntl.flock. This means that lock is not valid (since
402
# another process will just lock a different file) and we need to
403
# try again.
404
# See https://stackoverflow.com/a/51070775
405
os.close(fd)
406
else:
407
self._lock_file_fd = fd
408
return None
409
410
def _release(self):
411
fd = self._lock_file_fd
412
self._lock_file_fd = None
413
os.unlink(self._lock_file)
414
fcntl.flock(fd, fcntl.LOCK_UN)
415
os.close(fd)
416
return None
417
418
# Soft lock
419
# ~~~~~~~~~
420
421
class SoftFileLock(BaseFileLock):
422
"""
423
Simply watches the existence of the lock file.
424
"""
425
426
def _acquire(self):
427
open_mode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_TRUNC
428
try:
429
fd = os.open(self._lock_file, open_mode)
430
except (IOError, OSError):
431
pass
432
else:
433
self._lock_file_fd = fd
434
return None
435
436
def _release(self):
437
os.close(self._lock_file_fd)
438
self._lock_file_fd = None
439
440
try:
441
os.remove(self._lock_file)
442
# The file is already deleted and that's what we want.
443
except OSError:
444
pass
445
return None
446
447
448
# Platform filelock
449
# ~~~~~~~~~~~~~~~~~
450
451
#: Alias for the lock, which should be used for the current platform. On
452
#: Windows, this is an alias for :class:`WindowsFileLock`, on Unix for
453
#: :class:`UnixFileLock` and otherwise for :class:`SoftFileLock`.
454
FileLock = None
455
456
if msvcrt:
457
FileLock = WindowsFileLock
458
elif fcntl:
459
FileLock = UnixFileLock
460
else:
461
FileLock = SoftFileLock
462
463
if warnings is not None:
464
warnings.warn("only soft file lock is available")
465
466