maqet 0.0.1.3__py3-none-any.whl → 0.0.5__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.
- maqet/__init__.py +50 -6
- maqet/__main__.py +96 -0
- maqet/__version__.py +3 -0
- maqet/api/__init__.py +35 -0
- maqet/api/decorators.py +184 -0
- maqet/api/metadata.py +147 -0
- maqet/api/registry.py +182 -0
- maqet/cli.py +71 -0
- maqet/config/__init__.py +26 -0
- maqet/config/merger.py +237 -0
- maqet/config/parser.py +198 -0
- maqet/config/validators.py +519 -0
- maqet/config_handlers.py +684 -0
- maqet/constants.py +200 -0
- maqet/exceptions.py +226 -0
- maqet/formatters.py +294 -0
- maqet/generators/__init__.py +12 -0
- maqet/generators/base_generator.py +101 -0
- maqet/generators/cli_generator.py +635 -0
- maqet/generators/python_generator.py +247 -0
- maqet/generators/rest_generator.py +58 -0
- maqet/handlers/__init__.py +12 -0
- maqet/handlers/base.py +108 -0
- maqet/handlers/init.py +147 -0
- maqet/handlers/stage.py +196 -0
- maqet/ipc/__init__.py +29 -0
- maqet/ipc/retry.py +265 -0
- maqet/ipc/runner_client.py +285 -0
- maqet/ipc/unix_socket_server.py +239 -0
- maqet/logger.py +160 -55
- maqet/machine.py +884 -0
- maqet/managers/__init__.py +7 -0
- maqet/managers/qmp_manager.py +333 -0
- maqet/managers/snapshot_coordinator.py +327 -0
- maqet/managers/vm_manager.py +683 -0
- maqet/maqet.py +1120 -0
- maqet/os_interactions.py +46 -0
- maqet/process_spawner.py +395 -0
- maqet/qemu_args.py +76 -0
- maqet/qmp/__init__.py +10 -0
- maqet/qmp/commands.py +92 -0
- maqet/qmp/keyboard.py +311 -0
- maqet/qmp/qmp.py +17 -0
- maqet/snapshot.py +473 -0
- maqet/state.py +958 -0
- maqet/storage.py +702 -162
- maqet/validation/__init__.py +9 -0
- maqet/validation/config_validator.py +170 -0
- maqet/vm_runner.py +523 -0
- maqet-0.0.5.dist-info/METADATA +237 -0
- maqet-0.0.5.dist-info/RECORD +55 -0
- {maqet-0.0.1.3.dist-info → maqet-0.0.5.dist-info}/WHEEL +1 -1
- maqet-0.0.5.dist-info/entry_points.txt +2 -0
- maqet-0.0.5.dist-info/licenses/LICENSE +21 -0
- {maqet-0.0.1.3.dist-info → maqet-0.0.5.dist-info}/top_level.txt +0 -1
- maqet/core.py +0 -395
- maqet/functions.py +0 -104
- maqet-0.0.1.3.dist-info/METADATA +0 -104
- maqet-0.0.1.3.dist-info/RECORD +0 -33
- qemu/machine/__init__.py +0 -36
- qemu/machine/console_socket.py +0 -142
- qemu/machine/machine.py +0 -954
- qemu/machine/py.typed +0 -0
- qemu/machine/qtest.py +0 -191
- qemu/qmp/__init__.py +0 -59
- qemu/qmp/error.py +0 -50
- qemu/qmp/events.py +0 -717
- qemu/qmp/legacy.py +0 -319
- qemu/qmp/message.py +0 -209
- qemu/qmp/models.py +0 -146
- qemu/qmp/protocol.py +0 -1057
- qemu/qmp/py.typed +0 -0
- qemu/qmp/qmp_client.py +0 -655
- qemu/qmp/qmp_shell.py +0 -618
- qemu/qmp/qmp_tui.py +0 -655
- qemu/qmp/util.py +0 -219
- qemu/utils/__init__.py +0 -162
- qemu/utils/accel.py +0 -84
- qemu/utils/py.typed +0 -0
- qemu/utils/qemu_ga_client.py +0 -323
- qemu/utils/qom.py +0 -273
- qemu/utils/qom_common.py +0 -175
- qemu/utils/qom_fuse.py +0 -207
@@ -0,0 +1,285 @@
|
|
1
|
+
"""
|
2
|
+
Runner Client
|
3
|
+
|
4
|
+
Client for communicating with VM runner processes via Unix sockets.
|
5
|
+
Used by CLI to send commands to running VM runner processes.
|
6
|
+
|
7
|
+
Architecture:
|
8
|
+
- Each VM has unique socket path
|
9
|
+
- Client connects, sends request, receives response, disconnects
|
10
|
+
- Synchronous and async interfaces
|
11
|
+
- Error handling for connection issues
|
12
|
+
"""
|
13
|
+
|
14
|
+
import asyncio
|
15
|
+
import json
|
16
|
+
import os
|
17
|
+
import socket
|
18
|
+
from pathlib import Path
|
19
|
+
from typing import Any, Dict, Optional
|
20
|
+
|
21
|
+
from ..constants import Intervals, Retries, Timeouts
|
22
|
+
from ..logger import LOG
|
23
|
+
from .retry import async_retry_with_backoff, CircuitBreaker
|
24
|
+
|
25
|
+
# Optional dependency - imported inline with fallback
|
26
|
+
try:
|
27
|
+
import psutil
|
28
|
+
|
29
|
+
PSUTIL_AVAILABLE = True
|
30
|
+
except ImportError:
|
31
|
+
PSUTIL_AVAILABLE = False
|
32
|
+
|
33
|
+
|
34
|
+
class RunnerClientError(Exception):
|
35
|
+
"""Runner client communication errors."""
|
36
|
+
|
37
|
+
|
38
|
+
class RunnerClient:
|
39
|
+
"""
|
40
|
+
Client for communicating with a VM runner process.
|
41
|
+
|
42
|
+
Used by CLI to send commands to running VM runners via Unix sockets.
|
43
|
+
Provides both synchronous and asynchronous interfaces.
|
44
|
+
|
45
|
+
Example:
|
46
|
+
client = RunnerClient("vm1", state_manager)
|
47
|
+
result = client.send_command("qmp", "query-status")
|
48
|
+
"""
|
49
|
+
|
50
|
+
def __init__(self, vm_id: str, state_manager):
|
51
|
+
"""
|
52
|
+
Initialize runner client.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
vm_id: VM identifier
|
56
|
+
state_manager: StateManager instance for DB access
|
57
|
+
"""
|
58
|
+
self.vm_id = vm_id
|
59
|
+
self.state_manager = state_manager
|
60
|
+
self.socket_path = self._get_socket_path()
|
61
|
+
self._circuit_breaker = CircuitBreaker(
|
62
|
+
failure_threshold=5, timeout=60.0
|
63
|
+
)
|
64
|
+
|
65
|
+
LOG.debug(f"RunnerClient initialized for {vm_id}")
|
66
|
+
|
67
|
+
def is_runner_running(self) -> bool:
|
68
|
+
"""
|
69
|
+
Check if VM runner process is running.
|
70
|
+
|
71
|
+
Uses psutil if available for accurate check, otherwise
|
72
|
+
falls back to checking if socket exists.
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
True if runner is running, False otherwise
|
76
|
+
"""
|
77
|
+
# Get VM from database
|
78
|
+
vm = self.state_manager.get_vm(self.vm_id)
|
79
|
+
if not vm or not vm.runner_pid:
|
80
|
+
return False
|
81
|
+
|
82
|
+
# Check if process exists (if psutil available)
|
83
|
+
if PSUTIL_AVAILABLE:
|
84
|
+
return psutil.pid_exists(vm.runner_pid)
|
85
|
+
else:
|
86
|
+
# Fallback: check if socket exists
|
87
|
+
return self.socket_path.exists()
|
88
|
+
|
89
|
+
@async_retry_with_backoff(
|
90
|
+
max_attempts=Retries.IPC_MAX_RETRIES,
|
91
|
+
backoff_base=Intervals.IPC_BACKOFF_BASE,
|
92
|
+
exceptions=(ConnectionRefusedError, FileNotFoundError, OSError),
|
93
|
+
)
|
94
|
+
async def _connect_to_socket(self):
|
95
|
+
"""
|
96
|
+
Connect to Unix socket with retry logic.
|
97
|
+
|
98
|
+
Internal method that handles connection with automatic retry
|
99
|
+
on transient failures.
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Tuple of (reader, writer) for socket communication
|
103
|
+
|
104
|
+
Raises:
|
105
|
+
ConnectionRefusedError: If connection refused
|
106
|
+
FileNotFoundError: If socket doesn't exist
|
107
|
+
OSError: On other connection errors
|
108
|
+
"""
|
109
|
+
return await asyncio.wait_for(
|
110
|
+
asyncio.open_unix_connection(str(self.socket_path)),
|
111
|
+
timeout=Timeouts.IPC_CONNECT,
|
112
|
+
)
|
113
|
+
|
114
|
+
async def send_command_async(
|
115
|
+
self, method: str, *args, **kwargs
|
116
|
+
) -> Dict[str, Any]:
|
117
|
+
"""
|
118
|
+
Send command to VM runner asynchronously.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
method: Method name (e.g., "qmp", "stop", "status", "ping")
|
122
|
+
*args: Method arguments
|
123
|
+
**kwargs: Method keyword arguments
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
Result dictionary from runner
|
127
|
+
|
128
|
+
Raises:
|
129
|
+
RunnerClientError: If runner not running or communication fails
|
130
|
+
"""
|
131
|
+
# Check circuit breaker
|
132
|
+
if self._circuit_breaker.is_open():
|
133
|
+
raise RunnerClientError(
|
134
|
+
f"Circuit breaker open for {self.vm_id}. "
|
135
|
+
f"Too many consecutive failures. Try again later."
|
136
|
+
)
|
137
|
+
|
138
|
+
# Check runner running
|
139
|
+
if not self.is_runner_running():
|
140
|
+
raise RunnerClientError(
|
141
|
+
f"VM runner for {self.vm_id} is not running. "
|
142
|
+
f"Start VM first with: maqet start {self.vm_id}"
|
143
|
+
)
|
144
|
+
|
145
|
+
# Check socket exists
|
146
|
+
if not self.socket_path.exists():
|
147
|
+
raise RunnerClientError(
|
148
|
+
f"Socket not found: {self.socket_path}. "
|
149
|
+
f"VM runner may have crashed."
|
150
|
+
)
|
151
|
+
|
152
|
+
# Build request
|
153
|
+
request = {"method": method, "args": list(args), "kwargs": kwargs}
|
154
|
+
|
155
|
+
LOG.debug(f"Sending IPC request: method={method}")
|
156
|
+
|
157
|
+
try:
|
158
|
+
# Connect to Unix socket with retry logic
|
159
|
+
reader, writer = await self._connect_to_socket()
|
160
|
+
|
161
|
+
try:
|
162
|
+
# Send request
|
163
|
+
request_data = json.dumps(request).encode("utf-8")
|
164
|
+
writer.write(request_data)
|
165
|
+
await writer.drain()
|
166
|
+
|
167
|
+
# Receive response (up to 1MB)
|
168
|
+
response_data = await asyncio.wait_for(
|
169
|
+
reader.read(1024 * 1024), timeout=Timeouts.IPC_COMMAND
|
170
|
+
)
|
171
|
+
if not response_data:
|
172
|
+
raise RunnerClientError("Empty response from runner")
|
173
|
+
|
174
|
+
# Parse response
|
175
|
+
try:
|
176
|
+
response = json.loads(response_data.decode("utf-8"))
|
177
|
+
except json.JSONDecodeError as e:
|
178
|
+
raise RunnerClientError(f"Invalid JSON response: {e}")
|
179
|
+
|
180
|
+
# Check response status
|
181
|
+
if response.get("status") == "error":
|
182
|
+
raise RunnerClientError(response.get("error", "Unknown error"))
|
183
|
+
|
184
|
+
LOG.debug(f"IPC response received: status={response.get('status')}")
|
185
|
+
|
186
|
+
# Record success for circuit breaker
|
187
|
+
self._circuit_breaker.record_success()
|
188
|
+
|
189
|
+
return response.get("result", {})
|
190
|
+
|
191
|
+
finally:
|
192
|
+
# Always close connection
|
193
|
+
try:
|
194
|
+
writer.close()
|
195
|
+
await writer.wait_closed()
|
196
|
+
except Exception as e:
|
197
|
+
LOG.debug(f"Error closing connection: {e}")
|
198
|
+
|
199
|
+
except ConnectionRefusedError as e:
|
200
|
+
self._circuit_breaker.record_failure()
|
201
|
+
raise RunnerClientError(
|
202
|
+
f"Connection refused. VM runner for {self.vm_id} "
|
203
|
+
f"may have crashed or stopped. Error: {e}"
|
204
|
+
)
|
205
|
+
except FileNotFoundError as e:
|
206
|
+
self._circuit_breaker.record_failure()
|
207
|
+
raise RunnerClientError(
|
208
|
+
f"Socket not found: {self.socket_path}. "
|
209
|
+
f"VM runner may have exited. Error: {e}"
|
210
|
+
)
|
211
|
+
except asyncio.TimeoutError as e:
|
212
|
+
self._circuit_breaker.record_failure()
|
213
|
+
raise RunnerClientError(
|
214
|
+
f"IPC operation timed out for {self.vm_id}. "
|
215
|
+
f"Runner may be unresponsive. Error: {e}"
|
216
|
+
)
|
217
|
+
except OSError as e:
|
218
|
+
self._circuit_breaker.record_failure()
|
219
|
+
raise RunnerClientError(f"Communication error: {e}")
|
220
|
+
except RunnerClientError:
|
221
|
+
# Already a RunnerClientError, don't wrap again
|
222
|
+
self._circuit_breaker.record_failure()
|
223
|
+
raise
|
224
|
+
except Exception as e:
|
225
|
+
self._circuit_breaker.record_failure()
|
226
|
+
raise RunnerClientError(f"Unexpected error: {e}")
|
227
|
+
|
228
|
+
def send_command(self, method: str, *args, **kwargs) -> Dict[str, Any]:
|
229
|
+
"""
|
230
|
+
Send command to VM runner synchronously.
|
231
|
+
|
232
|
+
Convenience wrapper around send_command_async() for synchronous usage.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
method: Method name (e.g., "qmp", "stop", "status", "ping")
|
236
|
+
*args: Method arguments
|
237
|
+
**kwargs: Method keyword arguments
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
Result dictionary from runner
|
241
|
+
|
242
|
+
Raises:
|
243
|
+
RunnerClientError: If runner not running or communication fails
|
244
|
+
"""
|
245
|
+
return asyncio.run(self.send_command_async(method, *args, **kwargs))
|
246
|
+
|
247
|
+
def ping(self) -> bool:
|
248
|
+
"""
|
249
|
+
Ping VM runner to check if it's responsive.
|
250
|
+
|
251
|
+
Returns:
|
252
|
+
True if runner responds to ping, False otherwise
|
253
|
+
"""
|
254
|
+
try:
|
255
|
+
result = self.send_command("ping")
|
256
|
+
return result == "pong"
|
257
|
+
except RunnerClientError:
|
258
|
+
return False
|
259
|
+
|
260
|
+
def _get_socket_path(self) -> Path:
|
261
|
+
"""
|
262
|
+
Get Unix socket path for this VM.
|
263
|
+
|
264
|
+
Socket location: XDG_RUNTIME_DIR/maqet/sockets/{vm_id}.sock
|
265
|
+
Falls back to /tmp/maqet-{uid}/sockets/ if XDG_RUNTIME_DIR not available.
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
Path to Unix socket
|
269
|
+
"""
|
270
|
+
# Get runtime directory (prefer XDG_RUNTIME_DIR)
|
271
|
+
runtime_dir_base = os.environ.get(
|
272
|
+
"XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"
|
273
|
+
)
|
274
|
+
if not Path(runtime_dir_base).exists():
|
275
|
+
# Fallback to /tmp
|
276
|
+
runtime_dir_base = f"/tmp/maqet-{os.getuid()}"
|
277
|
+
|
278
|
+
runtime_dir = Path(runtime_dir_base) / "maqet"
|
279
|
+
socket_dir = runtime_dir / "sockets"
|
280
|
+
|
281
|
+
return socket_dir / f"{self.vm_id}.sock"
|
282
|
+
|
283
|
+
def __repr__(self) -> str:
|
284
|
+
"""String representation for debugging."""
|
285
|
+
return f"RunnerClient(vm_id={self.vm_id}, socket={self.socket_path})"
|
@@ -0,0 +1,239 @@
|
|
1
|
+
"""
|
2
|
+
Unix Socket IPC Server
|
3
|
+
|
4
|
+
Simple Unix domain socket server for IPC between CLI and VM runner.
|
5
|
+
Uses JSON-RPC style protocol for request/response communication.
|
6
|
+
|
7
|
+
Architecture:
|
8
|
+
- Each VM runner process has its own Unix socket
|
9
|
+
- Non-blocking async I/O using asyncio
|
10
|
+
- Simple request/response pattern
|
11
|
+
- Socket cleanup on server stop
|
12
|
+
"""
|
13
|
+
|
14
|
+
import asyncio
|
15
|
+
import json
|
16
|
+
from pathlib import Path
|
17
|
+
from typing import Any, Callable, Dict, Optional
|
18
|
+
|
19
|
+
from ..constants import Timeouts
|
20
|
+
from ..logger import LOG
|
21
|
+
|
22
|
+
|
23
|
+
class UnixSocketIPCServerError(Exception):
|
24
|
+
"""Unix socket IPC server errors."""
|
25
|
+
|
26
|
+
|
27
|
+
class UnixSocketIPCServer:
|
28
|
+
"""
|
29
|
+
Unix domain socket server for IPC between CLI and VM runner.
|
30
|
+
|
31
|
+
Protocol: JSON-RPC style
|
32
|
+
- Client sends: {"method": "qmp", "args": [...], "kwargs": {...}}
|
33
|
+
- Server responds: {"status": "success", "result": ...} or
|
34
|
+
{"status": "error", "error": "..."}
|
35
|
+
|
36
|
+
Socket lifecycle:
|
37
|
+
1. Server starts, binds to socket path
|
38
|
+
2. Accepts connections from CLI clients
|
39
|
+
3. Reads JSON request, calls handler
|
40
|
+
4. Writes JSON response
|
41
|
+
5. Closes connection
|
42
|
+
"""
|
43
|
+
|
44
|
+
def __init__(
|
45
|
+
self, socket_path: Path, handler: Callable[[Dict[str, Any]], Dict[str, Any]]
|
46
|
+
):
|
47
|
+
"""
|
48
|
+
Initialize Unix socket server.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
socket_path: Path to Unix socket file
|
52
|
+
handler: Async function to handle requests
|
53
|
+
Takes request dict, returns response dict
|
54
|
+
"""
|
55
|
+
self.socket_path = Path(socket_path)
|
56
|
+
self.handler = handler
|
57
|
+
self.server: Optional[asyncio.Server] = None
|
58
|
+
self._running = False
|
59
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
60
|
+
|
61
|
+
LOG.debug(f"UnixSocketIPCServer initialized for {socket_path}")
|
62
|
+
|
63
|
+
async def start(self) -> None:
|
64
|
+
"""
|
65
|
+
Start listening on Unix socket.
|
66
|
+
|
67
|
+
Process:
|
68
|
+
1. Remove existing socket if present (stale socket handling)
|
69
|
+
2. Create Unix socket server
|
70
|
+
3. Start accepting connections
|
71
|
+
4. Keep server running until stop() called
|
72
|
+
|
73
|
+
Raises:
|
74
|
+
UnixSocketIPCServerError: If socket already in use or bind fails
|
75
|
+
"""
|
76
|
+
# Store event loop for cross-thread communication
|
77
|
+
self._loop = asyncio.get_running_loop()
|
78
|
+
|
79
|
+
# Remove existing socket if present
|
80
|
+
if self.socket_path.exists():
|
81
|
+
# Try to connect to check if someone is using it
|
82
|
+
try:
|
83
|
+
reader, writer = await asyncio.wait_for(
|
84
|
+
asyncio.open_unix_connection(str(self.socket_path)),
|
85
|
+
timeout=Timeouts.IPC_HEALTH_CHECK,
|
86
|
+
)
|
87
|
+
writer.close()
|
88
|
+
await writer.wait_closed()
|
89
|
+
# Someone is using it
|
90
|
+
raise UnixSocketIPCServerError(
|
91
|
+
f"Socket already in use: {self.socket_path}"
|
92
|
+
)
|
93
|
+
except (ConnectionRefusedError, FileNotFoundError, asyncio.TimeoutError):
|
94
|
+
# Stale socket, remove it
|
95
|
+
LOG.debug(f"Removing stale socket {self.socket_path}")
|
96
|
+
self.socket_path.unlink()
|
97
|
+
|
98
|
+
# Create Unix socket server
|
99
|
+
try:
|
100
|
+
self.server = await asyncio.start_unix_server(
|
101
|
+
self._handle_client, path=str(self.socket_path)
|
102
|
+
)
|
103
|
+
self._running = True
|
104
|
+
LOG.info(f"IPC server listening on {self.socket_path}")
|
105
|
+
|
106
|
+
# Keep server running
|
107
|
+
async with self.server:
|
108
|
+
await self.server.serve_forever()
|
109
|
+
|
110
|
+
except asyncio.CancelledError:
|
111
|
+
# Server was stopped via stop() - this is normal
|
112
|
+
LOG.debug("IPC server cancelled (normal shutdown)")
|
113
|
+
except Exception as e:
|
114
|
+
raise UnixSocketIPCServerError(f"Failed to start IPC server: {e}")
|
115
|
+
|
116
|
+
async def _handle_client(
|
117
|
+
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
118
|
+
) -> None:
|
119
|
+
"""
|
120
|
+
Handle single client connection.
|
121
|
+
|
122
|
+
Process:
|
123
|
+
1. Read JSON request from client
|
124
|
+
2. Parse and validate request
|
125
|
+
3. Call handler function
|
126
|
+
4. Write JSON response to client
|
127
|
+
5. Close connection
|
128
|
+
|
129
|
+
Args:
|
130
|
+
reader: Async stream reader
|
131
|
+
writer: Async stream writer
|
132
|
+
"""
|
133
|
+
try:
|
134
|
+
# Read request (up to 1MB)
|
135
|
+
data = await reader.read(1024 * 1024)
|
136
|
+
if not data:
|
137
|
+
return
|
138
|
+
|
139
|
+
# Parse JSON request
|
140
|
+
try:
|
141
|
+
request = json.loads(data.decode("utf-8"))
|
142
|
+
except json.JSONDecodeError as e:
|
143
|
+
response = {"status": "error", "error": f"Invalid JSON: {e}"}
|
144
|
+
writer.write(json.dumps(response).encode("utf-8"))
|
145
|
+
await writer.drain()
|
146
|
+
return
|
147
|
+
|
148
|
+
LOG.debug(f"IPC request: {request.get('method', 'unknown')}")
|
149
|
+
|
150
|
+
# Call handler
|
151
|
+
try:
|
152
|
+
response = await self.handler(request)
|
153
|
+
except Exception as e:
|
154
|
+
LOG.error(f"Handler error: {e}")
|
155
|
+
response = {"status": "error", "error": str(e)}
|
156
|
+
|
157
|
+
# Write response
|
158
|
+
response_data = json.dumps(response).encode("utf-8")
|
159
|
+
writer.write(response_data)
|
160
|
+
await writer.drain()
|
161
|
+
|
162
|
+
LOG.debug(f"IPC response: {response.get('status', 'unknown')}")
|
163
|
+
|
164
|
+
except Exception as e:
|
165
|
+
LOG.error(f"Error handling client: {e}")
|
166
|
+
|
167
|
+
finally:
|
168
|
+
# Close connection
|
169
|
+
try:
|
170
|
+
writer.close()
|
171
|
+
await writer.wait_closed()
|
172
|
+
except Exception as e:
|
173
|
+
LOG.debug(f"Error closing connection: {e}")
|
174
|
+
|
175
|
+
async def stop(self) -> None:
|
176
|
+
"""
|
177
|
+
Stop server and cleanup socket (async version).
|
178
|
+
|
179
|
+
Process:
|
180
|
+
1. Close server (stop accepting connections)
|
181
|
+
2. Remove socket file
|
182
|
+
|
183
|
+
Note: This should be called from within the same event loop as start().
|
184
|
+
For cross-thread stopping, use stop_sync() instead.
|
185
|
+
"""
|
186
|
+
LOG.debug("Stopping IPC server")
|
187
|
+
self._running = False
|
188
|
+
|
189
|
+
# Close server
|
190
|
+
if self.server:
|
191
|
+
self.server.close()
|
192
|
+
await self.server.wait_closed()
|
193
|
+
|
194
|
+
# Remove socket file
|
195
|
+
if self.socket_path.exists():
|
196
|
+
try:
|
197
|
+
self.socket_path.unlink()
|
198
|
+
LOG.debug(f"Removed socket {self.socket_path}")
|
199
|
+
except Exception as e:
|
200
|
+
LOG.warning(f"Failed to remove socket: {e}")
|
201
|
+
|
202
|
+
def stop_sync(self) -> None:
|
203
|
+
"""
|
204
|
+
Stop server from another thread (synchronous).
|
205
|
+
|
206
|
+
This method is thread-safe and can be called from the main thread
|
207
|
+
to stop the IPC server running in a background thread.
|
208
|
+
|
209
|
+
Process:
|
210
|
+
1. Mark server as stopped
|
211
|
+
2. Close server socket (cancels serve_forever)
|
212
|
+
3. Remove socket file from filesystem
|
213
|
+
"""
|
214
|
+
LOG.debug("Stopping IPC server (sync)")
|
215
|
+
self._running = False
|
216
|
+
|
217
|
+
# Close server socket from any thread
|
218
|
+
# This will cause serve_forever() to raise CancelledError
|
219
|
+
if self.server:
|
220
|
+
# Call server.close() in a thread-safe way
|
221
|
+
if self._loop and self._loop.is_running():
|
222
|
+
# Schedule close in the IPC server's event loop
|
223
|
+
self._loop.call_soon_threadsafe(self.server.close)
|
224
|
+
else:
|
225
|
+
# Event loop not running, close directly
|
226
|
+
self.server.close()
|
227
|
+
|
228
|
+
# Remove socket file (filesystem operation, thread-safe)
|
229
|
+
if self.socket_path.exists():
|
230
|
+
try:
|
231
|
+
self.socket_path.unlink()
|
232
|
+
LOG.debug(f"Removed socket {self.socket_path}")
|
233
|
+
except Exception as e:
|
234
|
+
LOG.warning(f"Failed to remove socket: {e}")
|
235
|
+
|
236
|
+
@property
|
237
|
+
def is_running(self) -> bool:
|
238
|
+
"""Check if server is running."""
|
239
|
+
return self._running
|