Spaces:
Running
Running
File size: 14,694 Bytes
3085164 e15281a 615e7b3 3085164 615e7b3 e15281a 3085164 2011b89 3085164 e15281a 3085164 e15281a 3085164 e15281a 3085164 e15281a 3085164 e15281a 3085164 e15281a 3085164 e15281a 3085164 e15281a 3085164 e15281a 3085164 615e7b3 e15281a 615e7b3 e15281a 615e7b3 3085164 615e7b3 3085164 615e7b3 3085164 615e7b3 3085164 615e7b3 3085164 615e7b3 3085164 615e7b3 4047340 615e7b3 4047340 615e7b3 3085164 615e7b3 3085164 615e7b3 3085164 615e7b3 e15281a 615e7b3 3085164 615e7b3 3085164 615e7b3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
import os
import signal
import time
from playwright.sync_api import TimeoutError, Error as PlaywrightError
from utils.logger import setup_logging
from utils.cookie_manager import CookieManager
from browser.navigation import handle_successful_navigation, KeepAliveError
from browser.cookie_validator import CookieValidator
from camoufox.sync_api import Camoufox
from utils.paths import logs_dir
from utils.common import parse_headless_mode, ensure_dir
from utils.url_helper import extract_url_path, mask_url_for_logging, mask_path_for_logging
def run_browser_instance(config, shutdown_event=None):
"""
根据最终合并的配置,启动并管理一个单独的 Camoufox 浏览器实例。
使用CookieManager统一管理Cookie加载,避免重复的扫描逻辑。
"""
# 重置信号处理器,确保子进程能响应 SIGTERM
signal.signal(signal.SIGTERM, signal.SIG_DFL)
# 忽略 SIGINT (Ctrl+C),让主进程统一处理
signal.signal(signal.SIGINT, signal.SIG_IGN)
cookie_source = config.get('cookie_source')
if not cookie_source:
# 使用默认logger进行错误报告
logger = setup_logging(os.path.join(logs_dir(), 'app.log'))
logger.error("错误: 配置中缺少cookie_source对象")
return
instance_label = cookie_source.display_name
logger = setup_logging(
os.path.join(logs_dir(), 'app.log'), prefix=instance_label
)
diagnostic_tag = instance_label.replace(os.sep, "_")
expected_url = config.get('url')
proxy = config.get('proxy')
headless_setting = config.get('headless', 'virtual')
# 使用CookieManager加载Cookie
cookie_manager = CookieManager(logger)
all_cookies = []
try:
# 直接使用CookieSource对象加载Cookie
cookies = cookie_manager.load_cookies(cookie_source)
all_cookies.extend(cookies)
except Exception as e:
logger.error(f"从Cookie来源加载时出错: {e}")
return
# 3. 检查是否有任何Cookie可用
if not all_cookies:
logger.error("错误: 没有可用的Cookie(既没有有效的JSON文件,也没有环境变量)")
return
cookies = all_cookies
headless_mode = parse_headless_mode(headless_setting)
launch_options = {"headless": headless_mode}
# launch_options["block_images"] = True # 禁用图片加载
if proxy:
logger.info(f"使用代理: {proxy} 访问")
launch_options["proxy"] = {"server": proxy, "bypass": "localhost, 127.0.0.1"}
screenshot_dir = logs_dir()
ensure_dir(screenshot_dir)
# 重启控制变量
max_retries = int(os.getenv("MAX_RESTART_RETRIES", "5"))
retry_count = 0
base_delay = 3
while True:
# 检查是否收到全局关闭信号
if shutdown_event and shutdown_event.is_set():
logger.info("检测到全局关闭事件,浏览器实例不再启动,准备退出")
return
try:
with Camoufox(**launch_options) as browser:
context = browser.new_context()
context.add_cookies(cookies)
page = context.new_page()
# 创建Cookie验证器
cookie_validator = CookieValidator(page, context, logger)
# ####################################################################
# ############ 增强的 page.goto() 错误处理和日志记录 ###############
# ####################################################################
response = None
try:
logger.info(f"正在导航到: {mask_url_for_logging(expected_url)} (超时设置为 90 秒)")
# page.goto() 会返回一个 response 对象,我们可以用它来获取状态码等信息
response = page.goto(expected_url, wait_until='domcontentloaded', timeout=90000)
# 检查HTTP响应状态码
if response:
logger.info(f"导航初步成功,服务器响应状态码: {response.status} {response.status_text}")
if not response.ok: # response.ok 检查状态码是否在 200-299 范围内
logger.warning(f"警告:页面加载成功,但HTTP状态码表示错误: {response.status}")
# 即使状态码错误,也保存快照以供分析
page.screenshot(path=os.path.join(screenshot_dir, f"WARN_http_status_{response.status}_{diagnostic_tag}.png"))
else:
# 对于非http/https的导航(如 about:blank),response可能为None
logger.warning("page.goto 未返回响应对象,可能是一个非HTTP导航")
except TimeoutError:
# 这是最常见的错误:超时
logger.error(f"导航到 {mask_url_for_logging(expected_url)} 超时 (超过90秒)")
logger.error("可能原因:网络连接缓慢、目标网站服务器无响应、代理问题、或页面资源被阻塞")
# 尝试保存诊断信息
try:
# 截图对于看到页面卡在什么状态非常有帮助(例如,空白页、加载中、Chrome错误页)
screenshot_path = os.path.join(screenshot_dir, f"FAIL_timeout_{diagnostic_tag}.png")
page.screenshot(path=screenshot_path, full_page=True)
logger.info(f"已截取超时时的屏幕快照: {screenshot_path}")
# 保存HTML可以帮助分析DOM结构,即使在无头模式下也很有用
html_path = os.path.join(screenshot_dir, f"FAIL_timeout_{diagnostic_tag}.html")
with open(html_path, 'w', encoding='utf-8') as f:
f.write(page.content())
logger.info(f"已保存超时时的页面HTML: {html_path}")
except Exception as diag_e:
logger.error(f"在尝试进行超时诊断(截图/保存HTML)时发生额外错误: {diag_e}")
return # 超时后,后续操作无意义,直接终止
except PlaywrightError as e:
# 捕获其他Playwright相关的网络错误,例如DNS解析失败、连接被拒绝等
error_message = str(e)
logger.error(f"导航到 {mask_url_for_logging(expected_url)} 时发生 Playwright 网络错误")
logger.error(f"错误详情: {error_message}")
# Playwright的错误信息通常很具体,例如 "net::ERR_CONNECTION_REFUSED"
if "net::ERR_NAME_NOT_RESOLVED" in error_message:
logger.error("排查建议:检查DNS设置或域名是否正确")
elif "net::ERR_CONNECTION_REFUSED" in error_message:
logger.error("排查建议:目标服务器可能已关闭,或代理/防火墙阻止了连接")
elif "net::ERR_INTERNET_DISCONNECTED" in error_message:
logger.error("排查建议:检查本机的网络连接")
# 同样,尝试截图,尽管此时页面可能完全无法访问
try:
screenshot_path = os.path.join(screenshot_dir, f"FAIL_network_error_{diagnostic_tag}.png")
page.screenshot(path=screenshot_path)
logger.info(f"已截取网络错误时的屏幕快照: {screenshot_path}")
except Exception as diag_e:
logger.error(f"在尝试进行网络错误诊断(截图)时发生额外错误: {diag_e}")
return # 网络错误,终止
# --- 如果导航没有抛出异常,继续执行后续逻辑 ---
logger.info("页面初步加载完成,正在检查并处理初始弹窗...")
page.wait_for_timeout(2000)
final_url = page.url
logger.info(f"导航完成。最终URL为: {mask_url_for_logging(final_url)}")
# ... 你原有的URL检查逻辑保持不变 ...
if "accounts.google.com/v3/signin/identifier" in final_url:
logger.error("检测到Google登录页面(需要输入邮箱)。Cookie已完全失效")
page.screenshot(path=os.path.join(screenshot_dir, f"FAIL_identifier_page_{diagnostic_tag}.png"))
return
# 提取路径部分进行匹配(允许域名重定向)
expected_path = extract_url_path(expected_url).split('?')[0]
final_path = extract_url_path(final_url)
if expected_path and expected_path in final_path:
logger.info(f"URL验证通过。预期路径: {mask_path_for_logging(expected_path)}")
# --- 新的健壮策略:等待加载指示器消失 ---
# 这是解决竞态条件的关键。错误消息或内容只在初始加载完成后才会出现。
spinner_locator = page.locator('mat-spinner')
try:
logger.info("正在等待加载指示器 (spinner) 消失... (最长等待30秒)")
# 我们等待spinner变为'隐藏'状态或从DOM中消失。
spinner_locator.wait_for(state='hidden', timeout=30000)
logger.info("加载指示器已消失。页面已完成异步加载")
except TimeoutError:
logger.error("页面加载指示器在30秒内未消失。页面可能已卡住")
page.screenshot(path=os.path.join(screenshot_dir, f"FAIL_spinner_stuck_{diagnostic_tag}.png"))
raise KeepAliveError("页面加载指示器超时")
# --- 现在我们可以安全地检查错误消息 ---
# 我们使用最具体的文本以避免误判。
auth_error_text = "authentication error"
auth_error_locator = page.get_by_text(auth_error_text, exact=False)
# 这里我们只需要很短的超时时间,因为页面应该是稳定的。
if auth_error_locator.is_visible(timeout=2000):
logger.error(f"检测到认证失败的错误横幅: '{auth_error_text}'. Cookie已过期或无效")
screenshot_path = os.path.join(screenshot_dir, f"FAIL_auth_error_banner_{diagnostic_tag}.png")
page.screenshot(path=screenshot_path)
# html_path = os.path.join(screenshot_dir, f"FAIL_auth_error_banner_{diagnostic_tag}.html")
# with open(html_path, 'w', encoding='utf-8') as f:
# f.write(page.content())
# logger.info(f"已保存包含错误信息的页面HTML: {html_path}")
return # 明确的失败,因此我们退出。
# --- 如果没有错误,进行最终确认(作为后备方案) ---
logger.info("未检测到认证错误横幅。进行最终确认")
login_button_cn = page.get_by_role('button', name='登录')
login_button_en = page.get_by_role('button', name='Login')
if login_button_cn.is_visible(timeout=1000) or login_button_en.is_visible(timeout=1000):
logger.error("页面上仍显示'登录'按钮。Cookie无效")
page.screenshot(path=os.path.join(screenshot_dir, f"FAIL_login_button_visible_{diagnostic_tag}.png"))
return
# --- 如果所有检查都通过,我们假设成功 ---
logger.info("所有验证通过,确认已成功登录")
handle_successful_navigation(page, logger, diagnostic_tag, shutdown_event, cookie_validator)
elif "accounts.google.com/v3/signin/accountchooser" in final_url:
logger.warning("检测到Google账户选择页面。登录失败或Cookie已过期")
page.screenshot(path=os.path.join(screenshot_dir, f"FAIL_chooser_click_failed_{diagnostic_tag}.png"))
return
else:
logger.error(f"导航到了意外的URL")
logger.error(f" 预期路径: {mask_path_for_logging(expected_path)}")
logger.error(f" 最终路径: {mask_path_for_logging(final_path)}")
logger.error(f" 最终URL: {mask_url_for_logging(final_url)}")
page.screenshot(path=os.path.join(screenshot_dir, f"FAIL_unexpected_url_{diagnostic_tag}.png"))
return
# 如果运行到这里且没有异常,表示实例正常结束(例如收到关闭信号)
# 正常结束时重置重试计数器
retry_count = 0
return
except KeepAliveError as e:
retry_count += 1
if retry_count > max_retries:
logger.error(f"重试次数已达上限 ({max_retries}),实例不再重启,退出")
return
# 指数退避:3秒、6秒、12秒、24秒...最长60秒
delay = min(base_delay * (2 ** (retry_count - 1)), 60)
logger.error(f"浏览器实例出现错误 (重试 {retry_count}/{max_retries}),将在 {delay} 秒后重启浏览器实例: {e}")
time.sleep(delay)
continue
except KeyboardInterrupt:
logger.info(f"用户中断,正在关闭...")
return
except SystemExit as e:
# 捕获Cookie验证失败时的系统退出
if e.code == 1:
logger.error("Cookie验证失败,关闭进程实例")
else:
logger.info(f"实例正常退出,退出码: {e.code}")
return
except Exception as e:
# 这是一个最终的捕获,用于捕获所有未预料到的错误
logger.exception(f"运行 Camoufox 实例时发生未预料的严重错误: {e}")
return
|