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,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]
|