ipc-framework 1.0.0__py3-none-any.whl → 1.1.2__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 CHANGED
@@ -10,7 +10,7 @@ from .server import FrameworkServer
10
10
  from .client import FrameworkClient
11
11
  from .exceptions import IPCError, ConnectionError, RoutingError
12
12
 
13
- __version__ = "1.0.0"
13
+ __version__ = "1.1.2"
14
14
  __author__ = "IPC Framework"
15
15
 
16
16
  __all__ = [
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
- # 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())
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
  }