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
@@ -0,0 +1,28 @@
|
|
1
|
+
"""
|
2
|
+
IPC Framework - Efficient Inter-Process Communication Framework
|
3
|
+
|
4
|
+
A framework for handling client-server communication with hierarchical
|
5
|
+
application and channel management.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .core import IPCServer, IPCClient, Application, Channel, Message, MessageType
|
9
|
+
from .server import FrameworkServer
|
10
|
+
from .client import FrameworkClient
|
11
|
+
from .exceptions import IPCError, ConnectionError, RoutingError
|
12
|
+
|
13
|
+
__version__ = "1.0.0"
|
14
|
+
__author__ = "IPC Framework"
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
"IPCServer",
|
18
|
+
"IPCClient",
|
19
|
+
"Application",
|
20
|
+
"Channel",
|
21
|
+
"Message",
|
22
|
+
"MessageType",
|
23
|
+
"FrameworkServer",
|
24
|
+
"FrameworkClient",
|
25
|
+
"IPCError",
|
26
|
+
"ConnectionError",
|
27
|
+
"RoutingError"
|
28
|
+
]
|
ipc_framework/client.py
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
"""
|
2
|
+
Socket-based client implementation for the IPC Framework
|
3
|
+
"""
|
4
|
+
|
5
|
+
import socket
|
6
|
+
import threading
|
7
|
+
import time
|
8
|
+
import json
|
9
|
+
from typing import Optional, Callable, Dict, Any
|
10
|
+
from .core import IPCClient, Message, MessageType
|
11
|
+
from .exceptions import ConnectionError, SerializationError
|
12
|
+
|
13
|
+
|
14
|
+
class FrameworkClient(IPCClient):
|
15
|
+
"""Socket-based IPC Client implementation"""
|
16
|
+
|
17
|
+
def __init__(self, app_id: str, host: str = "localhost", port: int = 8888, connection_timeout: float = 10.0):
|
18
|
+
super().__init__(app_id, host, port)
|
19
|
+
self.connection_timeout = connection_timeout
|
20
|
+
self.socket: Optional[socket.socket] = None
|
21
|
+
self.receive_thread: Optional[threading.Thread] = None
|
22
|
+
self.pending_responses: Dict[str, Any] = {} # message_id -> response data
|
23
|
+
self.response_handlers: Dict[str, Callable] = {} # message_id -> handler
|
24
|
+
self._socket_lock = threading.RLock()
|
25
|
+
self._response_lock = threading.RLock()
|
26
|
+
|
27
|
+
def connect(self) -> bool:
|
28
|
+
"""Connect to the IPC server"""
|
29
|
+
if self.connected:
|
30
|
+
return True
|
31
|
+
|
32
|
+
try:
|
33
|
+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
34
|
+
self.socket.settimeout(self.connection_timeout)
|
35
|
+
self.socket.connect((self.host, self.port))
|
36
|
+
|
37
|
+
# Send connection message
|
38
|
+
connect_message = Message(
|
39
|
+
message_id="",
|
40
|
+
app_id=self.app_id,
|
41
|
+
channel_id="system",
|
42
|
+
message_type=MessageType.REQUEST,
|
43
|
+
payload={
|
44
|
+
'action': 'connect',
|
45
|
+
'connection_id': self.connection_id
|
46
|
+
},
|
47
|
+
timestamp=0
|
48
|
+
)
|
49
|
+
|
50
|
+
self._send_message(connect_message)
|
51
|
+
|
52
|
+
# Wait for acknowledgment
|
53
|
+
ack_data = self._receive_message()
|
54
|
+
if ack_data:
|
55
|
+
ack_message = Message.from_json(ack_data)
|
56
|
+
if (ack_message.message_type == MessageType.RESPONSE and
|
57
|
+
ack_message.payload.get('status') == 'connected'):
|
58
|
+
self.connected = True
|
59
|
+
|
60
|
+
# Start receive thread
|
61
|
+
self.receive_thread = threading.Thread(target=self._receive_loop, daemon=True)
|
62
|
+
self.receive_thread.start()
|
63
|
+
|
64
|
+
print(f"Connected to IPC server as {self.app_id}:{self.connection_id}")
|
65
|
+
return True
|
66
|
+
|
67
|
+
print("Failed to receive connection acknowledgment")
|
68
|
+
return False
|
69
|
+
|
70
|
+
except Exception as e:
|
71
|
+
print(f"Connection failed: {e}")
|
72
|
+
if self.socket:
|
73
|
+
try:
|
74
|
+
self.socket.close()
|
75
|
+
except:
|
76
|
+
pass
|
77
|
+
self.socket = None
|
78
|
+
return False
|
79
|
+
|
80
|
+
def disconnect(self):
|
81
|
+
"""Disconnect from the IPC server"""
|
82
|
+
if not self.connected:
|
83
|
+
return
|
84
|
+
|
85
|
+
self.connected = False
|
86
|
+
|
87
|
+
# Close socket
|
88
|
+
if self.socket:
|
89
|
+
try:
|
90
|
+
self.socket.close()
|
91
|
+
except:
|
92
|
+
pass
|
93
|
+
self.socket = None
|
94
|
+
|
95
|
+
# Wait for receive thread to finish
|
96
|
+
if self.receive_thread and self.receive_thread.is_alive():
|
97
|
+
self.receive_thread.join(timeout=1.0)
|
98
|
+
|
99
|
+
print(f"Disconnected from IPC server")
|
100
|
+
|
101
|
+
def _send_message(self, message: Message) -> str:
|
102
|
+
"""Internal method to send message"""
|
103
|
+
if not self.connected and message.message_type != MessageType.REQUEST:
|
104
|
+
raise ConnectionError("Not connected to server")
|
105
|
+
|
106
|
+
try:
|
107
|
+
with self._socket_lock:
|
108
|
+
if not self.socket:
|
109
|
+
raise ConnectionError("Socket is not available")
|
110
|
+
|
111
|
+
message_data = message.to_json().encode('utf-8')
|
112
|
+
message_length = len(message_data)
|
113
|
+
|
114
|
+
# Send message length first (4 bytes)
|
115
|
+
length_bytes = message_length.to_bytes(4, byteorder='big')
|
116
|
+
self.socket.sendall(length_bytes)
|
117
|
+
|
118
|
+
# Send message data
|
119
|
+
self.socket.sendall(message_data)
|
120
|
+
|
121
|
+
return message.message_id
|
122
|
+
|
123
|
+
except Exception as e:
|
124
|
+
raise ConnectionError(f"Failed to send message: {e}")
|
125
|
+
|
126
|
+
def _receive_message(self) -> Optional[str]:
|
127
|
+
"""Receive a message from the server"""
|
128
|
+
try:
|
129
|
+
with self._socket_lock:
|
130
|
+
if not self.socket:
|
131
|
+
return None
|
132
|
+
|
133
|
+
# First, receive the message length (4 bytes)
|
134
|
+
length_data = b""
|
135
|
+
while len(length_data) < 4:
|
136
|
+
chunk = self.socket.recv(4 - len(length_data))
|
137
|
+
if not chunk:
|
138
|
+
return None
|
139
|
+
length_data += chunk
|
140
|
+
|
141
|
+
message_length = int.from_bytes(length_data, byteorder='big')
|
142
|
+
|
143
|
+
# Then receive the message data
|
144
|
+
message_data = b""
|
145
|
+
while len(message_data) < message_length:
|
146
|
+
chunk = self.socket.recv(message_length - len(message_data))
|
147
|
+
if not chunk:
|
148
|
+
return None
|
149
|
+
message_data += chunk
|
150
|
+
|
151
|
+
return message_data.decode('utf-8')
|
152
|
+
|
153
|
+
except Exception as e:
|
154
|
+
if self.connected:
|
155
|
+
print(f"Error receiving message: {e}")
|
156
|
+
return None
|
157
|
+
|
158
|
+
def _receive_loop(self):
|
159
|
+
"""Background thread for receiving messages"""
|
160
|
+
while self.connected:
|
161
|
+
try:
|
162
|
+
data = self._receive_message()
|
163
|
+
if not data:
|
164
|
+
break
|
165
|
+
|
166
|
+
message = Message.from_json(data)
|
167
|
+
|
168
|
+
# Check if this is a response to a pending request
|
169
|
+
if message.reply_to:
|
170
|
+
with self._response_lock:
|
171
|
+
if message.reply_to in self.response_handlers:
|
172
|
+
handler = self.response_handlers.pop(message.reply_to)
|
173
|
+
try:
|
174
|
+
handler(message)
|
175
|
+
except Exception as e:
|
176
|
+
print(f"Response handler error: {e}")
|
177
|
+
else:
|
178
|
+
# Store response for sync requests
|
179
|
+
self.pending_responses[message.reply_to] = message
|
180
|
+
else:
|
181
|
+
# Handle regular messages
|
182
|
+
self.handle_message(message)
|
183
|
+
|
184
|
+
except Exception as e:
|
185
|
+
if self.connected:
|
186
|
+
print(f"Receive loop error: {e}")
|
187
|
+
break
|
188
|
+
|
189
|
+
# Connection lost
|
190
|
+
if self.connected:
|
191
|
+
print("Connection to server lost")
|
192
|
+
self.connected = False
|
193
|
+
|
194
|
+
def send_request(self, channel_id: str, data: Any, timeout: float = 5.0) -> Optional[Message]:
|
195
|
+
"""Send a request and wait for response"""
|
196
|
+
message_id = self.request(channel_id, data)
|
197
|
+
|
198
|
+
# Wait for response
|
199
|
+
start_time = time.time()
|
200
|
+
while time.time() - start_time < timeout:
|
201
|
+
with self._response_lock:
|
202
|
+
if message_id in self.pending_responses:
|
203
|
+
return self.pending_responses.pop(message_id)
|
204
|
+
time.sleep(0.01)
|
205
|
+
|
206
|
+
return None # Timeout
|
207
|
+
|
208
|
+
def send_request_async(self, channel_id: str, data: Any, callback: Callable[[Message], None]) -> str:
|
209
|
+
"""Send a request with async callback"""
|
210
|
+
message_id = self.request(channel_id, data)
|
211
|
+
|
212
|
+
with self._response_lock:
|
213
|
+
self.response_handlers[message_id] = callback
|
214
|
+
|
215
|
+
return message_id
|
216
|
+
|
217
|
+
def notify(self, channel_id: str, data: Any) -> str:
|
218
|
+
"""Send a notification (no response expected)"""
|
219
|
+
return self.send_message(channel_id, MessageType.NOTIFICATION, data)
|
220
|
+
|
221
|
+
def create_channel_handler(self, channel_id: str, handler: Callable[[Message], None]):
|
222
|
+
"""Create a handler for a specific channel"""
|
223
|
+
with self._lock:
|
224
|
+
self.message_handlers[channel_id] = handler
|
225
|
+
|
226
|
+
def remove_channel_handler(self, channel_id: str):
|
227
|
+
"""Remove handler for a channel"""
|
228
|
+
with self._lock:
|
229
|
+
self.message_handlers.pop(channel_id, None)
|
230
|
+
|
231
|
+
def wait_for_message(self, channel_id: str, timeout: float = 5.0) -> Optional[Message]:
|
232
|
+
"""Wait for a message on a specific channel"""
|
233
|
+
received_message = None
|
234
|
+
event = threading.Event()
|
235
|
+
|
236
|
+
def temp_handler(message: Message):
|
237
|
+
nonlocal received_message
|
238
|
+
received_message = message
|
239
|
+
event.set()
|
240
|
+
|
241
|
+
# Set temporary handler
|
242
|
+
original_handler = self.message_handlers.get(channel_id)
|
243
|
+
self.create_channel_handler(channel_id, temp_handler)
|
244
|
+
|
245
|
+
try:
|
246
|
+
# Wait for message
|
247
|
+
if event.wait(timeout):
|
248
|
+
return received_message
|
249
|
+
return None
|
250
|
+
finally:
|
251
|
+
# Restore original handler
|
252
|
+
if original_handler:
|
253
|
+
self.create_channel_handler(channel_id, original_handler)
|
254
|
+
else:
|
255
|
+
self.remove_channel_handler(channel_id)
|
256
|
+
|
257
|
+
def ping(self, timeout: float = 2.0) -> bool:
|
258
|
+
"""Ping the server to check connection"""
|
259
|
+
try:
|
260
|
+
response = self.send_request("system", {"action": "ping"}, timeout)
|
261
|
+
return response is not None and response.payload.get("status") == "pong"
|
262
|
+
except:
|
263
|
+
return False
|
264
|
+
|
265
|
+
def get_connection_info(self) -> Dict[str, Any]:
|
266
|
+
"""Get information about this connection"""
|
267
|
+
return {
|
268
|
+
'app_id': self.app_id,
|
269
|
+
'connection_id': self.connection_id,
|
270
|
+
'connected': self.connected,
|
271
|
+
'host': self.host,
|
272
|
+
'port': self.port,
|
273
|
+
'subscribed_channels': list(self.message_handlers.keys())
|
274
|
+
}
|
ipc_framework/core.py
ADDED
@@ -0,0 +1,353 @@
|
|
1
|
+
"""
|
2
|
+
Core classes for the IPC Framework
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import uuid
|
7
|
+
import time
|
8
|
+
import threading
|
9
|
+
from typing import Dict, List, Optional, Callable, Any
|
10
|
+
from dataclasses import dataclass, asdict
|
11
|
+
from enum import Enum
|
12
|
+
|
13
|
+
|
14
|
+
class MessageType(Enum):
|
15
|
+
"""Types of messages in the IPC system"""
|
16
|
+
REQUEST = "request"
|
17
|
+
RESPONSE = "response"
|
18
|
+
NOTIFICATION = "notification"
|
19
|
+
SUBSCRIBE = "subscribe"
|
20
|
+
UNSUBSCRIBE = "unsubscribe"
|
21
|
+
PUBLISH = "publish"
|
22
|
+
|
23
|
+
|
24
|
+
@dataclass
|
25
|
+
class Message:
|
26
|
+
"""Represents a message in the IPC system"""
|
27
|
+
message_id: str
|
28
|
+
app_id: str
|
29
|
+
channel_id: str
|
30
|
+
message_type: MessageType
|
31
|
+
payload: Any
|
32
|
+
timestamp: float
|
33
|
+
reply_to: Optional[str] = None
|
34
|
+
|
35
|
+
def __post_init__(self):
|
36
|
+
if not self.message_id:
|
37
|
+
self.message_id = str(uuid.uuid4())
|
38
|
+
if not self.timestamp:
|
39
|
+
self.timestamp = time.time()
|
40
|
+
|
41
|
+
def to_dict(self) -> Dict:
|
42
|
+
"""Convert message to dictionary for serialization"""
|
43
|
+
data = asdict(self)
|
44
|
+
data['message_type'] = self.message_type.value
|
45
|
+
return data
|
46
|
+
|
47
|
+
@classmethod
|
48
|
+
def from_dict(cls, data: Dict) -> 'Message':
|
49
|
+
"""Create message from dictionary"""
|
50
|
+
data['message_type'] = MessageType(data['message_type'])
|
51
|
+
return cls(**data)
|
52
|
+
|
53
|
+
def to_json(self) -> str:
|
54
|
+
"""Serialize message to JSON"""
|
55
|
+
return json.dumps(self.to_dict())
|
56
|
+
|
57
|
+
@classmethod
|
58
|
+
def from_json(cls, json_str: str) -> 'Message':
|
59
|
+
"""Deserialize message from JSON"""
|
60
|
+
return cls.from_dict(json.loads(json_str))
|
61
|
+
|
62
|
+
|
63
|
+
class Channel:
|
64
|
+
"""Represents a communication channel within an application"""
|
65
|
+
|
66
|
+
def __init__(self, channel_id: str, app_id: str):
|
67
|
+
self.channel_id = channel_id
|
68
|
+
self.app_id = app_id
|
69
|
+
self.subscribers: List[str] = [] # Connection IDs
|
70
|
+
self.handlers: Dict[MessageType, Callable] = {}
|
71
|
+
self.created_at = time.time()
|
72
|
+
self._lock = threading.RLock()
|
73
|
+
|
74
|
+
def add_subscriber(self, connection_id: str):
|
75
|
+
"""Add a subscriber to this channel"""
|
76
|
+
with self._lock:
|
77
|
+
if connection_id not in self.subscribers:
|
78
|
+
self.subscribers.append(connection_id)
|
79
|
+
|
80
|
+
def remove_subscriber(self, connection_id: str):
|
81
|
+
"""Remove a subscriber from this channel"""
|
82
|
+
with self._lock:
|
83
|
+
if connection_id in self.subscribers:
|
84
|
+
self.subscribers.remove(connection_id)
|
85
|
+
|
86
|
+
def set_handler(self, message_type: MessageType, handler: Callable):
|
87
|
+
"""Set a message handler for this channel"""
|
88
|
+
with self._lock:
|
89
|
+
self.handlers[message_type] = handler
|
90
|
+
|
91
|
+
def get_handler(self, message_type: MessageType) -> Optional[Callable]:
|
92
|
+
"""Get the handler for a message type"""
|
93
|
+
with self._lock:
|
94
|
+
return self.handlers.get(message_type)
|
95
|
+
|
96
|
+
def get_subscribers(self) -> List[str]:
|
97
|
+
"""Get list of current subscribers"""
|
98
|
+
with self._lock:
|
99
|
+
return self.subscribers.copy()
|
100
|
+
|
101
|
+
|
102
|
+
class Application:
|
103
|
+
"""Represents an application with multiple channels"""
|
104
|
+
|
105
|
+
def __init__(self, app_id: str, name: str = None):
|
106
|
+
self.app_id = app_id
|
107
|
+
self.name = name or app_id
|
108
|
+
self.channels: Dict[str, Channel] = {}
|
109
|
+
self.created_at = time.time()
|
110
|
+
self.last_activity = time.time()
|
111
|
+
self._lock = threading.RLock()
|
112
|
+
|
113
|
+
def create_channel(self, channel_id: str) -> Channel:
|
114
|
+
"""Create a new channel in this application"""
|
115
|
+
with self._lock:
|
116
|
+
if channel_id in self.channels:
|
117
|
+
return self.channels[channel_id]
|
118
|
+
|
119
|
+
channel = Channel(channel_id, self.app_id)
|
120
|
+
self.channels[channel_id] = channel
|
121
|
+
self.last_activity = time.time()
|
122
|
+
return channel
|
123
|
+
|
124
|
+
def get_channel(self, channel_id: str) -> Optional[Channel]:
|
125
|
+
"""Get a channel by ID"""
|
126
|
+
with self._lock:
|
127
|
+
return self.channels.get(channel_id)
|
128
|
+
|
129
|
+
def remove_channel(self, channel_id: str) -> bool:
|
130
|
+
"""Remove a channel"""
|
131
|
+
with self._lock:
|
132
|
+
if channel_id in self.channels:
|
133
|
+
del self.channels[channel_id]
|
134
|
+
self.last_activity = time.time()
|
135
|
+
return True
|
136
|
+
return False
|
137
|
+
|
138
|
+
def list_channels(self) -> List[str]:
|
139
|
+
"""List all channel IDs in this application"""
|
140
|
+
with self._lock:
|
141
|
+
return list(self.channels.keys())
|
142
|
+
|
143
|
+
def update_activity(self):
|
144
|
+
"""Update the last activity timestamp"""
|
145
|
+
self.last_activity = time.time()
|
146
|
+
|
147
|
+
|
148
|
+
class ConnectionManager:
|
149
|
+
"""Manages client connections"""
|
150
|
+
|
151
|
+
def __init__(self):
|
152
|
+
self.connections: Dict[str, Any] = {} # connection_id -> connection object
|
153
|
+
self.connection_apps: Dict[str, str] = {} # connection_id -> app_id
|
154
|
+
self._lock = threading.RLock()
|
155
|
+
|
156
|
+
def add_connection(self, connection_id: str, connection_obj: Any, app_id: str):
|
157
|
+
"""Add a new connection"""
|
158
|
+
with self._lock:
|
159
|
+
self.connections[connection_id] = connection_obj
|
160
|
+
self.connection_apps[connection_id] = app_id
|
161
|
+
|
162
|
+
def remove_connection(self, connection_id: str):
|
163
|
+
"""Remove a connection"""
|
164
|
+
with self._lock:
|
165
|
+
self.connections.pop(connection_id, None)
|
166
|
+
self.connection_apps.pop(connection_id, None)
|
167
|
+
|
168
|
+
def get_connection(self, connection_id: str) -> Optional[Any]:
|
169
|
+
"""Get a connection object"""
|
170
|
+
with self._lock:
|
171
|
+
return self.connections.get(connection_id)
|
172
|
+
|
173
|
+
def get_connections_for_app(self, app_id: str) -> List[str]:
|
174
|
+
"""Get all connection IDs for an application"""
|
175
|
+
with self._lock:
|
176
|
+
return [conn_id for conn_id, app in self.connection_apps.items() if app == app_id]
|
177
|
+
|
178
|
+
def list_connections(self) -> List[str]:
|
179
|
+
"""List all connection IDs"""
|
180
|
+
with self._lock:
|
181
|
+
return list(self.connections.keys())
|
182
|
+
|
183
|
+
|
184
|
+
class IPCServer:
|
185
|
+
"""Base IPC Server class"""
|
186
|
+
|
187
|
+
def __init__(self, host: str = "localhost", port: int = 8888):
|
188
|
+
self.host = host
|
189
|
+
self.port = port
|
190
|
+
self.applications: Dict[str, Application] = {}
|
191
|
+
self.connection_manager = ConnectionManager()
|
192
|
+
self.running = False
|
193
|
+
self._lock = threading.RLock()
|
194
|
+
|
195
|
+
def create_application(self, app_id: str, name: str = None) -> Application:
|
196
|
+
"""Create a new application"""
|
197
|
+
with self._lock:
|
198
|
+
if app_id in self.applications:
|
199
|
+
return self.applications[app_id]
|
200
|
+
|
201
|
+
app = Application(app_id, name)
|
202
|
+
self.applications[app_id] = app
|
203
|
+
return app
|
204
|
+
|
205
|
+
def get_application(self, app_id: str) -> Optional[Application]:
|
206
|
+
"""Get an application by ID"""
|
207
|
+
with self._lock:
|
208
|
+
return self.applications.get(app_id)
|
209
|
+
|
210
|
+
def route_message(self, message: Message) -> bool:
|
211
|
+
"""Route a message to the appropriate handlers"""
|
212
|
+
with self._lock:
|
213
|
+
app = self.get_application(message.app_id)
|
214
|
+
if not app:
|
215
|
+
return False
|
216
|
+
|
217
|
+
channel = app.get_channel(message.channel_id)
|
218
|
+
if not channel:
|
219
|
+
return False
|
220
|
+
|
221
|
+
app.update_activity()
|
222
|
+
|
223
|
+
# Handle different message types
|
224
|
+
if message.message_type == MessageType.PUBLISH:
|
225
|
+
return self._handle_publish(message, channel)
|
226
|
+
elif message.message_type == MessageType.SUBSCRIBE:
|
227
|
+
return self._handle_subscribe(message, channel)
|
228
|
+
elif message.message_type == MessageType.UNSUBSCRIBE:
|
229
|
+
return self._handle_unsubscribe(message, channel)
|
230
|
+
else:
|
231
|
+
handler = channel.get_handler(message.message_type)
|
232
|
+
if handler:
|
233
|
+
try:
|
234
|
+
handler(message)
|
235
|
+
return True
|
236
|
+
except Exception as e:
|
237
|
+
print(f"Handler error: {e}")
|
238
|
+
return False
|
239
|
+
|
240
|
+
return False
|
241
|
+
|
242
|
+
def _handle_publish(self, message: Message, channel: Channel) -> bool:
|
243
|
+
"""Handle publish message by broadcasting to subscribers"""
|
244
|
+
subscribers = channel.get_subscribers()
|
245
|
+
for subscriber_id in subscribers:
|
246
|
+
connection = self.connection_manager.get_connection(subscriber_id)
|
247
|
+
if connection:
|
248
|
+
self.send_to_connection(connection, message)
|
249
|
+
return True
|
250
|
+
|
251
|
+
def _handle_subscribe(self, message: Message, channel: Channel) -> bool:
|
252
|
+
"""Handle subscribe message"""
|
253
|
+
# Extract connection ID from message payload
|
254
|
+
connection_id = message.payload.get('connection_id')
|
255
|
+
if connection_id:
|
256
|
+
channel.add_subscriber(connection_id)
|
257
|
+
return True
|
258
|
+
return False
|
259
|
+
|
260
|
+
def _handle_unsubscribe(self, message: Message, channel: Channel) -> bool:
|
261
|
+
"""Handle unsubscribe message"""
|
262
|
+
connection_id = message.payload.get('connection_id')
|
263
|
+
if connection_id:
|
264
|
+
channel.remove_subscriber(connection_id)
|
265
|
+
return True
|
266
|
+
return False
|
267
|
+
|
268
|
+
def send_to_connection(self, connection, message: Message):
|
269
|
+
"""Send message to a specific connection - to be implemented by subclasses"""
|
270
|
+
raise NotImplementedError("Subclasses must implement send_to_connection")
|
271
|
+
|
272
|
+
def start(self):
|
273
|
+
"""Start the server - to be implemented by subclasses"""
|
274
|
+
raise NotImplementedError("Subclasses must implement start")
|
275
|
+
|
276
|
+
def stop(self):
|
277
|
+
"""Stop the server - to be implemented by subclasses"""
|
278
|
+
raise NotImplementedError("Subclasses must implement stop")
|
279
|
+
|
280
|
+
|
281
|
+
class IPCClient:
|
282
|
+
"""Base IPC Client class"""
|
283
|
+
|
284
|
+
def __init__(self, app_id: str, host: str = "localhost", port: int = 8888):
|
285
|
+
self.app_id = app_id
|
286
|
+
self.host = host
|
287
|
+
self.port = port
|
288
|
+
self.connection_id = str(uuid.uuid4())
|
289
|
+
self.connected = False
|
290
|
+
self.message_handlers: Dict[str, Callable] = {} # channel_id -> handler
|
291
|
+
self._lock = threading.RLock()
|
292
|
+
|
293
|
+
def connect(self) -> bool:
|
294
|
+
"""Connect to the server - to be implemented by subclasses"""
|
295
|
+
raise NotImplementedError("Subclasses must implement connect")
|
296
|
+
|
297
|
+
def disconnect(self):
|
298
|
+
"""Disconnect from the server - to be implemented by subclasses"""
|
299
|
+
raise NotImplementedError("Subclasses must implement disconnect")
|
300
|
+
|
301
|
+
def send_message(self, channel_id: str, message_type: MessageType, payload: Any, reply_to: str = None) -> str:
|
302
|
+
"""Send a message to a specific channel"""
|
303
|
+
message = Message(
|
304
|
+
message_id=str(uuid.uuid4()),
|
305
|
+
app_id=self.app_id,
|
306
|
+
channel_id=channel_id,
|
307
|
+
message_type=message_type,
|
308
|
+
payload=payload,
|
309
|
+
timestamp=time.time(),
|
310
|
+
reply_to=reply_to
|
311
|
+
)
|
312
|
+
return self._send_message(message)
|
313
|
+
|
314
|
+
def _send_message(self, message: Message) -> str:
|
315
|
+
"""Internal method to send message - to be implemented by subclasses"""
|
316
|
+
raise NotImplementedError("Subclasses must implement _send_message")
|
317
|
+
|
318
|
+
def subscribe(self, channel_id: str, handler: Callable = None) -> bool:
|
319
|
+
"""Subscribe to a channel"""
|
320
|
+
if handler:
|
321
|
+
with self._lock:
|
322
|
+
self.message_handlers[channel_id] = handler
|
323
|
+
|
324
|
+
payload = {'connection_id': self.connection_id}
|
325
|
+
self.send_message(channel_id, MessageType.SUBSCRIBE, payload)
|
326
|
+
return True
|
327
|
+
|
328
|
+
def unsubscribe(self, channel_id: str) -> bool:
|
329
|
+
"""Unsubscribe from a channel"""
|
330
|
+
with self._lock:
|
331
|
+
self.message_handlers.pop(channel_id, None)
|
332
|
+
|
333
|
+
payload = {'connection_id': self.connection_id}
|
334
|
+
self.send_message(channel_id, MessageType.UNSUBSCRIBE, payload)
|
335
|
+
return True
|
336
|
+
|
337
|
+
def publish(self, channel_id: str, data: Any) -> str:
|
338
|
+
"""Publish data to a channel"""
|
339
|
+
return self.send_message(channel_id, MessageType.PUBLISH, data)
|
340
|
+
|
341
|
+
def request(self, channel_id: str, data: Any) -> str:
|
342
|
+
"""Send a request message"""
|
343
|
+
return self.send_message(channel_id, MessageType.REQUEST, data)
|
344
|
+
|
345
|
+
def handle_message(self, message: Message):
|
346
|
+
"""Handle incoming message"""
|
347
|
+
with self._lock:
|
348
|
+
handler = self.message_handlers.get(message.channel_id)
|
349
|
+
if handler:
|
350
|
+
try:
|
351
|
+
handler(message)
|
352
|
+
except Exception as e:
|
353
|
+
print(f"Message handler error: {e}")
|