conson-xp 1.18.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 (176) hide show
  1. conson_xp-1.18.0.dist-info/METADATA +412 -0
  2. conson_xp-1.18.0.dist-info/RECORD +176 -0
  3. conson_xp-1.18.0.dist-info/WHEEL +4 -0
  4. conson_xp-1.18.0.dist-info/entry_points.txt +5 -0
  5. conson_xp-1.18.0.dist-info/licenses/LICENSE +29 -0
  6. xp/__init__.py +9 -0
  7. xp/cli/__init__.py +5 -0
  8. xp/cli/__main__.py +6 -0
  9. xp/cli/commands/__init__.py +153 -0
  10. xp/cli/commands/conbus/__init__.py +25 -0
  11. xp/cli/commands/conbus/conbus.py +128 -0
  12. xp/cli/commands/conbus/conbus_actiontable_commands.py +233 -0
  13. xp/cli/commands/conbus/conbus_autoreport_commands.py +108 -0
  14. xp/cli/commands/conbus/conbus_blink_commands.py +163 -0
  15. xp/cli/commands/conbus/conbus_config_commands.py +29 -0
  16. xp/cli/commands/conbus/conbus_custom_commands.py +57 -0
  17. xp/cli/commands/conbus/conbus_datapoint_commands.py +113 -0
  18. xp/cli/commands/conbus/conbus_discover_commands.py +61 -0
  19. xp/cli/commands/conbus/conbus_event_commands.py +81 -0
  20. xp/cli/commands/conbus/conbus_lightlevel_commands.py +207 -0
  21. xp/cli/commands/conbus/conbus_linknumber_commands.py +102 -0
  22. xp/cli/commands/conbus/conbus_modulenumber_commands.py +104 -0
  23. xp/cli/commands/conbus/conbus_msactiontable_commands.py +94 -0
  24. xp/cli/commands/conbus/conbus_output_commands.py +163 -0
  25. xp/cli/commands/conbus/conbus_raw_commands.py +62 -0
  26. xp/cli/commands/conbus/conbus_receive_commands.py +59 -0
  27. xp/cli/commands/conbus/conbus_scan_commands.py +58 -0
  28. xp/cli/commands/file_commands.py +186 -0
  29. xp/cli/commands/homekit/__init__.py +3 -0
  30. xp/cli/commands/homekit/homekit.py +118 -0
  31. xp/cli/commands/homekit/homekit_start_commands.py +43 -0
  32. xp/cli/commands/module_commands.py +187 -0
  33. xp/cli/commands/reverse_proxy_commands.py +178 -0
  34. xp/cli/commands/server/__init__.py +3 -0
  35. xp/cli/commands/server/server_commands.py +135 -0
  36. xp/cli/commands/telegram/__init__.py +5 -0
  37. xp/cli/commands/telegram/telegram.py +41 -0
  38. xp/cli/commands/telegram/telegram_blink_commands.py +79 -0
  39. xp/cli/commands/telegram/telegram_checksum_commands.py +112 -0
  40. xp/cli/commands/telegram/telegram_discover_commands.py +41 -0
  41. xp/cli/commands/telegram/telegram_linknumber_commands.py +86 -0
  42. xp/cli/commands/telegram/telegram_parse_commands.py +75 -0
  43. xp/cli/commands/telegram/telegram_version_commands.py +52 -0
  44. xp/cli/main.py +87 -0
  45. xp/cli/utils/__init__.py +1 -0
  46. xp/cli/utils/click_tree.py +57 -0
  47. xp/cli/utils/datapoint_type_choice.py +57 -0
  48. xp/cli/utils/decorators.py +351 -0
  49. xp/cli/utils/error_handlers.py +201 -0
  50. xp/cli/utils/formatters.py +312 -0
  51. xp/cli/utils/module_type_choice.py +56 -0
  52. xp/cli/utils/serial_number_type.py +52 -0
  53. xp/cli/utils/system_function_choice.py +57 -0
  54. xp/cli/utils/xp_module_type.py +53 -0
  55. xp/connection/__init__.py +13 -0
  56. xp/connection/exceptions.py +22 -0
  57. xp/models/__init__.py +36 -0
  58. xp/models/actiontable/__init__.py +1 -0
  59. xp/models/actiontable/actiontable.py +43 -0
  60. xp/models/actiontable/msactiontable_xp20.py +53 -0
  61. xp/models/actiontable/msactiontable_xp24.py +58 -0
  62. xp/models/actiontable/msactiontable_xp33.py +65 -0
  63. xp/models/conbus/__init__.py +1 -0
  64. xp/models/conbus/conbus.py +87 -0
  65. xp/models/conbus/conbus_autoreport.py +67 -0
  66. xp/models/conbus/conbus_blink.py +80 -0
  67. xp/models/conbus/conbus_client_config.py +55 -0
  68. xp/models/conbus/conbus_connection_status.py +40 -0
  69. xp/models/conbus/conbus_custom.py +58 -0
  70. xp/models/conbus/conbus_datapoint.py +89 -0
  71. xp/models/conbus/conbus_discover.py +64 -0
  72. xp/models/conbus/conbus_event_raw.py +47 -0
  73. xp/models/conbus/conbus_lightlevel.py +52 -0
  74. xp/models/conbus/conbus_linknumber.py +54 -0
  75. xp/models/conbus/conbus_output.py +57 -0
  76. xp/models/conbus/conbus_raw.py +45 -0
  77. xp/models/conbus/conbus_receive.py +42 -0
  78. xp/models/conbus/conbus_writeconfig.py +60 -0
  79. xp/models/homekit/__init__.py +1 -0
  80. xp/models/homekit/homekit_accessory.py +35 -0
  81. xp/models/homekit/homekit_config.py +106 -0
  82. xp/models/homekit/homekit_conson_config.py +86 -0
  83. xp/models/log_entry.py +130 -0
  84. xp/models/protocol/__init__.py +1 -0
  85. xp/models/protocol/conbus_protocol.py +312 -0
  86. xp/models/response.py +42 -0
  87. xp/models/telegram/__init__.py +1 -0
  88. xp/models/telegram/action_type.py +31 -0
  89. xp/models/telegram/datapoint_type.py +82 -0
  90. xp/models/telegram/event_telegram.py +140 -0
  91. xp/models/telegram/event_type.py +15 -0
  92. xp/models/telegram/input_action_type.py +69 -0
  93. xp/models/telegram/input_type.py +17 -0
  94. xp/models/telegram/module_type.py +188 -0
  95. xp/models/telegram/module_type_code.py +205 -0
  96. xp/models/telegram/output_telegram.py +103 -0
  97. xp/models/telegram/reply_telegram.py +297 -0
  98. xp/models/telegram/system_function.py +116 -0
  99. xp/models/telegram/system_telegram.py +94 -0
  100. xp/models/telegram/telegram.py +28 -0
  101. xp/models/telegram/telegram_type.py +19 -0
  102. xp/models/telegram/timeparam_type.py +51 -0
  103. xp/models/write_config_type.py +33 -0
  104. xp/services/__init__.py +26 -0
  105. xp/services/actiontable/__init__.py +1 -0
  106. xp/services/actiontable/actiontable_serializer.py +273 -0
  107. xp/services/actiontable/msactiontable_serializer.py +7 -0
  108. xp/services/actiontable/msactiontable_xp20_serializer.py +169 -0
  109. xp/services/actiontable/msactiontable_xp24_serializer.py +120 -0
  110. xp/services/actiontable/msactiontable_xp33_serializer.py +239 -0
  111. xp/services/conbus/__init__.py +1 -0
  112. xp/services/conbus/actiontable/__init__.py +1 -0
  113. xp/services/conbus/actiontable/actiontable_download_service.py +158 -0
  114. xp/services/conbus/actiontable/actiontable_list_service.py +91 -0
  115. xp/services/conbus/actiontable/actiontable_show_service.py +89 -0
  116. xp/services/conbus/actiontable/actiontable_upload_service.py +211 -0
  117. xp/services/conbus/actiontable/msactiontable_service.py +232 -0
  118. xp/services/conbus/conbus_blink_all_service.py +181 -0
  119. xp/services/conbus/conbus_blink_service.py +158 -0
  120. xp/services/conbus/conbus_custom_service.py +156 -0
  121. xp/services/conbus/conbus_datapoint_queryall_service.py +182 -0
  122. xp/services/conbus/conbus_datapoint_service.py +170 -0
  123. xp/services/conbus/conbus_discover_service.py +312 -0
  124. xp/services/conbus/conbus_event_raw_service.py +181 -0
  125. xp/services/conbus/conbus_output_service.py +194 -0
  126. xp/services/conbus/conbus_raw_service.py +122 -0
  127. xp/services/conbus/conbus_receive_service.py +115 -0
  128. xp/services/conbus/conbus_scan_service.py +150 -0
  129. xp/services/conbus/write_config_service.py +194 -0
  130. xp/services/homekit/__init__.py +1 -0
  131. xp/services/homekit/homekit_cache_service.py +307 -0
  132. xp/services/homekit/homekit_conbus_service.py +93 -0
  133. xp/services/homekit/homekit_config_validator.py +310 -0
  134. xp/services/homekit/homekit_conson_validator.py +121 -0
  135. xp/services/homekit/homekit_dimminglight.py +182 -0
  136. xp/services/homekit/homekit_dimminglight_service.py +148 -0
  137. xp/services/homekit/homekit_hap_service.py +342 -0
  138. xp/services/homekit/homekit_lightbulb.py +120 -0
  139. xp/services/homekit/homekit_lightbulb_service.py +86 -0
  140. xp/services/homekit/homekit_module_service.py +56 -0
  141. xp/services/homekit/homekit_outlet.py +168 -0
  142. xp/services/homekit/homekit_outlet_service.py +121 -0
  143. xp/services/homekit/homekit_service.py +359 -0
  144. xp/services/log_file_service.py +309 -0
  145. xp/services/module_type_service.py +257 -0
  146. xp/services/protocol/__init__.py +21 -0
  147. xp/services/protocol/conbus_event_protocol.py +360 -0
  148. xp/services/protocol/conbus_protocol.py +318 -0
  149. xp/services/protocol/protocol_factory.py +78 -0
  150. xp/services/protocol/telegram_protocol.py +264 -0
  151. xp/services/reverse_proxy_service.py +435 -0
  152. xp/services/server/__init__.py +1 -0
  153. xp/services/server/base_server_service.py +366 -0
  154. xp/services/server/cp20_server_service.py +65 -0
  155. xp/services/server/device_service_factory.py +94 -0
  156. xp/services/server/server_service.py +428 -0
  157. xp/services/server/xp130_server_service.py +67 -0
  158. xp/services/server/xp20_server_service.py +92 -0
  159. xp/services/server/xp230_server_service.py +58 -0
  160. xp/services/server/xp24_server_service.py +245 -0
  161. xp/services/server/xp33_server_service.py +535 -0
  162. xp/services/telegram/__init__.py +1 -0
  163. xp/services/telegram/telegram_blink_service.py +138 -0
  164. xp/services/telegram/telegram_checksum_service.py +149 -0
  165. xp/services/telegram/telegram_datapoint_service.py +82 -0
  166. xp/services/telegram/telegram_discover_service.py +277 -0
  167. xp/services/telegram/telegram_link_number_service.py +216 -0
  168. xp/services/telegram/telegram_output_service.py +322 -0
  169. xp/services/telegram/telegram_service.py +380 -0
  170. xp/services/telegram/telegram_version_service.py +288 -0
  171. xp/utils/__init__.py +12 -0
  172. xp/utils/checksum.py +61 -0
  173. xp/utils/dependencies.py +531 -0
  174. xp/utils/event_helper.py +31 -0
  175. xp/utils/serialization.py +205 -0
  176. xp/utils/time_utils.py +134 -0
