Files
mdrs-client-python/mdrsclient/api/utils.py
T
orrisroot 0cac30ccf8 fix(auth): serialise the token refresh across processes
The login cache is shared by every mdrs process, but the refresh was
guarded by a lock that only reaches inside one. Concurrent runs each
sent the same refresh token, and a provider that rotates them accepts
the first and refuses the rest.

- hold a lock that spans processes across the whole read-refresh-
  write, checking cheaply first so ordinary requests never take it
- write the cache through a temporary file: opening it for writing
  truncates it, and a reader landing in that window found it empty
  and threw the session away
- take the lock for every write, not just the refresh, so a login
  running beside one cannot be silently reverted
- wait for a busy lock on Windows rather than giving up after the ten
  attempts msvcrt allows, and retry the rename it refuses while a
  reader still holds the file open
- bound the refresh request on its own, so a provider that goes quiet
  cannot hold the lock indefinitely
- accept bare hostnames such as localhost, store URLs without the
  trailing slash, and join download paths through one helper
2026-08-14 16:36:50 +09:00

39 lines
1.6 KiB
Python

from mdrsclient.api.users import UsersApi
from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import UnauthorizedException
def token_check(connection: MDRSConnection) -> None:
"""
Bring the access token up to date before a request goes out.
Refreshing is a read-modify-write over a cache shared with every other client
process on this machine, and a rotating provider stops honouring the refresh token
it replaces. Two processes reaching this at once would otherwise both send the same
token, and the loser would be left holding one the server no longer accepts, so the
whole sequence runs under a lock that spans processes and the cache is re-read
inside it.
"""
with connection.lock:
token = connection.token
if token is None or not (token.is_refresh_required or token.is_expired):
# Nothing to do, which is the answer for almost every request. The lock below
# reaches across processes and is held for a round trip, so it is worth
# knowing that before taking it.
return
with connection.cache_lock():
connection.reload_cache()
token = connection.token
if token is None:
return
if token.is_expired:
connection.logout()
return
if not token.is_refresh_required:
return
user_api = UsersApi(connection)
try:
connection.token = user_api.tokenRefresh(token)
except UnauthorizedException:
connection.logout()