cfquant 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- LTtx/__init__.py +2 -0
- LTtx/tx/Config.txt +8 -0
- LTtx/tx/LTtx_server.py +1177 -0
- LTtx/tx/__init__.py +5 -0
- LTtx/tx/tx.py +1935 -0
- cfquant/__init__.py +26 -0
- cfquant/account_routing.py +119 -0
- cfquant/channels.py +76 -0
- cfquant/cli.py +266 -0
- cfquant/client.py +365 -0
- cfquant/config.py +86 -0
- cfquant/hub.py +304 -0
- cfquant/logging_i18n.py +348 -0
- cfquant/normal_bridge.py +676 -0
- cfquant/pipe_bridge.py +249 -0
- cfquant/pipe_client.py +223 -0
- cfquant/pipe_hub.py +602 -0
- cfquant/pipe_transport.py +493 -0
- cfquant/protocol.py +238 -0
- cfquant/qmt_bridge.py +1081 -0
- cfquant/runtime_report.py +334 -0
- cfquant/tx.py +1898 -0
- cfquant/tx_trade_bridge.py +1905 -0
- cfquant/version.py +5 -0
- cfquant/xtconstant.py +262 -0
- cfquant/xtdata.py +462 -0
- cfquant/xttrader.py +955 -0
- cfquant/xttype.py +586 -0
- cfquant-0.2.0.dist-info/LICENSE +21 -0
- cfquant-0.2.0.dist-info/METADATA +144 -0
- cfquant-0.2.0.dist-info/RECORD +90 -0
- cfquant-0.2.0.dist-info/WHEEL +5 -0
- cfquant-0.2.0.dist-info/entry_points.txt +4 -0
- cfquant-0.2.0.dist-info/top_level.txt +6 -0
- cfquant_pipe_hub.py +45 -0
- cfquant_web_server.py +12137 -0
- qmt_scripts/CFQUANT.py +507 -0
- qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +830 -0
- qmt_scripts/CFQUANT_LITE.py +4928 -0
- qmt_scripts/CFQUANT_TRADE_LOWLAT.py +382 -0
- qmt_scripts/__init__.py +1 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SH.py +866 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py +866 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SH.py +5020 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SZ.py +5020 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SH.py +436 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SZ.py +436 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/readme.md +95 -0
- web_dashboard/__init__.py +1 -0
- web_dashboard/app.js +9847 -0
- web_dashboard/avatars/amber-pulse.svg +15 -0
- web_dashboard/avatars/copper-grid.svg +15 -0
- web_dashboard/avatars/market-blue.svg +15 -0
- web_dashboard/avatars/rose-circuit.svg +15 -0
- web_dashboard/avatars/signal-green.svg +15 -0
- web_dashboard/avatars/slate-wave.svg +15 -0
- web_dashboard/avatars/teal-orbit.svg +16 -0
- web_dashboard/avatars/violet-node.svg +15 -0
- web_dashboard/favicon.svg +5 -0
- web_dashboard/image/qmt_deploy/image-10.png +0 -0
- web_dashboard/image/qmt_deploy/image-11.png +0 -0
- web_dashboard/image/qmt_deploy/image-12.png +0 -0
- web_dashboard/image/qmt_deploy/image-13.png +0 -0
- web_dashboard/image/qmt_deploy/image-14.png +0 -0
- web_dashboard/image/qmt_deploy/image-15.png +0 -0
- web_dashboard/image/qmt_deploy/image-16.png +0 -0
- web_dashboard/image/qmt_deploy/image-17.png +0 -0
- web_dashboard/image/qmt_deploy/image-18.png +0 -0
- web_dashboard/image/qmt_deploy/image-19.png +0 -0
- web_dashboard/image/qmt_deploy/image-20.png +0 -0
- web_dashboard/image/qmt_deploy/image-21.png +0 -0
- web_dashboard/image/qmt_deploy/image-4.png +0 -0
- web_dashboard/image/qmt_deploy/image-5.png +0 -0
- web_dashboard/image/qmt_deploy/image-6.png +0 -0
- web_dashboard/image/qmt_deploy/image-7.png +0 -0
- web_dashboard/image/qmt_deploy/image-8.png +0 -0
- web_dashboard/image/qmt_deploy/image-9.png +0 -0
- web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-1.png +0 -0
- web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-2.png +0 -0
- web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-3.png +0 -0
- web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479325179.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479745262.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480178700.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480428759.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783489807217.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783490503491.png +0 -0
- web_dashboard/index.html +2317 -0
- web_dashboard/styles.css +7817 -0
- web_dashboard/vendor/mermaid.min.js +2029 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""QMT runtime version marker helpers.
|
|
3
|
+
|
|
4
|
+
The marker file is intentionally independent from LTtx and pipe RPC so the web
|
|
5
|
+
process can identify the running QMT script version even while request channels
|
|
6
|
+
are blocked or still warming up.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import glob
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
RUNTIME_SCHEMA = "cfquant.qmt.runtime"
|
|
18
|
+
MARKER_FILENAME_PREFIX = "cfquant_qmt_runtime"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _now_text(ts=None):
|
|
22
|
+
ts = float(ts if ts is not None else time.time())
|
|
23
|
+
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _text(value):
|
|
27
|
+
if value is None:
|
|
28
|
+
return ""
|
|
29
|
+
try:
|
|
30
|
+
return str(value).strip()
|
|
31
|
+
except Exception:
|
|
32
|
+
return ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _path(value):
|
|
36
|
+
value = _text(value)
|
|
37
|
+
if not value:
|
|
38
|
+
return ""
|
|
39
|
+
try:
|
|
40
|
+
return os.path.abspath(os.path.expandvars(os.path.expanduser(value)))
|
|
41
|
+
except Exception:
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _safe_name(value, default="runtime"):
|
|
46
|
+
value = _text(value) or default
|
|
47
|
+
out = []
|
|
48
|
+
for ch in value:
|
|
49
|
+
if ch.isalnum() or ch in ("-", "_", "."):
|
|
50
|
+
out.append(ch)
|
|
51
|
+
else:
|
|
52
|
+
out.append("_")
|
|
53
|
+
name = "".join(out).strip("._")
|
|
54
|
+
return name or default
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _append_unique(paths, path):
|
|
58
|
+
path = _path(path)
|
|
59
|
+
if not path:
|
|
60
|
+
return
|
|
61
|
+
try:
|
|
62
|
+
key = os.path.normcase(os.path.abspath(path))
|
|
63
|
+
except Exception:
|
|
64
|
+
key = path.lower()
|
|
65
|
+
for item in paths:
|
|
66
|
+
try:
|
|
67
|
+
item_key = os.path.normcase(os.path.abspath(item))
|
|
68
|
+
except Exception:
|
|
69
|
+
item_key = item.lower()
|
|
70
|
+
if item_key == key:
|
|
71
|
+
return
|
|
72
|
+
paths.append(path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def configured_marker_dirs(config=None, entry_base_dir=None):
|
|
76
|
+
config = config if isinstance(config, dict) else {}
|
|
77
|
+
paths = []
|
|
78
|
+
|
|
79
|
+
for key in ("qmt_runtime_marker_dir", "runtime_marker_dir"):
|
|
80
|
+
_append_unique(paths, config.get(key))
|
|
81
|
+
for key in ("runtime_status_dir", "web_runtime_status_dir"):
|
|
82
|
+
base = _path(config.get(key))
|
|
83
|
+
if base:
|
|
84
|
+
_append_unique(paths, os.path.join(base, "qmt_runtime"))
|
|
85
|
+
for key in ("runtime_dir", "web_runtime_dir"):
|
|
86
|
+
base = _path(config.get(key))
|
|
87
|
+
if base:
|
|
88
|
+
_append_unique(paths, os.path.join(base, "status", "qmt_runtime"))
|
|
89
|
+
|
|
90
|
+
for name in ("CFQUANT_QMT_RUNTIME_MARKER_DIR", "CFQUANT_RUNTIME_MARKER_DIR"):
|
|
91
|
+
_append_unique(paths, os.environ.get(name))
|
|
92
|
+
for name in ("CFQUANT_RUNTIME_STATUS_DIR", "CFQUANT_WEB_RUNTIME_STATUS_DIR"):
|
|
93
|
+
base = _path(os.environ.get(name))
|
|
94
|
+
if base:
|
|
95
|
+
_append_unique(paths, os.path.join(base, "qmt_runtime"))
|
|
96
|
+
for name in ("CFQUANT_RUNTIME_DIR", "CFQUANT_WEB_RUNTIME_DIR"):
|
|
97
|
+
base = _path(os.environ.get(name))
|
|
98
|
+
if base:
|
|
99
|
+
_append_unique(paths, os.path.join(base, "status", "qmt_runtime"))
|
|
100
|
+
|
|
101
|
+
base_dir = _path(entry_base_dir)
|
|
102
|
+
if not base_dir:
|
|
103
|
+
entry_file = _path(config.get("entry_file"))
|
|
104
|
+
if entry_file:
|
|
105
|
+
base_dir = os.path.dirname(entry_file)
|
|
106
|
+
if base_dir:
|
|
107
|
+
parent_dir = os.path.dirname(base_dir)
|
|
108
|
+
_append_unique(paths, os.path.join(base_dir, "runtime", "status", "qmt_runtime"))
|
|
109
|
+
if parent_dir and parent_dir != base_dir:
|
|
110
|
+
_append_unique(paths, os.path.join(parent_dir, "runtime", "status", "qmt_runtime"))
|
|
111
|
+
if os.path.basename(base_dir).lower() == "python":
|
|
112
|
+
_append_unique(paths, os.path.join(parent_dir, "bin.x64", "runtime", "status", "qmt_runtime"))
|
|
113
|
+
|
|
114
|
+
if not paths:
|
|
115
|
+
_append_unique(paths, os.path.join(tempfile.gettempdir(), "cfquant", "runtime", "status", "qmt_runtime"))
|
|
116
|
+
return paths
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_qmt_runtime_report(
|
|
120
|
+
reason="startup",
|
|
121
|
+
version="",
|
|
122
|
+
core_version="",
|
|
123
|
+
entry_version="",
|
|
124
|
+
entry_script="",
|
|
125
|
+
entry_file="",
|
|
126
|
+
bridge="",
|
|
127
|
+
bridge_id="default",
|
|
128
|
+
account_id="",
|
|
129
|
+
account_type="",
|
|
130
|
+
account_key="",
|
|
131
|
+
mode="",
|
|
132
|
+
transport="",
|
|
133
|
+
runtime_mode="",
|
|
134
|
+
channel_key="runtime",
|
|
135
|
+
request_channel="",
|
|
136
|
+
callback_event_channel="",
|
|
137
|
+
channels=None,
|
|
138
|
+
pipe_name="",
|
|
139
|
+
market="",
|
|
140
|
+
market_role="",
|
|
141
|
+
market_route_parent_bridge_id="",
|
|
142
|
+
config=None,
|
|
143
|
+
globals_dict=None,
|
|
144
|
+
module_file="",
|
|
145
|
+
started_at=None,
|
|
146
|
+
extra=None,
|
|
147
|
+
):
|
|
148
|
+
config = config if isinstance(config, dict) else {}
|
|
149
|
+
globals_dict = globals_dict if isinstance(globals_dict, dict) else {}
|
|
150
|
+
channels = channels if isinstance(channels, dict) else (config.get("channels") if isinstance(config.get("channels"), dict) else {})
|
|
151
|
+
channel_key = _text(channel_key) or "runtime"
|
|
152
|
+
request_channel = _text(request_channel) or _text(channels.get(channel_key))
|
|
153
|
+
entry_file = _path(entry_file or globals_dict.get("__file__") or config.get("entry_file"))
|
|
154
|
+
entry_script = _text(entry_script or config.get("entry_script"))
|
|
155
|
+
if not entry_script and entry_file:
|
|
156
|
+
entry_script = os.path.basename(entry_file)
|
|
157
|
+
entry_version = _text(
|
|
158
|
+
entry_version
|
|
159
|
+
or globals_dict.get("_ENTRY_VERSION")
|
|
160
|
+
or globals_dict.get("LITE_ENTRY_VERSION")
|
|
161
|
+
or globals_dict.get("ENTRY_VERSION")
|
|
162
|
+
or config.get("entry_version")
|
|
163
|
+
or config.get("qmt_runtime_entry_version")
|
|
164
|
+
)
|
|
165
|
+
core_version = _text(
|
|
166
|
+
core_version
|
|
167
|
+
or globals_dict.get("CORE_VERSION")
|
|
168
|
+
or globals_dict.get("_CORE_VERSION")
|
|
169
|
+
or version
|
|
170
|
+
or config.get("core_version")
|
|
171
|
+
or config.get("qmt_runtime_core_version")
|
|
172
|
+
)
|
|
173
|
+
version = _text(version or core_version)
|
|
174
|
+
started = started_at
|
|
175
|
+
if started in (None, "", 0):
|
|
176
|
+
started = globals_dict.get("_runtime_started_at") or config.get("started_at") or time.time()
|
|
177
|
+
try:
|
|
178
|
+
started = float(started)
|
|
179
|
+
except Exception:
|
|
180
|
+
started = time.time()
|
|
181
|
+
now = time.time()
|
|
182
|
+
report = {
|
|
183
|
+
"schema": RUNTIME_SCHEMA,
|
|
184
|
+
"report_schema": "cfquant.qmt.runtime_marker.v1",
|
|
185
|
+
"version": version,
|
|
186
|
+
"core_version": core_version or version,
|
|
187
|
+
"entry_version": entry_version,
|
|
188
|
+
"entry_script": entry_script,
|
|
189
|
+
"entry_file": entry_file,
|
|
190
|
+
"bridge": _text(bridge),
|
|
191
|
+
"bridge_id": _text(bridge_id or config.get("bridge_id") or globals_dict.get("BRIDGE_ID") or "default") or "default",
|
|
192
|
+
"account_id": _text(account_id or config.get("account_id") or globals_dict.get("DEFAULT_ACCOUNT_ID")),
|
|
193
|
+
"account_type": _text(account_type or config.get("account_type") or globals_dict.get("DEFAULT_ACCOUNT_TYPE")),
|
|
194
|
+
"account_key": _text(account_key or config.get("account_key")),
|
|
195
|
+
"mode": _text(mode or config.get("mode")),
|
|
196
|
+
"transport": _text(transport),
|
|
197
|
+
"runtime_mode": _text(runtime_mode),
|
|
198
|
+
"channel_key": channel_key,
|
|
199
|
+
"request_channel": request_channel,
|
|
200
|
+
"callback_event_channel": _text(callback_event_channel),
|
|
201
|
+
"channels": dict(channels or {}),
|
|
202
|
+
"pipe_name": _text(pipe_name or config.get("pipe_name")),
|
|
203
|
+
"market": _text(market or config.get("market") or globals_dict.get("QMT_MARKET")),
|
|
204
|
+
"market_role": _text(market_role or config.get("market_role")),
|
|
205
|
+
"market_route_parent_bridge_id": _text(market_route_parent_bridge_id or config.get("market_route_parent_bridge_id")),
|
|
206
|
+
"core_dir": os.path.dirname(os.path.abspath(__file__)),
|
|
207
|
+
"version_file": os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.py"),
|
|
208
|
+
"module_file": _path(module_file or __file__),
|
|
209
|
+
"config_path": _path(globals_dict.get("RUNTIME_CONFIG_PATH") or config.get("config_path") or config.get("path")),
|
|
210
|
+
"python": sys.executable,
|
|
211
|
+
"pid": os.getpid(),
|
|
212
|
+
"cwd": _path(os.getcwd()),
|
|
213
|
+
"started_at": started,
|
|
214
|
+
"started_at_text": _now_text(started),
|
|
215
|
+
"reported_at": now,
|
|
216
|
+
"reported_at_text": _now_text(now),
|
|
217
|
+
"reason": _text(reason or "startup"),
|
|
218
|
+
"source": "qmt_startup_marker",
|
|
219
|
+
}
|
|
220
|
+
if isinstance(extra, dict):
|
|
221
|
+
report.update(extra)
|
|
222
|
+
return report
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def marker_filename(report):
|
|
226
|
+
report = report if isinstance(report, dict) else {}
|
|
227
|
+
bridge_id = _safe_name(report.get("bridge_id") or "default", "default")
|
|
228
|
+
channel_key = _safe_name(report.get("channel_key") or "runtime", "runtime")
|
|
229
|
+
entry_script = _safe_name(os.path.splitext(os.path.basename(_text(report.get("entry_script"))))[0], "entry")
|
|
230
|
+
pid = _safe_name(report.get("pid") or os.getpid(), "pid")
|
|
231
|
+
return "%s_%s_%s_%s_%s.json" % (MARKER_FILENAME_PREFIX, bridge_id, channel_key, entry_script, pid)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def write_qmt_runtime_marker(report, marker_dirs=None, config=None, entry_base_dir=None):
|
|
235
|
+
report = dict(report or {})
|
|
236
|
+
dirs = []
|
|
237
|
+
for path in marker_dirs or []:
|
|
238
|
+
_append_unique(dirs, path)
|
|
239
|
+
for path in configured_marker_dirs(config=config, entry_base_dir=entry_base_dir):
|
|
240
|
+
_append_unique(dirs, path)
|
|
241
|
+
|
|
242
|
+
files = []
|
|
243
|
+
errors = []
|
|
244
|
+
name = marker_filename(report)
|
|
245
|
+
for directory in dirs:
|
|
246
|
+
try:
|
|
247
|
+
os.makedirs(directory, exist_ok=True)
|
|
248
|
+
path = os.path.join(directory, name)
|
|
249
|
+
temp_path = "%s.%s.tmp" % (path, os.getpid())
|
|
250
|
+
data = dict(report)
|
|
251
|
+
data["marker_file"] = path
|
|
252
|
+
data["marker_dir"] = directory
|
|
253
|
+
data["marker_written_at"] = time.time()
|
|
254
|
+
data["marker_written_at_text"] = _now_text(data["marker_written_at"])
|
|
255
|
+
with open(temp_path, "w", encoding="utf-8") as f:
|
|
256
|
+
json.dump(data, f, ensure_ascii=False, indent=2, sort_keys=True)
|
|
257
|
+
os.replace(temp_path, path)
|
|
258
|
+
files.append(path)
|
|
259
|
+
except Exception as e:
|
|
260
|
+
errors.append("%s: %s" % (directory, e))
|
|
261
|
+
return {
|
|
262
|
+
"ok": bool(files),
|
|
263
|
+
"files": files,
|
|
264
|
+
"primary_file": files[0] if files else "",
|
|
265
|
+
"errors": errors,
|
|
266
|
+
"report": report,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def publish_qmt_runtime_marker(marker_dirs=None, config=None, entry_base_dir=None, **kwargs):
|
|
271
|
+
report = build_qmt_runtime_report(config=config, **kwargs)
|
|
272
|
+
return write_qmt_runtime_marker(
|
|
273
|
+
report,
|
|
274
|
+
marker_dirs=marker_dirs,
|
|
275
|
+
config=config,
|
|
276
|
+
entry_base_dir=entry_base_dir,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _read_marker_file(path):
|
|
281
|
+
try:
|
|
282
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
283
|
+
data = json.load(f)
|
|
284
|
+
if not isinstance(data, dict):
|
|
285
|
+
return None
|
|
286
|
+
if data.get("schema") != RUNTIME_SCHEMA and not data.get("core_version"):
|
|
287
|
+
return None
|
|
288
|
+
stat_result = os.stat(path)
|
|
289
|
+
data.setdefault("marker_file", path)
|
|
290
|
+
data.setdefault("marker_dir", os.path.dirname(path))
|
|
291
|
+
data.setdefault("marker_mtime", stat_result.st_mtime)
|
|
292
|
+
data.setdefault("marker_mtime_text", _now_text(stat_result.st_mtime))
|
|
293
|
+
return data
|
|
294
|
+
except Exception:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def read_qmt_runtime_markers(marker_dirs, max_age_seconds=None, max_files=256):
|
|
299
|
+
if isinstance(marker_dirs, str):
|
|
300
|
+
marker_dirs = [marker_dirs]
|
|
301
|
+
paths = []
|
|
302
|
+
for directory in marker_dirs or []:
|
|
303
|
+
directory = _path(directory)
|
|
304
|
+
if not directory or not os.path.isdir(directory):
|
|
305
|
+
continue
|
|
306
|
+
paths.extend(glob.glob(os.path.join(directory, "%s_*.json" % MARKER_FILENAME_PREFIX)))
|
|
307
|
+
unique = []
|
|
308
|
+
seen = set()
|
|
309
|
+
for path in paths:
|
|
310
|
+
key = os.path.normcase(os.path.abspath(path))
|
|
311
|
+
if key in seen:
|
|
312
|
+
continue
|
|
313
|
+
seen.add(key)
|
|
314
|
+
unique.append(path)
|
|
315
|
+
unique.sort(key=lambda item: os.path.getmtime(item) if os.path.exists(item) else 0, reverse=True)
|
|
316
|
+
if max_files:
|
|
317
|
+
unique = unique[: int(max_files)]
|
|
318
|
+
|
|
319
|
+
now = time.time()
|
|
320
|
+
reports = []
|
|
321
|
+
for path in unique:
|
|
322
|
+
data = _read_marker_file(path)
|
|
323
|
+
if not data or not data.get("core_version"):
|
|
324
|
+
continue
|
|
325
|
+
if max_age_seconds is not None:
|
|
326
|
+
stamp = data.get("reported_at") or data.get("marker_written_at") or data.get("marker_mtime") or 0
|
|
327
|
+
try:
|
|
328
|
+
stamp = float(stamp)
|
|
329
|
+
except Exception:
|
|
330
|
+
stamp = 0.0
|
|
331
|
+
if stamp and now - stamp > float(max_age_seconds):
|
|
332
|
+
continue
|
|
333
|
+
reports.append(data)
|
|
334
|
+
return reports
|