Files
sanguo_vnpy_v2/scripts/qmt_relogin/qmt_relogin.py
T
claude_dev 434d7cb357
CI/CD / test (push) Failing after 7s
CI/CD / nas-deploy (push) Has been cancelled
CI/CD / nas-verify (push) Has been cancelled
feat(config): QMT 部署身份单一权威源 config/qmt_identity.json——P2-18 账号硬编码根治(⚖️-2 用户拍板:确认永为模拟号+令根治硬编码)
根因三层(全案=audit/20261005_qmt_identity_env_plan.md): schtask 无 per-task env+机器级 env 未设→wrapper 被迫字面量;「部署身份」无单一权威源(env 名分裂三个);模拟号零安全压力致 32 文件 88 处 copy-paste 扩散。

- qmt_gate_common.load_identity 正典装载器(:14-59): env SANGUO_QMT_ACCOUNT/BIGQMT_ACCOUNT_ID(兼容别名)>json>RuntimeError fail-loud;SANGUO_IDENTITY_JSON 显式指路不存在=立即报错;查找=repo 相对>C:\sanguo_vnpy_v2\config\(生产副本回退位)
- 灾备腿同源: relogin/qmt_bridge_probe/setclip 改 from qmt_gate_common import ACCOUNT
- 桥腿镜像同链: xt_gateway._qmt_account/qmt_gateway_client._identity_default_account/is_price_source_vps/bridge_switch_ops(3 处 env 注入+1 处内嵌探针串)/check_xtquant——env 带默认的硬编码默认值全数退役
- xt_eod_wrapper.ps1 改 Get-Content identity json(保持 ASCII);码内 docstring 字面量→占位符(engine/runner/runner_live/live.yaml 注释/README)
- .gitignore 白名单 !config/qmt_identity.json(全局 *.json 曾吞之,git check-ignore 实证)
- tests/trader/test_qmt_identity.py 五钉: 装载链语义(env 双名覆盖/显式指路 fail)+单源一致性(identity≡live.yaml≡watch_accounts≡gate_common≡各腿镜像,休市表 CI 同步测先例)+码面字面量 grep-pin(生产码面零 66639661,只 config 三件套可含)
- 监控 spec: §3.2 账号来源 bullet/§9.A-18 坑条/§12 行数+行号勘正(插入块致 bridge_ping/calendar_probe/relogin_running 引用行号推移);runbook: 非代码资产两行(副本回退路径+生产 wrapper 待改清单=10-08 窗口后 §五③)

生产 wrapper(C:\sanguo_bigqmt 库外)字面量收尾=10-08 复市验证窗口后非代码资产流程;VPS 班车同窗口(护 10-08 07:50 relogin 首班旧码纯净)。trader+api 620 绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-05 17:32:42 +08:00

245 lines
9.5 KiB
Python

