ipc-framework 1.0.0__py3-none-any.whl → 1.1.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/client.py +278 -273
- ipc_framework/core.py +364 -352
- ipc_framework-1.1.0.dist-info/METADATA +273 -0
- {ipc_framework-1.0.0.dist-info → ipc_framework-1.1.0.dist-info}/RECORD +8 -8
- ipc_framework-1.0.0.dist-info/METADATA +0 -512
- {ipc_framework-1.0.0.dist-info → ipc_framework-1.1.0.dist-info}/WHEEL +0 -0
- {ipc_framework-1.0.0.dist-info → ipc_framework-1.1.0.dist-info}/entry_points.txt +0 -0
- {ipc_framework-1.0.0.dist-info → ipc_framework-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {ipc_framework-1.0.0.dist-info → ipc_framework-1.1.0.dist-info}/top_level.txt +0 -0
ipc_framework/client.py
CHANGED
@@ -1,274 +1,279 @@
|
|
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
|
-
#
|
61
|
-
|
62
|
-
self.
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
print(
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
self.
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
# Send message
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
return
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
def
|
227
|
-
"""
|
228
|
-
with self._lock:
|
229
|
-
self.message_handlers
|
230
|
-
|
231
|
-
def
|
232
|
-
"""
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
#
|
252
|
-
if
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
'
|
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
|
+
# CRITICAL FIX: Remove timeout after successful connection
|
61
|
+
# This prevents recv() calls from timing out during normal operation
|
62
|
+
self.socket.settimeout(None)
|
63
|
+
|
64
|
+
# Start receive thread
|
65
|
+
self.receive_thread = threading.Thread(target=self._receive_loop, daemon=True)
|
66
|
+
self.receive_thread.start()
|
67
|
+
|
68
|
+
print(f"Connected to IPC server as {self.app_id}:{self.connection_id}")
|
69
|
+
return True
|
70
|
+
|
71
|
+
print("Failed to receive connection acknowledgment")
|
72
|
+
return False
|
73
|
+
|
74
|
+
except Exception as e:
|
75
|
+
print(f"Connection failed: {e}")
|
76
|
+
if self.socket:
|
77
|
+
try:
|
78
|
+
self.socket.close()
|
79
|
+
except:
|
80
|
+
pass
|
81
|
+
self.socket = None
|
82
|
+
return False
|
83
|
+
|
84
|
+
def disconnect(self):
|
85
|
+
"""Disconnect from the IPC server"""
|
86
|
+
if not self.connected:
|
87
|
+
return
|
88
|
+
|
89
|
+
self.connected = False
|
90
|
+
|
91
|
+
# Close socket
|
92
|
+
if self.socket:
|
93
|
+
try:
|
94
|
+
self.socket.close()
|
95
|
+
except:
|
96
|
+
pass
|
97
|
+
self.socket = None
|
98
|
+
|
99
|
+
# Wait for receive thread to finish
|
100
|
+
if self.receive_thread and self.receive_thread.is_alive():
|
101
|
+
self.receive_thread.join(timeout=1.0)
|
102
|
+
|
103
|
+
print(f"Disconnected from IPC server")
|
104
|
+
|
105
|
+
def _send_message(self, message: Message) -> str:
|
106
|
+
"""Internal method to send message"""
|
107
|
+
if not self.connected and message.message_type != MessageType.REQUEST:
|
108
|
+
raise ConnectionError("Not connected to server")
|
109
|
+
|
110
|
+
try:
|
111
|
+
with self._socket_lock:
|
112
|
+
if not self.socket:
|
113
|
+
raise ConnectionError("Socket is not available")
|
114
|
+
|
115
|
+
message_data = message.to_json().encode('utf-8')
|
116
|
+
message_length = len(message_data)
|
117
|
+
|
118
|
+
# Send message length first (4 bytes)
|
119
|
+
length_bytes = message_length.to_bytes(4, byteorder='big')
|
120
|
+
self.socket.sendall(length_bytes)
|
121
|
+
|
122
|
+
# Send message data
|
123
|
+
self.socket.sendall(message_data)
|
124
|
+
|
125
|
+
return message.message_id
|
126
|
+
|
127
|
+
except Exception as e:
|
128
|
+
raise ConnectionError(f"Failed to send message: {e}")
|
129
|
+
|
130
|
+
def _receive_message(self) -> Optional[str]:
|
131
|
+
"""Receive a message from the server"""
|
132
|
+
try:
|
133
|
+
# CRITICAL FIX: Remove socket lock from receive to prevent deadlock
|
134
|
+
# Only the receive thread calls this method, so no synchronization needed
|
135
|
+
if not self.socket:
|
136
|
+
return None
|
137
|
+
|
138
|
+
# First, receive the message length (4 bytes)
|
139
|
+
length_data = b""
|
140
|
+
while len(length_data) < 4:
|
141
|
+
chunk = self.socket.recv(4 - len(length_data))
|
142
|
+
if not chunk:
|
143
|
+
return None
|
144
|
+
length_data += chunk
|
145
|
+
|
146
|
+
message_length = int.from_bytes(length_data, byteorder='big')
|
147
|
+
|
148
|
+
# Then receive the message data
|
149
|
+
message_data = b""
|
150
|
+
while len(message_data) < message_length:
|
151
|
+
chunk = self.socket.recv(message_length - len(message_data))
|
152
|
+
if not chunk:
|
153
|
+
return None
|
154
|
+
message_data += chunk
|
155
|
+
|
156
|
+
return message_data.decode('utf-8')
|
157
|
+
|
158
|
+
except Exception as e:
|
159
|
+
if self.connected:
|
160
|
+
print(f"Error receiving message: {e}")
|
161
|
+
return None
|
162
|
+
|
163
|
+
def _receive_loop(self):
|
164
|
+
"""Background thread for receiving messages"""
|
165
|
+
while self.connected:
|
166
|
+
try:
|
167
|
+
data = self._receive_message()
|
168
|
+
if not data:
|
169
|
+
break
|
170
|
+
|
171
|
+
message = Message.from_json(data)
|
172
|
+
|
173
|
+
# Check if this is a response to a pending request
|
174
|
+
if message.reply_to:
|
175
|
+
with self._response_lock:
|
176
|
+
if message.reply_to in self.response_handlers:
|
177
|
+
handler = self.response_handlers.pop(message.reply_to)
|
178
|
+
try:
|
179
|
+
handler(message)
|
180
|
+
except Exception as e:
|
181
|
+
print(f"Response handler error: {e}")
|
182
|
+
else:
|
183
|
+
# Store response for sync requests using the original message ID as key
|
184
|
+
self.pending_responses[message.reply_to] = message
|
185
|
+
else:
|
186
|
+
# Handle regular messages
|
187
|
+
self.handle_message(message)
|
188
|
+
|
189
|
+
except Exception as e:
|
190
|
+
if self.connected:
|
191
|
+
print(f"Receive loop error: {e}")
|
192
|
+
break
|
193
|
+
|
194
|
+
# Connection lost
|
195
|
+
if self.connected:
|
196
|
+
print("Connection to server lost")
|
197
|
+
self.connected = False
|
198
|
+
|
199
|
+
def send_request(self, channel_id: str, data: Any, timeout: float = 5.0) -> Optional[Message]:
|
200
|
+
"""Send a request and wait for response"""
|
201
|
+
message_id = self.request(channel_id, data)
|
202
|
+
|
203
|
+
# Wait for response
|
204
|
+
start_time = time.time()
|
205
|
+
while time.time() - start_time < timeout:
|
206
|
+
with self._response_lock:
|
207
|
+
if message_id in self.pending_responses:
|
208
|
+
return self.pending_responses.pop(message_id)
|
209
|
+
time.sleep(0.01)
|
210
|
+
|
211
|
+
return None # Timeout
|
212
|
+
|
213
|
+
def send_request_async(self, channel_id: str, data: Any, callback: Callable[[Message], None]) -> str:
|
214
|
+
"""Send a request with async callback"""
|
215
|
+
message_id = self.request(channel_id, data)
|
216
|
+
|
217
|
+
with self._response_lock:
|
218
|
+
self.response_handlers[message_id] = callback
|
219
|
+
|
220
|
+
return message_id
|
221
|
+
|
222
|
+
def notify(self, channel_id: str, data: Any) -> str:
|
223
|
+
"""Send a notification (no response expected)"""
|
224
|
+
return self.send_message(channel_id, MessageType.NOTIFICATION, data)
|
225
|
+
|
226
|
+
def create_channel_handler(self, channel_id: str, handler: Callable[[Message], None]):
|
227
|
+
"""Create a handler for a specific channel"""
|
228
|
+
with self._lock:
|
229
|
+
self.message_handlers[channel_id] = handler
|
230
|
+
|
231
|
+
def remove_channel_handler(self, channel_id: str):
|
232
|
+
"""Remove handler for a channel"""
|
233
|
+
with self._lock:
|
234
|
+
self.message_handlers.pop(channel_id, None)
|
235
|
+
|
236
|
+
def wait_for_message(self, channel_id: str, timeout: float = 5.0) -> Optional[Message]:
|
237
|
+
"""Wait for a message on a specific channel"""
|
238
|
+
received_message = None
|
239
|
+
event = threading.Event()
|
240
|
+
|
241
|
+
def temp_handler(message: Message):
|
242
|
+
nonlocal received_message
|
243
|
+
received_message = message
|
244
|
+
event.set()
|
245
|
+
|
246
|
+
# Set temporary handler
|
247
|
+
original_handler = self.message_handlers.get(channel_id)
|
248
|
+
self.create_channel_handler(channel_id, temp_handler)
|
249
|
+
|
250
|
+
try:
|
251
|
+
# Wait for message
|
252
|
+
if event.wait(timeout):
|
253
|
+
return received_message
|
254
|
+
return None
|
255
|
+
finally:
|
256
|
+
# Restore original handler
|
257
|
+
if original_handler:
|
258
|
+
self.create_channel_handler(channel_id, original_handler)
|
259
|
+
else:
|
260
|
+
self.remove_channel_handler(channel_id)
|
261
|
+
|
262
|
+
def ping(self, timeout: float = 2.0) -> bool:
|
263
|
+
"""Ping the server to check connection"""
|
264
|
+
try:
|
265
|
+
response = self.send_request("system", {"action": "ping"}, timeout)
|
266
|
+
return response is not None and response.payload.get("status") == "pong"
|
267
|
+
except:
|
268
|
+
return False
|
269
|
+
|
270
|
+
def get_connection_info(self) -> Dict[str, Any]:
|
271
|
+
"""Get information about this connection"""
|
272
|
+
return {
|
273
|
+
'app_id': self.app_id,
|
274
|
+
'connection_id': self.connection_id,
|
275
|
+
'connected': self.connected,
|
276
|
+
'host': self.host,
|
277
|
+
'port': self.port,
|
278
|
+
'subscribed_channels': list(self.message_handlers.keys())
|
274
279
|
}
|