claude-mpm 4.2.7__py3-none-any.whl → 4.2.11__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.
Files changed (57) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/dashboard.py +62 -120
  3. claude_mpm/cli/commands/monitor.py +71 -212
  4. claude_mpm/cli/commands/run.py +33 -33
  5. claude_mpm/cli/parser.py +79 -2
  6. claude_mpm/cli/parsers/__init__.py +29 -0
  7. claude_mpm/dashboard/static/css/code-tree.css +16 -4
  8. claude_mpm/dashboard/static/css/dashboard.css +15 -1
  9. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  10. claude_mpm/dashboard/static/dist/components/file-viewer.js +2 -0
  11. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  12. claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +1 -1
  13. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  14. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  15. claude_mpm/dashboard/static/js/components/code-tree.js +775 -142
  16. claude_mpm/dashboard/static/js/components/file-viewer.js +538 -0
  17. claude_mpm/dashboard/static/js/components/module-viewer.js +26 -0
  18. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +166 -14
  19. claude_mpm/dashboard/static/js/dashboard.js +108 -91
  20. claude_mpm/dashboard/static/js/socket-client.js +9 -7
  21. claude_mpm/dashboard/templates/index.html +5 -2
  22. claude_mpm/hooks/claude_hooks/hook_handler.py +1 -11
  23. claude_mpm/hooks/claude_hooks/services/connection_manager.py +54 -59
  24. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +112 -72
  25. claude_mpm/services/agents/deployment/agent_format_converter.py +3 -3
  26. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -5
  27. claude_mpm/services/cli/unified_dashboard_manager.py +354 -0
  28. claude_mpm/services/monitor/__init__.py +20 -0
  29. claude_mpm/services/monitor/daemon.py +256 -0
  30. claude_mpm/services/monitor/event_emitter.py +279 -0
  31. claude_mpm/services/monitor/handlers/__init__.py +20 -0
  32. claude_mpm/services/monitor/handlers/code_analysis.py +334 -0
  33. claude_mpm/services/monitor/handlers/dashboard.py +298 -0
  34. claude_mpm/services/monitor/handlers/hooks.py +491 -0
  35. claude_mpm/services/monitor/management/__init__.py +18 -0
  36. claude_mpm/services/monitor/management/health.py +124 -0
  37. claude_mpm/services/monitor/management/lifecycle.py +298 -0
  38. claude_mpm/services/monitor/server.py +442 -0
  39. claude_mpm/services/socketio/client_proxy.py +20 -12
  40. claude_mpm/services/socketio/dashboard_server.py +4 -4
  41. claude_mpm/services/socketio/monitor_client.py +4 -6
  42. claude_mpm/tools/code_tree_analyzer.py +33 -17
  43. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/METADATA +1 -1
  44. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/RECORD +48 -43
  45. claude_mpm/cli/commands/socketio_monitor.py +0 -233
  46. claude_mpm/scripts/socketio_daemon.py +0 -571
  47. claude_mpm/scripts/socketio_daemon_hardened.py +0 -937
  48. claude_mpm/scripts/socketio_daemon_wrapper.py +0 -78
  49. claude_mpm/scripts/socketio_server_manager.py +0 -349
  50. claude_mpm/services/cli/dashboard_launcher.py +0 -423
  51. claude_mpm/services/cli/socketio_manager.py +0 -595
  52. claude_mpm/services/dashboard/stable_server.py +0 -962
  53. claude_mpm/services/socketio/monitor_server.py +0 -505
  54. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/WHEEL +0 -0
  55. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/entry_points.txt +0 -0
  56. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/licenses/LICENSE +0 -0
  57. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,354 @@
