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.
Files changed (90) hide show
  1. LTtx/__init__.py +2 -0
  2. LTtx/tx/Config.txt +8 -0
  3. LTtx/tx/LTtx_server.py +1177 -0
  4. LTtx/tx/__init__.py +5 -0
  5. LTtx/tx/tx.py +1935 -0
  6. cfquant/__init__.py +26 -0
  7. cfquant/account_routing.py +119 -0
  8. cfquant/channels.py +76 -0
  9. cfquant/cli.py +266 -0
  10. cfquant/client.py +365 -0
  11. cfquant/config.py +86 -0
  12. cfquant/hub.py +304 -0
  13. cfquant/logging_i18n.py +348 -0
  14. cfquant/normal_bridge.py +676 -0
  15. cfquant/pipe_bridge.py +249 -0
  16. cfquant/pipe_client.py +223 -0
  17. cfquant/pipe_hub.py +602 -0
  18. cfquant/pipe_transport.py +493 -0
  19. cfquant/protocol.py +238 -0
  20. cfquant/qmt_bridge.py +1081 -0
  21. cfquant/runtime_report.py +334 -0
  22. cfquant/tx.py +1898 -0
  23. cfquant/tx_trade_bridge.py +1905 -0
  24. cfquant/version.py +5 -0
  25. cfquant/xtconstant.py +262 -0
  26. cfquant/xtdata.py +462 -0
  27. cfquant/xttrader.py +955 -0
  28. cfquant/xttype.py +586 -0
  29. cfquant-0.2.0.dist-info/LICENSE +21 -0
  30. cfquant-0.2.0.dist-info/METADATA +144 -0
  31. cfquant-0.2.0.dist-info/RECORD +90 -0
  32. cfquant-0.2.0.dist-info/WHEEL +5 -0
  33. cfquant-0.2.0.dist-info/entry_points.txt +4 -0
  34. cfquant-0.2.0.dist-info/top_level.txt +6 -0
  35. cfquant_pipe_hub.py +45 -0
  36. cfquant_web_server.py +12137 -0
  37. qmt_scripts/CFQUANT.py +507 -0
  38. qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +830 -0
  39. qmt_scripts/CFQUANT_LITE.py +4928 -0
  40. qmt_scripts/CFQUANT_TRADE_LOWLAT.py +382 -0
  41. qmt_scripts/__init__.py +1 -0
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. web_dashboard/__init__.py +1 -0
  50. web_dashboard/app.js +9847 -0
  51. web_dashboard/avatars/amber-pulse.svg +15 -0
  52. web_dashboard/avatars/copper-grid.svg +15 -0
  53. web_dashboard/avatars/market-blue.svg +15 -0
  54. web_dashboard/avatars/rose-circuit.svg +15 -0
  55. web_dashboard/avatars/signal-green.svg +15 -0
  56. web_dashboard/avatars/slate-wave.svg +15 -0
  57. web_dashboard/avatars/teal-orbit.svg +16 -0
  58. web_dashboard/avatars/violet-node.svg +15 -0
  59. web_dashboard/favicon.svg +5 -0
  60. web_dashboard/image/qmt_deploy/image-10.png +0 -0
  61. web_dashboard/image/qmt_deploy/image-11.png +0 -0
  62. web_dashboard/image/qmt_deploy/image-12.png +0 -0
  63. web_dashboard/image/qmt_deploy/image-13.png +0 -0
  64. web_dashboard/image/qmt_deploy/image-14.png +0 -0
  65. web_dashboard/image/qmt_deploy/image-15.png +0 -0
  66. web_dashboard/image/qmt_deploy/image-16.png +0 -0
  67. web_dashboard/image/qmt_deploy/image-17.png +0 -0
  68. web_dashboard/image/qmt_deploy/image-18.png +0 -0
  69. web_dashboard/image/qmt_deploy/image-19.png +0 -0
  70. web_dashboard/image/qmt_deploy/image-20.png +0 -0
  71. web_dashboard/image/qmt_deploy/image-21.png +0 -0
  72. web_dashboard/image/qmt_deploy/image-4.png +0 -0
  73. web_dashboard/image/qmt_deploy/image-5.png +0 -0
  74. web_dashboard/image/qmt_deploy/image-6.png +0 -0
  75. web_dashboard/image/qmt_deploy/image-7.png +0 -0
  76. web_dashboard/image/qmt_deploy/image-8.png +0 -0
  77. web_dashboard/image/qmt_deploy/image-9.png +0 -0
  78. 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
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. web_dashboard/index.html +2317 -0
  89. web_dashboard/styles.css +7817 -0
  90. web_dashboard/vendor/mermaid.min.js +2029 -0
