cdxcore 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cdxcore might be problematic. Click here for more details.

cdxcore/filelock.py ADDED
@@ -0,0 +1,430 @@
1
+ """
2
+ subdir
3
+ Simple class to keep track of directory sturctures and for automated caching on disk
4
+ Hans Buehler 2020
5
+ """
6
+
7
+ from .logger import Logger
8
+ from .verbose import Context
9
+ from .util import datetime, fmt_datetime, fmt_seconds
10
+ from .subdir import SubDir
11
+ _log = Logger(__file__)
12
+
13
+ import os
14
+ import os.path
15
+ import time
16
+ import platform as platform
17
+
18
+ IS_WINDOWS = platform.system()[0] == "W"
19
+
20
+ if IS_WINDOWS:
21
+ # http://timgolden.me.uk/pywin32-docs/Windows_NT_Files_.2d.2d_Locking.html
22
+ # need to install pywin32
23
+ try:
24
+ import win32file as win32file
25
+ except Exception as e:
26
+ raise ModuleNotFoundError("pywin32") from e
27
+
28
+ import win32con
29
+ import pywintypes
30
+ import win32security
31
+ import win32api
32
+ WIN_HIGHBITS=0xffff0000 #high-order 32 bits of byte range to lock
33
+
34
+ else:
35
+ win32file = None
36
+
37
+ import os
38
+
39
+ class FileLock(object):
40
+ """
41
+ Systemwide Lock (Mutex) using files
42
+ https://code.activestate.com/recipes/519626-simple-file-based-mutex-for-very-basic-ipc/
43
+ """
44
+
45
+ __LOCK_ID = 0
46
+
47
+ def __init__(self, filename, * ,
48
+ acquire : bool = False,
49
+ release_on_exit : bool = True,
50
+ wait : bool = True,
51
+ timeout_seconds : int = None,
52
+ timeout_retry : int = None,
53
+ raise_on_fail : bool = True,
54
+ verbose : Context = Context.quiet ):
55
+ """
56
+ Initialize new lock with name 'filename'
57
+ Acquire the lock if 'acquire' is True
58
+
59
+ Parameters
60
+ ----------
61
+ filename :
62
+ Filename of the lock.
63
+ 'filename' may start with '!/' to refer to the temp directory, or '~/' to refer to the user directory.
64
+ On Unix /dev/shm/ can be used to refer to shared memory.
65
+ acquire :
66
+ Whether to attempt aquiring the lock upon initialization
67
+ release_on_exit :
68
+ Whether to auto-release the lock upon exit.
69
+ wait :
70
+ If False, return immediately if the lock cannot be acquired.
71
+ If True, wait with below parameters; in particular if these are left as defaults the lock will wait indefinitely.
72
+ timeout_seconds :
73
+ Number of seconds to wait before retrying.
74
+ Set to 0 to fail immediately.
75
+ If set to None, then its value will depend on 'wait'.
76
+ If wait is True, then timeout_seconds==1; if wait is False, then timeout_seconds==0
77
+ timeout_retry :
78
+ How many times to retry before timing out.
79
+ Set to None to retry indefinitely.
80
+ raise_on_fail :
81
+ If the constructor fails to obtain the lock, raise an Exception:
82
+ This will be either of type
83
+ TimeoutError if 'timeout_seconds' > 0 and 'wait' is True, or
84
+ BlockingIOError if 'timeout_seconds' == 0 or 'wait' is False.
85
+ verbose :
86
+ Context which will print out operating information of the lock. This is helpful for debugging.
87
+ In particular, it will track __del__() function calls.
88
+ Set to None to print all context.
89
+
90
+ Exceptions
91
+ ----------
92
+ If acquire is True, then this constructor may raise an exception:
93
+ TimeoutError if 'timeout_seconds' > 0 and 'wait' is True, or
94
+ BlockingIOError if 'timeout_seconds' == 0 or 'wait' is False.
95
+ """
96
+ self._filename = SubDir.expandStandardRoot(filename)
97
+ self._fd = None
98
+ self._pid = os.getpid()
99
+ self._cnt = 0
100
+ self._lid = "LOCK" + fmt_datetime(datetime.datetime.now()) + (",%03ld:" % FileLock.__LOCK_ID) + filename
101
+ self.verbose = verbose if not verbose is None else Context(None)
102
+ self.release_on_exit = release_on_exit
103
+ FileLock.__LOCK_ID +=1
104
+
105
+ if acquire: self.acquire( wait=wait,
106
+ timeout_seconds=timeout_seconds,
107
+ timeout_retry=timeout_retry,
108
+ raise_on_fail=raise_on_fail )
109
+
110
+ def __del__(self):#NOQA
111
+ if self.release_on_exit and not self._fd is None:
112
+ self.verbose.write("%s: deleting locked object", self._lid)
113
+ self.release( force=True )
114
+ self._filename = None
115
+
116
+ def __str__(self) -> str:
117
+ """ Returns the current file name and the number of locks """
118
+ return "{%s:%ld}" % (self._filename, self._cnt)
119
+
120
+ def __bool__(self) -> bool:
121
+ """ Whether the lock is held """
122
+ return self.locked
123
+ @property
124
+ def num_acquisitions(self) -> int:
125
+ """ Return number of times acquire() was called. Zero if the lock is not held """
126
+ return self._cnt
127
+ @property
128
+ def locked(self) -> bool:
129
+ """ Returns True if the current object owns the lock """
130
+ return self._cnt > 0
131
+ @property
132
+ def filename(self) -> str:
133
+ """ Return filename """
134
+ return self._filename
135
+
136
+ def acquire(self, wait = True,
137
+ *, timeout_seconds : int = 1,
138
+ timeout_retry : int = 5,
139
+ raise_on_fail : bool = True) -> int:
140
+ """
141
+ Acquire lock
142
+
143
+ Parameters
144
+ ----------
145
+ wait :
146
+ If False, return immediately if the lock cannot be acquired.
147
+ If True, wait with below parameters
148
+ timeout_seconds :
149
+ Number of seconds to wait before retrying. If wait is False, this must be zero.
150
+ Set to 0 to fail immediately.
151
+ If set to None, then its value will depend on 'wait':
152
+ If wait is True, then timeout_seconds==1; if wait is False, then timeout_seconds==0
153
+ timeout_retry :
154
+ How many times to retry before timing out.
155
+ Set to zero to try ony once.
156
+ Set to None to retry indefinitely.
157
+ raise_on_fail :
158
+ If the function fails to obtain the lock, raise an Exception:
159
+ This will be either of type
160
+ TimeoutError if 'timeout_seconds' > 0 and 'wait' is True, or
161
+ BlockingIOError if 'timeout_seconds' == 0 or 'wait' is False.
162
+
163
+ Returns
164
+ -------
165
+ Number of total locks the current process holds, or 0 if the function
166
+ failed to attain a lock.
167
+ """
168
+ timeout_seconds = int(timeout_seconds) if not timeout_seconds is None else None
169
+ timeout_retry = int(timeout_retry) if not timeout_retry is None else None
170
+ assert not self._filename is None, ("self._filename is None. That probably means 'self' was deleted.")
171
+
172
+ if timeout_seconds is None:
173
+ timeout_seconds = 0 if not wait else 1
174
+ else:
175
+ _log.verify( timeout_seconds>=0, "'timeout_seconds' cannot be negative")
176
+ _log.verify( not wait or timeout_seconds>0, "Using 'timeout_seconds==0' and 'wait=True' is inconsistent.")
177
+
178
+ if not self._fd is None:
179
+ self._cnt += 1
180
+ self.verbose.write("%s: acquire(): raised lock counter to %ld", self._lid, self._cnt)
181
+ return self._cnt
182
+ assert self._cnt == 0
183
+ self._cnt = 0
184
+
185
+ i = 0
186
+ while True:
187
+ self.verbose.write("\r%s: acquire(): locking [%s]... ", self._lid, "windows" if IS_WINDOWS else "linux", end='')
188
+ if not IS_WINDOWS:
189
+ # Linux
190
+ # -----
191
+ try:
192
+ self._fd = os.open(self._filename, os.O_CREAT|os.O_EXCL|os.O_RDWR)
193
+ os.write(self._fd, bytes("%d" % self._pid, 'utf-8'))
194
+ except OSError as e:
195
+ if not self._fd is None:
196
+ os.close(self._fd)
197
+ self._fd = None
198
+ if e.errno != 17:
199
+ self.verbose.write("failed: %s", str(e), head=False)
200
+ raise e
201
+ else:
202
+ # Windows
203
+ # ------
204
+ secur_att = win32security.SECURITY_ATTRIBUTES()
205
+ secur_att.Initialize()
206
+ try:
207
+ self._fd = win32file.CreateFile( self._filename,
208
+ win32con.GENERIC_READ|win32con.GENERIC_WRITE,
209
+ win32con.FILE_SHARE_READ|win32con.FILE_SHARE_WRITE,
210
+ secur_att,
211
+ win32con.OPEN_ALWAYS,
212
+ win32con.FILE_ATTRIBUTE_NORMAL , 0 )
213
+
214
+ ov=pywintypes.OVERLAPPED() #used to indicate starting region to lock
215
+ win32file.LockFileEx(self._fd,win32con.LOCKFILE_EXCLUSIVE_LOCK|win32con.LOCKFILE_FAIL_IMMEDIATELY,0,WIN_HIGHBITS,ov)
216
+ except BaseException as e:
217
+ if not self._fd is None:
218
+ self._fd.Close()
219
+ self._fd = None
220
+ if e.winerror not in [17,33]:
221
+ self.verbose.write("failed: %s", str(e), head=False)
222
+ raise e
223
+
224
+ if not self._fd is None:
225
+ # success
226
+ self._cnt = 1
227
+ self.verbose.write("done; lock counter set to 1", head=False)
228
+ return self._cnt
229
+
230
+ if timeout_seconds <= 0:
231
+ break
232
+
233
+ if not timeout_retry is None:
234
+ i += 1
235
+ if i>timeout_retry:
236
+ break
237
+ self.verbose.write("locked; waiting %s retry %ld/%ld", fmt_seconds(timeout_seconds), i+1, timeout_retry, head=False)
238
+ else:
239
+ self.verbose.write("locked; waiting %s", fmt_seconds(timeout_seconds), head=False)
240
+
241
+ time.sleep(timeout_seconds)
242
+
243
+ if timeout_seconds == 0:
244
+ self.verbose.write("failed.", head=False)
245
+ if raise_on_fail: raise BlockingIOError(self._filename)
246
+ else:
247
+ self.verbose.write("timed out. Cannot access lock.", head=False)
248
+ if raise_on_fail: raise TimeoutError(self._filename, dict(timeout_retry=timeout_retry, timeout_seconds=timeout_seconds))
249
+ return 0
250
+
251
+ def release(self, *, force : bool = False ):
252
+ """
253
+ Release lock
254
+ By default will only release the lock once the number of acquisitions is zero.
255
+ Use 'force' to always unlock.
256
+
257
+ Parameters
258
+ ----------
259
+ force :
260
+ Whether to close the file regardless of its internal counter.
261
+
262
+ Returns
263
+ -------
264
+ Returns numbner of remaining lock counters; in other words returns 0 if the lock is no longer locked by this process.
265
+ """
266
+ if self._fd is None:
267
+ assert force, ("File was not locked by this process. Use 'force' to avoid this message if need be.")
268
+ self._cnt = 0
269
+ return 0
270
+ assert self._cnt > 0
271
+ self._cnt -= 1
272
+ if self._cnt > 0 and not force:
273
+ self.verbose.write("%s: release(): lock counter lowered to %ld", self._lid, self._cnt)
274
+ return self._cnt
275
+
276
+ self.verbose.write("%s: release(): unlocking [%s]... ", self._lid, "windows" if IS_WINDOWS else "linux", end='')
277
+ err = ""
278
+ if not IS_WINDOWS:
279
+ # Linux
280
+ # Locks on Linxu are remarably shaky.
281
+ # In particular, it is possible to remove a locked file.
282
+ try:
283
+ os.remove(self._filename)
284
+ except:
285
+ pass
286
+ try:
287
+ os.close(self._fd)
288
+ except:
289
+ err = "*** WARNING: could not close file."
290
+ pass
291
+ else:
292
+ try:
293
+ ov=pywintypes.OVERLAPPED() #used to indicate starting region to lock
294
+ win32file.UnlockFileEx(self._fd,0,WIN_HIGHBITS,ov)
295
+ except:
296
+ err = "*** WARNING: could not unlock file."
297
+ pass
298
+ try:
299
+ self._fd.Close()
300
+ except:
301
+ err = "*** WARNING: could not close file."
302
+ pass
303
+ try:
304
+ win32file.DeleteFile(self._filename)
305
+ except:
306
+ err = "*** WARNING: could not delete file." if err == "" else err
307
+ pass
308
+ self.verbose.write("file deleted." if err=="" else err, head=False)
309
+ self._fd = None
310
+ self._cnt = 0
311
+ return 0
312
+
313
+ # context manager
314
+ # ---------------
315
+
316
+ def __enter__(self):
317
+ """ Simply returns 'self' """
318
+ return self
319
+
320
+ def __exit__(self, *kargs, **kwargs):
321
+ """ Force-release the lock """
322
+ self.release( force=True )
323
+ return False # raise exceptions
324
+
325
+ def AttemptLock(filename, * ,
326
+ release_on_exit : bool = True,
327
+ wait : bool = True,
328
+ timeout_seconds : int = None,
329
+ timeout_retry : int = None,
330
+ verbose : Context = Context.quiet ) -> FileLock:
331
+ """
332
+ Attempt to acquire a new lock with name 'filename', and return None upon failure.
333
+ Note that in contrast to FileLock() 'raise_on_fail' defaults to False for this function call.
334
+
335
+ Parameters
336
+ ----------
337
+ filename :
338
+ Filename of the lock.
339
+ 'filename' may start with '!/' to refer to the temp directory, or '~/' to refer to the user directory.
340
+ On Unix /dev/shm/ can be used to refer to shared memory.
341
+ release_on_exit :
342
+ Whether to auto-release the lock upon exit.
343
+ wait :
344
+ If False, return immediately if the lock cannot be acquired.
345
+ If True, wait with below parameters; in particular if these are left as defaults the lock will wait indefinitely.
346
+ timeout_seconds :
347
+ Number of seconds to wait before retrying.
348
+ Set to 0 to fail immediately.
349
+ If set to None, then its value will depend on 'wait'.
350
+ If wait is True, then timeout_seconds==1; if wait is False, then timeout_seconds==0
351
+ timeout_retry :
352
+ How many times to retry before timing out.
353
+ Set to None to retry indefinitely.
354
+ verbose :
355
+ Context which will print out operating information of the lock. This is helpful for debugging.
356
+ In particular, it will track __del__() function calls.
357
+ Set to None to print all context.
358
+
359
+ Exceptions
360
+ ----------
361
+ Will not raise any exceptions
362
+
363
+ Returns
364
+ -------
365
+ Filelock if acquired or None
366
+ """
367
+
368
+ lock = FileLock( filename=filename,
369
+ acquire=True,
370
+ release_on_exit=release_on_exit,
371
+ wait=wait,
372
+ timeout_seconds=timeout_seconds,
373
+ timeout_retry=timeout_retry,
374
+ raise_on_fail=False,
375
+ verbose=verbose )
376
+ return lock if lock.locked else None
377
+
378
+ def AcquireLock(filename, * ,
379
+ wait : bool = True,
380
+ timeout_seconds : int = None,
381
+ timeout_retry : int = None,
382
+ verbose : Context = Context.quiet ) -> FileLock:
383
+ """
384
+ Aquires a filelock indentified by `filename`. Raises an exception of this was not successful.
385
+ This function is a short cut for FileLock(..., acquire=True ) to be used in context blocks:
386
+
387
+ with AcquireLock( "!/my.lock" ):
388
+ ...
389
+ ....
390
+
391
+ Parameters
392
+ ----------
393
+ filename :
394
+ Filename of the lock.
395
+ 'filename' may start with '!/' to refer to the temp directory, or '~/' to refer to the user directory.
396
+ On Unix /dev/shm/ can be used to refer to shared memory.
397
+ wait :
398
+ If False, return immediately if the lock cannot be acquired.
399
+ If True, wait with below parameters; in particular if these are left as defaults the lock will wait indefinitely.
400
+ timeout_seconds :
401
+ Number of seconds to wait before retrying.
402
+ Set to 0 to fail immediately.
403
+ If set to None, then its value will depend on 'wait'.
404
+ If wait is True, then timeout_seconds==1; if wait is False, then timeout_seconds==0
405
+ timeout_retry :
406
+ How many times to retry before timing out.
407
+ Set to None to retry indefinitely.
408
+ verbose :
409
+ Context which will print out operating information of the lock. This is helpful for debugging.
410
+ In particular, it will track __del__() function calls.
411
+ Set to None to print all context.
412
+
413
+ Exceptions
414
+ ----------
415
+ Will raise an exception if timeout is not indefinitely and the lock could not be acquired.
416
+
417
+ Returns
418
+ -------
419
+ Filelock if acquired or None
420
+ """
421
+
422
+ return FileLock( filename=filename,
423
+ acquire=True,
424
+ release_on_exit=True,
425
+ wait=wait,
426
+ timeout_seconds=timeout_seconds,
427
+ timeout_retry=timeout_retry,
428
+ raise_on_fail=True,
429
+ verbose=verbose )
430
+