1
+ """
2
+ Unified Dashboard Manager Service
3
+ =================================
4
+
5
+ WHY: This service provides a centralized way to manage dashboard functionality using
6
+ the UnifiedMonitorDaemon. It replaces the old DashboardLauncher and SocketIOManager
7
+ services with a cleaner implementation that uses the unified daemon architecture.
8
+
9
+ DESIGN DECISIONS:
10
+ - Uses UnifiedMonitorDaemon for all server functionality
11
+ - Provides the same interface as the old services for compatibility
12
+ - Handles browser opening, process management, and status checking
13
+ - Integrates with PortManager for port allocation
14
+ - Thread-safe daemon management
15
+ """
16
+
17
+ import threading
18
+ import time
19
+ import webbrowser
20
+ from abc import ABC, abstractmethod
21
+ from dataclasses import dataclass
22
+ from typing import Optional, Tuple
23
+
24
+ import requests
25
+
26
+ from ...core.logging_config import get_logger
27
+ from ...services.monitor.daemon import UnifiedMonitorDaemon
28
+ from ...services.port_manager import PortManager
29
+
30
+
31
+ @dataclass
32
+ class DashboardInfo:
33
+ """Information about a running dashboard."""
34
+
35
+ url: str
36
+ port: int
37
+ pid: Optional[int] = None
38
+ status: str = "running"
39
+
40
+
41
+ class IUnifiedDashboardManager(ABC):
42
+ """Interface for unified dashboard management."""
43
+
44
+ @abstractmethod
45
+ def start_dashboard(
46
+ self, port: int = 8765, background: bool = False, open_browser: bool = True
47
+ ) -> Tuple[bool, bool]:
48
+ """Start the dashboard using unified daemon."""
49
+
50
+ @abstractmethod
51
+ def stop_dashboard(self, port: int = 8765) -> bool:
52
+ """Stop the dashboard."""
53
+
54
+ @abstractmethod
55
+ def is_dashboard_running(self, port: int = 8765) -> bool:
56
+ """Check if dashboard is running."""
57
+
58
+ @abstractmethod
59
+ def get_dashboard_url(self, port: int = 8765) -> str:
60
+ """Get dashboard URL."""
61
+
62
+ @abstractmethod
63
+ def open_browser(self, url: str) -> bool:
64
+ """Open URL in browser."""
65
+
66
+
67
+ class UnifiedDashboardManager(IUnifiedDashboardManager):
68
+ """Unified dashboard manager using UnifiedMonitorDaemon."""
69
+
70
+ def __init__(self, logger=None):
71
+ """Initialize the unified dashboard manager."""
72
+ self.logger = logger or get_logger("UnifiedDashboardManager")
73
+ self.port_manager = PortManager()
74
+ self._background_daemons = {} # port -> daemon instance
75
+ self._lock = threading.Lock()
76
+
77
+ def start_dashboard(
78
+ self, port: int = 8765, background: bool = False, open_browser: bool = True
79
+ ) -> Tuple[bool, bool]:
80
+ """
81
+ Start the dashboard using unified daemon.
82
+
83
+ Args:
84
+ port: Port to run dashboard on
85
+ background: Whether to run in background mode
86
+ open_browser: Whether to open browser automatically
87
+
88
+ Returns:
89
+ Tuple of (success, browser_opened)
90
+ """
91
+ try:
92
+ # Check if already running
93
+ if self.is_dashboard_running(port):
94
+ self.logger.info(f"Dashboard already running on port {port}")
95
+ browser_opened = False
96
+ if open_browser:
97
+ browser_opened = self.open_browser(self.get_dashboard_url(port))
98
+ return True, browser_opened
99
+
100
+ self.logger.info(
101
+ f"Starting unified dashboard on port {port} (background: {background})"
102
+ )
103
+
104
+ if background:
105
+ # Start daemon in background mode
106
+ daemon = UnifiedMonitorDaemon(
107
+ host="localhost", port=port, daemon_mode=True
108
+ )
109
+
110
+ success = daemon.start()
111
+ if success:
112
+ with self._lock:
113
+ self._background_daemons[port] = daemon
114
+
115
+ # Wait for daemon to be ready
116
+ if self._wait_for_dashboard(port, timeout=10):
117
+ browser_opened = False
118
+ if open_browser:
119
+ browser_opened = self.open_browser(
120
+ self.get_dashboard_url(port)
121
+ )
122
+ return True, browser_opened
123
+ self.logger.error("Dashboard daemon started but not responding")
124
+ return False, False
125
+ self.logger.error("Failed to start dashboard daemon")
126
+ return False, False
127
+ # For foreground mode, the caller should handle the daemon directly
128
+ # This is used by the CLI command that runs in foreground
129
+ self.logger.info("Foreground mode should be handled by caller")
130
+ return True, False
131
+
132
+ except Exception as e:
133
+ self.logger.error(f"Error starting dashboard: {e}")
134
+ return False, False
135
+
136
+ def stop_dashboard(self, port: int = 8765) -> bool:
137
+ """
138
+ Stop the dashboard.
139
+
140
+ Args:
141
+ port: Port of dashboard to stop
142
+
143
+ Returns:
144
+ True if successfully stopped
145
+ """
146
+ try:
147
+ # Check if we have a background daemon for this port
148
+ with self._lock:
149
+ daemon = self._background_daemons.get(port)
150
+ if daemon:
151
+ daemon.stop()
152
+ del self._background_daemons[port]
153
+ self.logger.info(f"Stopped background daemon on port {port}")
154
+ return True
155
+
156
+ # Try to stop via process management
157
+ if self.port_manager.is_port_in_use(port):
158
+ # Use port manager to find and stop the process
159
+ active_instances = self.port_manager.list_active_instances()
160
+ for instance in active_instances:
161
+ if instance.get("port") == port:
162
+ pid = instance.get("pid")
163
+ if pid:
164
+ try:
165
+ import psutil
166
+
167
+ process = psutil.Process(pid)
168
+ process.terminate()
169
+ process.wait(timeout=5)
170
+ self.logger.info(
171
+ f"Terminated dashboard process {pid} on port {port}"
172
+ )
173
+ return True
174
+ except Exception as e:
175
+ self.logger.warning(
176
+ f"Failed to terminate process {pid}: {e}"
177
+ )
178
+
179
+ self.logger.warning(f"No dashboard found running on port {port}")
180
+ return False
181
+
182
+ except Exception as e:
183
+ self.logger.error(f"Error stopping dashboard: {e}")
184
+ return False
185
+
186
+ def is_dashboard_running(self, port: int = 8765) -> bool:
187
+ """
188
+ Check if dashboard is running.
189
+
190
+ Args:
191
+ port: Port to check
192
+
193
+ Returns:
194
+ True if dashboard is running
195
+ """
196
+ try:
197
+ response = requests.get(f"http://localhost:{port}/health", timeout=2)
198
+ return response.status_code == 200
199
+ except requests.exceptions.RequestException:
200
+ return False
201
+
202
+ def get_dashboard_url(self, port: int = 8765) -> str:
203
+ """
204
+ Get dashboard URL.
205
+
206
+ Args:
207
+ port: Port number
208
+
209
+ Returns:
210
+ Dashboard URL
211
+ """
212
+ return f"http://localhost:{port}"
213
+
214
+ def open_browser(self, url: str) -> bool:
215
+ """
216
+ Open URL in browser.
217
+
218
+ Args:
219
+ url: URL to open
220
+
221
+ Returns:
222
+ True if browser was opened successfully
223
+ """
224
+ try:
225
+ self.logger.info(f"Opening browser to {url}")
226
+ webbrowser.open(url)
227
+ return True
228
+ except Exception as e:
229
+ self.logger.warning(f"Failed to open browser: {e}")
230
+ return False
231
+
232
+ def _wait_for_dashboard(self, port: int, timeout: int = 30) -> bool:
233
+ """
234
+ Wait for dashboard to be ready.
235
+
236
+ Args:
237
+ port: Port to check
238
+ timeout: Maximum time to wait
239
+
240
+ Returns:
241
+ True if dashboard became ready
242
+ """
243
+ start_time = time.time()
244
+ while time.time() - start_time < timeout:
245
+ if self.is_dashboard_running(port):
246
+ return True
247
+ time.sleep(0.5)
248
+ return False
249
+
250
+ def get_dashboard_info(self, port: int = 8765) -> Optional[DashboardInfo]:
251
+ """
252
+ Get information about running dashboard.
253
+
254
+ Args:
255
+ port: Port to check
256
+
257
+ Returns:
258
+ DashboardInfo if running, None otherwise
259
+ """
260
+ if self.is_dashboard_running(port):
261
+ return DashboardInfo(
262
+ url=self.get_dashboard_url(port), port=port, status="running"
263
+ )
264
+ return None
265
+
266
+ def ensure_dependencies(self) -> Tuple[bool, Optional[str]]:
267
+ """
268
+ Ensure required dependencies are available.
269
+
270
+ Returns:
271
+ Tuple of (dependencies_ok, error_message)
272
+ """
273
+ try:
274
+ import aiohttp
275
+ import socketio
276
+
277
+ return True, None
278
+ except ImportError as e:
279
+ error_msg = f"Required dependencies missing: {e}"
280
+ return False, error_msg
281
+
282
+ def find_available_port(self, preferred_port: int = 8765) -> int:
283
+ """
284
+ Find an available port starting from the preferred port.
285
+
286
+ Args:
287
+ preferred_port: Preferred port to start checking from
288
+
289
+ Returns:
290
+ Available port number
291
+ """
292
+ return self.port_manager.find_available_port(preferred_port)
293
+
294
+ def start_server(
295
+ self, port: Optional[int] = None, timeout: int = 30
296
+ ) -> Tuple[bool, DashboardInfo]:
297
+ """
298
+ Start the server (compatibility method for SocketIOManager interface).
299
+
300
+ Args:
301
+ port: Port to use (finds available if None)
302
+ timeout: Timeout for startup
303
+
304
+ Returns:
305
+ Tuple of (success, DashboardInfo)
306
+ """
307
+ if port is None:
308
+ port = self.find_available_port()
309
+
310
+ success, browser_opened = self.start_dashboard(
311
+ port=port, background=True, open_browser=False
312
+ )
313
+
314
+ if success:
315
+ dashboard_info = DashboardInfo(
316
+ url=self.get_dashboard_url(port), port=port, status="running"
317
+ )
318
+ return True, dashboard_info
319
+ return False, DashboardInfo(url="", port=port, status="failed")
320
+
321
+ def is_server_running(self, port: int) -> bool:
322
+ """
323
+ Check if server is running (compatibility method for SocketIOManager interface).
324
+
325
+ Args:
326
+ port: Port to check
327
+
328
+ Returns:
329
+ True if server is running
330
+ """
331
+ return self.is_dashboard_running(port)
332
+
333
+ def stop_server(self, port: Optional[int] = None, timeout: int = 10) -> bool:
334
+ """
335
+ Stop the server (compatibility method for SocketIOManager interface).
336
+
337
+ Args:
338
+ port: Port to stop (stops all if None)
339
+ timeout: Timeout for shutdown
340
+
341
+ Returns:
342
+ True if stopped successfully
343
+ """
344
+ if port is None:
345
+ # Stop all background daemons
346
+ with self._lock:
347
+ ports_to_stop = list(self._background_daemons.keys())
348
+
349
+ success = True
350
+ for p in ports_to_stop:
351
+ if not self.stop_dashboard(p):
352
+ success = False
353
+ return success
354
+ return self.stop_dashboard(port)
@@ -0,0 +1,20 @@
1
+ """
2
+ Unified Monitor Service for Claude MPM
3
+ =====================================
4
+
5
+ WHY: This module provides a single, stable daemon process that combines all
6
+ monitoring functionality into one cohesive service. It replaces the multiple
7
+ competing server implementations with a unified solution.
8
+
9
+ DESIGN DECISIONS:
10
+ - Single process handles HTTP dashboard, Socket.IO events, and real AST analysis
11
+ - Uses proven aiohttp + socketio foundation
12
+ - Integrates real CodeTreeAnalyzer instead of mock data
13
+ - Built for daemon operation with proper lifecycle management
14
+ - Single port (8765) for all functionality
15
+ """
16
+
17
+ from .daemon import UnifiedMonitorDaemon
18
+ from .server import UnifiedMonitorServer
19
+
20
+ __all__ = ["UnifiedMonitorDaemon", "UnifiedMonitorServer"]
@@ -0,0 +1,256 @@
1
+ """
2
+ Unified Monitor Daemon for Claude MPM
3
+ =====================================
4
+
5
+ WHY: This is the main daemon process that provides a single, stable way to
6
+ launch all monitoring functionality. It combines HTTP dashboard serving,
7
+ Socket.IO event handling, real AST analysis, and Claude Code hook ingestion.
8
+
9
+ DESIGN DECISIONS:
10
+ - Single process replaces multiple competing server implementations
11
+ - Daemon-ready with proper lifecycle management
12
+ - Real AST analysis using CodeTreeAnalyzer
13
+ - Single port (8765) for all functionality
14
+ - Built on proven aiohttp + socketio foundation
15
+ """
16
+
17
+ import os
18
+ import signal
19
+ import sys
20
+ import threading
21
+ import time
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ from ...core.logging_config import get_logger
26
+ from .management.health import HealthMonitor
27
+ from .management.lifecycle import DaemonLifecycle
28
+ from .server import UnifiedMonitorServer
29
+
30
+
31
+ class UnifiedMonitorDaemon:
32
+ """Unified daemon process for all Claude MPM monitoring functionality.
33
+
34
+ WHY: Provides a single, stable entry point for launching monitoring services.
35
+ Replaces the multiple competing server implementations with one cohesive daemon.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ host: str = "localhost",
41
+ port: int = 8765,
42
+ daemon_mode: bool = False,
43
+ pid_file: Optional[str] = None,
44
+ log_file: Optional[str] = None,
45
+ ):
46
+ """Initialize the unified monitor daemon.
47
+
48
+ Args:
49
+ host: Host to bind to
50
+ port: Port to bind to
51
+ daemon_mode: Whether to run as background daemon
52
+ pid_file: Path to PID file for daemon mode
53
+ log_file: Path to log file for daemon mode
54
+ """
55
+ self.host = host
56
+ self.port = port
57
+ self.daemon_mode = daemon_mode
58
+ self.logger = get_logger(__name__)
59
+
60
+ # Daemon management
61
+ self.lifecycle = DaemonLifecycle(
62
+ pid_file=pid_file or self._get_default_pid_file(), log_file=log_file
63
+ )
64
+
65
+ # Core server
66
+ self.server = UnifiedMonitorServer(host=host, port=port)
67
+
68
+ # Health monitoring
69
+ self.health_monitor = HealthMonitor(port=port)
70
+
71
+ # State
72
+ self.running = False
73
+ self.shutdown_event = threading.Event()
74
+
75
+ def _get_default_pid_file(self) -> str:
76
+ """Get default PID file path."""
77
+ project_root = Path.cwd()
78
+ claude_mpm_dir = project_root / ".claude-mpm"
79
+ claude_mpm_dir.mkdir(exist_ok=True)
80
+ return str(claude_mpm_dir / "monitor-daemon.pid")
81
+
82
+ def start(self) -> bool:
83
+ """Start the unified monitor daemon.
84
+
85
+ Returns:
86
+ True if started successfully, False otherwise
87
+ """
88
+ try:
89
+ if self.daemon_mode:
90
+ return self._start_daemon()
91
+ return self._start_foreground()
92
+ except Exception as e:
93
+ self.logger.error(f"Failed to start unified monitor daemon: {e}")
94
+ return False
95
+
96
+ def _start_daemon(self) -> bool:
97
+ """Start as background daemon process."""
98
+ self.logger.info("Starting unified monitor daemon in background mode")
99
+
100
+ # Check if already running
101
+ if self.lifecycle.is_running():
102
+ existing_pid = self.lifecycle.get_pid()
103
+ self.logger.warning(f"Daemon already running with PID {existing_pid}")
104
+ return False
105
+
106
+ # Daemonize the process
107
+ success = self.lifecycle.daemonize()
108
+ if not success:
109
+ return False
110
+
111
+ # Start the server in daemon mode
112
+ return self._run_server()
113
+
114
+ def _start_foreground(self) -> bool:
115
+ """Start in foreground mode."""
116
+ self.logger.info(f"Starting unified monitor daemon on {self.host}:{self.port}")
117
+
118
+ # Setup signal handlers for graceful shutdown
119
+ self._setup_signal_handlers()
120
+
121
+ # Start the server
122
+ return self._run_server()
123
+
124
+ def _run_server(self) -> bool:
125
+ """Run the main server loop."""
126
+ try:
127
+ # Start health monitoring
128
+ self.health_monitor.start()
129
+
130
+ # Start the unified server
131
+ success = self.server.start()
132
+ if not success:
133
+ self.logger.error("Failed to start unified monitor server")
134
+ return False
135
+
136
+ self.running = True
137
+ self.logger.info("Unified monitor daemon started successfully")
138
+
139
+ # Keep running until shutdown
140
+ if self.daemon_mode:
141
+ # In daemon mode, run until shutdown signal
142
+ while self.running and not self.shutdown_event.is_set():
143
+ time.sleep(1)
144
+ else:
145
+ # In foreground mode, run until interrupted
146
+ try:
147
+ while self.running:
148
+ time.sleep(1)
149
+ except KeyboardInterrupt:
150
+ self.logger.info("Received keyboard interrupt, shutting down...")
151
+
152
+ return True
153
+
154
+ except Exception as e:
155
+ self.logger.error(f"Error running unified monitor daemon: {e}")
156
+ return False
157
+ finally:
158
+ self._cleanup()
159
+
160
+ def stop(self) -> bool:
161
+ """Stop the unified monitor daemon.
162
+
163
+ Returns:
164
+ True if stopped successfully, False otherwise
165
+ """
166
+ try:
167
+ self.logger.info("Stopping unified monitor daemon")
168
+
169
+ # Signal shutdown
170
+ self.running = False
171
+ self.shutdown_event.set()
172
+
173
+ # Stop server
174
+ if self.server:
175
+ self.server.stop()
176
+
177
+ # Stop health monitoring
178
+ if self.health_monitor:
179
+ self.health_monitor.stop()
180
+
181
+ # Cleanup daemon files
182
+ if self.daemon_mode:
183
+ self.lifecycle.cleanup()
184
+
185
+ self.logger.info("Unified monitor daemon stopped")
186
+ return True
187
+
188
+ except Exception as e:
189
+ self.logger.error(f"Error stopping unified monitor daemon: {e}")
190
+ return False
191
+
192
+ def restart(self) -> bool:
193
+ """Restart the unified monitor daemon.
194
+
195
+ Returns:
196
+ True if restarted successfully, False otherwise
197
+ """
198
+ self.logger.info("Restarting unified monitor daemon")
199
+
200
+ # Stop first
201
+ if not self.stop():
202
+ return False
203
+
204
+ # Wait a moment
205
+ time.sleep(2)
206
+
207
+ # Start again
208
+ return self.start()
209
+
210
+ def status(self) -> dict:
211
+ """Get daemon status information.
212
+
213
+ Returns:
214
+ Dictionary with status information
215
+ """
216
+ is_running = self.lifecycle.is_running() if self.daemon_mode else self.running
217
+ pid = self.lifecycle.get_pid() if self.daemon_mode else os.getpid()
218
+
219
+ status = {
220
+ "running": is_running,
221
+ "pid": pid,
222
+ "host": self.host,
223
+ "port": self.port,
224
+ "daemon_mode": self.daemon_mode,
225
+ "health": (
226
+ self.health_monitor.get_status() if self.health_monitor else "unknown"
227
+ ),
228
+ }
229
+
230
+ if self.server:
231
+ status.update(self.server.get_status())
232
+
233
+ return status
234
+
235
+ def _setup_signal_handlers(self):
236
+ """Setup signal handlers for graceful shutdown."""
237
+
238
+ def signal_handler(signum, frame):
239
+ self.logger.info(f"Received signal {signum}, shutting down...")
240
+ self.stop()
241
+ sys.exit(0)
242
+
243
+ signal.signal(signal.SIGINT, signal_handler)
244
+ signal.signal(signal.SIGTERM, signal_handler)
245
+
246
+ def _cleanup(self):
247
+ """Cleanup resources."""
248
+ try:
249
+ if self.server:
250
+ self.server.stop()
251
+
252
+ if self.health_monitor:
253
+ self.health_monitor.stop()
254
+
255
+ except Exception as e:
256
+ self.logger.error(f"Error during cleanup: {e}")