fix: avoid blocking browser init
This commit is contained in:
@@ -7,6 +7,7 @@ API 浏览器 - 用纯 HTTP 请求实现浏览功能
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import atexit
|
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")
|
INDEX_URL_PATTERN = getattr(config, "ZSGL_INDEX_URL_PATTERN", "index.aspx")
|
||||||
COOKIES_DIR = getattr(config, "COOKIES_DIR", "data/cookies")
|
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"
|
_cookie_domain_fallback = urlsplit(BASE_URL).hostname or "postoa.aidunsoft.com"
|
||||||
|
|
||||||
_api_browser_instances: "weakref.WeakSet[APIBrowser]" = weakref.WeakSet()
|
_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):
|
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
|
last_error = None
|
||||||
|
|
||||||
for attempt in range(1, max_retries + 1):
|
for attempt in range(1, max_retries + 1):
|
||||||
|
|||||||
8
app.py
8
app.py
@@ -226,7 +226,13 @@ if __name__ == "__main__":
|
|||||||
logger.warning(f"警告: 加载并发配置失败,使用默认值: {e}")
|
logger.warning(f"警告: 加载并发配置失败,使用默认值: {e}")
|
||||||
|
|
||||||
logger.info("正在初始化浏览器管理器...")
|
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("启动定时任务调度器...")
|
logger.info("启动定时任务调度器...")
|
||||||
threading.Thread(target=scheduled_task_worker, daemon=True, name="scheduled-task-worker").start()
|
threading.Thread(target=scheduled_task_worker, daemon=True, name="scheduled-task-worker").start()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from crypto_utils import encrypt_password as encrypt_account_password
|
|||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from services.accounts_service import load_user_accounts
|
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.browse_types import BROWSE_TYPE_SHOULD_READ, normalize_browse_type, validate_browse_type
|
||||||
from services.client_log import log_to_client
|
from services.client_log import log_to_client
|
||||||
from services.models import Account
|
from services.models import Account
|
||||||
@@ -230,9 +230,9 @@ def start_account(account_id):
|
|||||||
if not browse_type:
|
if not browse_type:
|
||||||
return jsonify({"error": "浏览类型无效"}), 400
|
return jsonify({"error": "浏览类型无效"}), 400
|
||||||
enable_screenshot = data.get("enable_screenshot", True)
|
enable_screenshot = data.get("enable_screenshot", True)
|
||||||
|
if enable_screenshot:
|
||||||
if not init_browser_manager():
|
# 异步初始化浏览器环境,避免首次下载/安装 Chromium 阻塞请求导致“网页无响应”
|
||||||
return jsonify({"error": "浏览器初始化失败"}), 500
|
init_browser_manager_async()
|
||||||
|
|
||||||
ok, message = submit_account_task(
|
ok, message = submit_account_task(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -308,6 +308,9 @@ def manual_screenshot(account_id):
|
|||||||
|
|
||||||
account.last_browse_type = browse_type
|
account.last_browse_type = browse_type
|
||||||
|
|
||||||
|
# 异步初始化浏览器环境,避免首次下载/安装 Chromium 阻塞请求
|
||||||
|
init_browser_manager_async()
|
||||||
|
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=take_screenshot_for_account,
|
target=take_screenshot_for_account,
|
||||||
args=(user_id, account_id, browse_type, "manual_screenshot"),
|
args=(user_id, account_id, browse_type, "manual_screenshot"),
|
||||||
@@ -333,6 +336,10 @@ def batch_start_accounts():
|
|||||||
if not account_ids:
|
if not account_ids:
|
||||||
return jsonify({"error": "请选择要启动的账号"}), 400
|
return jsonify({"error": "请选择要启动的账号"}), 400
|
||||||
|
|
||||||
|
if enable_screenshot:
|
||||||
|
# 异步初始化浏览器环境,避免首次下载/安装 Chromium 阻塞请求
|
||||||
|
init_browser_manager_async()
|
||||||
|
|
||||||
started = []
|
started = []
|
||||||
failed = []
|
failed = []
|
||||||
|
|
||||||
@@ -407,4 +414,3 @@ def batch_stop_accounts():
|
|||||||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||||||
|
|
||||||
return jsonify({"success": True, "stopped_count": len(stopped), "stopped": stopped})
|
return jsonify({"success": True, "stopped_count": len(stopped), "stopped": stopped})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from app_logger import get_logger
|
from app_logger import get_logger
|
||||||
@@ -13,28 +14,99 @@ logger = get_logger("browser_manager")
|
|||||||
|
|
||||||
_browser_manager: Optional[PlaywrightBrowserManager] = None
|
_browser_manager: Optional[PlaywrightBrowserManager] = None
|
||||||
_lock = threading.Lock()
|
_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]:
|
def get_browser_manager() -> Optional[PlaywrightBrowserManager]:
|
||||||
return _browser_manager
|
return _browser_manager
|
||||||
|
|
||||||
|
|
||||||
def init_browser_manager() -> bool:
|
def is_browser_manager_ready() -> bool:
|
||||||
global _browser_manager
|
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:
|
if _browser_manager is not None:
|
||||||
return True
|
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浏览器管理器...")
|
logger.info("正在初始化Playwright浏览器管理器...")
|
||||||
if not check_and_install_browser(log_callback=lambda msg, account_id=None: logger.info(str(msg))):
|
if not check_and_install_browser(log_callback=lambda msg, account_id=None: logger.info(str(msg))):
|
||||||
|
error = "浏览器环境检查失败"
|
||||||
logger.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(
|
with _cond:
|
||||||
headless=True,
|
if ok and manager is not None:
|
||||||
log_callback=lambda msg, account_id=None: logger.info(str(msg)),
|
_browser_manager = manager
|
||||||
)
|
else:
|
||||||
logger.info("Playwright浏览器管理器创建成功!")
|
_init_error = error or "初始化失败"
|
||||||
return True
|
_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()
|
||||||
|
|||||||
Reference in New Issue
Block a user