fix: avoid blocking browser init

This commit is contained in:
2025-12-18 09:38:02 +08:00
parent 5851120f87
commit 433a3cb806
4 changed files with 108 additions and 17 deletions

View File

@@ -7,6 +7,7 @@ API 浏览器 - 用纯 HTTP 请求实现浏览功能
import requests
from bs4 import BeautifulSoup
import os
import re
import time
import atexit
@@ -24,6 +25,12 @@ LOGIN_URL = getattr(config, "ZSGL_LOGIN_URL", f"{BASE_URL}/admin/login.aspx")
INDEX_URL_PATTERN = getattr(config, "ZSGL_INDEX_URL_PATTERN", "index.aspx")
COOKIES_DIR = getattr(config, "COOKIES_DIR", "data/cookies")
try:
_API_REQUEST_TIMEOUT_SECONDS = float(os.environ.get("API_REQUEST_TIMEOUT_SECONDS") or os.environ.get("API_REQUEST_TIMEOUT") or "15")
except Exception:
_API_REQUEST_TIMEOUT_SECONDS = 15.0
_API_REQUEST_TIMEOUT_SECONDS = max(3.0, _API_REQUEST_TIMEOUT_SECONDS)
_cookie_domain_fallback = urlsplit(BASE_URL).hostname or "postoa.aidunsoft.com"
_api_browser_instances: "weakref.WeakSet[APIBrowser]" = weakref.WeakSet()
@@ -125,7 +132,7 @@ class APIBrowser:
def _request_with_retry(self, method, url, max_retries=3, retry_delay=1, **kwargs):
"""带重试机制的请求方法"""
kwargs.setdefault('timeout', 5)
kwargs.setdefault('timeout', _API_REQUEST_TIMEOUT_SECONDS)
last_error = None
for attempt in range(1, max_retries + 1):

8
app.py
View File

@@ -226,7 +226,13 @@ if __name__ == "__main__":
logger.warning(f"警告: 加载并发配置失败,使用默认值: {e}")
logger.info("正在初始化浏览器管理器...")
init_browser_manager()
try:
from services.browser_manager import init_browser_manager_async
logger.info("启动浏览器环境初始化(后台进行,不阻塞服务启动)...")
init_browser_manager_async()
except Exception as e:
logger.warning(f"警告: 启动浏览器初始化失败: {e}")
logger.info("启动定时任务调度器...")
threading.Thread(target=scheduled_task_worker, daemon=True, name="scheduled-task-worker").start()

View File

@@ -11,7 +11,7 @@ from crypto_utils import encrypt_password as encrypt_account_password
from flask import Blueprint, jsonify, request
from flask_login import current_user, login_required
from services.accounts_service import load_user_accounts
from services.browser_manager import init_browser_manager
from services.browser_manager import init_browser_manager_async
from services.browse_types import BROWSE_TYPE_SHOULD_READ, normalize_browse_type, validate_browse_type
from services.client_log import log_to_client
from services.models import Account
@@ -230,9 +230,9 @@ def start_account(account_id):
if not browse_type:
return jsonify({"error": "浏览类型无效"}), 400
enable_screenshot = data.get("enable_screenshot", True)
if not init_browser_manager():
return jsonify({"error": "浏览器初始化失败"}), 500
if enable_screenshot:
# 异步初始化浏览器环境,避免首次下载/安装 Chromium 阻塞请求导致“网页无响应”
init_browser_manager_async()
ok, message = submit_account_task(
user_id=user_id,
@@ -308,6 +308,9 @@ def manual_screenshot(account_id):
account.last_browse_type = browse_type
# 异步初始化浏览器环境,避免首次下载/安装 Chromium 阻塞请求
init_browser_manager_async()
threading.Thread(
target=take_screenshot_for_account,
args=(user_id, account_id, browse_type, "manual_screenshot"),
@@ -333,6 +336,10 @@ def batch_start_accounts():
if not account_ids:
return jsonify({"error": "请选择要启动的账号"}), 400
if enable_screenshot:
# 异步初始化浏览器环境,避免首次下载/安装 Chromium 阻塞请求
init_browser_manager_async()
started = []
failed = []
@@ -407,4 +414,3 @@ def batch_stop_accounts():
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
return jsonify({"success": True, "stopped_count": len(stopped), "stopped": stopped})

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import threading
import time
from typing import Optional
from app_logger import get_logger
@@ -13,28 +14,99 @@ logger = get_logger("browser_manager")
_browser_manager: Optional[PlaywrightBrowserManager] = None
_lock = threading.Lock()
_cond = threading.Condition(_lock)
_init_in_progress = False
_init_error: Optional[str] = None
_init_thread: Optional[threading.Thread] = None
def get_browser_manager() -> Optional[PlaywrightBrowserManager]:
return _browser_manager
def init_browser_manager() -> bool:
global _browser_manager
def is_browser_manager_ready() -> bool:
return _browser_manager is not None
with _lock:
def get_browser_manager_init_error() -> Optional[str]:
return _init_error
def init_browser_manager(*, block: bool = True, timeout: Optional[float] = None) -> bool:
global _browser_manager
global _init_in_progress, _init_error
deadline = time.monotonic() + float(timeout) if timeout is not None else None
with _cond:
if _browser_manager is not None:
return True
if _init_in_progress:
if not block:
return False
while _init_in_progress:
if deadline is None:
_cond.wait(timeout=0.5)
continue
remaining = deadline - time.monotonic()
if remaining <= 0:
break
_cond.wait(timeout=min(0.5, remaining))
return _browser_manager is not None
_init_in_progress = True
_init_error = None
ok = False
error: Optional[str] = None
manager: Optional[PlaywrightBrowserManager] = None
try:
logger.info("正在初始化Playwright浏览器管理器...")
if not check_and_install_browser(log_callback=lambda msg, account_id=None: logger.info(str(msg))):
error = "浏览器环境检查失败"
logger.error("浏览器环境检查失败!")
return False
ok = False
else:
manager = PlaywrightBrowserManager(
headless=True,
log_callback=lambda msg, account_id=None: logger.info(str(msg)),
)
ok = True
logger.info("Playwright浏览器管理器创建成功")
except Exception as exc:
error = f"{type(exc).__name__}: {exc}"
logger.exception("初始化Playwright浏览器管理器时发生异常")
ok = False
_browser_manager = PlaywrightBrowserManager(
headless=True,
log_callback=lambda msg, account_id=None: logger.info(str(msg)),
)
logger.info("Playwright浏览器管理器创建成功")
return True
with _cond:
if ok and manager is not None:
_browser_manager = manager
else:
_init_error = error or "初始化失败"
_init_in_progress = False
_cond.notify_all()
return ok
def init_browser_manager_async() -> None:
"""异步初始化浏览器环境,避免阻塞 Web 请求/服务启动。"""
global _init_thread
def _worker():
try:
init_browser_manager(block=True)
except Exception:
logger.exception("异步初始化浏览器管理器失败")
with _cond:
if _browser_manager is not None:
return
if _init_thread and _init_thread.is_alive():
return
if _init_in_progress:
return
_init_thread = threading.Thread(target=_worker, daemon=True, name="browser-manager-init")
_init_thread.start()