This commit is contained in:
Manchik
2026-05-15 17:17:42 +03:00
parent 13dc22f74f
commit 9cce9b5b36
16 changed files with 332 additions and 108 deletions
+211 -42
View File
@@ -1,5 +1,8 @@
import asyncio
import json
import shutil
import sys
from pathlib import Path
from loguru import logger
@@ -13,6 +16,93 @@ STEAM_DOMAINS = [
".login.steampowered.com",
]
# Common Chrome/Chromium install locations on Windows
_WINDOWS_CHROME_PATHS = [
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
r"C:\Program Files\Google\Chrome Beta\Application\chrome.exe",
r"C:\Program Files\Google\Chrome Dev\Application\chrome.exe",
r"C:\Program Files\Chromium\Application\chrome.exe",
r"C:\Program Files (x86)\Chromium\Application\chrome.exe",
]
def _find_chrome() -> str | None:
"""Try to locate a Chrome/Chromium binary. Returns path or None."""
# 1. Check PATH
for name in ("chrome", "chromium", "chromium-browser", "google-chrome"):
found = shutil.which(name)
if found:
return found
# 2. Windows-specific fixed paths + Registry
if sys.platform == "win32":
import os
local = os.environ.get("LOCALAPPDATA", "")
user_path = Path(local) / "Google" / "Chrome" / "Application" / "chrome.exe"
candidates = _WINDOWS_CHROME_PATHS + ([str(user_path)] if local else [])
for p in candidates:
if Path(p).exists():
return p
# Registry lookup (handles non-standard install paths)
try:
import winreg
reg_keys = [
(
winreg.HKEY_LOCAL_MACHINE,
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe",
),
(
winreg.HKEY_CURRENT_USER,
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe",
),
]
for hive, key_path in reg_keys:
try:
with winreg.OpenKey(hive, key_path) as key:
path, _ = winreg.QueryValueEx(key, "")
if path and Path(path).exists():
return path
except FileNotFoundError:
pass
except Exception:
pass
return None
async def _wait_connection_closed(browser) -> None:
"""Fallback: wait until CDP connection closes (with grace period)."""
await asyncio.sleep(3)
consecutive_closed = 0
tick = 0
while True:
await asyncio.sleep(1)
tick += 1
try:
closed = not browser or not browser.connection or browser.connection.closed
except Exception as e:
logger.debug(f"[browser] connection check error at tick={tick}: {e}")
closed = True
if tick % 5 == 0:
logger.debug(
f"[browser] fallback tick={tick} closed={closed} consecutive={consecutive_closed}"
)
if closed:
consecutive_closed += 1
logger.debug(
f"[browser] connection closed tick={tick}, consecutive={consecutive_closed}"
)
if consecutive_closed >= 3:
break
else:
consecutive_closed = 0
async def open_browser_with_cookies(account: dict) -> None:
"""Open a Chrome browser with Steam session cookies using nodriver."""
@@ -24,55 +114,134 @@ async def open_browser_with_cookies(account: dict) -> None:
cookies = json.loads(cookies_json)
browser = await uc.start()
# Try to find Chrome ourselves so we can give a better error
chrome_path = _find_chrome()
if chrome_path:
logger.debug(f"[browser] Using Chrome at: {chrome_path}")
else:
logger.warning(
"[browser] Chrome not found in standard locations — "
"nodriver will attempt its own detection"
)
try:
if chrome_path:
browser = await uc.start(browser_executable_path=chrome_path)
else:
browser = await uc.start()
except Exception as exc:
exc_str = str(exc).lower()
if (
"chrome" in exc_str
or "binary" in exc_str
or "executable" in exc_str
or "not found" in exc_str
):
raise RuntimeError(
"Chrome не найден. Установите Google Chrome с https://www.google.com/chrome/ "
f"(искали в: {chrome_path or 'стандартные пути'})"
) from exc
raise RuntimeError(f"Не удалось запустить браузер: {exc}") from exc
_active_browsers.append(browser)
tab = await browser.get("about:blank")
for c in cookies:
domain = c.get("domain") or ".steamcommunity.com"
domains_to_set = [domain]
if "steam" in domain:
domains_to_set = list(set([domain] + STEAM_DOMAINS))
for d in domains_to_set:
try:
await tab.send(uc.cdp.network.set_cookie(
name=c["name"],
value=c["value"],
domain=d,
path=c.get("path") or "/",
secure=c.get("secure", True),
http_only=c.get("httpOnly", False),
))
except Exception:
pass
steam_id = account.get("steam_id") or ""
if steam_id:
start_url = f"https://steamcommunity.com/profiles/{steam_id}/"
else:
start_url = "https://steamcommunity.com/"
await browser.get(start_url)
logger.info(f"Browser opened for {account['login']}")
# Wait until browser is closed by user, suppress connection errors
try:
while browser:
await asyncio.sleep(1)
tab = await browser.get("about:blank")
for c in cookies:
domain = c.get("domain") or ".steamcommunity.com"
domains_to_set = [domain]
if "steam" in domain:
domains_to_set = list(set([domain] + STEAM_DOMAINS))
for d in domains_to_set:
try:
await tab.send(
uc.cdp.network.set_cookie(
name=c["name"],
value=c["value"],
domain=d,
path=c.get("path") or "/",
secure=c.get("secure", True),
http_only=c.get("httpOnly", False),
)
)
except Exception:
pass
steam_id = account.get("steam_id") or ""
start_url = (
f"https://steamcommunity.com/profiles/{steam_id}/"
if steam_id
else "https://steamcommunity.com/"
)
await browser.get(start_url)
logger.info(f"[browser] Opened for {account.get('login', account.get('id'))}")
# Wait until the Chrome process exits.
# We check the process PID directly — this is immune to CDP
# connection state fluctuations during page navigation.
await asyncio.sleep(2) # let Chrome fully start
pid = getattr(browser, "_process_pid", None) or getattr(
getattr(browser, "_process", None), "pid", None
)
logger.debug(f"[browser] monitoring Chrome: pid={pid}")
if pid:
try:
if not browser.connection or browser.connection.closed:
break
except Exception:
break
except Exception:
pass
import psutil # type: ignore
proc = psutil.Process(pid)
logger.debug(
f"[browser] Chrome pid={pid} status={proc.status()} — waiting for user to close"
)
tick = 0
while True:
await asyncio.sleep(1)
tick += 1
try:
running = proc.is_running()
status = proc.status()
except psutil.NoSuchProcess:
logger.debug(
f"[browser] Chrome pid={pid} — process no longer exists (tick={tick})"
)
break
except Exception as e:
logger.debug(
f"[browser] Chrome pid={pid} — psutil error (tick={tick}): {e}"
)
break
if tick % 5 == 0:
conn_closed = getattr(
getattr(browser, "connection", None), "closed", "?"
)
logger.debug(
f"[browser] Chrome pid={pid} alive tick={tick} status={status} conn.closed={conn_closed}"
)
if not running or status == psutil.STATUS_ZOMBIE:
logger.debug(
f"[browser] Chrome pid={pid} exited at tick={tick}, final status={status}"
)
break
except ImportError:
logger.debug(
"[browser] psutil not installed — falling back to connection check"
)
await _wait_connection_closed(browser)
except Exception as e:
logger.debug(f"[browser] pid monitoring error: {e}")
else:
logger.debug(
"[browser] could not get Chrome PID — falling back to connection check"
)
await _wait_connection_closed(browser)
finally:
if browser in _active_browsers:
_active_browsers.remove(browser)
# Temporarily suppress nodriver's internal task errors during cleanup
# (Browser.update_targets() raises ConnectionRefusedError after close)
loop = asyncio.get_running_loop()
_original_handler = loop.get_exception_handler()
@@ -93,4 +262,4 @@ async def open_browser_with_cookies(account: dict) -> None:
await asyncio.sleep(0.5)
loop.set_exception_handler(_original_handler)
logger.debug(f"Browser closed for {account['login']}")
logger.debug(f"[browser] Closed for {account.get('login', account.get('id'))}")