claude-mpm 4.2.9__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 (50) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/dashboard.py +59 -126
  3. claude_mpm/cli/commands/monitor.py +71 -212
  4. claude_mpm/cli/commands/run.py +33 -33
  5. claude_mpm/dashboard/static/css/code-tree.css +8 -16
  6. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  7. claude_mpm/dashboard/static/dist/components/file-viewer.js +2 -0
  8. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  9. claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +1 -1
  10. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  11. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  12. claude_mpm/dashboard/static/js/components/code-tree.js +692 -114
  13. claude_mpm/dashboard/static/js/components/file-viewer.js +538 -0
  14. claude_mpm/dashboard/static/js/components/module-viewer.js +26 -0
  15. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +166 -14
  16. claude_mpm/dashboard/static/js/dashboard.js +108 -91
  17. claude_mpm/dashboard/static/js/socket-client.js +9 -7
  18. claude_mpm/dashboard/templates/index.html +2 -7
  19. claude_mpm/hooks/claude_hooks/hook_handler.py +1 -11
  20. claude_mpm/hooks/claude_hooks/services/connection_manager.py +54 -59
  21. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +112 -72
  22. claude_mpm/services/agents/deployment/agent_template_builder.py +0 -1
  23. claude_mpm/services/cli/unified_dashboard_manager.py +354 -0
  24. claude_mpm/services/monitor/__init__.py +20 -0
  25. claude_mpm/services/monitor/daemon.py +256 -0
  26. claude_mpm/services/monitor/event_emitter.py +279 -0
  27. claude_mpm/services/monitor/handlers/__init__.py +20 -0
  28. claude_mpm/services/monitor/handlers/code_analysis.py +334 -0
  29. claude_mpm/services/monitor/handlers/dashboard.py +298 -0
  30. claude_mpm/services/monitor/handlers/hooks.py +491 -0
  31. claude_mpm/services/monitor/management/__init__.py +18 -0
  32. claude_mpm/services/monitor/management/health.py +124 -0
  33. claude_mpm/services/monitor/management/lifecycle.py +298 -0
  34. claude_mpm/services/monitor/server.py +442 -0
  35. claude_mpm/tools/code_tree_analyzer.py +33 -17
  36. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/METADATA +1 -1
  37. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/RECORD +41 -36
  38. claude_mpm/cli/commands/socketio_monitor.py +0 -233
  39. claude_mpm/scripts/socketio_daemon.py +0 -571
  40. claude_mpm/scripts/socketio_daemon_hardened.py +0 -937
  41. claude_mpm/scripts/socketio_daemon_wrapper.py +0 -78
  42. claude_mpm/scripts/socketio_server_manager.py +0 -349
  43. claude_mpm/services/cli/dashboard_launcher.py +0 -423
  44. claude_mpm/services/cli/socketio_manager.py +0 -595
  45. claude_mpm/services/dashboard/stable_server.py +0 -1020
  46. claude_mpm/services/socketio/monitor_server.py +0 -505
  47. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/WHEEL +0 -0
  48. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/entry_points.txt +0 -0
  49. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.11.dist-info}/licenses/LICENSE +0 -0
  50. {claude_mpm-4.2.9.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