Files
mdrs-client-python/mdrsclient/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

46 lines
1.4 KiB
Python

import os
import time
from typing import IO, Any, Final
from urllib.parse import parse_qs, urlparse
if os.name == "nt":
import msvcrt
elif os.name == "posix":
import fcntl
class FileLock:
# Long enough to outlast a token refresh, which is what the lock is held across.
WAIT_SECONDS: Final[float] = 60.0
@staticmethod
def lock(file: IO[Any]) -> None:
if os.name == "nt":
# msvcrt.LK_LOCK gives up after ten one-second attempts, which is shorter
# than the refresh it now has to wait for, so do the waiting here instead.
deadline = time.monotonic() + FileLock.WAIT_SECONDS
while True:
try:
msvcrt.locking(file.fileno(), msvcrt.LK_NBLCK, 1)
return
except OSError:
if time.monotonic() >= deadline:
raise
time.sleep(0.1)
elif os.name == "posix":
fcntl.flock(file.fileno(), fcntl.LOCK_EX)
@staticmethod
def unlock(file: IO[Any]) -> None:
if os.name == "nt":
msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1)
elif os.name == "posix":
fcntl.flock(file.fileno(), fcntl.LOCK_UN)
def page_num_from_url(url: str) -> int | None:
parsed_url = urlparse(url)
params = parse_qs(parsed_url.query)
page = params.get("page", [None])[0]
return int(page) if page is not None else None