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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/dashboard.py +62 -120
- claude_mpm/cli/commands/monitor.py +71 -212
- claude_mpm/cli/commands/run.py +33 -33
- claude_mpm/cli/parser.py +79 -2
- claude_mpm/cli/parsers/__init__.py +29 -0
- claude_mpm/dashboard/static/css/code-tree.css +16 -4
- claude_mpm/dashboard/static/css/dashboard.css +15 -1
- claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/code-tree.js +775 -142
- claude_mpm/dashboard/static/js/components/file-viewer.js +538 -0
- claude_mpm/dashboard/static/js/components/module-viewer.js +26 -0
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +166 -14
- claude_mpm/dashboard/static/js/dashboard.js +108 -91
- claude_mpm/dashboard/static/js/socket-client.js +9 -7
- claude_mpm/dashboard/templates/index.html +5 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +1 -11
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +54 -59
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +112 -72
- claude_mpm/services/agents/deployment/agent_format_converter.py +3 -3
- claude_mpm/services/agents/deployment/agent_template_builder.py +3 -5
- claude_mpm/services/cli/unified_dashboard_manager.py +354 -0
- claude_mpm/services/monitor/__init__.py +20 -0
- claude_mpm/services/monitor/daemon.py +256 -0
- claude_mpm/services/monitor/event_emitter.py +279 -0
- claude_mpm/services/monitor/handlers/__init__.py +20 -0
- claude_mpm/services/monitor/handlers/code_analysis.py +334 -0
- claude_mpm/services/monitor/handlers/dashboard.py +298 -0
- claude_mpm/services/monitor/handlers/hooks.py +491 -0
- claude_mpm/services/monitor/management/__init__.py +18 -0
- claude_mpm/services/monitor/management/health.py +124 -0
- claude_mpm/services/monitor/management/lifecycle.py +298 -0
- claude_mpm/services/monitor/server.py +442 -0
- claude_mpm/services/socketio/client_proxy.py +20 -12
- claude_mpm/services/socketio/dashboard_server.py +4 -4
- claude_mpm/services/socketio/monitor_client.py +4 -6
- claude_mpm/tools/code_tree_analyzer.py +33 -17
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/RECORD +48 -43
- claude_mpm/cli/commands/socketio_monitor.py +0 -233
- claude_mpm/scripts/socketio_daemon.py +0 -571
- claude_mpm/scripts/socketio_daemon_hardened.py +0 -937
- claude_mpm/scripts/socketio_daemon_wrapper.py +0 -78
- claude_mpm/scripts/socketio_server_manager.py +0 -349
- claude_mpm/services/cli/dashboard_launcher.py +0 -423
- claude_mpm/services/cli/socketio_manager.py +0 -595
- claude_mpm/services/dashboard/stable_server.py +0 -962
- claude_mpm/services/socketio/monitor_server.py +0 -505
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/licenses/LICENSE +0 -0
- {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}")
|