ipc-framework 1.0.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.
@@ -0,0 +1,298 @@
1
+ """
2
+ Socket-based server implementation for the IPC Framework
3
+ """
4
+
5
+ import socket
6
+ import threading
7
+ import json
8
+ import select
9
+ from typing import Dict, Set, Optional
10
+ from .core import IPCServer, Message, MessageType
11
+ from .exceptions import ConnectionError, RoutingError, SerializationError
12
+
13
+
14
+ class FrameworkServer(IPCServer):
15
+ """Socket-based IPC Server implementation"""
16
+
17
+ def __init__(self, host: str = "localhost", port: int = 8888, max_connections: int = 100):
18
+ super().__init__(host, port)
19
+ self.max_connections = max_connections
20
+ self.server_socket: Optional[socket.socket] = None
21
+ self.client_sockets: Dict[str, socket.socket] = {}
22
+ self.socket_to_connection_id: Dict[socket.socket, str] = {}
23
+ self.running = False
24
+ self.server_thread: Optional[threading.Thread] = None
25
+ self._clients_lock = threading.RLock()
26
+
27
+ def start(self):
28
+ """Start the IPC server"""
29
+ if self.running:
30
+ return
31
+
32
+ try:
33
+ self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
34
+ self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
35
+ self.server_socket.bind((self.host, self.port))
36
+ self.server_socket.listen(self.max_connections)
37
+ self.running = True
38
+
39
+ print(f"IPC Server started on {self.host}:{self.port}")
40
+
41
+ # Start server thread
42
+ self.server_thread = threading.Thread(target=self._run_server, daemon=True)
43
+ self.server_thread.start()
44
+
45
+ except Exception as e:
46
+ raise ConnectionError(f"Failed to start server: {e}")
47
+
48
+ def stop(self):
49
+ """Stop the IPC server"""
50
+ if not self.running:
51
+ return
52
+
53
+ self.running = False
54
+
55
+ # Close all client connections
56
+ with self._clients_lock:
57
+ for client_socket in list(self.client_sockets.values()):
58
+ try:
59
+ client_socket.close()
60
+ except:
61
+ pass
62
+ self.client_sockets.clear()
63
+ self.socket_to_connection_id.clear()
64
+
65
+ # Close server socket
66
+ if self.server_socket:
67
+ try:
68
+ self.server_socket.close()
69
+ except:
70
+ pass
71
+
72
+ # Wait for server thread to finish
73
+ if self.server_thread and self.server_thread.is_alive():
74
+ self.server_thread.join(timeout=1.0)
75
+
76
+ print("IPC Server stopped")
77
+
78
+ def _run_server(self):
79
+ """Main server loop"""
80
+ while self.running:
81
+ try:
82
+ # Use select to avoid blocking when server is stopping
83
+ ready_sockets, _, _ = select.select([self.server_socket], [], [], 1.0)
84
+
85
+ if ready_sockets and self.running:
86
+ client_socket, client_address = self.server_socket.accept()
87
+ print(f"New client connected from {client_address}")
88
+
89
+ # Start client handler thread
90
+ client_thread = threading.Thread(
91
+ target=self._handle_client,
92
+ args=(client_socket, client_address),
93
+ daemon=True
94
+ )
95
+ client_thread.start()
96
+
97
+ except Exception as e:
98
+ if self.running:
99
+ print(f"Server error: {e}")
100
+ break
101
+
102
+ def _handle_client(self, client_socket: socket.socket, client_address):
103
+ """Handle individual client connection"""
104
+ connection_id = None
105
+
106
+ try:
107
+ # Wait for initial connection message with app_id
108
+ data = self._receive_message(client_socket)
109
+ if not data:
110
+ return
111
+
112
+ # Parse connection message
113
+ try:
114
+ message = Message.from_json(data)
115
+ if message.message_type != MessageType.REQUEST or not message.payload.get('action') == 'connect':
116
+ print(f"Invalid connection message from {client_address}")
117
+ return
118
+
119
+ connection_id = message.payload.get('connection_id')
120
+ app_id = message.app_id
121
+
122
+ if not connection_id or not app_id:
123
+ print(f"Missing connection_id or app_id from {client_address}")
124
+ return
125
+
126
+ except Exception as e:
127
+ print(f"Failed to parse connection message: {e}")
128
+ return
129
+
130
+ # Register the connection
131
+ with self._clients_lock:
132
+ self.client_sockets[connection_id] = client_socket
133
+ self.socket_to_connection_id[client_socket] = connection_id
134
+ self.connection_manager.add_connection(connection_id, client_socket, app_id)
135
+
136
+ # Create or get application
137
+ app = self.create_application(app_id)
138
+ print(f"Client {connection_id} connected to application {app_id}")
139
+
140
+ # Send connection acknowledgment
141
+ ack_message = Message(
142
+ message_id="",
143
+ app_id=app_id,
144
+ channel_id="system",
145
+ message_type=MessageType.RESPONSE,
146
+ payload={'status': 'connected', 'connection_id': connection_id},
147
+ timestamp=0
148
+ )
149
+ self.send_to_connection(client_socket, ack_message)
150
+
151
+ # Handle messages from this client
152
+ while self.running:
153
+ try:
154
+ data = self._receive_message(client_socket)
155
+ if not data:
156
+ break
157
+
158
+ message = Message.from_json(data)
159
+
160
+ # Auto-create channel if it doesn't exist
161
+ if not app.get_channel(message.channel_id):
162
+ app.create_channel(message.channel_id)
163
+
164
+ # Route the message
165
+ if not self.route_message(message):
166
+ print(f"Failed to route message: {message.to_dict()}")
167
+
168
+ except Exception as e:
169
+ print(f"Error handling message from {connection_id}: {e}")
170
+ break
171
+
172
+ except Exception as e:
173
+ print(f"Client handler error for {client_address}: {e}")
174
+
175
+ finally:
176
+ # Clean up connection
177
+ if connection_id:
178
+ with self._clients_lock:
179
+ self.client_sockets.pop(connection_id, None)
180
+ self.socket_to_connection_id.pop(client_socket, None)
181
+ self.connection_manager.remove_connection(connection_id)
182
+ print(f"Client {connection_id} disconnected")
183
+
184
+ try:
185
+ client_socket.close()
186
+ except:
187
+ pass
188
+
189
+ def _receive_message(self, client_socket: socket.socket) -> Optional[str]:
190
+ """Receive a message from a client socket"""
191
+ try:
192
+ # First, receive the message length (4 bytes)
193
+ length_data = b""
194
+ while len(length_data) < 4:
195
+ chunk = client_socket.recv(4 - len(length_data))
196
+ if not chunk:
197
+ return None
198
+ length_data += chunk
199
+
200
+ message_length = int.from_bytes(length_data, byteorder='big')
201
+
202
+ # Then receive the message data
203
+ message_data = b""
204
+ while len(message_data) < message_length:
205
+ chunk = client_socket.recv(message_length - len(message_data))
206
+ if not chunk:
207
+ return None
208
+ message_data += chunk
209
+
210
+ return message_data.decode('utf-8')
211
+
212
+ except Exception as e:
213
+ print(f"Error receiving message: {e}")
214
+ return None
215
+
216
+ def send_to_connection(self, connection, message: Message):
217
+ """Send message to a specific connection"""
218
+ try:
219
+ if isinstance(connection, socket.socket):
220
+ self._send_message(connection, message)
221
+ else:
222
+ print(f"Invalid connection type: {type(connection)}")
223
+ except Exception as e:
224
+ print(f"Error sending message: {e}")
225
+
226
+ def _send_message(self, client_socket: socket.socket, message: Message):
227
+ """Send a message to a client socket"""
228
+ try:
229
+ message_data = message.to_json().encode('utf-8')
230
+ message_length = len(message_data)
231
+
232
+ # Send message length first (4 bytes)
233
+ length_bytes = message_length.to_bytes(4, byteorder='big')
234
+ client_socket.sendall(length_bytes)
235
+
236
+ # Send message data
237
+ client_socket.sendall(message_data)
238
+
239
+ except Exception as e:
240
+ print(f"Error sending message: {e}")
241
+ # Remove broken connection
242
+ connection_id = self.socket_to_connection_id.get(client_socket)
243
+ if connection_id:
244
+ with self._clients_lock:
245
+ self.client_sockets.pop(connection_id, None)
246
+ self.socket_to_connection_id.pop(client_socket, None)
247
+ self.connection_manager.remove_connection(connection_id)
248
+
249
+ def broadcast_to_application(self, app_id: str, message: Message):
250
+ """Broadcast a message to all connections in an application"""
251
+ connection_ids = self.connection_manager.get_connections_for_app(app_id)
252
+ for connection_id in connection_ids:
253
+ with self._clients_lock:
254
+ client_socket = self.client_sockets.get(connection_id)
255
+ if client_socket:
256
+ self.send_to_connection(client_socket, message)
257
+
258
+ def list_applications(self) -> Dict[str, Dict]:
259
+ """Get information about all applications"""
260
+ with self._lock:
261
+ result = {}
262
+ for app_id, app in self.applications.items():
263
+ result[app_id] = {
264
+ 'name': app.name,
265
+ 'channels': app.list_channels(),
266
+ 'connections': len(self.connection_manager.get_connections_for_app(app_id)),
267
+ 'created_at': app.created_at,
268
+ 'last_activity': app.last_activity
269
+ }
270
+ return result
271
+
272
+ def list_channels(self, app_id: str) -> Dict[str, Dict]:
273
+ """Get information about channels in an application"""
274
+ app = self.get_application(app_id)
275
+ if not app:
276
+ return {}
277
+
278
+ result = {}
279
+ for channel_id in app.list_channels():
280
+ channel = app.get_channel(channel_id)
281
+ if channel:
282
+ result[channel_id] = {
283
+ 'subscribers': len(channel.get_subscribers()),
284
+ 'created_at': channel.created_at
285
+ }
286
+ return result
287
+
288
+ def get_stats(self) -> Dict:
289
+ """Get server statistics"""
290
+ with self._lock:
291
+ return {
292
+ 'running': self.running,
293
+ 'host': self.host,
294
+ 'port': self.port,
295
+ 'total_applications': len(self.applications),
296
+ 'total_connections': len(self.connection_manager.list_connections()),
297
+ 'max_connections': self.max_connections
298
+ }