-
Notifications
You must be signed in to change notification settings - Fork 0
Harden settings file concurrency, email key isolation, and import guard #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9a8ed8d
32af264
d5dc394
ac9f7d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,3 +7,6 @@ repositories/ | |
| __pycache__/ | ||
| *.pyc | ||
| flask_session/ | ||
|
|
||
| # Runtime lock files | ||
| config/*.lock | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,10 +13,83 @@ | |||||||||
|
|
||||||||||
| from .config import SETTINGS_FILE | ||||||||||
|
|
||||||||||
| # Shared in-process lock protecting all reads/writes to SETTINGS_FILE. | ||||||||||
|
|
||||||||||
| class _InterProcessSettingsFileLock: | ||||||||||
| """Combined thread + process lock for protecting SETTINGS_FILE. | ||||||||||
|
|
||||||||||
| Serialises concurrent access across both threads within a single worker | ||||||||||
| and across multiple Gunicorn worker processes by combining a | ||||||||||
| ``threading.Lock`` with an ``fcntl.flock`` advisory file lock. On | ||||||||||
| platforms where ``fcntl`` is unavailable (e.g. Windows) the class | ||||||||||
| falls back gracefully to thread-only locking. | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| def __init__(self, lock_file_path: str) -> None: | ||||||||||
| self._lock_file_path = lock_file_path | ||||||||||
| self._thread_lock = threading.Lock() | ||||||||||
| self._fd: int | None = None | ||||||||||
|
|
||||||||||
| def acquire(self, blocking: bool = True) -> bool: | ||||||||||
| acquired = self._thread_lock.acquire(blocking) | ||||||||||
| if not acquired: | ||||||||||
| return False | ||||||||||
| fd = None | ||||||||||
| try: | ||||||||||
| import fcntl | ||||||||||
| fd = os.open(self._lock_file_path, os.O_CREAT | os.O_RDWR, 0o600) | ||||||||||
| flags = fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB) | ||||||||||
| fcntl.flock(fd, flags) | ||||||||||
|
Comment on lines
+38
to
+41
|
||||||||||
| self._fd = fd | ||||||||||
| except ImportError: | ||||||||||
| # fcntl not available (Windows); thread lock is sufficient. | ||||||||||
| if fd is not None: | ||||||||||
| os.close(fd) | ||||||||||
| except BlockingIOError: | ||||||||||
| if fd is not None: | ||||||||||
| os.close(fd) | ||||||||||
| self._thread_lock.release() | ||||||||||
| return False | ||||||||||
| except Exception: | ||||||||||
| if fd is not None: | ||||||||||
| os.close(fd) | ||||||||||
| self._thread_lock.release() | ||||||||||
| raise | ||||||||||
| return True | ||||||||||
|
|
||||||||||
| def release(self) -> None: | ||||||||||
| try: | ||||||||||
| if self._fd is not None: | ||||||||||
| try: | ||||||||||
| import fcntl | ||||||||||
| fcntl.flock(self._fd, fcntl.LOCK_UN) | ||||||||||
| except (ImportError, OSError): | ||||||||||
| pass | ||||||||||
| finally: | ||||||||||
| try: | ||||||||||
| os.close(self._fd) | ||||||||||
| except OSError: | ||||||||||
| pass | ||||||||||
| self._fd = None | ||||||||||
| finally: | ||||||||||
| self._thread_lock.release() | ||||||||||
|
|
||||||||||
| def __enter__(self) -> "_InterProcessSettingsFileLock": | ||||||||||
| self.acquire() | ||||||||||
|
||||||||||
| self.acquire() | |
| acquired = self.acquire() | |
| if not acquired: | |
| raise RuntimeError("Failed to acquire settings file lock") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The warning hard-codes
app_settings.json. IfSETTINGS_FILEis ever renamed or configured differently, this message becomes misleading. Prefer including the actualSETTINGS_FILE(path or name) in the log message.