diff --git a/msal_extensions/cache_lock.py b/msal_extensions/cache_lock.py new file mode 100644 index 0000000..9ef90fd --- /dev/null +++ b/msal_extensions/cache_lock.py @@ -0,0 +1,32 @@ +import os +import sys +import errno +import portalocker + + +class CrossPlatLock(object): + """ + Offers a mechanism for waiting until another process is finished interacting with a shared resource. This is + specifically written to interact with a class of the same name in the .NET extensions library. + """ + def __init__(self, lockfile_path): + self._lockpath = lockfile_path + + def __enter__(self): + pid = os.getpid() + + self._fh = open(self._lockpath, 'wb+', buffering=0) + portalocker.lock(self._fh, portalocker.LOCK_EX) + self._fh.write('{} {}'.format(pid, sys.argv[0]).encode('utf-8')) + + def __exit__(self, *args): + self._fh.close() + try: + # Attempt to delete the lockfile. In either of the failure cases enumerated below, it is likely that + # another process has raced this one and ended up clearing or locking the file for itself. + os.remove(self._lockpath) + except PermissionError: + pass + except OSError as ex: + if ex.errno != errno.ENOENT: + raise diff --git a/setup.py b/setup.py index 17d9fca..cbfe080 100644 --- a/setup.py +++ b/setup.py @@ -15,5 +15,8 @@ classifiers=[ 'Development Status :: 2 - Pre-Alpha', ], + install_requires=[ + 'portalocker~=1.0', + ], tests_require=['pytest'], ) diff --git a/tests/lock_acquire.py b/tests/lock_acquire.py new file mode 100644 index 0000000..c1b5b21 --- /dev/null +++ b/tests/lock_acquire.py @@ -0,0 +1,28 @@ +import sys +import os +import time +import datetime +from msal_extensions import CrossPlatLock + + +def main(hold_time): + # type: (datetime.timedelta) -> None + """ + Grabs a lock from a well-known file in order to test the CrossPlatLock class across processes. + :param hold_time: The approximate duration that this process should hold onto the lock. + :return: None + """ + pid = os.getpid() + print('{} starting'.format(pid)) + with CrossPlatLock('./delete_me.lockfile'): + print('{} has acquired the lock'.format(pid)) + time.sleep(hold_time.total_seconds()) + print('{} is releasing the lock'.format(pid)) + print('{} done.'.format(pid)) + + +if __name__ == '__main__': + lock_hold_time = datetime.timedelta(seconds=5) + if len(sys.argv) > 1: + hold_time = datetime.timedelta(seconds=int(sys.argv[1])) + main(lock_hold_time) diff --git a/tests/test_crossplatlock.py b/tests/test_crossplatlock.py new file mode 100644 index 0000000..ea3c9d5 --- /dev/null +++ b/tests/test_crossplatlock.py @@ -0,0 +1,18 @@ +import pytest +from msal_extensions.cache_lock import CrossPlatLock + + +def test_ensure_file_deleted(): + lockfile = './test_lock_1.txt' + + try: + FileNotFoundError + except NameError: + FileNotFoundError = IOError + + with CrossPlatLock(lockfile): + pass + + with pytest.raises(FileNotFoundError): + with open(lockfile): + pass