da2-mcp-socket 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.
- da2_mcp_socket-0.2.0.dist-info/METADATA +204 -0
- da2_mcp_socket-0.2.0.dist-info/RECORD +8 -0
- da2_mcp_socket-0.2.0.dist-info/WHEEL +4 -0
- da2_mcp_socket-0.2.0.dist-info/entry_points.txt +2 -0
- da2_mcp_socket-0.2.0.dist-info/licenses/LICENSE +21 -0
- mcp_socket/__init__.py +6 -0
- mcp_socket/server.py +559 -0
- mcp_socket/socket_manager.py +1103 -0
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026 Danny, DA2 Studio (https://da2.35g.tw)
|
|
3
|
+
Socket Manager - Core TCP/IP socket connection management logic.
|
|
4
|
+
|
|
5
|
+
Handles socket connection, disconnection, and data transmission.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import socket
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("mcp-socket")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SocketConfig:
|
|
22
|
+
"""Socket connection configuration."""
|
|
23
|
+
host: str
|
|
24
|
+
port: int
|
|
25
|
+
timeout: float = 10.0
|
|
26
|
+
read_timeout: float = 5.0
|
|
27
|
+
buffer_size: int = 4096
|
|
28
|
+
max_buffer_packets: int = 100 # Maximum packets in buffer
|
|
29
|
+
auto_listen: bool = True # Auto start background listener
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ReceiveProtocol:
|
|
34
|
+
"""Receive protocol configuration for packet parsing."""
|
|
35
|
+
mode: str = "line" # raw, line, packet (default: line mode with CRLF)
|
|
36
|
+
start_delimiter: bytes = b"" # Start of packet delimiter (default: empty)
|
|
37
|
+
end_delimiter: bytes = b"\r\n" # End of packet delimiter (default: CRLF = 0D0A)
|
|
38
|
+
include_delimiters: bool = False # Include delimiters in received data
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class OpenSocket:
|
|
43
|
+
"""Represents an open socket connection."""
|
|
44
|
+
connection_id: str
|
|
45
|
+
config: SocketConfig
|
|
46
|
+
socket: socket.socket
|
|
47
|
+
protocol: ReceiveProtocol = field(default_factory=ReceiveProtocol)
|
|
48
|
+
opened_at: float = field(default_factory=time.time)
|
|
49
|
+
bytes_sent: int = 0
|
|
50
|
+
bytes_received: int = 0
|
|
51
|
+
receive_buffer: bytes = b"" # Buffer for partial packet data
|
|
52
|
+
# Background listener fields
|
|
53
|
+
packet_buffer: list = field(default_factory=list) # Parsed packets buffer
|
|
54
|
+
listener_thread: Optional[threading.Thread] = None
|
|
55
|
+
listener_running: bool = False
|
|
56
|
+
overflow_count: int = 0 # Count of dropped packets due to overflow
|
|
57
|
+
_packet_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SocketManager:
|
|
61
|
+
"""Manages TCP/IP socket connections and operations."""
|
|
62
|
+
|
|
63
|
+
def __init__(self):
|
|
64
|
+
"""Initialize the socket manager."""
|
|
65
|
+
self._connections: dict[str, OpenSocket] = {}
|
|
66
|
+
self._lock = threading.Lock()
|
|
67
|
+
|
|
68
|
+
def _generate_connection_id(self, host: str, port: int) -> str:
|
|
69
|
+
"""Generate a unique connection ID."""
|
|
70
|
+
return f"{host}:{port}_{uuid.uuid4().hex[:8]}"
|
|
71
|
+
|
|
72
|
+
def connect(
|
|
73
|
+
self,
|
|
74
|
+
host: str,
|
|
75
|
+
port: int,
|
|
76
|
+
timeout: float = 10.0,
|
|
77
|
+
read_timeout: float = 5.0,
|
|
78
|
+
buffer_size: int = 4096,
|
|
79
|
+
max_buffer_packets: int = 100,
|
|
80
|
+
auto_listen: bool = False
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Connect to a TCP server.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
host: Target host (IP address or hostname).
|
|
86
|
+
port: Target port number.
|
|
87
|
+
timeout: Connection timeout in seconds.
|
|
88
|
+
read_timeout: Read timeout in seconds.
|
|
89
|
+
buffer_size: Buffer size for receiving data.
|
|
90
|
+
max_buffer_packets: Maximum packets to buffer (default 100).
|
|
91
|
+
auto_listen: Auto start background listener after set_protocol.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dictionary containing success status and connection info.
|
|
95
|
+
"""
|
|
96
|
+
with self._lock:
|
|
97
|
+
try:
|
|
98
|
+
# Validate parameters
|
|
99
|
+
if not host:
|
|
100
|
+
return {
|
|
101
|
+
"success": False,
|
|
102
|
+
"error": "Host cannot be empty",
|
|
103
|
+
"error_type": "InvalidParameter"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if not (1 <= port <= 65535):
|
|
107
|
+
return {
|
|
108
|
+
"success": False,
|
|
109
|
+
"error": f"Invalid port: {port}. Must be between 1 and 65535.",
|
|
110
|
+
"error_type": "InvalidParameter"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Create socket
|
|
114
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
115
|
+
sock.settimeout(timeout)
|
|
116
|
+
|
|
117
|
+
# Connect to server
|
|
118
|
+
logger.info(f"Connecting to {host}:{port}...")
|
|
119
|
+
sock.connect((host, port))
|
|
120
|
+
|
|
121
|
+
# Set read timeout after connection
|
|
122
|
+
sock.settimeout(read_timeout)
|
|
123
|
+
|
|
124
|
+
# Create configuration
|
|
125
|
+
config = SocketConfig(
|
|
126
|
+
host=host,
|
|
127
|
+
port=port,
|
|
128
|
+
timeout=timeout,
|
|
129
|
+
read_timeout=read_timeout,
|
|
130
|
+
buffer_size=buffer_size,
|
|
131
|
+
max_buffer_packets=max_buffer_packets,
|
|
132
|
+
auto_listen=auto_listen
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Generate connection ID
|
|
136
|
+
connection_id = self._generate_connection_id(host, port)
|
|
137
|
+
|
|
138
|
+
# Store the connection
|
|
139
|
+
self._connections[connection_id] = OpenSocket(
|
|
140
|
+
connection_id=connection_id,
|
|
141
|
+
config=config,
|
|
142
|
+
socket=sock
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
logger.info(f"Connected to {host}:{port} with ID: {connection_id}")
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"success": True,
|
|
149
|
+
"message": f"Connected to {host}:{port}",
|
|
150
|
+
"connection_id": connection_id,
|
|
151
|
+
"config": {
|
|
152
|
+
"host": host,
|
|
153
|
+
"port": port,
|
|
154
|
+
"timeout": timeout,
|
|
155
|
+
"read_timeout": read_timeout,
|
|
156
|
+
"buffer_size": buffer_size,
|
|
157
|
+
"max_buffer_packets": max_buffer_packets,
|
|
158
|
+
"auto_listen": auto_listen
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
except socket.timeout:
|
|
163
|
+
logger.error(f"Connection to {host}:{port} timed out")
|
|
164
|
+
return {
|
|
165
|
+
"success": False,
|
|
166
|
+
"error": f"Connection to {host}:{port} timed out after {timeout} seconds",
|
|
167
|
+
"error_type": "ConnectionTimeout"
|
|
168
|
+
}
|
|
169
|
+
except socket.gaierror as e:
|
|
170
|
+
logger.error(f"Failed to resolve host {host}: {e}")
|
|
171
|
+
return {
|
|
172
|
+
"success": False,
|
|
173
|
+
"error": f"Failed to resolve host: {host}",
|
|
174
|
+
"error_type": "HostResolutionError"
|
|
175
|
+
}
|
|
176
|
+
except ConnectionRefusedError:
|
|
177
|
+
logger.error(f"Connection refused by {host}:{port}")
|
|
178
|
+
return {
|
|
179
|
+
"success": False,
|
|
180
|
+
"error": f"Connection refused by {host}:{port}",
|
|
181
|
+
"error_type": "ConnectionRefused"
|
|
182
|
+
}
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.error(f"Failed to connect to {host}:{port}: {e}")
|
|
185
|
+
return {
|
|
186
|
+
"success": False,
|
|
187
|
+
"error": str(e),
|
|
188
|
+
"error_type": type(e).__name__
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
def disconnect(self, connection_id: str) -> dict[str, Any]:
|
|
192
|
+
"""Close a socket connection.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
connection_id: The connection ID to close.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Dictionary containing success status and statistics.
|
|
199
|
+
"""
|
|
200
|
+
with self._lock:
|
|
201
|
+
if connection_id not in self._connections:
|
|
202
|
+
return {
|
|
203
|
+
"success": False,
|
|
204
|
+
"error": f"Connection {connection_id} not found",
|
|
205
|
+
"error_type": "ConnectionNotFound"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
conn = self._connections[connection_id]
|
|
210
|
+
conn.socket.close()
|
|
211
|
+
|
|
212
|
+
# Calculate statistics
|
|
213
|
+
duration = time.time() - conn.opened_at
|
|
214
|
+
|
|
215
|
+
del self._connections[connection_id]
|
|
216
|
+
|
|
217
|
+
logger.info(f"Disconnected: {connection_id}")
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"success": True,
|
|
221
|
+
"message": f"Connection {connection_id} closed",
|
|
222
|
+
"connection_id": connection_id,
|
|
223
|
+
"statistics": {
|
|
224
|
+
"duration_seconds": round(duration, 2),
|
|
225
|
+
"bytes_sent": conn.bytes_sent,
|
|
226
|
+
"bytes_received": conn.bytes_received
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.error(f"Error disconnecting {connection_id}: {e}")
|
|
232
|
+
return {
|
|
233
|
+
"success": False,
|
|
234
|
+
"error": str(e),
|
|
235
|
+
"error_type": type(e).__name__
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
def disconnect_all(self) -> dict[str, Any]:
|
|
239
|
+
"""Close all socket connections.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Dictionary containing success status and closed connections.
|
|
243
|
+
"""
|
|
244
|
+
with self._lock:
|
|
245
|
+
closed_connections = []
|
|
246
|
+
errors = []
|
|
247
|
+
|
|
248
|
+
for conn_id in list(self._connections.keys()):
|
|
249
|
+
try:
|
|
250
|
+
self._connections[conn_id].socket.close()
|
|
251
|
+
closed_connections.append(conn_id)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
errors.append({"connection_id": conn_id, "error": str(e)})
|
|
254
|
+
|
|
255
|
+
self._connections.clear()
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
"success": len(errors) == 0,
|
|
259
|
+
"closed_connections": closed_connections,
|
|
260
|
+
"count": len(closed_connections),
|
|
261
|
+
"errors": errors if errors else None
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
def get_connection_status(self, connection_id: Optional[str] = None) -> dict[str, Any]:
|
|
265
|
+
"""Get status of connection(s).
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
connection_id: Specific connection to query, or None for all.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Dictionary containing connection status information.
|
|
272
|
+
"""
|
|
273
|
+
with self._lock:
|
|
274
|
+
if connection_id:
|
|
275
|
+
if connection_id not in self._connections:
|
|
276
|
+
return {
|
|
277
|
+
"success": False,
|
|
278
|
+
"error": f"Connection {connection_id} not found",
|
|
279
|
+
"error_type": "ConnectionNotFound"
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
conn = self._connections[connection_id]
|
|
283
|
+
return {
|
|
284
|
+
"success": True,
|
|
285
|
+
"connection_id": connection_id,
|
|
286
|
+
"config": {
|
|
287
|
+
"host": conn.config.host,
|
|
288
|
+
"port": conn.config.port,
|
|
289
|
+
"timeout": conn.config.timeout,
|
|
290
|
+
"read_timeout": conn.config.read_timeout,
|
|
291
|
+
"buffer_size": conn.config.buffer_size
|
|
292
|
+
},
|
|
293
|
+
"statistics": {
|
|
294
|
+
"opened_at": conn.opened_at,
|
|
295
|
+
"duration_seconds": round(time.time() - conn.opened_at, 2),
|
|
296
|
+
"bytes_sent": conn.bytes_sent,
|
|
297
|
+
"bytes_received": conn.bytes_received
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else:
|
|
301
|
+
# Return all connections
|
|
302
|
+
connections_status = {}
|
|
303
|
+
for conn_id, conn in self._connections.items():
|
|
304
|
+
connections_status[conn_id] = {
|
|
305
|
+
"host": conn.config.host,
|
|
306
|
+
"port": conn.config.port,
|
|
307
|
+
"bytes_sent": conn.bytes_sent,
|
|
308
|
+
"bytes_received": conn.bytes_received,
|
|
309
|
+
"duration_seconds": round(time.time() - conn.opened_at, 2)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
"success": True,
|
|
314
|
+
"connections": connections_status,
|
|
315
|
+
"count": len(connections_status)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
def send_data(
|
|
319
|
+
self,
|
|
320
|
+
connection_id: str,
|
|
321
|
+
data: str,
|
|
322
|
+
encoding: str = "utf-8",
|
|
323
|
+
as_hex: bool = False
|
|
324
|
+
) -> dict[str, Any]:
|
|
325
|
+
"""Send data through a socket connection.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
connection_id: The connection to send through.
|
|
329
|
+
data: Data to send (string or hex string).
|
|
330
|
+
encoding: Text encoding (default utf-8).
|
|
331
|
+
as_hex: If True, interpret data as hex string.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Dictionary containing success status and bytes sent.
|
|
335
|
+
"""
|
|
336
|
+
with self._lock:
|
|
337
|
+
if connection_id not in self._connections:
|
|
338
|
+
return {
|
|
339
|
+
"success": False,
|
|
340
|
+
"error": f"Connection {connection_id} not found",
|
|
341
|
+
"error_type": "ConnectionNotFound"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
conn = self._connections[connection_id]
|
|
346
|
+
|
|
347
|
+
if as_hex:
|
|
348
|
+
# Parse hex string
|
|
349
|
+
hex_str = data.replace(" ", "").replace("0x", "").replace(",", "")
|
|
350
|
+
try:
|
|
351
|
+
byte_data = bytes.fromhex(hex_str)
|
|
352
|
+
except ValueError as e:
|
|
353
|
+
return {
|
|
354
|
+
"success": False,
|
|
355
|
+
"error": f"Invalid hex string: {e}",
|
|
356
|
+
"error_type": "InvalidHexString"
|
|
357
|
+
}
|
|
358
|
+
else:
|
|
359
|
+
byte_data = data.encode(encoding)
|
|
360
|
+
|
|
361
|
+
bytes_sent = conn.socket.send(byte_data)
|
|
362
|
+
conn.bytes_sent += bytes_sent
|
|
363
|
+
|
|
364
|
+
logger.debug(f"Sent {bytes_sent} bytes through {connection_id}")
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
"success": True,
|
|
368
|
+
"connection_id": connection_id,
|
|
369
|
+
"bytes_sent": bytes_sent,
|
|
370
|
+
"data_hex": byte_data.hex().upper(),
|
|
371
|
+
"data_preview": data[:50] + ("..." if len(data) > 50 else "")
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
except socket.timeout:
|
|
375
|
+
return {
|
|
376
|
+
"success": False,
|
|
377
|
+
"error": "Send timeout",
|
|
378
|
+
"error_type": "SendTimeout"
|
|
379
|
+
}
|
|
380
|
+
except BrokenPipeError:
|
|
381
|
+
return {
|
|
382
|
+
"success": False,
|
|
383
|
+
"error": "Connection broken (remote end closed)",
|
|
384
|
+
"error_type": "BrokenPipe"
|
|
385
|
+
}
|
|
386
|
+
except Exception as e:
|
|
387
|
+
logger.error(f"Error sending to {connection_id}: {e}")
|
|
388
|
+
return {
|
|
389
|
+
"success": False,
|
|
390
|
+
"error": str(e),
|
|
391
|
+
"error_type": type(e).__name__
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
def receive_data(
|
|
395
|
+
self,
|
|
396
|
+
connection_id: str,
|
|
397
|
+
size: Optional[int] = None,
|
|
398
|
+
timeout: Optional[float] = None,
|
|
399
|
+
as_hex: bool = False
|
|
400
|
+
) -> dict[str, Any]:
|
|
401
|
+
"""Receive data from a socket connection.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
connection_id: The connection to receive from.
|
|
405
|
+
size: Maximum bytes to receive (default: buffer_size).
|
|
406
|
+
timeout: Read timeout in seconds (None = use connection default).
|
|
407
|
+
as_hex: If True, return data as hex string only.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dictionary containing success status and received data.
|
|
411
|
+
"""
|
|
412
|
+
with self._lock:
|
|
413
|
+
if connection_id not in self._connections:
|
|
414
|
+
return {
|
|
415
|
+
"success": False,
|
|
416
|
+
"error": f"Connection {connection_id} not found",
|
|
417
|
+
"error_type": "ConnectionNotFound"
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
conn = self._connections[connection_id]
|
|
422
|
+
recv_size = size or conn.config.buffer_size
|
|
423
|
+
|
|
424
|
+
# Temporarily change timeout if specified
|
|
425
|
+
original_timeout = conn.socket.gettimeout()
|
|
426
|
+
if timeout is not None:
|
|
427
|
+
conn.socket.settimeout(timeout)
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
data = conn.socket.recv(recv_size)
|
|
431
|
+
finally:
|
|
432
|
+
if timeout is not None:
|
|
433
|
+
conn.socket.settimeout(original_timeout)
|
|
434
|
+
|
|
435
|
+
conn.bytes_received += len(data)
|
|
436
|
+
|
|
437
|
+
result = {
|
|
438
|
+
"success": True,
|
|
439
|
+
"connection_id": connection_id,
|
|
440
|
+
"bytes_received": len(data),
|
|
441
|
+
"data_hex": data.hex().upper() if data else ""
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
# Try to decode as text
|
|
445
|
+
if not as_hex:
|
|
446
|
+
try:
|
|
447
|
+
result["data_text"] = data.decode("utf-8", errors="replace")
|
|
448
|
+
except:
|
|
449
|
+
result["data_text"] = None
|
|
450
|
+
|
|
451
|
+
return result
|
|
452
|
+
|
|
453
|
+
except socket.timeout:
|
|
454
|
+
return {
|
|
455
|
+
"success": True,
|
|
456
|
+
"connection_id": connection_id,
|
|
457
|
+
"bytes_received": 0,
|
|
458
|
+
"data_hex": "",
|
|
459
|
+
"data_text": "",
|
|
460
|
+
"message": "No data received (timeout)"
|
|
461
|
+
}
|
|
462
|
+
except Exception as e:
|
|
463
|
+
logger.error(f"Error receiving from {connection_id}: {e}")
|
|
464
|
+
return {
|
|
465
|
+
"success": False,
|
|
466
|
+
"error": str(e),
|
|
467
|
+
"error_type": type(e).__name__
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
def receive_until(
|
|
471
|
+
self,
|
|
472
|
+
connection_id: str,
|
|
473
|
+
terminator: str = "\n",
|
|
474
|
+
size: int = 4096,
|
|
475
|
+
timeout: Optional[float] = None
|
|
476
|
+
) -> dict[str, Any]:
|
|
477
|
+
"""Receive data until a specific terminator is found.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
connection_id: The connection to receive from.
|
|
481
|
+
terminator: Terminator string to look for.
|
|
482
|
+
size: Maximum bytes to receive.
|
|
483
|
+
timeout: Read timeout in seconds.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Dictionary containing success status and received data.
|
|
487
|
+
"""
|
|
488
|
+
with self._lock:
|
|
489
|
+
if connection_id not in self._connections:
|
|
490
|
+
return {
|
|
491
|
+
"success": False,
|
|
492
|
+
"error": f"Connection {connection_id} not found",
|
|
493
|
+
"error_type": "ConnectionNotFound"
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
conn = self._connections[connection_id]
|
|
498
|
+
terminator_bytes = terminator.encode("utf-8")
|
|
499
|
+
|
|
500
|
+
# Temporarily change timeout if specified
|
|
501
|
+
original_timeout = conn.socket.gettimeout()
|
|
502
|
+
if timeout is not None:
|
|
503
|
+
conn.socket.settimeout(timeout)
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
buffer = b""
|
|
507
|
+
while len(buffer) < size:
|
|
508
|
+
chunk = conn.socket.recv(1)
|
|
509
|
+
if not chunk:
|
|
510
|
+
break
|
|
511
|
+
buffer += chunk
|
|
512
|
+
if buffer.endswith(terminator_bytes):
|
|
513
|
+
break
|
|
514
|
+
finally:
|
|
515
|
+
if timeout is not None:
|
|
516
|
+
conn.socket.settimeout(original_timeout)
|
|
517
|
+
|
|
518
|
+
conn.bytes_received += len(buffer)
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
"success": True,
|
|
522
|
+
"connection_id": connection_id,
|
|
523
|
+
"data_text": buffer.decode("utf-8", errors="replace"),
|
|
524
|
+
"data_hex": buffer.hex().upper(),
|
|
525
|
+
"bytes_received": len(buffer)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
except socket.timeout:
|
|
529
|
+
return {
|
|
530
|
+
"success": True,
|
|
531
|
+
"connection_id": connection_id,
|
|
532
|
+
"bytes_received": 0,
|
|
533
|
+
"data_hex": "",
|
|
534
|
+
"data_text": "",
|
|
535
|
+
"message": "No data received (timeout)"
|
|
536
|
+
}
|
|
537
|
+
except Exception as e:
|
|
538
|
+
logger.error(f"Error receiving from {connection_id}: {e}")
|
|
539
|
+
return {
|
|
540
|
+
"success": False,
|
|
541
|
+
"error": str(e),
|
|
542
|
+
"error_type": type(e).__name__
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
def receive_line(
|
|
546
|
+
self,
|
|
547
|
+
connection_id: str,
|
|
548
|
+
timeout: Optional[float] = None,
|
|
549
|
+
encoding: str = "utf-8"
|
|
550
|
+
) -> dict[str, Any]:
|
|
551
|
+
"""Receive a line (until newline) from a socket connection.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
connection_id: The connection to receive from.
|
|
555
|
+
timeout: Read timeout in seconds.
|
|
556
|
+
encoding: Text encoding for decoding.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Dictionary containing success status and received line.
|
|
560
|
+
"""
|
|
561
|
+
result = self.receive_until(
|
|
562
|
+
connection_id=connection_id,
|
|
563
|
+
terminator="\n",
|
|
564
|
+
timeout=timeout
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if result.get("success") and result.get("data_text"):
|
|
568
|
+
# Strip trailing newline characters
|
|
569
|
+
result["line"] = result["data_text"].rstrip("\r\n")
|
|
570
|
+
|
|
571
|
+
return result
|
|
572
|
+
|
|
573
|
+
def set_protocol(
|
|
574
|
+
self,
|
|
575
|
+
connection_id: str,
|
|
576
|
+
mode: str = "raw",
|
|
577
|
+
start_delimiter: Optional[str] = None,
|
|
578
|
+
end_delimiter: Optional[str] = None,
|
|
579
|
+
start_delimiter_hex: Optional[str] = None,
|
|
580
|
+
end_delimiter_hex: Optional[str] = None,
|
|
581
|
+
include_delimiters: bool = False
|
|
582
|
+
) -> dict[str, Any]:
|
|
583
|
+
"""Set the receive protocol for a connection.
|
|
584
|
+
|
|
585
|
+
Configure how data should be received and parsed.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
connection_id: The connection to configure.
|
|
589
|
+
mode: Receive mode - 'raw', 'line', or 'packet'.
|
|
590
|
+
- raw: Receive data as-is
|
|
591
|
+
- line: Receive data line by line (end_delimiter = newline)
|
|
592
|
+
- packet: Parse packets using start/end delimiters
|
|
593
|
+
start_delimiter: Start of packet delimiter (text format).
|
|
594
|
+
end_delimiter: End of packet delimiter (text format).
|
|
595
|
+
start_delimiter_hex: Start delimiter in hex format (e.g., "0A").
|
|
596
|
+
end_delimiter_hex: End delimiter in hex format (e.g., "0D0A").
|
|
597
|
+
include_delimiters: Include delimiters in received data.
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Dictionary containing success status and protocol settings.
|
|
601
|
+
|
|
602
|
+
Examples:
|
|
603
|
+
- set_protocol("conn_id", mode="line") # Line mode (\\n terminator)
|
|
604
|
+
- set_protocol("conn_id", mode="packet", end_delimiter_hex="0D0A")
|
|
605
|
+
- set_protocol("conn_id", mode="packet", start_delimiter_hex="0A", end_delimiter_hex="0D0A")
|
|
606
|
+
"""
|
|
607
|
+
with self._lock:
|
|
608
|
+
if connection_id not in self._connections:
|
|
609
|
+
return {
|
|
610
|
+
"success": False,
|
|
611
|
+
"error": f"Connection {connection_id} not found",
|
|
612
|
+
"error_type": "ConnectionNotFound"
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
# Validate mode
|
|
616
|
+
valid_modes = ["raw", "line", "packet"]
|
|
617
|
+
if mode not in valid_modes:
|
|
618
|
+
return {
|
|
619
|
+
"success": False,
|
|
620
|
+
"error": f"Invalid mode: {mode}. Valid options: {valid_modes}",
|
|
621
|
+
"error_type": "InvalidParameter"
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
# Parse delimiters
|
|
625
|
+
start_bytes = b""
|
|
626
|
+
end_bytes = b"\n" # Default for line mode
|
|
627
|
+
|
|
628
|
+
if mode == "packet":
|
|
629
|
+
# Parse start delimiter
|
|
630
|
+
if start_delimiter_hex:
|
|
631
|
+
try:
|
|
632
|
+
start_bytes = bytes.fromhex(start_delimiter_hex.replace(" ", ""))
|
|
633
|
+
except ValueError as e:
|
|
634
|
+
return {
|
|
635
|
+
"success": False,
|
|
636
|
+
"error": f"Invalid start_delimiter_hex: {e}",
|
|
637
|
+
"error_type": "InvalidHexString"
|
|
638
|
+
}
|
|
639
|
+
elif start_delimiter:
|
|
640
|
+
start_bytes = start_delimiter.encode("utf-8")
|
|
641
|
+
|
|
642
|
+
# Parse end delimiter
|
|
643
|
+
if end_delimiter_hex:
|
|
644
|
+
try:
|
|
645
|
+
end_bytes = bytes.fromhex(end_delimiter_hex.replace(" ", ""))
|
|
646
|
+
except ValueError as e:
|
|
647
|
+
return {
|
|
648
|
+
"success": False,
|
|
649
|
+
"error": f"Invalid end_delimiter_hex: {e}",
|
|
650
|
+
"error_type": "InvalidHexString"
|
|
651
|
+
}
|
|
652
|
+
elif end_delimiter:
|
|
653
|
+
end_bytes = end_delimiter.encode("utf-8")
|
|
654
|
+
|
|
655
|
+
# Update protocol
|
|
656
|
+
conn = self._connections[connection_id]
|
|
657
|
+
conn.protocol = ReceiveProtocol(
|
|
658
|
+
mode=mode,
|
|
659
|
+
start_delimiter=start_bytes,
|
|
660
|
+
end_delimiter=end_bytes,
|
|
661
|
+
include_delimiters=include_delimiters
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
# Clear receive buffer when changing protocol
|
|
665
|
+
conn.receive_buffer = b""
|
|
666
|
+
|
|
667
|
+
logger.info(f"Set protocol for {connection_id}: mode={mode}, start={start_bytes.hex().upper()}, end={end_bytes.hex().upper()}")
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
"success": True,
|
|
671
|
+
"connection_id": connection_id,
|
|
672
|
+
"protocol": {
|
|
673
|
+
"mode": mode,
|
|
674
|
+
"start_delimiter_hex": start_bytes.hex().upper() if start_bytes else "",
|
|
675
|
+
"end_delimiter_hex": end_bytes.hex().upper(),
|
|
676
|
+
"include_delimiters": include_delimiters
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
def receive_packet(
|
|
681
|
+
self,
|
|
682
|
+
connection_id: str,
|
|
683
|
+
timeout: Optional[float] = None,
|
|
684
|
+
max_size: int = 4096
|
|
685
|
+
) -> dict[str, Any]:
|
|
686
|
+
"""Receive a complete packet based on the configured protocol.
|
|
687
|
+
|
|
688
|
+
Uses the start/end delimiters configured via set_protocol().
|
|
689
|
+
For packet mode: waits for data between start_delimiter and end_delimiter.
|
|
690
|
+
For line mode: equivalent to receive_line().
|
|
691
|
+
For raw mode: equivalent to receive_data().
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
connection_id: The connection to receive from.
|
|
695
|
+
timeout: Read timeout in seconds.
|
|
696
|
+
max_size: Maximum packet size to prevent memory issues.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
Dictionary containing success status, packet data, and metadata.
|
|
700
|
+
"""
|
|
701
|
+
with self._lock:
|
|
702
|
+
if connection_id not in self._connections:
|
|
703
|
+
return {
|
|
704
|
+
"success": False,
|
|
705
|
+
"error": f"Connection {connection_id} not found",
|
|
706
|
+
"error_type": "ConnectionNotFound"
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
conn = self._connections[connection_id]
|
|
710
|
+
protocol = conn.protocol
|
|
711
|
+
|
|
712
|
+
# For raw mode, just receive data
|
|
713
|
+
if protocol.mode == "raw":
|
|
714
|
+
# Release lock temporarily for receive_data
|
|
715
|
+
pass
|
|
716
|
+
|
|
717
|
+
# Handle different modes (outside lock to avoid deadlock with receive_data)
|
|
718
|
+
if protocol.mode == "raw":
|
|
719
|
+
return self.receive_data(connection_id, size=max_size, timeout=timeout)
|
|
720
|
+
elif protocol.mode == "line":
|
|
721
|
+
return self.receive_line(connection_id, timeout=timeout)
|
|
722
|
+
|
|
723
|
+
# Packet mode - parse with start/end delimiters
|
|
724
|
+
with self._lock:
|
|
725
|
+
if connection_id not in self._connections:
|
|
726
|
+
return {
|
|
727
|
+
"success": False,
|
|
728
|
+
"error": f"Connection {connection_id} not found",
|
|
729
|
+
"error_type": "ConnectionNotFound"
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
conn = self._connections[connection_id]
|
|
733
|
+
protocol = conn.protocol
|
|
734
|
+
start_delim = protocol.start_delimiter
|
|
735
|
+
end_delim = protocol.end_delimiter
|
|
736
|
+
|
|
737
|
+
try:
|
|
738
|
+
original_timeout = conn.socket.gettimeout()
|
|
739
|
+
if timeout is not None:
|
|
740
|
+
conn.socket.settimeout(timeout)
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
buffer = conn.receive_buffer
|
|
744
|
+
packet_found = False
|
|
745
|
+
start_pos = -1
|
|
746
|
+
end_pos = -1
|
|
747
|
+
|
|
748
|
+
while len(buffer) < max_size:
|
|
749
|
+
# Check if we already have a complete packet in buffer
|
|
750
|
+
if start_delim:
|
|
751
|
+
start_pos = buffer.find(start_delim)
|
|
752
|
+
if start_pos >= 0:
|
|
753
|
+
search_start = start_pos + len(start_delim)
|
|
754
|
+
end_pos = buffer.find(end_delim, search_start)
|
|
755
|
+
if end_pos >= 0:
|
|
756
|
+
packet_found = True
|
|
757
|
+
break
|
|
758
|
+
else:
|
|
759
|
+
# No start delimiter, just look for end
|
|
760
|
+
end_pos = buffer.find(end_delim)
|
|
761
|
+
if end_pos >= 0:
|
|
762
|
+
start_pos = 0
|
|
763
|
+
packet_found = True
|
|
764
|
+
break
|
|
765
|
+
|
|
766
|
+
# Need more data
|
|
767
|
+
try:
|
|
768
|
+
chunk = conn.socket.recv(conn.config.buffer_size)
|
|
769
|
+
if not chunk:
|
|
770
|
+
break # Connection closed
|
|
771
|
+
buffer += chunk
|
|
772
|
+
except socket.timeout:
|
|
773
|
+
break
|
|
774
|
+
|
|
775
|
+
# Update connection buffer
|
|
776
|
+
if packet_found:
|
|
777
|
+
# Extract packet
|
|
778
|
+
if start_delim:
|
|
779
|
+
data_start = start_pos + len(start_delim)
|
|
780
|
+
else:
|
|
781
|
+
data_start = 0
|
|
782
|
+
|
|
783
|
+
packet_data = buffer[data_start:end_pos]
|
|
784
|
+
|
|
785
|
+
# Include delimiters if configured
|
|
786
|
+
if protocol.include_delimiters:
|
|
787
|
+
if start_delim:
|
|
788
|
+
packet_with_delim = start_delim + packet_data + end_delim
|
|
789
|
+
else:
|
|
790
|
+
packet_with_delim = packet_data + end_delim
|
|
791
|
+
else:
|
|
792
|
+
packet_with_delim = packet_data
|
|
793
|
+
|
|
794
|
+
# Remove processed data from buffer
|
|
795
|
+
conn.receive_buffer = buffer[end_pos + len(end_delim):]
|
|
796
|
+
conn.bytes_received += len(packet_with_delim)
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
"success": True,
|
|
800
|
+
"connection_id": connection_id,
|
|
801
|
+
"packet_found": True,
|
|
802
|
+
"data_hex": packet_with_delim.hex().upper(),
|
|
803
|
+
"data_text": packet_with_delim.decode("utf-8", errors="replace"),
|
|
804
|
+
"payload_hex": packet_data.hex().upper(),
|
|
805
|
+
"payload_text": packet_data.decode("utf-8", errors="replace"),
|
|
806
|
+
"bytes_received": len(packet_with_delim)
|
|
807
|
+
}
|
|
808
|
+
else:
|
|
809
|
+
# No complete packet found, save buffer for next call
|
|
810
|
+
conn.receive_buffer = buffer
|
|
811
|
+
return {
|
|
812
|
+
"success": True,
|
|
813
|
+
"connection_id": connection_id,
|
|
814
|
+
"packet_found": False,
|
|
815
|
+
"message": "No complete packet received (timeout or max_size reached)",
|
|
816
|
+
"buffer_size": len(buffer)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
finally:
|
|
820
|
+
if timeout is not None:
|
|
821
|
+
conn.socket.settimeout(original_timeout)
|
|
822
|
+
|
|
823
|
+
except Exception as e:
|
|
824
|
+
logger.error(f"Error receiving packet from {connection_id}: {e}")
|
|
825
|
+
return {
|
|
826
|
+
"success": False,
|
|
827
|
+
"error": str(e),
|
|
828
|
+
"error_type": type(e).__name__
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
def get_protocol(self, connection_id: str) -> dict[str, Any]:
|
|
832
|
+
"""Get the current receive protocol settings for a connection.
|
|
833
|
+
|
|
834
|
+
Args:
|
|
835
|
+
connection_id: The connection to query.
|
|
836
|
+
|
|
837
|
+
Returns:
|
|
838
|
+
Dictionary containing protocol settings.
|
|
839
|
+
"""
|
|
840
|
+
with self._lock:
|
|
841
|
+
if connection_id not in self._connections:
|
|
842
|
+
return {
|
|
843
|
+
"success": False,
|
|
844
|
+
"error": f"Connection {connection_id} not found",
|
|
845
|
+
"error_type": "ConnectionNotFound"
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
conn = self._connections[connection_id]
|
|
849
|
+
protocol = conn.protocol
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
"success": True,
|
|
853
|
+
"connection_id": connection_id,
|
|
854
|
+
"protocol": {
|
|
855
|
+
"mode": protocol.mode,
|
|
856
|
+
"start_delimiter_hex": protocol.start_delimiter.hex().upper() if protocol.start_delimiter else "",
|
|
857
|
+
"end_delimiter_hex": protocol.end_delimiter.hex().upper(),
|
|
858
|
+
"include_delimiters": protocol.include_delimiters
|
|
859
|
+
},
|
|
860
|
+
"buffer_size": len(conn.receive_buffer)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
# ==================== Background Listener ====================
|
|
864
|
+
|
|
865
|
+
def _listener_loop(self, connection_id: str) -> None:
|
|
866
|
+
"""Background thread loop for receiving packets."""
|
|
867
|
+
logger.info(f"Starting background listener for {connection_id}")
|
|
868
|
+
|
|
869
|
+
while True:
|
|
870
|
+
with self._lock:
|
|
871
|
+
if connection_id not in self._connections:
|
|
872
|
+
logger.info(f"Connection {connection_id} closed, stopping listener")
|
|
873
|
+
return
|
|
874
|
+
conn = self._connections[connection_id]
|
|
875
|
+
if not conn.listener_running:
|
|
876
|
+
logger.info(f"Listener stopped for {connection_id}")
|
|
877
|
+
return
|
|
878
|
+
|
|
879
|
+
protocol = conn.protocol
|
|
880
|
+
start_delim = protocol.start_delimiter
|
|
881
|
+
end_delim = protocol.end_delimiter
|
|
882
|
+
max_packets = conn.config.max_buffer_packets
|
|
883
|
+
|
|
884
|
+
try:
|
|
885
|
+
# Receive data outside lock to avoid blocking
|
|
886
|
+
with self._lock:
|
|
887
|
+
if connection_id not in self._connections:
|
|
888
|
+
return
|
|
889
|
+
conn = self._connections[connection_id]
|
|
890
|
+
sock = conn.socket
|
|
891
|
+
buffer = conn.receive_buffer
|
|
892
|
+
|
|
893
|
+
# Try to receive data with short timeout
|
|
894
|
+
try:
|
|
895
|
+
sock.settimeout(0.5)
|
|
896
|
+
chunk = sock.recv(conn.config.buffer_size)
|
|
897
|
+
if chunk:
|
|
898
|
+
buffer += chunk
|
|
899
|
+
except socket.timeout:
|
|
900
|
+
pass
|
|
901
|
+
except (ConnectionResetError, BrokenPipeError, OSError):
|
|
902
|
+
logger.warning(f"Connection {connection_id} lost")
|
|
903
|
+
with self._lock:
|
|
904
|
+
if connection_id in self._connections:
|
|
905
|
+
self._connections[connection_id].listener_running = False
|
|
906
|
+
return
|
|
907
|
+
|
|
908
|
+
# Parse packets from buffer
|
|
909
|
+
packets_found = []
|
|
910
|
+
while True:
|
|
911
|
+
if start_delim:
|
|
912
|
+
start_pos = buffer.find(start_delim)
|
|
913
|
+
if start_pos < 0:
|
|
914
|
+
break
|
|
915
|
+
search_start = start_pos + len(start_delim)
|
|
916
|
+
end_pos = buffer.find(end_delim, search_start)
|
|
917
|
+
if end_pos < 0:
|
|
918
|
+
break
|
|
919
|
+
# Extract packet data
|
|
920
|
+
data_start = start_pos + len(start_delim)
|
|
921
|
+
packet_data = buffer[data_start:end_pos]
|
|
922
|
+
# Include delimiters if configured
|
|
923
|
+
if protocol.include_delimiters:
|
|
924
|
+
packet_with_delim = start_delim + packet_data + end_delim
|
|
925
|
+
else:
|
|
926
|
+
packet_with_delim = packet_data
|
|
927
|
+
packets_found.append(packet_with_delim)
|
|
928
|
+
buffer = buffer[end_pos + len(end_delim):]
|
|
929
|
+
else:
|
|
930
|
+
# No start delimiter
|
|
931
|
+
end_pos = buffer.find(end_delim)
|
|
932
|
+
if end_pos < 0:
|
|
933
|
+
break
|
|
934
|
+
packet_data = buffer[:end_pos]
|
|
935
|
+
if protocol.include_delimiters:
|
|
936
|
+
packet_with_delim = packet_data + end_delim
|
|
937
|
+
else:
|
|
938
|
+
packet_with_delim = packet_data
|
|
939
|
+
packets_found.append(packet_with_delim)
|
|
940
|
+
buffer = buffer[end_pos + len(end_delim):]
|
|
941
|
+
|
|
942
|
+
# Store parsed packets
|
|
943
|
+
with self._lock:
|
|
944
|
+
if connection_id not in self._connections:
|
|
945
|
+
return
|
|
946
|
+
conn = self._connections[connection_id]
|
|
947
|
+
conn.receive_buffer = buffer
|
|
948
|
+
|
|
949
|
+
with conn._packet_lock:
|
|
950
|
+
for packet in packets_found:
|
|
951
|
+
conn.bytes_received += len(packet)
|
|
952
|
+
if len(conn.packet_buffer) >= max_packets:
|
|
953
|
+
# Drop oldest (drop_oldest policy)
|
|
954
|
+
conn.packet_buffer.pop(0)
|
|
955
|
+
conn.overflow_count += 1
|
|
956
|
+
conn.packet_buffer.append({
|
|
957
|
+
"data_hex": packet.hex().upper(),
|
|
958
|
+
"data_text": packet.decode("utf-8", errors="replace"),
|
|
959
|
+
"timestamp": time.time(),
|
|
960
|
+
"size": len(packet)
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
except Exception as e:
|
|
964
|
+
logger.error(f"Listener error for {connection_id}: {e}")
|
|
965
|
+
time.sleep(0.5)
|
|
966
|
+
|
|
967
|
+
def start_listening(self, connection_id: str) -> dict[str, Any]:
|
|
968
|
+
"""Start background packet listener for a connection.
|
|
969
|
+
|
|
970
|
+
Args:
|
|
971
|
+
connection_id: The connection ID.
|
|
972
|
+
|
|
973
|
+
Returns:
|
|
974
|
+
Dictionary containing success status.
|
|
975
|
+
"""
|
|
976
|
+
with self._lock:
|
|
977
|
+
if connection_id not in self._connections:
|
|
978
|
+
return {
|
|
979
|
+
"success": False,
|
|
980
|
+
"error": f"Connection {connection_id} not found",
|
|
981
|
+
"error_type": "ConnectionNotFound"
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
conn = self._connections[connection_id]
|
|
985
|
+
|
|
986
|
+
if conn.listener_running:
|
|
987
|
+
return {
|
|
988
|
+
"success": True,
|
|
989
|
+
"message": "Listener already running",
|
|
990
|
+
"connection_id": connection_id
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
conn.listener_running = True
|
|
994
|
+
conn.listener_thread = threading.Thread(
|
|
995
|
+
target=self._listener_loop,
|
|
996
|
+
args=(connection_id,),
|
|
997
|
+
daemon=True
|
|
998
|
+
)
|
|
999
|
+
conn.listener_thread.start()
|
|
1000
|
+
|
|
1001
|
+
logger.info(f"Started listener for {connection_id}")
|
|
1002
|
+
|
|
1003
|
+
return {
|
|
1004
|
+
"success": True,
|
|
1005
|
+
"message": "Background listener started",
|
|
1006
|
+
"connection_id": connection_id,
|
|
1007
|
+
"max_buffer_packets": conn.config.max_buffer_packets
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
def stop_listening(self, connection_id: str) -> dict[str, Any]:
|
|
1011
|
+
"""Stop background packet listener for a connection.
|
|
1012
|
+
|
|
1013
|
+
Args:
|
|
1014
|
+
connection_id: The connection ID.
|
|
1015
|
+
|
|
1016
|
+
Returns:
|
|
1017
|
+
Dictionary containing success status.
|
|
1018
|
+
"""
|
|
1019
|
+
with self._lock:
|
|
1020
|
+
if connection_id not in self._connections:
|
|
1021
|
+
return {
|
|
1022
|
+
"success": False,
|
|
1023
|
+
"error": f"Connection {connection_id} not found",
|
|
1024
|
+
"error_type": "ConnectionNotFound"
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
conn = self._connections[connection_id]
|
|
1028
|
+
conn.listener_running = False
|
|
1029
|
+
|
|
1030
|
+
logger.info(f"Stopped listener for {connection_id}")
|
|
1031
|
+
|
|
1032
|
+
return {
|
|
1033
|
+
"success": True,
|
|
1034
|
+
"message": "Background listener stopped",
|
|
1035
|
+
"connection_id": connection_id
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
def get_packets(self, connection_id: str, clear: bool = True) -> dict[str, Any]:
|
|
1039
|
+
"""Get all buffered packets from a connection.
|
|
1040
|
+
|
|
1041
|
+
Args:
|
|
1042
|
+
connection_id: The connection ID.
|
|
1043
|
+
clear: Clear buffer after getting packets (default True).
|
|
1044
|
+
|
|
1045
|
+
Returns:
|
|
1046
|
+
Dictionary containing packets array.
|
|
1047
|
+
"""
|
|
1048
|
+
with self._lock:
|
|
1049
|
+
if connection_id not in self._connections:
|
|
1050
|
+
return {
|
|
1051
|
+
"success": False,
|
|
1052
|
+
"error": f"Connection {connection_id} not found",
|
|
1053
|
+
"error_type": "ConnectionNotFound"
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
conn = self._connections[connection_id]
|
|
1057
|
+
|
|
1058
|
+
with conn._packet_lock:
|
|
1059
|
+
packets = list(conn.packet_buffer)
|
|
1060
|
+
if clear:
|
|
1061
|
+
conn.packet_buffer.clear()
|
|
1062
|
+
|
|
1063
|
+
return {
|
|
1064
|
+
"success": True,
|
|
1065
|
+
"connection_id": connection_id,
|
|
1066
|
+
"packets": packets,
|
|
1067
|
+
"count": len(packets),
|
|
1068
|
+
"overflow_count": conn.overflow_count
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
def get_buffer_status(self, connection_id: str) -> dict[str, Any]:
|
|
1072
|
+
"""Get buffer status for a connection.
|
|
1073
|
+
|
|
1074
|
+
Args:
|
|
1075
|
+
connection_id: The connection ID.
|
|
1076
|
+
|
|
1077
|
+
Returns:
|
|
1078
|
+
Dictionary containing buffer status.
|
|
1079
|
+
"""
|
|
1080
|
+
with self._lock:
|
|
1081
|
+
if connection_id not in self._connections:
|
|
1082
|
+
return {
|
|
1083
|
+
"success": False,
|
|
1084
|
+
"error": f"Connection {connection_id} not found",
|
|
1085
|
+
"error_type": "ConnectionNotFound"
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
conn = self._connections[connection_id]
|
|
1089
|
+
|
|
1090
|
+
with conn._packet_lock:
|
|
1091
|
+
count = len(conn.packet_buffer)
|
|
1092
|
+
|
|
1093
|
+
return {
|
|
1094
|
+
"success": True,
|
|
1095
|
+
"connection_id": connection_id,
|
|
1096
|
+
"buffer": {
|
|
1097
|
+
"count": count,
|
|
1098
|
+
"max": conn.config.max_buffer_packets,
|
|
1099
|
+
"overflow_count": conn.overflow_count,
|
|
1100
|
+
"raw_buffer_size": len(conn.receive_buffer)
|
|
1101
|
+
},
|
|
1102
|
+
"listener_running": conn.listener_running
|
|
1103
|
+
}
|