# -*- coding: utf-8 -*-
"""Morning auto-relogin for the Big QMT client (surgical: XtItClient.exe only).
Flow: discover install dir (psutil, cached to disk) -> check session state
-> flush stale bridge queue (read-only guard: abort if any ORDER method found)
-> taskkill XtItClient.exe (polite first; mini/miniquote untouched, xt_eod
depends on them) -> settle -> open_qmt: exe mode first (remembered-session
auto-login, proven by the daily 08:50 built-in restart), login mode with
typed credentials as fallback -> poll bridge RPC until it answers.
Force-kill note: a forced shutdown is known to SKIP instance auto-run on next
boot; kill_client still forces as last resort but the bridge check will then
fail loudly (exit 5) instead of lying green.
Known boundaries (measured 09-09 night):
- The login dialog has a 4-char graphic captcha (refreshes every 40s) and
ships with remember-password / auto-login unchecked. Typed-credential login
WILL most likely die on the captcha; the fix is a one-time manual login with
both checkboxes ticked, after which cold boots take the exe auto-login path.
- Polite close (WM_CLOSE) logs the session OUT -> next boot shows the login
dialog; force-kill KEEPS the session -> next boot auto-logins straight to
the main window (proven 19:27 vs 20:53 same night).
- XtMiniQmt.exe / miniquote.exe are the xt_eod data leg. They were observed
dying alongside our surgical XtItClient kills even though taskkill /IM
matches only one name -- so we log their state after every kill for early
detection, and warn (not fail) if they are gone at success time.
Credentials come from BIGQMT_LOGIN_USER / BIGQMT_LOGIN_PASSWORD env, set by the
PowerShell wrapper from a DPAPI-encrypted blob: never in argv, never on disk.
Exit codes: 0 ok | 3 creds missing | 4 session locked (fallback blocked)
6 client would not close/open | 7 order-class items in stale queue
5 bridge rpc still down after login
"""
import datetime
import os
import subprocess
import sys
import time
from qmt_gate_common import ACCOUNT # 部署身份单一权威源(P2-18)
CACHE_PATH = r"C:\sanguo_bigqmt\qmt_install_dir.txt"
QUEUE_KEY = "bigqmt:rpc:queue:%s" % ACCOUNT
def log(msg):
print("[%s] %s" % (datetime.datetime.now().strftime("%m-%d %H:%M:%S"), msg), flush=True)
def discover_install_dir():
"""Find bin.x64 from the running XtItClient.exe; cache for cold starts."""
try:
import psutil
for p in psutil.process_iter(["name"]):
try:
if (p.info["name"] or "").lower() == "xtitclient.exe":
exe = p.exe()
bin_dir = os.path.dirname(exe)
with open(CACHE_PATH, "w", encoding="utf-8") as f:
f.write(bin_dir)
log("install_dir discovered live: %r" % bin_dir)
return bin_dir
except Exception:
continue
except Exception as exc:
log("psutil discovery failed: %r" % exc)
if os.path.isfile(CACHE_PATH):
with open(CACHE_PATH, encoding="utf-8") as f:
cached = f.read().strip()
if cached:
log("install_dir from cache: %r" % cached)
return cached
return None
def _procs_alive():
"""Name -> bool for the QMT family processes we care about."""
out = subprocess.run(["tasklist", "/FO", "CSV", "/NH"],
capture_output=True, text=True).stdout or ""
return {name: ('"%s"' % name) in out
for name in ("XtItClient.exe", "XtMiniQmt.exe", "miniquote.exe")}
def kill_client(grace_seconds=8):
"""Kill only XtItClient.exe. Polite first (WM_CLOSE lets it flush), force after."""
subprocess.run(["taskkill", "/IM", "XtItClient.exe"],
capture_output=True)
deadline = time.time() + grace_seconds
while time.time() < deadline:
out = subprocess.run(["tasklist", "/FI", "IMAGENAME eq XtItClient.exe"],
capture_output=True, text=True).stdout or ""
if "XtItClient.exe" not in out:
log("XtItClient closed cleanly; QMT family now: %s"
% _procs_alive())
return True
time.sleep(1)
log("still alive after %ds; forcing (auto-run may be skipped!)" % grace_seconds)
subprocess.run(["taskkill", "/IM", "XtItClient.exe", "/F"], capture_output=True)
time.sleep(3)
out = subprocess.run(["tasklist", "/FI", "IMAGENAME eq XtItClient.exe"],
capture_output=True, text=True).stdout or ""
ok = "XtItClient.exe" not in out
log("force kill done, gone=%s" % ok)
return ok
def flush_stale_queue():
"""Delete stale read-only requests; REFUSE if any order-class item waits."""
import json
import re
import redis
from bigqmt_signal_trader.redis_rpc import (ORDER_METHODS,
decode_rpc_request_payload)
with open(r"C:\redis\redis.conf") as f:
pw = re.search(r"^requirepass (\S+)", f.read(), re.M).group(1)
rc = redis.Redis(host="127.0.0.1", port=6379, db=5, password=pw,
decode_responses=True)
n = rc.llen(QUEUE_KEY)
orders = 0
for it in rc.lrange(QUEUE_KEY, 0, -1):
try:
obj = json.loads(decode_rpc_request_payload(it))
if obj.get("method") in ORDER_METHODS:
orders += 1
except Exception:
continue
if orders:
log("FATAL: %d order-class items in stale queue; manual review" % orders)
return False
rc.delete(QUEUE_KEY)
log("queue flushed: %d stale read-only items" % n)
return True
def bridge_alive(timeout_seconds=150, rc=None):
"""Poll the bridge RPC server until it answers. Any dict envelope counts --
'ping' may not be a registered method, so fall back to an ORDER-class query
(read-only, and ORDER data is available immediately after login)."""
from bigqmt_signal_trader.redis_rpc import call_redis_rpc
if rc is None:
import re
import redis
with open(r"C:\redis\redis.conf") as f:
pw = re.search(r"^requirepass (\S+)", f.read(), re.M).group(1)
rc = redis.Redis(host="127.0.0.1", port=6379, db=5, password=pw,
decode_responses=True)
deadline = time.time() + timeout_seconds
last = None
while time.time() < deadline:
for method, params in (("ping", {}), ("query_stock_orders", {"account_id": ACCOUNT})):
try:
r = call_redis_rpc(rc, ACCOUNT, method, params, timeout_seconds=10)
if isinstance(r, dict):
return method, r
last = "%s -> %r" % (method, r)
except Exception as exc:
last = "%s -> %s" % (method, exc)
time.sleep(5)
log("bridge rpc last: %s" % last)
return None
def warn_data_leg():
"""2026-09-11: xt_eod switched to the big-QMT bridge (xtquant_bridge
shim), so XtMiniQmt.exe absence is NORMAL and no longer a data gap.
Kept as a no-op placeholder because the relogin flow used to log a
misleading warning here; see xt-eod-bridge-leg-switch-20260911."""
return None
def main():
user = os.environ.get("BIGQMT_LOGIN_USER", "")
pwd = os.environ.get("BIGQMT_LOGIN_PASSWORD", "")
log("=== qmt_relogin start (user=%s pwdlen=%d) ===" % (user, len(pwd)))
from bigqmt_signal_trader import qmt_launcher as L
install_dir = discover_install_dir()
if not install_dir or not os.path.isdir(install_dir):
log("FATAL: cannot resolve install dir: %r" % install_dir)
return 6
if not flush_stale_queue():
return 7
if not kill_client():
log("FATAL: XtItClient.exe would not die")
return 6
time.sleep(5) # socket linger settle (upstream restart_qmt uses 5s)
creds = {"user": user, "password": pwd} if user and pwd else None
locked = L.session_is_locked()
if locked:
log("warn: console session locked; exe auto-login still allowed, "
"credential-typing fallback will not be possible")
mode_used = None
try:
waited = L.open_qmt(install_dir, mode="exe", ready_timeout_seconds=240)
mode_used = "exe"
log("open_qmt(exe) ready in %.1fs" % waited)
except Exception as exc:
log("exe-mode failed: %r" % exc)
mode_used = None
got = bridge_alive(150) if mode_used else None
if got:
log("bridge up after exe-mode (remembered session) via %r ok=%s"
% (got[0], got[1].get("ok")))
log("=== RELOGIN_DONE ===")
return 0
# fallback: credential-typing login mode
if not creds:
log("FATAL: bridge down and no credentials for login fallback")
return 3
if locked:
log("FATAL: bridge down, fallback needs typing but session locked")
return 4
if mode_used: # client is up but bridge dead: close before login retry
if not kill_client():
return 6
time.sleep(5)
try:
waited = L.open_qmt(install_dir, mode="login", credentials=creds,
window_title_prefix="QMT", ready_timeout_seconds=240)
log("open_qmt(login) ready in %.1fs" % waited)
except Exception as exc:
log("FATAL: open_qmt(login) failed: %r" % exc)
return 6
got = bridge_alive(150)
if not got:
log("FATAL: bridge rpc did not answer after login")
return 5
log("bridge rpc answering via %r: ok=%s" % (got[0], got[1].get("ok")))
log("=== RELOGIN_DONE ===")
return 0
if __name__ == "__main__":
sys.exit(main())