@@ -0,0 +1,435 @@
1
+ """Conbus Reverse Proxy Service for TCP relay with telegram monitoring.
2
+
3
+ This service implements a TCP reverse proxy that listens on port 10001 and forwards
4
+ all telegrams to the configured Conbus server while printing bidirectional traffic.
5
+ """
6
+
7
+ import logging
8
+ import socket
9
+ import threading
10
+ import time
11
+ from datetime import datetime
12
+ from typing import Dict, Optional
13
+
14
+ from xp.models import ConbusClientConfig
15
+ from xp.models.response import Response
16
+
17
+
18
+ class ReverseProxyError(Exception):
19
+ """Raised when Conbus reverse proxy operations fail."""
20
+
21
+ pass
22
+
23
+
24
+ class ReverseProxyService:
25
+ """
26
+ TCP reverse proxy for Conbus communications.
27
+
28
+ Accepts client connections on port 10001 and forwards all telegrams
29
+ to the target server configured in cli.yml. Monitors and prints all
30
+ bidirectional traffic with timestamps.
31
+
32
+ Attributes:
33
+ logger: Logger instance for the service.
34
+ listen_port: Port to listen on for client connections.
35
+ server_socket: Main server socket for accepting connections.
36
+ is_running: Flag indicating if proxy is running.
37
+ active_connections: Dictionary of active connection information.
38
+ connection_counter: Counter for connection IDs.
39
+ cli_config: Conbus client configuration.
40
+ target_ip: Target server IP address.
41
+ target_port: Target server port number.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ cli_config: ConbusClientConfig,
47
+ listen_port: int,
48
+ ):
49
+ """Initialize the Conbus reverse proxy service.
50
+
51
+ Args:
52
+ cli_config: Conbus client configuration.
53
+ listen_port: Port to listen on for client connections.
54
+ """
55
+ # Set up logging first
56
+ self.logger = logging.getLogger(__name__)
57
+
58
+ self.listen_port = listen_port
59
+ self.server_socket: Optional[socket.socket] = None
60
+ self.is_running = False
61
+ self.active_connections: Dict[str, dict] = {}
62
+ self.connection_counter = 0
63
+
64
+ # Target server configuration
65
+ self.cli_config = cli_config
66
+
67
+ @property
68
+ def target_ip(self) -> str:
69
+ """Get target server IP.
70
+
71
+ Returns:
72
+ Target server IP address.
73
+ """
74
+ return self.cli_config.conbus.ip
75
+
76
+ @property
77
+ def target_port(self) -> int:
78
+ """Get target server port.
79
+
80
+ Returns:
81
+ Target server port number.
82
+ """
83
+ return self.cli_config.conbus.port
84
+
85
+ def start_proxy(self) -> Response:
86
+ """Start the reverse proxy server.
87
+
88
+ Returns:
89
+ Response object with success status and proxy details.
90
+ """
91
+ if self.is_running:
92
+ return Response(
93
+ success=False, data=None, error="Reverse proxy is already running"
94
+ )
95
+
96
+ try:
97
+ # Create TCP socket
98
+ self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
99
+ self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
100
+
101
+ # Bind to listen port on all interfaces
102
+ self.server_socket.bind(("0.0.0.0", self.listen_port))
103
+ self.server_socket.listen(5) # Allow multiple connections in queue
104
+
105
+ self.is_running = True
106
+ self.logger.info(f"Reverse proxy started on port {self.listen_port}")
107
+ self.logger.info(
108
+ f"Forwarding to {self.cli_config.conbus.ip}:{self.cli_config.conbus.port}"
109
+ )
110
+
111
+ # Print startup message
112
+ print(f"Conbus Reverse Proxy started on port {self.listen_port}")
113
+ print(
114
+ f"Forwarding telegrams to {self.cli_config.conbus.ip}:{self.cli_config.conbus.port}"
115
+ )
116
+ print("Monitoring all traffic...\n")
117
+
118
+ # Start accepting connections in background thread
119
+ accept_thread = threading.Thread(
120
+ target=self._accept_connections, daemon=True
121
+ )
122
+ accept_thread.start()
123
+
124
+ return Response(
125
+ success=True,
126
+ data={
127
+ "listen_port": self.listen_port,
128
+ "target_ip": self.cli_config.conbus.ip,
129
+ "target_port": self.cli_config.conbus.port,
130
+ "message": "Reverse proxy started successfully",
131
+ },
132
+ error=None,
133
+ )
134
+
135
+ except Exception as e:
136
+ self.logger.error(f"Failed to start reverse proxy: {e}")
137
+ return Response(
138
+ success=False, data=None, error=f"Failed to start reverse proxy: {e}"
139
+ )
140
+
141
+ def stop_proxy(self) -> Response:
142
+ """Stop the reverse proxy server.
143
+
144
+ Returns:
145
+ Response object with success status.
146
+ """
147
+ if not self.is_running:
148
+ return Response(
149
+ success=False, data=None, error="Reverse proxy is not running"
150
+ )
151
+
152
+ self.is_running = False
153
+
154
+ # Close all active connections
155
+ for conn_id, conn_info in list(self.active_connections.items()):
156
+ self._close_connection_pair(conn_id)
157
+
158
+ # Close server socket
159
+ if self.server_socket:
160
+ try:
161
+ self.server_socket.close()
162
+ self.logger.info("Reverse proxy stopped")
163
+ print("Reverse proxy stopped")
164
+ except Exception as e:
165
+ self.logger.error(f"Error closing server socket: {e}")
166
+
167
+ return Response(
168
+ success=True,
169
+ data={"message": "Reverse proxy stopped successfully"},
170
+ error=None,
171
+ )
172
+
173
+ def get_status(self) -> Response:
174
+ """Get current proxy status and active connections.
175
+
176
+ Returns:
177
+ Response object with proxy status and connection details.
178
+ """
179
+ return Response(
180
+ success=True,
181
+ data={
182
+ "running": self.is_running,
183
+ "listen_port": self.listen_port,
184
+ "target_ip": self.cli_config.conbus.ip,
185
+ "target_port": self.cli_config.conbus.port,
186
+ "active_connections": len(self.active_connections),
187
+ "connections": {
188
+ conn_id: {
189
+ "client_address": info["client_address"],
190
+ "connected_at": info["connected_at"].isoformat(),
191
+ "bytes_relayed": info.get("bytes_relayed", 0),
192
+ }
193
+ for conn_id, info in self.active_connections.items()
194
+ },
195
+ },
196
+ error=None,
197
+ )
198
+
199
+ def _accept_connections(self) -> None:
200
+ """Accept and handle client connections."""
201
+ while self.is_running:
202
+ try:
203
+ # Accept connection
204
+ if self.server_socket is None:
205
+ break
206
+ client_socket, client_address = self.server_socket.accept()
207
+
208
+ # Generate connection ID
209
+ self.connection_counter += 1
210
+ conn_id = f"conn_{self.connection_counter}"
211
+
212
+ self.logger.info(f"Client connected from {client_address} [{conn_id}]")
213
+ print(
214
+ f"{self.timestamp()} [CONNECTION] Client {client_address} connected [{conn_id}]"
215
+ )
216
+
217
+ # Handle client in separate thread
218
+ client_thread = threading.Thread(
219
+ target=self._handle_client,
220
+ args=(client_socket, client_address, conn_id),
221
+ daemon=True,
222
+ )
223
+ client_thread.start()
224
+
225
+ except Exception as e:
226
+ if self.is_running:
227
+ self.logger.error(f"Error accepting connection: {e}")
228
+ break
229
+
230
+ def _handle_client(
231
+ self, client_socket: socket.socket, client_address: tuple, conn_id: str
232
+ ) -> None:
233
+ """Handle individual client connection with server relay.
234
+
235
+ Args:
236
+ client_socket: Client socket connection.
237
+ client_address: Client address tuple (ip, port).
238
+ conn_id: Connection identifier.
239
+ """
240
+ try:
241
+ # Connect to target server
242
+ server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
243
+ server_socket.settimeout(self.cli_config.conbus.timeout)
244
+ server_socket.connect(
245
+ (self.cli_config.conbus.ip, self.cli_config.conbus.port)
246
+ )
247
+
248
+ # Store connection info
249
+ self.active_connections[conn_id] = {
250
+ "client_socket": client_socket,
251
+ "server_socket": server_socket,
252
+ "client_address": client_address,
253
+ "connected_at": datetime.now(),
254
+ "bytes_relayed": 0,
255
+ }
256
+
257
+ self.logger.info(
258
+ f"Connected to target server {self.cli_config.conbus.ip}:{self.cli_config.conbus.port} [{conn_id}]"
259
+ )
260
+
261
+ # Set timeouts for idle connections
262
+ client_socket.settimeout(30.0)
263
+ server_socket.settimeout(30.0)
264
+
265
+ # Start bidirectional relay threads
266
+ client_to_server_thread = threading.Thread(
267
+ target=self._relay_data,
268
+ args=(
269
+ client_socket,
270
+ server_socket,
271
+ "CLIENT→PROXY",
272
+ "PROXY→SERVER",
273
+ conn_id,
274
+ ),
275
+ daemon=True,
276
+ )
277
+ server_to_client_thread = threading.Thread(
278
+ target=self._relay_data,
279
+ args=(
280
+ server_socket,
281
+ client_socket,
282
+ "SERVER→PROXY",
283
+ "PROXY→CLIENT",
284
+ conn_id,
285
+ ),
286
+ daemon=True,
287
+ )
288
+
289
+ client_to_server_thread.start()
290
+ server_to_client_thread.start()
291
+
292
+ # Wait for either thread to finish (indicating connection closure)
293
+ client_to_server_thread.join()
294
+ server_to_client_thread.join()
295
+
296
+ except socket.timeout:
297
+ self.logger.info(f"Connection to target server timed out [{conn_id}]")
298
+ print(
299
+ f"{self.timestamp()} [ERROR] Connection to target server timed out [{conn_id}]"
300
+ )
301
+ except Exception as e:
302
+ self.logger.error(
303
+ f"Error handling client {client_address}: {e} [{conn_id}]"
304
+ )
305
+ print(f"{self.timestamp()} [ERROR] Connection error: {e} [{conn_id}]")
306
+ finally:
307
+ self._close_connection_pair(conn_id)
308
+
309
+ def _relay_data(
310
+ self,
311
+ source_socket: socket.socket,
312
+ dest_socket: socket.socket,
313
+ source_label: str,
314
+ dest_label: str,
315
+ conn_id: str,
316
+ ) -> None:
317
+ """Relay data between sockets with telegram monitoring.
318
+
319
+ Args:
320
+ source_socket: Source socket to receive from.
321
+ dest_socket: Destination socket to send to.
322
+ source_label: Label for source in logs.
323
+ dest_label: Label for destination in logs.
324
+ conn_id: Connection identifier.
325
+ """
326
+ try:
327
+ while self.is_running:
328
+ # Receive data from source
329
+ data = source_socket.recv(1024)
330
+ if not data:
331
+ break
332
+
333
+ # Decode and print telegram
334
+ try:
335
+ message = data.decode("latin-1").strip()
336
+ if message:
337
+ print(f"{self.timestamp()} [{source_label}] {message}")
338
+
339
+ # Forward to destination
340
+ dest_socket.send(data)
341
+ print(f"{self.timestamp()} [{dest_label}] {message}")
342
+
343
+ # Update bytes relayed counter
344
+ if conn_id in self.active_connections:
345
+ self.active_connections[conn_id]["bytes_relayed"] += len(
346
+ data
347
+ )
348
+
349
+ except UnicodeDecodeError:
350
+ # Handle binary data
351
+ print(
352
+ f"{self.timestamp()} [{source_label}] <binary data: {len(data)} bytes>"
353
+ )
354
+ dest_socket.send(data)
355
+ print(
356
+ f"{self.timestamp()} [{dest_label}] <binary data: {len(data)} bytes>"
357
+ )
358
+
359
+ if conn_id in self.active_connections:
360
+ self.active_connections[conn_id]["bytes_relayed"] += len(data)
361
+
362
+ except socket.timeout:
363
+ self.logger.debug(f"Socket timeout in relay [{conn_id}]")
364
+ except Exception as e:
365
+ if self.is_running:
366
+ self.logger.error(f"Error in data relay: {e} [{conn_id}]")
367
+
368
+ def _close_connection_pair(self, conn_id: str) -> None:
369
+ """Close both client and server sockets for a connection.
370
+
371
+ Args:
372
+ conn_id: Connection identifier.
373
+ """
374
+ if conn_id not in self.active_connections:
375
+ return
376
+
377
+ conn_info = self.active_connections[conn_id]
378
+
379
+ # Close client socket
380
+ try:
381
+ if "client_socket" in conn_info:
382
+ conn_info["client_socket"].close()
383
+ except Exception as e:
384
+ self.logger.error(f"Error closing client socket: {e} [{conn_id}]")
385
+
386
+ # Close server socket
387
+ try:
388
+ if "server_socket" in conn_info:
389
+ conn_info["server_socket"].close()
390
+ except Exception as e:
391
+ self.logger.error(f"Error closing server socket: {e} [{conn_id}]")
392
+
393
+ # Log disconnection
394
+ client_address = conn_info.get("client_address", "unknown")
395
+ bytes_relayed = conn_info.get("bytes_relayed", 0)
396
+
397
+ self.logger.info(
398
+ f"Client {client_address} disconnected [{conn_id}] - {bytes_relayed} bytes relayed"
399
+ )
400
+ print(
401
+ f"{self.timestamp()} [DISCONNECTION] "
402
+ f"Client {client_address} "
403
+ f"disconnected [{conn_id}] - "
404
+ f"{bytes_relayed} bytes relayed"
405
+ )
406
+
407
+ # Remove from active connections
408
+ del self.active_connections[conn_id]
409
+
410
+ @staticmethod
411
+ def timestamp() -> str:
412
+ """Generate timestamp string for logging.
413
+
414
+ Returns:
415
+ Timestamp string in HH:MM:SS,mmm format.
416
+ """
417
+ return datetime.now().strftime("%H:%M:%S,%f")[:-3]
418
+
419
+ def run_blocking(self) -> None:
420
+ """Run the proxy in blocking mode (for CLI usage).
421
+
422
+ Raises:
423
+ ReverseProxyError: If proxy fails to start.
424
+ """
425
+ result = self.start_proxy()
426
+ if not result.success:
427
+ raise ReverseProxyError(result.error)
428
+
429
+ try:
430
+ # Keep running until interrupted
431
+ while self.is_running:
432
+ time.sleep(1)
433
+ except KeyboardInterrupt:
434
+ print(f"\n{self.timestamp()} [SHUTDOWN] Received interrupt signal")
435
+ self.stop_proxy()
@@ -0,0 +1 @@
1
+ """Server services for XP protocol variants."""