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,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Daemon Lifecycle Management for Unified Monitor
|
|
3
|
+
===============================================
|
|
4
|
+
|
|
5
|
+
WHY: This module provides proper daemon process lifecycle management including
|
|
6
|
+
daemonization, PID file management, and graceful shutdown for the unified
|
|
7
|
+
monitor daemon.
|
|
8
|
+
|
|
9
|
+
DESIGN DECISIONS:
|
|
10
|
+
- Standard Unix daemon patterns
|
|
11
|
+
- PID file management for process tracking
|
|
12
|
+
- Proper signal handling for graceful shutdown
|
|
13
|
+
- Log file redirection for daemon mode
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import signal
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from ....core.logging_config import get_logger
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DaemonLifecycle:
|
|
27
|
+
"""Manages daemon process lifecycle for the unified monitor.
|
|
28
|
+
|
|
29
|
+
WHY: Provides proper daemon process management with PID files, signal
|
|
30
|
+
handling, and graceful shutdown capabilities.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, pid_file: str, log_file: Optional[str] = None):
|
|
34
|
+
"""Initialize daemon lifecycle manager.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
pid_file: Path to PID file
|
|
38
|
+
log_file: Path to log file for daemon mode
|
|
39
|
+
"""
|
|
40
|
+
self.pid_file = Path(pid_file)
|
|
41
|
+
self.log_file = Path(log_file) if log_file else None
|
|
42
|
+
self.logger = get_logger(__name__)
|
|
43
|
+
|
|
44
|
+
def daemonize(self) -> bool:
|
|
45
|
+
"""Daemonize the current process.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
True if daemonization successful, False otherwise
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
# First fork
|
|
52
|
+
pid = os.fork()
|
|
53
|
+
if pid > 0:
|
|
54
|
+
# Parent process exits
|
|
55
|
+
sys.exit(0)
|
|
56
|
+
except OSError as e:
|
|
57
|
+
self.logger.error(f"First fork failed: {e}")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
# Decouple from parent environment
|
|
61
|
+
os.chdir("/")
|
|
62
|
+
os.setsid()
|
|
63
|
+
os.umask(0)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Second fork
|
|
67
|
+
pid = os.fork()
|
|
68
|
+
if pid > 0:
|
|
69
|
+
# Parent process exits
|
|
70
|
+
sys.exit(0)
|
|
71
|
+
except OSError as e:
|
|
72
|
+
self.logger.error(f"Second fork failed: {e}")
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
# Redirect standard file descriptors
|
|
76
|
+
self._redirect_streams()
|
|
77
|
+
|
|
78
|
+
# Write PID file
|
|
79
|
+
self._write_pid_file()
|
|
80
|
+
|
|
81
|
+
# Setup signal handlers
|
|
82
|
+
self._setup_signal_handlers()
|
|
83
|
+
|
|
84
|
+
self.logger.info(f"Daemon process started with PID {os.getpid()}")
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
def _redirect_streams(self):
|
|
88
|
+
"""Redirect standard streams for daemon mode."""
|
|
89
|
+
try:
|
|
90
|
+
# Flush streams
|
|
91
|
+
sys.stdout.flush()
|
|
92
|
+
sys.stderr.flush()
|
|
93
|
+
|
|
94
|
+
# Redirect stdin to /dev/null
|
|
95
|
+
with open("/dev/null") as null_in:
|
|
96
|
+
os.dup2(null_in.fileno(), sys.stdin.fileno())
|
|
97
|
+
|
|
98
|
+
# Redirect stdout and stderr
|
|
99
|
+
if self.log_file:
|
|
100
|
+
# Redirect to log file
|
|
101
|
+
with open(self.log_file, "a") as log_out:
|
|
102
|
+
os.dup2(log_out.fileno(), sys.stdout.fileno())
|
|
103
|
+
os.dup2(log_out.fileno(), sys.stderr.fileno())
|
|
104
|
+
else:
|
|
105
|
+
# Redirect to /dev/null
|
|
106
|
+
with open("/dev/null", "w") as null_out:
|
|
107
|
+
os.dup2(null_out.fileno(), sys.stdout.fileno())
|
|
108
|
+
os.dup2(null_out.fileno(), sys.stderr.fileno())
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
self.logger.error(f"Error redirecting streams: {e}")
|
|
112
|
+
|
|
113
|
+
def _write_pid_file(self):
|
|
114
|
+
"""Write PID to PID file."""
|
|
115
|
+
try:
|
|
116
|
+
# Ensure parent directory exists
|
|
117
|
+
self.pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
|
|
119
|
+
# Write PID
|
|
120
|
+
with open(self.pid_file, "w") as f:
|
|
121
|
+
f.write(str(os.getpid()))
|
|
122
|
+
|
|
123
|
+
self.logger.debug(f"PID file written: {self.pid_file}")
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.logger.error(f"Error writing PID file: {e}")
|
|
127
|
+
raise
|
|
128
|
+
|
|
129
|
+
def _setup_signal_handlers(self):
|
|
130
|
+
"""Setup signal handlers for graceful shutdown."""
|
|
131
|
+
|
|
132
|
+
def signal_handler(signum, frame):
|
|
133
|
+
self.logger.info(f"Received signal {signum}, initiating shutdown")
|
|
134
|
+
self.cleanup()
|
|
135
|
+
sys.exit(0)
|
|
136
|
+
|
|
137
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
138
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
139
|
+
|
|
140
|
+
def is_running(self) -> bool:
|
|
141
|
+
"""Check if daemon is currently running.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if daemon is running, False otherwise
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
pid = self.get_pid()
|
|
148
|
+
if pid is None:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
# Check if process exists
|
|
152
|
+
os.kill(pid, 0) # Signal 0 just checks if process exists
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
except (OSError, ProcessLookupError):
|
|
156
|
+
# Process doesn't exist
|
|
157
|
+
self._cleanup_stale_pid_file()
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
def get_pid(self) -> Optional[int]:
|
|
161
|
+
"""Get PID from PID file.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
PID if found, None otherwise
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
if not self.pid_file.exists():
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
with open(self.pid_file) as f:
|
|
171
|
+
pid_str = f.read().strip()
|
|
172
|
+
return int(pid_str) if pid_str else None
|
|
173
|
+
|
|
174
|
+
except (OSError, ValueError):
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def stop_daemon(self) -> bool:
|
|
178
|
+
"""Stop the running daemon.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if stopped successfully, False otherwise
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
pid = self.get_pid()
|
|
185
|
+
if pid is None:
|
|
186
|
+
self.logger.warning("No PID file found, daemon may not be running")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
# Send SIGTERM for graceful shutdown
|
|
190
|
+
self.logger.info(f"Stopping daemon with PID {pid}")
|
|
191
|
+
os.kill(pid, signal.SIGTERM)
|
|
192
|
+
|
|
193
|
+
# Wait for process to exit
|
|
194
|
+
for _ in range(30): # Wait up to 30 seconds
|
|
195
|
+
if not self.is_running():
|
|
196
|
+
self.logger.info("Daemon stopped successfully")
|
|
197
|
+
return True
|
|
198
|
+
time.sleep(1)
|
|
199
|
+
|
|
200
|
+
# Force kill if still running
|
|
201
|
+
self.logger.warning("Daemon didn't stop gracefully, forcing kill")
|
|
202
|
+
os.kill(pid, signal.SIGKILL)
|
|
203
|
+
|
|
204
|
+
# Wait a bit more
|
|
205
|
+
for _ in range(5):
|
|
206
|
+
if not self.is_running():
|
|
207
|
+
self.logger.info("Daemon force-killed successfully")
|
|
208
|
+
return True
|
|
209
|
+
time.sleep(1)
|
|
210
|
+
|
|
211
|
+
self.logger.error("Failed to stop daemon")
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
except ProcessLookupError:
|
|
215
|
+
# Process already dead
|
|
216
|
+
self._cleanup_stale_pid_file()
|
|
217
|
+
self.logger.info("Daemon was already stopped")
|
|
218
|
+
return True
|
|
219
|
+
except Exception as e:
|
|
220
|
+
self.logger.error(f"Error stopping daemon: {e}")
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def restart_daemon(self) -> bool:
|
|
224
|
+
"""Restart the daemon.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
True if restarted successfully, False otherwise
|
|
228
|
+
"""
|
|
229
|
+
self.logger.info("Restarting daemon")
|
|
230
|
+
|
|
231
|
+
# Stop first
|
|
232
|
+
if not self.stop_daemon():
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
# Wait a moment
|
|
236
|
+
time.sleep(2)
|
|
237
|
+
|
|
238
|
+
# Start again (this would need to be called from the main daemon)
|
|
239
|
+
# For now, just return True as the actual restart logic is in the daemon
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
def cleanup(self):
|
|
243
|
+
"""Cleanup daemon resources."""
|
|
244
|
+
try:
|
|
245
|
+
# Remove PID file
|
|
246
|
+
if self.pid_file.exists():
|
|
247
|
+
self.pid_file.unlink()
|
|
248
|
+
self.logger.debug(f"PID file removed: {self.pid_file}")
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
self.logger.error(f"Error during cleanup: {e}")
|
|
252
|
+
|
|
253
|
+
def _cleanup_stale_pid_file(self):
|
|
254
|
+
"""Remove stale PID file."""
|
|
255
|
+
try:
|
|
256
|
+
if self.pid_file.exists():
|
|
257
|
+
self.pid_file.unlink()
|
|
258
|
+
self.logger.debug("Removed stale PID file")
|
|
259
|
+
except Exception as e:
|
|
260
|
+
self.logger.error(f"Error removing stale PID file: {e}")
|
|
261
|
+
|
|
262
|
+
def get_status(self) -> dict:
|
|
263
|
+
"""Get daemon status information.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Dictionary with status information
|
|
267
|
+
"""
|
|
268
|
+
pid = self.get_pid()
|
|
269
|
+
running = self.is_running()
|
|
270
|
+
|
|
271
|
+
status = {
|
|
272
|
+
"running": running,
|
|
273
|
+
"pid": pid,
|
|
274
|
+
"pid_file": str(self.pid_file),
|
|
275
|
+
"log_file": str(self.log_file) if self.log_file else None,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if running and pid:
|
|
279
|
+
try:
|
|
280
|
+
# Get process info
|
|
281
|
+
import psutil
|
|
282
|
+
|
|
283
|
+
process = psutil.Process(pid)
|
|
284
|
+
status.update(
|
|
285
|
+
{
|
|
286
|
+
"cpu_percent": process.cpu_percent(),
|
|
287
|
+
"memory_info": process.memory_info()._asdict(),
|
|
288
|
+
"create_time": process.create_time(),
|
|
289
|
+
"status": process.status(),
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
except ImportError:
|
|
293
|
+
# psutil not available
|
|
294
|
+
pass
|
|
295
|
+
except Exception as e:
|
|
296
|
+
self.logger.debug(f"Error getting process info: {e}")
|
|
297
|
+
|
|
298
|
+
return status
|