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.
- ipc_framework/__init__.py +28 -0
- ipc_framework/client.py +274 -0
- ipc_framework/core.py +353 -0
- ipc_framework/demo.py +268 -0
- ipc_framework/examples/__init__.py +13 -0
- ipc_framework/examples/basic_server.py +209 -0
- ipc_framework/examples/chat_client.py +168 -0
- ipc_framework/examples/file_client.py +211 -0
- ipc_framework/examples/monitoring_client.py +252 -0
- ipc_framework/exceptions.py +43 -0
- ipc_framework/py.typed +2 -0
- ipc_framework/server.py +298 -0
- ipc_framework-1.0.0.dist-info/METADATA +512 -0
- ipc_framework-1.0.0.dist-info/RECORD +18 -0
- ipc_framework-1.0.0.dist-info/WHEEL +5 -0
- ipc_framework-1.0.0.dist-info/entry_points.txt +6 -0
- ipc_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
- ipc_framework-1.0.0.dist-info/top_level.txt +1 -0
ipc_framework/server.py
ADDED
@@ -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
|
+
}
|