Fix deadlock: renew_token must not call login while holding _token_lock

Non-reentrant Lock caused /v1/ume/token/refresh to hang when handshake failed or token empty inside renew; UI stuck on 续期中.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-05-12 10:03:20 +08:00
parent 0afc77c06b
commit ea7931496c

View file

@ -298,42 +298,46 @@ class UMEClient:
def renew_token(self) -> str: def renew_token(self) -> str:
self._assert_ready() self._assert_ready()
token = self._token_value.strip() if not self._token_value.strip():
if not token:
return self.login(force=True) return self.login(force=True)
do_login = False
renewed_value = ""
with self._token_lock: with self._token_lock:
token = self._token_value.strip() token = self._token_value.strip()
if not token: if not token:
return self.login(force=True) do_login = True
url = self._build_url(self.token_handshake_path) else:
try: url = self._build_url(self.token_handshake_path)
with self._client() as client:
resp = client.post(url, headers=self._headers(include_token=True))
if not resp.is_success:
# handshake may fail if token expired; fallback to full login
return self.login(force=True)
# Per UME guide, oauth_handshake may return no body; treat HTTP 2xx as success.
next_token = ""
ttl: int | None = None
try: try:
text = (resp.text or "").strip() with self._client() as client:
if text: resp = client.post(url, headers=self._headers(include_token=True))
data = _coerce_dict(resp.json()) if not resp.is_success:
next_token, ttl = self._extract_token_and_ttl(data) do_login = True
else:
next_token = ""
ttl: int | None = None
try:
text = (resp.text or "").strip()
if text:
data = _coerce_dict(resp.json())
next_token, ttl = self._extract_token_and_ttl(data)
except Exception:
next_token = ""
ttl = None
if next_token:
self._token_value = next_token
use_ttl = max(60, int(ttl)) if ttl is not None else self.token_ttl_s
self._token_expires_at = time() + use_ttl
self._last_token_source = "memory"
self._persist_token_to_store()
renewed_value = self._token_value
except Exception: except Exception:
# ignore json parse errors, success is based on status code do_login = True
next_token = ""
ttl = None if do_login:
if next_token: return self.login(force=True)
self._token_value = next_token return renewed_value
# If handshake doesn't provide ttl, fall back to configured ttl.
use_ttl = max(60, int(ttl)) if ttl is not None else self.token_ttl_s
self._token_expires_at = time() + use_ttl
self._last_token_source = "memory"
self._persist_token_to_store()
return self._token_value
except Exception:
return self.login(force=True)
def logout_token(self) -> bool: def logout_token(self) -> bool:
token = self._token_value.strip() token = self._token_value.strip()