# -*- 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())