fix(data): bs_eod挂死根治——login/logout纳入_with_timeout保护伞+中文系统运行判定三词修正。①根因(08-20 19:26实锤,日志形态逐字吻合):bs.login()打印success后仍有后续往返,baostock recv不遵守socket default timeout,主线程卡死在login内部recv(50min+零输出,连60s超时err都没有);fetch早已套_with_timeout但login/logout是裸调——relogin/login_once全部纳入(login60s/logout30s),超时按登录失败处理走既有重试链;②bs_fundamentals._is_running_text:中文系统实际输出「正在运行」不含连续子串「运行中」,原两词判定在中文VPS恒False=守卫恒fail-open(当晚若不禁用bs-fund将真撞车);三词都认+回归钉测试;③check_task_running.py通用探针(修复版)替换VPS上带同bug的check_bs_eod.py;+4测试(登录超时返False/relogin全程超时不raise/login与logout必经保护伞契约/正在运行判定) [vps]
This commit is contained in:
@@ -81,7 +81,10 @@ log = logging.getLogger(__name__)
|
||||
|
||||
def login_once():
|
||||
try:
|
||||
lg = bs.login()
|
||||
# 2026-08-20 实锤: bs.login() 打印 "login success!" 后仍有后续往返, 服务端
|
||||
# hiccup 时主线程可卡死在 login 内部 recv(08-20 19:26 挂死 50min+ 形态:
|
||||
# logout✓ login success!打印后零输出) —— 与 fetch 同罩 _with_timeout。
|
||||
lg = _with_timeout(bs.login, timeout=60)
|
||||
if lg.error_code == "0":
|
||||
return True
|
||||
log.error("login fail: %s %s", lg.error_code, lg.error_msg)
|
||||
@@ -93,14 +96,14 @@ def login_once():
|
||||
|
||||
def relogin():
|
||||
try:
|
||||
bs.logout()
|
||||
_with_timeout(bs.logout, timeout=30)
|
||||
except Exception:
|
||||
pass
|
||||
if login_once():
|
||||
return True
|
||||
time.sleep(2)
|
||||
try:
|
||||
bs.logout()
|
||||
_with_timeout(bs.logout, timeout=30)
|
||||
except Exception:
|
||||
pass
|
||||
return login_once()
|
||||
|
||||
@@ -269,8 +269,12 @@ def run_reports(stocks, today, force=False):
|
||||
# ======================== bs_eod 在跑守卫 ========================
|
||||
|
||||
def _is_running_text(text):
|
||||
"""schtasks /query 输出判断 Running(schtasks 可能英/中输出, 两词都认)."""
|
||||
return ("Running" in text) or ("运行中" in text)
|
||||
"""schtasks /query 输出判断 Running(schtasks 可能英/中输出, 各词都认).
|
||||
|
||||
2026-08-20 实锤: 中文系统输出是「正在运行」, 不含连续子串「运行中」——
|
||||
原两词判定在中文 VPS 恒 False(守卫恒 fail-open), 三词都认才对。
|
||||
"""
|
||||
return any(w in text for w in ("Running", "运行中", "正在运行"))
|
||||
|
||||
|
||||
def _bs_eod_running():
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""check_task_running.py — schtask 是否在跑(exit 0=在跑, 1=已退出/查无)。
|
||||
运维探针(watcher/cron 轮询用)。中文输出「正在运行」不含子串「运行中」——三词都认
|
||||
(2026-08-20 假阳性实锤: 两词判定曾致 bs_eod 挂死被误判已退出)。"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
task = sys.argv[1] if len(sys.argv) > 1 else 'sanguo-bs-daily'
|
||||
out = subprocess.run(['schtasks', '/query', '/tn', task, '/fo', 'LIST'],
|
||||
capture_output=True, timeout=30).stdout.decode('gbk', 'ignore')
|
||||
running = any(w in out for w in ('Running', '运行中', '正在运行'))
|
||||
print('%s: %s' % (task, 'RUNNING' if running else 'DONE/NOT-FOUND'))
|
||||
sys.exit(0 if running else 1)
|
||||
@@ -511,3 +511,41 @@ def test_main_exits_2_when_all_login_retries_fail(isolated_main_env, monkeypatch
|
||||
with pytest.raises(SystemExit) as ei:
|
||||
bs_eod.main()
|
||||
assert ei.value.code == 2
|
||||
|
||||
|
||||
# ---------- login/logout 超时保护伞 (2026-08-20 19:26 挂死实锤) ----------
|
||||
# 形态: 日志停在 "logout success!/login success!" 后零输出 50min+ —— bs.login()
|
||||
# 打印 success 后仍有后续往返, recv 不遵守 socket default timeout, 主线程卡死
|
||||
# 在 login 内部. fetch 早已套 _with_timeout, login/logout 当时是裸调.
|
||||
|
||||
def test_login_once_timeout_returns_false(monkeypatch):
|
||||
"""bs.login 卡死(_with_timeout 抛 TimeoutError) → login_once 返 False 不挂死."""
|
||||
monkeypatch.setattr(
|
||||
bs_eod, "_with_timeout",
|
||||
MagicMock(side_effect=TimeoutError("login 超过 60s")))
|
||||
assert bs_eod.login_once() is False
|
||||
|
||||
|
||||
def test_relogin_survives_total_timeout(monkeypatch):
|
||||
"""logout+login 全部超时 → relogin 返 False 全程不 raise(上层跳过该股继续)."""
|
||||
monkeypatch.setattr(
|
||||
bs_eod, "_with_timeout",
|
||||
MagicMock(side_effect=TimeoutError("socket 死")))
|
||||
monkeypatch.setattr(bs_eod.time, "sleep", lambda s: None)
|
||||
assert bs_eod.relogin() is False
|
||||
|
||||
|
||||
def test_login_logout_gone_through_timeout_umbrella(monkeypatch):
|
||||
"""契约: bs.login/bs.logout 必须经 _with_timeout 调用(裸调=挂死回归口)."""
|
||||
ok_login = MagicMock()
|
||||
ok_login.error_code = "0"
|
||||
mock_bs = MagicMock()
|
||||
mock_bs.login.return_value = ok_login
|
||||
monkeypatch.setattr(bs_eod, "bs", mock_bs)
|
||||
monkeypatch.setattr(
|
||||
bs_eod, "_with_timeout",
|
||||
MagicMock(side_effect=lambda fn, **kw: fn()))
|
||||
assert bs_eod.relogin() is True
|
||||
called = [c.args[0] for c in bs_eod._with_timeout.call_args_list]
|
||||
assert mock_bs.logout in called
|
||||
assert mock_bs.login in called
|
||||
|
||||
@@ -221,6 +221,9 @@ def test_reports_skip_non_sunday_and_second_sunday(tmp_out, small_stocks,
|
||||
def test_is_running_text_variants():
|
||||
assert bf._is_running_text("Status: Running") is True
|
||||
assert bf._is_running_text("状态: 运行中") is True
|
||||
# 2026-08-20 实锤回归钉: 中文系统实际输出「正在运行」, 不含连续子串「运行中」
|
||||
# —— 两词判定恒 False 曾致守卫 fail-open(且当晚 check 探针误报任务已退出)
|
||||
assert bf._is_running_text("状态: 正在运行") is True
|
||||
assert bf._is_running_text("Status: Ready") is False
|
||||
assert bf._is_running_text("模式: 就绪") is False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user