diff --git a/desktop-plugins/ailab-limit/gw_usage.py b/desktop-plugins/ailab-limit/gw_usage.py index 54fb0da..8f696b9 100644 --- a/desktop-plugins/ailab-limit/gw_usage.py +++ b/desktop-plugins/ailab-limit/gw_usage.py @@ -156,24 +156,77 @@ return out +def _kill_tree(pid): + """Гасит процесс вместе с детьми: на Windows taskkill /T, иначе — группа.""" + try: + if os.name == "nt": + subprocess.run( + ["taskkill", "/F", "/T", "/PID", str(pid)], + capture_output=True, timeout=5, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + else: + os.killpg(os.getpgid(pid), 9) + except Exception: + try: + os.kill(pid, 9) + except Exception: + pass + + def token_from_cmd(cmd): - """Запуск key_cmd: единственная строка JSON, ключ — access_token.""" + """Запуск key_cmd: единственная строка JSON, ключ — access_token. + + Бюджет жёсткий: shell.exec на бэкенде рвётся на 30 с, поэтому key_cmd + получает 12 с на попытку и не больше двух попыток в пределах 20 с — + иначе холодный старт gw-key.ps1 (DPAPI + запрос к хабу) съедал весь + лимит RPC, и чип падал с «command_timeout» вместо данных. + """ kwargs = {} if os.name == "nt": kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0) - proc = subprocess.run( - cmd, shell=True, capture_output=True, text=True, timeout=20, **kwargs - ) - text = (proc.stdout or "").strip() - if proc.returncode != 0 or not text: - err = (proc.stderr or "").strip().splitlines() - hint = err[-1][:120] if err else "код %s" % proc.returncode - raise RuntimeError("key_cmd: %s" % hint) - data = json.loads(text.splitlines()[-1]) - token = data.get("access_token") or data.get("key") - if not token: - raise RuntimeError("key_cmd: в ответе нет access_token") - return token + + deadline = time.monotonic() + 20 + last = "key_cmd: не выполнился" + for _ in range(2): + left = deadline - time.monotonic() + if left < 12: + break + try: + proc = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, **kwargs + ) + except OSError as e: + last = "key_cmd: %s" % str(e)[:120] + continue + try: + out, err_out = proc.communicate(timeout=min(12.0, left)) + except subprocess.TimeoutExpired: + # timeout у shell-обёртки не убивает дочерний powershell: гасим дерево, + # иначе communicate() ждёт ребёнка и RPC уходит за свои 30 с. + _kill_tree(proc.pid) + try: + proc.communicate(timeout=5) + except Exception: + pass + last = "key_cmd: таймаут 12 с" + continue + text = (out or "").strip() + if proc.returncode != 0 or not text: + err = (err_out or "").strip().splitlines() + last = "key_cmd: %s" % (err[-1][:120] if err else "код %s" % proc.returncode) + continue + try: + data = json.loads(text.splitlines()[-1]) + except ValueError: + last = "key_cmd: ответ не JSON" + continue + token = data.get("access_token") or data.get("key") + if token: + return token + last = "key_cmd: в ответе нет access_token" + raise RuntimeError(last) def key_candidates():