Skip to content

Commit 74812a4

Browse files
committed
Make portalocker optional
1 parent ba841e6 commit 74812a4

14 files changed

+114
-25
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,5 @@ ASALocalRun/
334334
.mfractor/
335335

336336
.eggs/
337+
.env
338+
Session.vim

Dockerfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# TODO: Can this Dockerfile use multi-stage build?
22
# Final size 690MB. (It would be 1.16 GB if started with python:3 as base)
3-
FROM python:3.12-slim
3+
FROM python:3.13-slim
44

55
# Install Generic PyGObject (sans GTK)
66
#The following somehow won't work:
@@ -9,7 +9,6 @@ RUN apt-get update && apt-get install -y \
99
libcairo2-dev \
1010
libgirepository1.0-dev \
1111
python3-dev
12-
RUN pip install "pygobject>=3,<4"
1312

1413
# Install MSAL Extensions dependencies
1514
# Don't know how to get container talk to dbus on host,
@@ -19,10 +18,10 @@ RUN apt-get install -y \
1918
gnome-keyring
2019

2120
# Not strictly necessary, but we include a pytest (which is only 3MB) to facilitate testing.
22-
RUN pip install "pytest>=6,<7"
21+
RUN pip install "pygobject>=3,<4" "pytest>=6,<7"
2322

2423
# Install MSAL Extensions. Upgrade the pinned version number to trigger a new image build.
25-
RUN pip install "msal-extensions==1.1"
24+
RUN pip install "msal-extensions==1.2"
2625

2726
# This setup is inspired from https://github.com/jaraco/keyring#using-keyring-on-headless-linux-systems-in-a-docker-container
2827
ENTRYPOINT ["dbus-run-session", "--"]

docker_run.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ docker build -t $IMAGE_NAME - < Dockerfile
66
echo "==== Integration Test for Persistence on Linux (libsecret) ===="
77
echo "After seeing the bash prompt, run the following to test encryption on Linux:"
88
echo " pip install -e ."
9-
echo " pytest -s tests/chosen_test_file.py"
10-
echo "Note that you probably need to set up ENV VAR for the test cases to run"
9+
echo " pytest --capture=no -s tests/chosen_test_file.py"
10+
echo "Note: It will test portalocker-based lock when portalocker is installed, or test file-based lock otherwise."
1111
docker run --rm -it \
1212
--privileged \
13-
--env-file .env \
1413
-w /home -v $PWD:/home \
1514
$IMAGE_NAME \
1615
$1

msal_extensions/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@
88
KeychainPersistence,
99
LibsecretPersistence,
1010
)
11-
from .cache_lock import CrossPlatLock
12-
from .token_cache import PersistedTokenCache
11+
from .token_cache import PersistedTokenCache, CrossPlatLock, LockError
1312

msal_extensions/cache_lock.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
import time
66
import logging
77

8-
import portalocker
8+
import portalocker # pylint: disable=import-error
99

1010

1111
logger = logging.getLogger(__name__)
1212

1313

14+
LockError = portalocker.exceptions.LockException
15+
16+
1417
class CrossPlatLock(object):
1518
"""Offers a mechanism for waiting until another process is finished interacting with a shared
1619
resource. This is specifically written to interact with a class of the same name in the .NET

msal_extensions/filelock.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""A cross-process lock based on exclusive creation of a given file name"""
2+
import os
3+
import sys
4+
import errno
5+
import time
6+
import logging
7+
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class LockError(RuntimeError):
13+
"""It will be raised when unable to obtain a lock"""
14+
15+
16+
class CrossPlatLock(object):
17+
"""This implementation relies only on ``open(..., 'x')``"""
18+
def __init__(self, lockfile_path):
19+
self._lockpath = lockfile_path
20+
21+
def __enter__(self):
22+
self._create_lock_file('{} {}'.format(
23+
os.getpid(),
24+
sys.argv[0],
25+
).encode('utf-8')) # pylint: disable=consider-using-f-string
26+
return self
27+
28+
def _create_lock_file(self, content):
29+
timeout = 5
30+
check_interval = 0.25
31+
current_time = getattr(time, "monotonic", time.time)
32+
timeout_end = current_time() + timeout
33+
while timeout_end > current_time():
34+
try:
35+
with open(self._lockpath, 'xb') as lock_file: # pylint: disable=unspecified-encoding
36+
lock_file.write(content)
37+
return None # Happy path
38+
except ValueError: # This needs to be the first clause, for Python 2 to hit it
39+
raise LockError("Python 2 does not support atomic creation of file")
40+
except FileExistsError: # Only Python 3 will reach this clause
41+
logger.debug(
42+
"Process %d found existing lock file, will retry after %f second",
43+
os.getpid(), check_interval)
44+
time.sleep(check_interval)
45+
raise LockError(
46+
"Unable to obtain lock, despite trying for {} second(s). "
47+
"You may want to manually remove the stale lock file {}".format(
48+
timeout,
49+
self._lockpath,
50+
))
51+
52+
def __exit__(self, *args):
53+
try:
54+
os.remove(self._lockpath)
55+
except OSError as ex: # pylint: disable=invalid-name
56+
if ex.errno in (errno.ENOENT, errno.EACCES):
57+
# Probably another process has raced this one
58+
# and ended up clearing or locking the file for itself.
59+
logger.debug("Unable to remove lock file")
60+
else:
61+
raise
62+

msal_extensions/token_cache.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
import msal
77

8-
from .cache_lock import CrossPlatLock
8+
try:
9+
from .cache_lock import CrossPlatLock, LockError # It needs portalocker
10+
except ImportError: # Falls back to file-based lock
11+
from .filelock import CrossPlatLock, LockError # It uses plain file
912
from .persistence import _mkdir_p, PersistenceNotFound
1013

1114

setup.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@
2020
python_requires=">=3.7",
2121
install_requires=[
2222
'msal>=1.29,<2', # Use TokenCache.search() from MSAL Python 1.29+
23-
'portalocker<3,>=1.4',
2423

2524
## We choose to NOT define a hard dependency on this.
2625
# "pygobject>=3,<4;platform_system=='Linux'",
2726
],
27+
extras_require={
28+
"portalocker": [
29+
'portalocker<3,>=1.4',
30+
],
31+
},
2832
tests_require=['pytest'],
2933
)

tests/__init__.py

Whitespace-only changes.

tests/cache_file_generator.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
import sys
1515
import time
1616

17-
from portalocker import exceptions
17+
from msal_extensions import FilePersistence, CrossPlatLock, LockError
1818

19-
from msal_extensions import FilePersistence, CrossPlatLock
19+
20+
print("Testing with {}".format(CrossPlatLock))
2021

2122

2223
def _acquire_lock_and_write_to_cache(cache_location, sleep_interval):
@@ -31,7 +32,7 @@ def _acquire_lock_and_write_to_cache(cache_location, sleep_interval):
3132
time.sleep(sleep_interval)
3233
data += "> " + str(os.getpid()) + "\n"
3334
cache_accessor.save(data)
34-
except exceptions.LockException as e:
35+
except LockError as e:
3536
logging.warning("Unable to acquire lock %s", e)
3637

3738

0 commit comments

Comments
 (0)