From ea7931496c79dc958f5716557a9683343f5ab44e Mon Sep 17 00:00:00 2001 From: oliver Date: Tue, 12 May 2026 10:03:20 +0800 Subject: [PATCH] Fix deadlock: renew_token must not call login while holding _token_lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- netx_api/ume_client.py | 64 ++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/netx_api/ume_client.py b/netx_api/ume_client.py index c5d20be..0c6dac6 100644 --- a/netx_api/ume_client.py +++ b/netx_api/ume_client.py @@ -298,42 +298,46 @@ class UMEClient: def renew_token(self) -> str: self._assert_ready() - token = self._token_value.strip() - if not token: + if not self._token_value.strip(): return self.login(force=True) + + do_login = False + renewed_value = "" with self._token_lock: token = self._token_value.strip() if not token: - return self.login(force=True) - url = self._build_url(self.token_handshake_path) - try: - 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 + do_login = True + else: + url = self._build_url(self.token_handshake_path) try: - text = (resp.text or "").strip() - if text: - data = _coerce_dict(resp.json()) - next_token, ttl = self._extract_token_and_ttl(data) + with self._client() as client: + resp = client.post(url, headers=self._headers(include_token=True)) + if not resp.is_success: + 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: - # ignore json parse errors, success is based on status code - next_token = "" - ttl = None - if next_token: - self._token_value = next_token - # 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) + do_login = True + + if do_login: + return self.login(force=True) + return renewed_value def logout_token(self) -> bool: token = self._token_value.strip()