244 lines
9.4 KiB
Python
244 lines
9.4 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
|
|
|
|
ACCOUNT = "66639661"
|
|
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())
|