diff --git a/Dockerfile b/Dockerfile index 1dfedf4..89d215a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,22 +9,28 @@ ENV DEBIAN_FRONTEND=noninteractive # Debian 12 (Bookworm) 的 apt 源文件变成了 /etc/apt/sources.list.d/debian.sources # 这里替换源以加速下载,并安装必要的系统依赖 -# 必须安装:Xvfb(虚拟屏幕), Chromium(浏览器核心), 中文字体(防乱码) +# Linux 生产环境优先使用 Google Chrome Stable,避免 Chromium 在 +# DevTools WebSocket 握手阶段与 DrissionPage 出现兼容性问题。 RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \ sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list 2>/dev/null || true && \ apt-get update && \ apt-get install -y --no-install-recommends \ - xvfb \ - xauth \ - chromium \ - chromium-driver \ + ca-certificates \ + curl \ + gnupg \ fonts-wqy-zenhei \ tzdata \ - && rm -rf /var/lib/apt/lists/* + && install -m 0755 -d /etc/apt/keyrings && \ + curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /etc/apt/keyrings/google-chrome.gpg && \ + echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends google-chrome-stable && \ + rm -rf /var/lib/apt/lists/* # 设置时区为中国上海 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +ENV CHROME_BIN=/usr/bin/google-chrome # 复制依赖清单并安装 Python 库 COPY requirements.txt . @@ -38,9 +44,8 @@ COPY . . EXPOSE 5050 # 启动脚本: -# 1. 清理可能因异常重启遗留的虚拟屏幕锁文件(防止 xvfb 报错退出) -# 2. 切换到 web_ui 目录执行 gunicorn -# 3. 使用 xvfb-run -a 自动分配空闲的虚拟屏幕 -# 4. 浏览器自动化服务必须单 worker 运行,避免多个 Gunicorn 进程同时抢占 Chromium DevTools 端口 -# 5. 使用 gthread 提升单进程下的并发响应能力 -CMD sh -c "rm -f /tmp/.X*-lock && cd web_ui && xvfb-run -a --server-args='-screen 0 1920x1080x24' gunicorn -w 1 --threads 8 --worker-class gthread -b 0.0.0.0:5050 --access-logfile - --timeout 120 app:app" +# 1. 切换到 web_ui 目录执行 gunicorn +# 2. 浏览器自动化服务必须单 worker 运行,避免多个 Gunicorn 进程同时抢占 DevTools 会话 +# 3. Headless Chrome 已足够,无需再叠加 Xvfb,减少 Linux 初始化链路的不确定性 +# 4. 使用 gthread 提升单进程下的并发响应能力 +CMD sh -c "cd web_ui && gunicorn -w 1 --threads 8 --worker-class gthread -b 0.0.0.0:5050 --access-logfile - --timeout 120 app:app" diff --git a/browser_login/fetch_abnormal_report.py b/browser_login/fetch_abnormal_report.py index eeb6dff..57540b2 100644 --- a/browser_login/fetch_abnormal_report.py +++ b/browser_login/fetch_abnormal_report.py @@ -74,7 +74,8 @@ if btn: print("数据已保存至:", path) sys.exit(0) except Exception as e: - pass + import traceback + print(f"发生全局异常: {e}\n{traceback.format_exc()}") print("没有找到匹配的数据") else: diff --git a/browser_login/fetch_issue_receipt_incremental.py b/browser_login/fetch_issue_receipt_incremental.py index 3ddc1ee..18ed61c 100644 --- a/browser_login/fetch_issue_receipt_incremental.py +++ b/browser_login/fetch_issue_receipt_incremental.py @@ -314,7 +314,8 @@ def fetch_issue_receipt_incremental(): log("OK", f"🎉 发料单增量同步大功告成!总计新增了 {total_inserted} 条记录入库!") except Exception as e: - log("ERR", f"发生全局异常: {e}") + import traceback + log("ERR", f"发生全局异常: {e}\n{traceback.format_exc()}") finally: if 'conn' in locals() and conn: conn.close() diff --git a/browser_login/login.py b/browser_login/login.py index 57ad04a..cb58cdc 100644 --- a/browser_login/login.py +++ b/browser_login/login.py @@ -3,8 +3,11 @@ ERP 登录模块 - DrissionPage """ import os +import sys import time +import shutil import datetime +import subprocess import urllib.request from pathlib import Path from dotenv import load_dotenv @@ -29,6 +32,11 @@ def is_docker_env() -> bool: return os.path.exists("/.dockerenv") +def is_linux_env() -> bool: + """判断当前是否运行在 Linux 环境。""" + return sys.platform.startswith("linux") + + def get_docker_tmp_root() -> Path: """ 指定 DrissionPage 在 Docker 中的临时根目录。 @@ -39,6 +47,57 @@ def get_docker_tmp_root() -> Path: return tmp_root +def resolve_browser_path() -> str: + """ + 统一解析浏览器路径。 + Linux 生产环境优先使用 Google Chrome Stable,避免 Chromium 与 + DrissionPage 在 DevTools WebSocket 握手阶段出现兼容性问题。 + """ + env_candidates = [ + os.getenv("DRISSION_BROWSER_PATH", "").strip(), + os.getenv("CHROME_BIN", "").strip(), + os.getenv("BROWSER_PATH", "").strip(), + ] + for candidate in env_candidates: + if candidate and os.path.exists(candidate): + return candidate + + if is_linux_env(): + browser_candidates = [ + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + ] + else: + browser_candidates = [ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + shutil.which("google-chrome") or "", + shutil.which("chromium") or "", + shutil.which("chromium-browser") or "", + ] + + for candidate in browser_candidates: + if candidate and os.path.exists(candidate): + return candidate + + return "" + + +def cleanup_debug_port(address: str) -> None: + """按实际 DevTools 端口清理僵尸浏览器进程。""" + if not address or ":" not in address: + return + + debug_port = address.rsplit(":", 1)[-1] + subprocess.run( + f"lsof -ti tcp:{debug_port} | xargs -r kill -9", + shell=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + # ── 日志 ────────────────────────────────────────────────────────────────────── def log(level: str, msg: str): icons = {"INFO": "ℹ️ ", "OK": "✅", "WARN": "⚠️ ", "ERR": "❌"} @@ -63,43 +122,40 @@ def dump_page_state(page: ChromiumPage, label: str = ""): # ── 浏览器 ──────────────────────────────────────────────────────────────────── def get_page(headless: bool = False, port: int = 9222) -> ChromiumPage: co = ChromiumOptions() - # 强制在生产环境下使用无头模式 is_docker = is_docker_env() - - if headless or is_docker: + browser_path = resolve_browser_path() + effective_headless = headless or is_docker + + if effective_headless: co.set_argument("--headless=new") - co.set_argument("--disable-gpu") # Docker 无头模式下强烈建议禁用 GPU,防止 404 和渲染崩溃 - + co.set_argument("--disable-gpu") + co.set_argument("--disable-blink-features=AutomationControlled") co.set_argument("--no-sandbox") - co.set_argument("--disable-dev-shm-usage") # 防止 Docker 共享内存耗尽导致浏览器崩溃 - co.set_argument("--disable-software-rasterizer") # 配合无头模式禁用软件光栅化器 - co.set_argument("--remote-allow-origins=*") # 解决 Docker 下 websocket 404 问题 + co.set_argument("--disable-dev-shm-usage") + co.set_argument("--disable-software-rasterizer") + co.set_argument("--remote-allow-origins=*") co.set_argument("--remote-debugging-address=127.0.0.1") co.set_argument("--disable-web-security") co.set_argument("--ignore-certificate-errors") - co.set_argument("--proxy-server=direct://") # 禁用代理 + co.set_argument("--proxy-server=direct://") co.set_argument("--proxy-bypass-list=*") co.set_argument("--window-size=1440,900") - + + if browser_path: + co.set_browser_path(browser_path) + log("INFO", f"选用浏览器内核: {browser_path}") + else: + log("WARN", "未解析到明确浏览器路径,将使用 DrissionPage 默认浏览器发现逻辑。") + if is_docker: - # Docker 生产环境:由 DrissionPage 自动分配独立端口和 profile,避免僵尸会话导致 404 tmp_root = get_docker_tmp_root() co.set_tmp_path(str(tmp_root)) co.auto_port(True) log("INFO", f"Docker Drission 临时目录: {tmp_root}") - # 很多 Debian/Ubuntu 系统的 Chromium 实际上是通过 wrapper 脚本调用的 - # 直接指定确切的执行路径,防止 DrissionPage 底层启动失败 - if os.path.exists('/usr/bin/chromium'): - co.set_browser_path('/usr/bin/chromium') - elif os.path.exists('/usr/bin/chromium-browser'): - co.set_browser_path('/usr/bin/chromium-browser') - elif os.path.exists('/usr/bin/google-chrome'): - co.set_browser_path('/usr/bin/google-chrome') else: - # 本地开发环境:使用固定端口,方便复用 co.set_local_port(port) - + # #region debug-point A:drission-target opt = handle_options(co) log( @@ -122,6 +178,18 @@ def get_page(headless: bool = False, port: int = 9222) -> ChromiumPage: page = ChromiumPage(opt) return page except Exception as e: + actual_address = opt.address or f"127.0.0.1:{port}" + log("WARN", f"[DEBUG] ChromiumPage 初始化失败: {e},尝试清理地址 {actual_address} 后重试...") + try: + cleanup_debug_port(actual_address) + time.sleep(1) + page = ChromiumPage(opt) + log("OK", "[DEBUG] 清理后重试成功!") + return page + except Exception as retry_e: + log("ERR", f"[DEBUG] 清理后重试依然失败: {retry_e}") + e = retry_e + # #region debug-point B:devtools-http-probe if opt.address: for endpoint in ("json/version", "json/list"): diff --git a/docker-compose.yml b/docker-compose.yml index 21e2f21..9c25f09 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - TZ=Asia/Shanghai - ENABLE_BACKGROUND_SCHEDULER=1 - DRISSION_TMP_ROOT=/tmp + - CHROME_BIN=/usr/bin/google-chrome volumes: # 既然用 Git 拉取了完整代码,直接用相对路径挂载更优雅 # 直接挂载整个 output 文件夹,里面的 erp_data.db 自动持久化