claude-mpm 4.2.9__py3-none-any.whl → 4.2.12__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 (51) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/dashboard.py +59 -126
  3. claude_mpm/cli/commands/monitor.py +82 -212
  4. claude_mpm/cli/commands/run.py +33 -33
  5. claude_mpm/cli/parsers/monitor_parser.py +12 -2
  6. claude_mpm/dashboard/static/css/code-tree.css +8 -16
  7. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  8. claude_mpm/dashboard/static/dist/components/file-viewer.js +2 -0
  9. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  10. claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +1 -1
  11. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  12. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  13. claude_mpm/dashboard/static/js/components/code-tree.js +692 -114
  14. claude_mpm/dashboard/static/js/components/file-viewer.js +538 -0
  15. claude_mpm/dashboard/static/js/components/module-viewer.js +26 -0
  16. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +166 -14
  17. claude_mpm/dashboard/static/js/dashboard.js +108 -91
  18. claude_mpm/dashboard/static/js/socket-client.js +9 -7
  19. claude_mpm/dashboard/templates/index.html +2 -7
  20. claude_mpm/hooks/claude_hooks/hook_handler.py +1 -11
  21. claude_mpm/hooks/claude_hooks/services/connection_manager.py +54 -59
  22. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +112 -72
  23. claude_mpm/services/agents/deployment/agent_template_builder.py +0 -1
  24. claude_mpm/services/cli/unified_dashboard_manager.py +354 -0
  25. claude_mpm/services/monitor/__init__.py +20 -0
  26. claude_mpm/services/monitor/daemon.py +378 -0
  27. claude_mpm/services/monitor/event_emitter.py +342 -0
  28. claude_mpm/services/monitor/handlers/__init__.py +20 -0
  29. claude_mpm/services/monitor/handlers/code_analysis.py +334 -0
  30. claude_mpm/services/monitor/handlers/dashboard.py +298 -0
  31. claude_mpm/services/monitor/handlers/hooks.py +491 -0
  32. claude_mpm/services/monitor/management/__init__.py +18 -0
  33. claude_mpm/services/monitor/management/health.py +124 -0
  34. claude_mpm/services/monitor/management/lifecycle.py +338 -0
  35. claude_mpm/services/monitor/server.py +596 -0
  36. claude_mpm/tools/code_tree_analyzer.py +33 -17
  37. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/METADATA +1 -1
  38. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/RECORD +42 -37
  39. claude_mpm/cli/commands/socketio_monitor.py +0 -233
  40. claude_mpm/scripts/socketio_daemon.py +0 -571
  41. claude_mpm/scripts/socketio_daemon_hardened.py +0 -937
  42. claude_mpm/scripts/socketio_daemon_wrapper.py +0 -78
  43. claude_mpm/scripts/socketio_server_manager.py +0 -349
  44. claude_mpm/services/cli/dashboard_launcher.py +0 -423
  45. claude_mpm/services/cli/socketio_manager.py +0 -595
  46. claude_mpm/services/dashboard/stable_server.py +0 -1020
  47. claude_mpm/services/socketio/monitor_server.py +0 -505
  48. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/WHEEL +0 -0
  49. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/entry_points.txt +0 -0
  50. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/licenses/LICENSE +0 -0
  51. {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,338 @@
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
+ # Clean up any existing asyncio event loops before forking
52
+ self._cleanup_event_loops()
53
+
54
+ # First fork
55
+ pid = os.fork()
56
+ if pid > 0:
57
+ # Parent process exits
58
+ sys.exit(0)
59
+ except OSError as e:
60
+ self.logger.error(f"First fork failed: {e}")
61
+ return False
62
+
63
+ # Decouple from parent environment
64
+ os.chdir("/")
65
+ os.setsid()
66
+ os.umask(0)
67
+
68
+ try:
69
+ # Second fork
70
+ pid = os.fork()
71
+ if pid > 0:
72
+ # Parent process exits
73
+ sys.exit(0)
74
+ except OSError as e:
75
+ self.logger.error(f"Second fork failed: {e}")
76
+ return False
77
+
78
+ # Redirect standard file descriptors
79
+ self._redirect_streams()
80
+
81
+ # Write PID file
82
+ self.write_pid_file()
83
+
84
+ # Setup signal handlers
85
+ self._setup_signal_handlers()
86
+
87
+ self.logger.info(f"Daemon process started with PID {os.getpid()}")
88
+ return True
89
+
90
+ def _redirect_streams(self):
91
+ """Redirect standard streams for daemon mode."""
92
+ try:
93
+ # Flush streams
94
+ sys.stdout.flush()
95
+ sys.stderr.flush()
96
+
97
+ # Redirect stdin to /dev/null
98
+ with open("/dev/null") as null_in:
99
+ os.dup2(null_in.fileno(), sys.stdin.fileno())
100
+
101
+ # Redirect stdout and stderr
102
+ if self.log_file:
103
+ # Redirect to log file
104
+ with open(self.log_file, "a") as log_out:
105
+ os.dup2(log_out.fileno(), sys.stdout.fileno())
106
+ os.dup2(log_out.fileno(), sys.stderr.fileno())
107
+ else:
108
+ # Redirect to /dev/null
109
+ with open("/dev/null", "w") as null_out:
110
+ os.dup2(null_out.fileno(), sys.stdout.fileno())
111
+ os.dup2(null_out.fileno(), sys.stderr.fileno())
112
+
113
+ except Exception as e:
114
+ self.logger.error(f"Error redirecting streams: {e}")
115
+
116
+ def write_pid_file(self):
117
+ """Write PID to PID file."""
118
+ try:
119
+ # Ensure parent directory exists
120
+ self.pid_file.parent.mkdir(parents=True, exist_ok=True)
121
+
122
+ # Write PID
123
+ with open(self.pid_file, "w") as f:
124
+ f.write(str(os.getpid()))
125
+
126
+ self.logger.debug(f"PID file written: {self.pid_file}")
127
+
128
+ except Exception as e:
129
+ self.logger.error(f"Error writing PID file: {e}")
130
+ raise
131
+
132
+ def _setup_signal_handlers(self):
133
+ """Setup signal handlers for graceful shutdown."""
134
+
135
+ def signal_handler(signum, frame):
136
+ self.logger.info(f"Received signal {signum}, initiating shutdown")
137
+ self.cleanup()
138
+ sys.exit(0)
139
+
140
+ signal.signal(signal.SIGTERM, signal_handler)
141
+ signal.signal(signal.SIGINT, signal_handler)
142
+
143
+ def is_running(self) -> bool:
144
+ """Check if daemon is currently running.
145
+
146
+ Returns:
147
+ True if daemon is running, False otherwise
148
+ """
149
+ try:
150
+ pid = self.get_pid()
151
+ if pid is None:
152
+ return False
153
+
154
+ # Check if process exists
155
+ os.kill(pid, 0) # Signal 0 just checks if process exists
156
+ return True
157
+
158
+ except (OSError, ProcessLookupError):
159
+ # Process doesn't exist
160
+ self._cleanup_stale_pid_file()
161
+ return False
162
+
163
+ def get_pid(self) -> Optional[int]:
164
+ """Get PID from PID file.
165
+
166
+ Returns:
167
+ PID if found, None otherwise
168
+ """
169
+ try:
170
+ if not self.pid_file.exists():
171
+ return None
172
+
173
+ with open(self.pid_file) as f:
174
+ pid_str = f.read().strip()
175
+ return int(pid_str) if pid_str else None
176
+
177
+ except (OSError, ValueError):
178
+ return None
179
+
180
+ def stop_daemon(self) -> bool:
181
+ """Stop the running daemon.
182
+
183
+ Returns:
184
+ True if stopped successfully, False otherwise
185
+ """
186
+ try:
187
+ pid = self.get_pid()
188
+ if pid is None:
189
+ self.logger.warning("No PID file found, daemon may not be running")
190
+ return False
191
+
192
+ # Send SIGTERM for graceful shutdown
193
+ self.logger.info(f"Stopping daemon with PID {pid}")
194
+ os.kill(pid, signal.SIGTERM)
195
+
196
+ # Wait for process to exit
197
+ for _ in range(30): # Wait up to 30 seconds
198
+ if not self.is_running():
199
+ self.logger.info("Daemon stopped successfully")
200
+ return True
201
+ time.sleep(1)
202
+
203
+ # Force kill if still running
204
+ self.logger.warning("Daemon didn't stop gracefully, forcing kill")
205
+ os.kill(pid, signal.SIGKILL)
206
+
207
+ # Wait a bit more
208
+ for _ in range(5):
209
+ if not self.is_running():
210
+ self.logger.info("Daemon force-killed successfully")
211
+ return True
212
+ time.sleep(1)
213
+
214
+ self.logger.error("Failed to stop daemon")
215
+ return False
216
+
217
+ except ProcessLookupError:
218
+ # Process already dead
219
+ self._cleanup_stale_pid_file()
220
+ self.logger.info("Daemon was already stopped")
221
+ return True
222
+ except Exception as e:
223
+ self.logger.error(f"Error stopping daemon: {e}")
224
+ return False
225
+
226
+ def restart_daemon(self) -> bool:
227
+ """Restart the daemon.
228
+
229
+ Returns:
230
+ True if restarted successfully, False otherwise
231
+ """
232
+ self.logger.info("Restarting daemon")
233
+
234
+ # Stop first
235
+ if not self.stop_daemon():
236
+ return False
237
+
238
+ # Wait a moment
239
+ time.sleep(2)
240
+
241
+ # Start again (this would need to be called from the main daemon)
242
+ # For now, just return True as the actual restart logic is in the daemon
243
+ return True
244
+
245
+ def cleanup(self):
246
+ """Cleanup daemon resources."""
247
+ try:
248
+ # Remove PID file
249
+ if self.pid_file.exists():
250
+ self.pid_file.unlink()
251
+ self.logger.debug(f"PID file removed: {self.pid_file}")
252
+
253
+ except Exception as e:
254
+ self.logger.error(f"Error during cleanup: {e}")
255
+
256
+ def _cleanup_stale_pid_file(self):
257
+ """Remove stale PID file."""
258
+ try:
259
+ if self.pid_file.exists():
260
+ self.pid_file.unlink()
261
+ self.logger.debug("Removed stale PID file")
262
+ except Exception as e:
263
+ self.logger.error(f"Error removing stale PID file: {e}")
264
+
265
+ def _cleanup_event_loops(self):
266
+ """Clean up any existing asyncio event loops before forking.
267
+
268
+ This prevents the 'I/O operation on closed kqueue object' error
269
+ that occurs when forked processes inherit event loops.
270
+ """
271
+ try:
272
+ import asyncio
273
+ import gc
274
+
275
+ # Try to get the current event loop
276
+ try:
277
+ loop = asyncio.get_event_loop()
278
+ if loop and loop.is_running():
279
+ # Can't close a running loop, but we can stop it
280
+ loop.stop()
281
+ self.logger.debug("Stopped running event loop before fork")
282
+ elif loop:
283
+ # Close the loop if it exists and is not running
284
+ loop.close()
285
+ self.logger.debug("Closed event loop before fork")
286
+ except RuntimeError:
287
+ # No event loop in current thread
288
+ pass
289
+
290
+ # Clear the event loop policy to ensure clean state
291
+ asyncio.set_event_loop(None)
292
+
293
+ # Force garbage collection to clean up any loop resources
294
+ gc.collect()
295
+
296
+ except ImportError:
297
+ # asyncio not available (unlikely but handle it)
298
+ pass
299
+ except Exception as e:
300
+ self.logger.debug(f"Error cleaning up event loops before fork: {e}")
301
+
302
+ def get_status(self) -> dict:
303
+ """Get daemon status information.
304
+
305
+ Returns:
306
+ Dictionary with status information
307
+ """
308
+ pid = self.get_pid()
309
+ running = self.is_running()
310
+
311
+ status = {
312
+ "running": running,
313
+ "pid": pid,
314
+ "pid_file": str(self.pid_file),
315
+ "log_file": str(self.log_file) if self.log_file else None,
316
+ }
317
+
318
+ if running and pid:
319
+ try:
320
+ # Get process info
321
+ import psutil
322
+
323
+ process = psutil.Process(pid)
324
+ status.update(
325
+ {
326
+ "cpu_percent": process.cpu_percent(),
327
+ "memory_info": process.memory_info()._asdict(),
328
+ "create_time": process.create_time(),
329
+ "status": process.status(),
330
+ }
331
+ )
332
+ except ImportError:
333
+ # psutil not available
334
+ pass
335
+ except Exception as e:
336
+ self.logger.debug(f"Error getting process info: {e}")
337
+
338
+ return status