@@ -0,0 +1,493 @@
1
+ # -*- coding: utf-8 -*-
2
+ import ctypes
3
+ import json
4
+ import os
5
+ import queue
6
+ import struct
7
+ import threading
8
+ import time
9
+ from ctypes import wintypes
10
+
11
+ from .logging_i18n import get_log_enabled, translate_log
12
+
13
+
14
+ DEFAULT_PIPE_NAME = os.environ.get("CFQUANT_PIPE_NAME", r"\\.\pipe\cfquant_pipe_hub")
15
+ PIPE_MESSAGE_PREFIX = "cfpipe:"
16
+
17
+ GENERIC_READ = 0x80000000
18
+ GENERIC_WRITE = 0x40000000
19
+ OPEN_EXISTING = 3
20
+ PIPE_ACCESS_DUPLEX = 0x00000003
21
+ PIPE_TYPE_BYTE = 0x00000000
22
+ PIPE_READMODE_BYTE = 0x00000000
23
+ PIPE_WAIT = 0x00000000
24
+ PIPE_UNLIMITED_INSTANCES = 255
25
+ NMPWAIT_WAIT_FOREVER = 0xFFFFFFFF
26
+
27
+ ERROR_FILE_NOT_FOUND = 2
28
+ ERROR_ACCESS_DENIED = 5
29
+ ERROR_BROKEN_PIPE = 109
30
+ ERROR_PIPE_BUSY = 231
31
+ ERROR_NO_DATA = 232
32
+ ERROR_PIPE_NOT_CONNECTED = 233
33
+ ERROR_PIPE_CONNECTED = 535
34
+
35
+ DEFAULT_BUFFER_SIZE = 65536
36
+ DEFAULT_MAX_FRAME_SIZE = 64 * 1024 * 1024
37
+
38
+
39
+ def is_windows():
40
+ return os.name == "nt"
41
+
42
+
43
+ def normalize_pipe_name(pipe_name=None):
44
+ pipe_name = str(pipe_name or DEFAULT_PIPE_NAME)
45
+ if pipe_name.startswith("\\\\.\\pipe\\"):
46
+ return pipe_name
47
+ name = pipe_name.strip("\\/")
48
+ return r"\\.\pipe\%s" % name
49
+
50
+
51
+ def dumps_pipe_message(payload):
52
+ data = dict(payload)
53
+ data.setdefault("protocol", "cfquant_pipe")
54
+ data.setdefault("ts", int(time.time() * 1000))
55
+ return PIPE_MESSAGE_PREFIX + json.dumps(data, ensure_ascii=False, separators=(",", ":"))
56
+
57
+
58
+ def loads_pipe_message(raw):
59
+ if isinstance(raw, bytes):
60
+ raw = raw.decode("utf-8", errors="replace")
61
+ if not isinstance(raw, str) or not raw.startswith(PIPE_MESSAGE_PREFIX):
62
+ return None
63
+ try:
64
+ data = json.loads(raw[len(PIPE_MESSAGE_PREFIX):])
65
+ except Exception:
66
+ return None
67
+ if data.get("protocol") != "cfquant_pipe":
68
+ return None
69
+ return data
70
+
71
+
72
+ class _Kernel32(object):
73
+ def __init__(self):
74
+ if not is_windows():
75
+ raise OSError("named pipe transport requires Windows")
76
+ self.dll = ctypes.WinDLL("kernel32", use_last_error=True)
77
+ self.INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
78
+
79
+ self.dll.CreateFileW.argtypes = [
80
+ wintypes.LPCWSTR,
81
+ wintypes.DWORD,
82
+ wintypes.DWORD,
83
+ wintypes.LPVOID,
84
+ wintypes.DWORD,
85
+ wintypes.DWORD,
86
+ wintypes.HANDLE,
87
+ ]
88
+ self.dll.CreateFileW.restype = wintypes.HANDLE
89
+
90
+ self.dll.CreateNamedPipeW.argtypes = [
91
+ wintypes.LPCWSTR,
92
+ wintypes.DWORD,
93
+ wintypes.DWORD,
94
+ wintypes.DWORD,
95
+ wintypes.DWORD,
96
+ wintypes.DWORD,
97
+ wintypes.DWORD,
98
+ wintypes.LPVOID,
99
+ ]
100
+ self.dll.CreateNamedPipeW.restype = wintypes.HANDLE
101
+
102
+ self.dll.ConnectNamedPipe.argtypes = [wintypes.HANDLE, wintypes.LPVOID]
103
+ self.dll.ConnectNamedPipe.restype = wintypes.BOOL
104
+
105
+ self.dll.DisconnectNamedPipe.argtypes = [wintypes.HANDLE]
106
+ self.dll.DisconnectNamedPipe.restype = wintypes.BOOL
107
+
108
+ self.dll.ReadFile.argtypes = [
109
+ wintypes.HANDLE,
110
+ wintypes.LPVOID,
111
+ wintypes.DWORD,
112
+ ctypes.POINTER(wintypes.DWORD),
113
+ wintypes.LPVOID,
114
+ ]
115
+ self.dll.ReadFile.restype = wintypes.BOOL
116
+
117
+ self.dll.WriteFile.argtypes = [
118
+ wintypes.HANDLE,
119
+ wintypes.LPCVOID,
120
+ wintypes.DWORD,
121
+ ctypes.POINTER(wintypes.DWORD),
122
+ wintypes.LPVOID,
123
+ ]
124
+ self.dll.WriteFile.restype = wintypes.BOOL
125
+
126
+ self.dll.FlushFileBuffers.argtypes = [wintypes.HANDLE]
127
+ self.dll.FlushFileBuffers.restype = wintypes.BOOL
128
+
129
+ self.dll.CloseHandle.argtypes = [wintypes.HANDLE]
130
+ self.dll.CloseHandle.restype = wintypes.BOOL
131
+
132
+ self.dll.WaitNamedPipeW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD]
133
+ self.dll.WaitNamedPipeW.restype = wintypes.BOOL
134
+
135
+ self.cancel_io_ex = getattr(self.dll, "CancelIoEx", None)
136
+ if self.cancel_io_ex is not None:
137
+ self.cancel_io_ex.argtypes = [wintypes.HANDLE, wintypes.LPVOID]
138
+ self.cancel_io_ex.restype = wintypes.BOOL
139
+
140
+ def last_error(self):
141
+ return ctypes.get_last_error()
142
+
143
+ def raise_last_error(self, message):
144
+ error = self.last_error()
145
+ raise OSError(error, "%s failed with winerror=%s" % (message, error))
146
+
147
+ def invalid_handle(self, handle):
148
+ return handle in (None, 0, self.INVALID_HANDLE_VALUE)
149
+
150
+
151
+ _kernel32 = None
152
+ _kernel32_lock = threading.Lock()
153
+
154
+
155
+ def kernel32():
156
+ global _kernel32
157
+ with _kernel32_lock:
158
+ if _kernel32 is None:
159
+ _kernel32 = _Kernel32()
160
+ return _kernel32
161
+
162
+
163
+ class NamedPipeConnection(object):
164
+ def __init__(self, handle, name="", owner_server_side=False, max_frame_size=DEFAULT_MAX_FRAME_SIZE):
165
+ self.handle = handle
166
+ self.name = name
167
+ self.owner_server_side = bool(owner_server_side)
168
+ self.max_frame_size = int(max_frame_size)
169
+ self.write_lock = threading.RLock()
170
+ self.closed = False
171
+
172
+ def read_frame(self):
173
+ header = self._read_exact(8)
174
+ if header is None:
175
+ return None
176
+ size = struct.unpack("!Q", header)[0]
177
+ if size > self.max_frame_size:
178
+ raise ValueError("named pipe frame too large: %s > %s" % (size, self.max_frame_size))
179
+ if size == 0:
180
+ return ""
181
+ data = self._read_exact(size)
182
+ if data is None:
183
+ return None
184
+ return data.decode("utf-8", errors="replace")
185
+
186
+ def write_frame(self, payload):
187
+ if isinstance(payload, str):
188
+ data = payload.encode("utf-8")
189
+ else:
190
+ data = bytes(payload)
191
+ frame = struct.pack("!Q", len(data)) + data
192
+ self._write_all(frame)
193
+
194
+ def close(self):
195
+ if self.closed:
196
+ return
197
+ self.closed = True
198
+ k32 = kernel32()
199
+ try:
200
+ if k32.cancel_io_ex is not None:
201
+ try:
202
+ k32.cancel_io_ex(self.handle, None)
203
+ except Exception:
204
+ pass
205
+ if self.owner_server_side:
206
+ try:
207
+ k32.dll.DisconnectNamedPipe(self.handle)
208
+ except Exception:
209
+ pass
210
+ finally:
211
+ try:
212
+ k32.dll.CloseHandle(self.handle)
213
+ except Exception:
214
+ pass
215
+
216
+ def _read_exact(self, size):
217
+ chunks = []
218
+ remaining = int(size)
219
+ k32 = kernel32()
220
+ while remaining > 0:
221
+ chunk_size = min(remaining, DEFAULT_BUFFER_SIZE)
222
+ buf = ctypes.create_string_buffer(chunk_size)
223
+ read = wintypes.DWORD(0)
224
+ ok = k32.dll.ReadFile(self.handle, buf, chunk_size, ctypes.byref(read), None)
225
+ if not ok:
226
+ error = k32.last_error()
227
+ if error in (ERROR_BROKEN_PIPE, ERROR_NO_DATA, ERROR_PIPE_NOT_CONNECTED):
228
+ return None
229
+ raise OSError(error, "ReadFile failed with winerror=%s" % error)
230
+ if read.value == 0:
231
+ return None
232
+ chunks.append(buf.raw[:read.value])
233
+ remaining -= read.value
234
+ return b"".join(chunks)
235
+
236
+ def _write_all(self, data):
237
+ offset = 0
238
+ total = len(data)
239
+ k32 = kernel32()
240
+ with self.write_lock:
241
+ while offset < total:
242
+ chunk = data[offset:offset + DEFAULT_BUFFER_SIZE]
243
+ buf = ctypes.create_string_buffer(chunk)
244
+ written = wintypes.DWORD(0)
245
+ ok = k32.dll.WriteFile(self.handle, buf, len(chunk), ctypes.byref(written), None)
246
+ if not ok:
247
+ error = k32.last_error()
248
+ raise OSError(error, "WriteFile failed with winerror=%s" % error)
249
+ if written.value <= 0:
250
+ raise OSError("WriteFile wrote zero bytes")
251
+ offset += written.value
252
+
253
+
254
+ def connect_pipe(pipe_name=None, timeout_ms=3000):
255
+ pipe_name = normalize_pipe_name(pipe_name)
256
+ k32 = kernel32()
257
+ deadline = time.time() + max(float(timeout_ms), 1.0) / 1000.0
258
+ last_error = None
259
+ while True:
260
+ handle = k32.dll.CreateFileW(
261
+ pipe_name,
262
+ GENERIC_READ | GENERIC_WRITE,
263
+ 0,
264
+ None,
265
+ OPEN_EXISTING,
266
+ 0,
267
+ None,
268
+ )
269
+ if not k32.invalid_handle(handle):
270
+ return NamedPipeConnection(handle, pipe_name, owner_server_side=False)
271
+ last_error = k32.last_error()
272
+ if last_error == ERROR_PIPE_BUSY:
273
+ wait_ms = min(250, max(1, int((deadline - time.time()) * 1000)))
274
+ k32.dll.WaitNamedPipeW(pipe_name, wait_ms)
275
+ elif last_error in (ERROR_FILE_NOT_FOUND, ERROR_ACCESS_DENIED):
276
+ time.sleep(0.05)
277
+ else:
278
+ time.sleep(0.05)
279
+ if time.time() >= deadline:
280
+ raise OSError(last_error or 0, "connect named pipe timeout pipe=%s winerror=%s" % (pipe_name, last_error))
281
+
282
+
283
+ def create_pipe_instance(pipe_name=None, in_buffer_size=DEFAULT_BUFFER_SIZE, out_buffer_size=DEFAULT_BUFFER_SIZE):
284
+ pipe_name = normalize_pipe_name(pipe_name)
285
+ k32 = kernel32()
286
+ handle = k32.dll.CreateNamedPipeW(
287
+ pipe_name,
288
+ PIPE_ACCESS_DUPLEX,
289
+ PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
290
+ PIPE_UNLIMITED_INSTANCES,
291
+ int(out_buffer_size),
292
+ int(in_buffer_size),
293
+ 0,
294
+ None,
295
+ )
296
+ if k32.invalid_handle(handle):
297
+ k32.raise_last_error("CreateNamedPipeW")
298
+ return NamedPipeConnection(handle, pipe_name, owner_server_side=True)
299
+
300
+
301
+ def wait_for_pipe_client(connection):
302
+ k32 = kernel32()
303
+ ok = k32.dll.ConnectNamedPipe(connection.handle, None)
304
+ if ok:
305
+ return True
306
+ error = k32.last_error()
307
+ if error == ERROR_PIPE_CONNECTED:
308
+ return True
309
+ raise OSError(error, "ConnectNamedPipe failed with winerror=%s" % error)
310
+
311
+
312
+ class PipeTxClient(object):
313
+ """
314
+ QMT-side tx-like adapter over a named pipe hub.
315
+
316
+ It intentionally exposes start_tx/start_txg/push/close so bridge classes can
317
+ reuse the existing LTtx-oriented dispatch code without changing behavior.
318
+ """
319
+
320
+ def __init__(
321
+ self,
322
+ pipe_name=None,
323
+ request_channel="cfquant.request",
324
+ request_channels=None,
325
+ bridge_id="default",
326
+ endpoint_name="qmt",
327
+ show=True,
328
+ connect_timeout_ms=3000,
329
+ reconnect_interval=1.0,
330
+ ):
331
+ self.pipe_name = normalize_pipe_name(pipe_name)
332
+ self.request_channel = request_channel
333
+ self.request_channels = self._normalize_channels(request_channels or [request_channel])
334
+ self.bridge_id = bridge_id or "default"
335
+ self.endpoint_name = endpoint_name or "qmt"
336
+ self.show = show
337
+ self.connect_timeout_ms = int(connect_timeout_ms)
338
+ self.reconnect_interval = float(reconnect_interval)
339
+ self.Q = queue.Queue(maxsize=10000)
340
+ self.running = False
341
+ self.rx_conn = None
342
+ self.tx_conn = None
343
+ self.conn_lock = threading.RLock()
344
+ self.thread = None
345
+
346
+ def start(self):
347
+ if self.running:
348
+ return self
349
+ self.running = True
350
+ self.thread = threading.Thread(target=self._connect_loop)
351
+ self.thread.daemon = True
352
+ self.thread.start()
353
+ return self
354
+
355
+ def start_tx(self):
356
+ return self.start()
357
+
358
+ def start_txg(self, request_channel=None):
359
+ if request_channel:
360
+ self.request_channel = request_channel
361
+ return {"code": 0, "msg": "pipe request channel registered"}
362
+
363
+ def push(self, key, payload, channel):
364
+ if isinstance(payload, bytes):
365
+ payload = payload.decode("utf-8", errors="replace")
366
+ envelope = dumps_pipe_message({
367
+ "type": "publish",
368
+ "role": "qmt_tx",
369
+ "bridge_id": self.bridge_id,
370
+ "request_channel": self.request_channel,
371
+ "request_channels": self.request_channels,
372
+ "endpoint_name": self.endpoint_name,
373
+ "key": key,
374
+ "channel": channel,
375
+ "payload": payload,
376
+ })
377
+ conn = self._get_tx_conn()
378
+ if conn is None:
379
+ return {"code": -1, "msg": "pipe not connected"}
380
+ try:
381
+ conn.write_frame(envelope)
382
+ return {"code": 0, "msg": "ok"}
383
+ except Exception as e:
384
+ self._log("pipe push failed: %s" % e)
385
+ self._drop_conn(conn)
386
+ return {"code": -1, "msg": str(e)}
387
+
388
+ def close(self):
389
+ self.running = False
390
+ try:
391
+ self.Q.put_nowait(None)
392
+ except Exception:
393
+ pass
394
+ self._drop_conns(self._get_conns())
395
+
396
+ def _connect_loop(self):
397
+ while self.running:
398
+ rx_conn = None
399
+ tx_conn = None
400
+ try:
401
+ rx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
402
+ rx_conn.write_frame(dumps_pipe_message({
403
+ "type": "hello",
404
+ "role": "qmt_rx",
405
+ "bridge_id": self.bridge_id,
406
+ "request_channel": self.request_channel,
407
+ "request_channels": self.request_channels,
408
+ "endpoint_name": self.endpoint_name,
409
+ }))
410
+ tx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
411
+ tx_conn.write_frame(dumps_pipe_message({
412
+ "type": "hello",
413
+ "role": "qmt_tx",
414
+ "bridge_id": self.bridge_id,
415
+ "request_channel": self.request_channel,
416
+ "request_channels": self.request_channels,
417
+ "endpoint_name": self.endpoint_name,
418
+ }))
419
+ with self.conn_lock:
420
+ self.rx_conn = rx_conn
421
+ self.tx_conn = tx_conn
422
+ self._log(
423
+ "pipe connected pipe=%s request_channel=%s bridge_id=%s"
424
+ % (self.pipe_name, self.request_channel, self.bridge_id)
425
+ )
426
+ self._read_loop(rx_conn)
427
+ except Exception as e:
428
+ if self.running:
429
+ self._log("pipe connect/read failed: %s" % e)
430
+ finally:
431
+ self._drop_conns((rx_conn, tx_conn))
432
+ if self.running:
433
+ time.sleep(self.reconnect_interval)
434
+
435
+ def _read_loop(self, conn):
436
+ while self.running and self._get_rx_conn() is conn:
437
+ raw = conn.read_frame()
438
+ if raw is None:
439
+ break
440
+ envelope = loads_pipe_message(raw)
441
+ if envelope:
442
+ payload = envelope.get("payload")
443
+ if payload:
444
+ self.Q.put(payload)
445
+ continue
446
+ self.Q.put(raw)
447
+
448
+ def _get_rx_conn(self):
449
+ with self.conn_lock:
450
+ return self.rx_conn
451
+
452
+ def _get_tx_conn(self):
453
+ with self.conn_lock:
454
+ return self.tx_conn
455
+
456
+ def _get_conn(self):
457
+ return self._get_rx_conn()
458
+
459
+ def _get_conns(self):
460
+ with self.conn_lock:
461
+ return self.rx_conn, self.tx_conn
462
+
463
+ def _drop_conn(self, conn):
464
+ self._drop_conns((conn,))
465
+
466
+ def _drop_conns(self, conns):
467
+ with self.conn_lock:
468
+ for conn in conns:
469
+ if conn is None:
470
+ continue
471
+ if self.rx_conn is conn:
472
+ self.rx_conn = None
473
+ if self.tx_conn is conn:
474
+ self.tx_conn = None
475
+ for conn in conns:
476
+ if conn is None:
477
+ continue
478
+ try:
479
+ conn.close()
480
+ except Exception:
481
+ pass
482
+
483
+ def _log(self, msg):
484
+ if self.show and get_log_enabled():
485
+ print("cfquant pipe tx %s" % translate_log(msg))
486
+
487
+ def _normalize_channels(self, channels):
488
+ result = []
489
+ for channel in channels or []:
490
+ channel = str(channel or "").strip()
491
+ if channel and channel not in result:
492
+ result.append(channel)
493
+ return result or [self.request_channel]