claude-mpm 5.0.2__py3-none-any.whl → 5.4.3__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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +2002 -0
- claude_mpm/agents/PM_INSTRUCTIONS.md +1218 -905
- claude_mpm/agents/agent_loader.py +10 -17
- claude_mpm/agents/base_agent_loader.py +10 -35
- claude_mpm/agents/frontmatter_validator.py +68 -0
- claude_mpm/agents/templates/circuit-breakers.md +431 -45
- claude_mpm/cli/__init__.py +0 -1
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/agent_state_manager.py +67 -23
- claude_mpm/cli/commands/agents.py +446 -25
- claude_mpm/cli/commands/auto_configure.py +535 -233
- claude_mpm/cli/commands/configure.py +1500 -147
- claude_mpm/cli/commands/configure_agent_display.py +13 -6
- claude_mpm/cli/commands/mpm_init/core.py +158 -1
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/postmortem.py +401 -0
- claude_mpm/cli/commands/run.py +1 -39
- claude_mpm/cli/commands/skills.py +322 -19
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +8 -0
- claude_mpm/cli/interactive/agent_wizard.py +302 -195
- claude_mpm/cli/parsers/agents_parser.py +137 -0
- claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
- claude_mpm/cli/parsers/base_parser.py +9 -0
- claude_mpm/cli/parsers/skills_parser.py +7 -0
- claude_mpm/cli/startup.py +133 -85
- claude_mpm/commands/mpm-agents-auto-configure.md +2 -2
- claude_mpm/commands/mpm-agents-list.md +2 -2
- claude_mpm/commands/mpm-config-view.md +2 -2
- claude_mpm/commands/mpm-help.md +3 -0
- claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
- claude_mpm/commands/mpm-postmortem.md +123 -0
- claude_mpm/commands/mpm-session-resume.md +2 -2
- claude_mpm/commands/mpm-ticket-view.md +2 -2
- claude_mpm/config/agent_presets.py +312 -82
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/config/skill_presets.py +392 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +2 -25
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework/loaders/file_loader.py +54 -101
- claude_mpm/core/interactive_session.py +19 -5
- claude_mpm/core/oneshot_session.py +16 -4
- claude_mpm/core/output_style_manager.py +173 -43
- claude_mpm/core/protocols/__init__.py +23 -0
- claude_mpm/core/protocols/runner_protocol.py +103 -0
- claude_mpm/core/protocols/session_protocol.py +131 -0
- claude_mpm/core/shared/singleton_manager.py +11 -4
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/system_context.py +38 -0
- claude_mpm/core/unified_agent_registry.py +134 -16
- claude_mpm/core/unified_config.py +22 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
- claude_mpm/models/agent_definition.py +7 -0
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/services/agents/agent_recommendation_service.py +279 -0
- claude_mpm/services/agents/cache_git_manager.py +621 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +110 -3
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +518 -55
- claude_mpm/services/agents/git_source_manager.py +20 -0
- claude_mpm/services/agents/sources/git_source_sync_service.py +45 -6
- claude_mpm/services/agents/toolchain_detector.py +6 -5
- claude_mpm/services/analysis/__init__.py +35 -0
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/analysis/postmortem_reporter.py +474 -0
- claude_mpm/services/analysis/postmortem_service.py +765 -0
- claude_mpm/services/command_deployment_service.py +106 -5
- claude_mpm/services/core/base.py +7 -2
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +8 -8
- claude_mpm/services/mcp_config_manager.py +75 -145
- claude_mpm/services/mcp_service_verifier.py +6 -3
- claude_mpm/services/monitor/daemon.py +37 -10
- claude_mpm/services/monitor/daemon_manager.py +134 -21
- claude_mpm/services/monitor/server.py +225 -19
- claude_mpm/services/project/project_organizer.py +4 -0
- claude_mpm/services/runner_configuration_service.py +16 -3
- claude_mpm/services/session_management_service.py +16 -4
- claude_mpm/services/socketio/event_normalizer.py +15 -1
- claude_mpm/services/socketio/server/core.py +160 -21
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/utils/agent_filters.py +261 -0
- claude_mpm/utils/gitignore.py +3 -0
- claude_mpm/utils/migration.py +372 -0
- claude_mpm/utils/progress.py +5 -1
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +69 -84
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +112 -153
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- claude_mpm/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -971
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
|
@@ -44,6 +44,7 @@ class UnifiedMonitorDaemon:
|
|
|
44
44
|
daemon_mode: bool = False,
|
|
45
45
|
pid_file: Optional[str] = None,
|
|
46
46
|
log_file: Optional[str] = None,
|
|
47
|
+
enable_hot_reload: bool = False,
|
|
47
48
|
):
|
|
48
49
|
"""Initialize the unified monitor daemon.
|
|
49
50
|
|
|
@@ -53,10 +54,12 @@ class UnifiedMonitorDaemon:
|
|
|
53
54
|
daemon_mode: Whether to run as background daemon
|
|
54
55
|
pid_file: Path to PID file for daemon mode
|
|
55
56
|
log_file: Path to log file for daemon mode
|
|
57
|
+
enable_hot_reload: Enable file watching and hot reload for development
|
|
56
58
|
"""
|
|
57
59
|
self.host = host
|
|
58
60
|
self.port = port
|
|
59
61
|
self.daemon_mode = daemon_mode
|
|
62
|
+
self.enable_hot_reload = enable_hot_reload
|
|
60
63
|
self.logger = get_logger(__name__)
|
|
61
64
|
|
|
62
65
|
# Use new consolidated DaemonManager for all daemon operations
|
|
@@ -75,7 +78,9 @@ class UnifiedMonitorDaemon:
|
|
|
75
78
|
)
|
|
76
79
|
|
|
77
80
|
# Core server
|
|
78
|
-
self.server = UnifiedMonitorServer(
|
|
81
|
+
self.server = UnifiedMonitorServer(
|
|
82
|
+
host=host, port=port, enable_hot_reload=enable_hot_reload
|
|
83
|
+
)
|
|
79
84
|
|
|
80
85
|
# Health monitoring
|
|
81
86
|
self.health_monitor = HealthMonitor(port=port)
|
|
@@ -88,11 +93,12 @@ class UnifiedMonitorDaemon:
|
|
|
88
93
|
self.shutdown_event = threading.Event()
|
|
89
94
|
|
|
90
95
|
def _get_default_pid_file(self) -> str:
|
|
91
|
-
"""Get default PID file path."""
|
|
96
|
+
"""Get default PID file path with port number to support multiple daemons."""
|
|
92
97
|
project_root = Path.cwd()
|
|
93
98
|
claude_mpm_dir = project_root / ".claude-mpm"
|
|
94
99
|
claude_mpm_dir.mkdir(exist_ok=True)
|
|
95
|
-
|
|
100
|
+
# Include port in filename to support multiple daemon instances
|
|
101
|
+
return str(claude_mpm_dir / f"monitor-daemon-{self.port}.pid")
|
|
96
102
|
|
|
97
103
|
def start(self, force_restart: bool = False) -> bool:
|
|
98
104
|
"""Start the unified monitor daemon.
|
|
@@ -145,16 +151,26 @@ class UnifiedMonitorDaemon:
|
|
|
145
151
|
if self.daemon_manager.is_running():
|
|
146
152
|
existing_pid = self.daemon_manager.get_pid()
|
|
147
153
|
if not force_restart:
|
|
148
|
-
|
|
154
|
+
msg = f"Daemon already running on port {self.port} with PID {existing_pid}"
|
|
155
|
+
self.logger.warning(msg)
|
|
156
|
+
# If we're in subprocess mode, this is an error - we should have cleaned up
|
|
157
|
+
if os.environ.get("CLAUDE_MPM_SUBPROCESS_DAEMON") == "1":
|
|
158
|
+
self.logger.error(
|
|
159
|
+
f"SUBPROCESS ERROR: {msg} - This should not happen in subprocess mode!"
|
|
160
|
+
)
|
|
149
161
|
return False
|
|
150
162
|
# Force restart was already handled above
|
|
151
163
|
|
|
152
164
|
# Check for our service on the port
|
|
153
165
|
is_ours, pid = self.daemon_manager.is_our_service()
|
|
154
166
|
if is_ours and pid and not force_restart:
|
|
155
|
-
self.
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
msg = f"Our service already running on port {self.port} (PID: {pid})"
|
|
168
|
+
self.logger.warning(msg)
|
|
169
|
+
# If we're in subprocess mode, this is an error - we should have cleaned up
|
|
170
|
+
if os.environ.get("CLAUDE_MPM_SUBPROCESS_DAEMON") == "1":
|
|
171
|
+
self.logger.error(
|
|
172
|
+
f"SUBPROCESS ERROR: {msg} - This should not happen in subprocess mode!"
|
|
173
|
+
)
|
|
158
174
|
return False
|
|
159
175
|
|
|
160
176
|
# Use subprocess approach for clean daemon startup (v4.2.40)
|
|
@@ -169,20 +185,29 @@ class UnifiedMonitorDaemon:
|
|
|
169
185
|
if os.environ.get("CLAUDE_MPM_SUBPROCESS_DAEMON") == "1":
|
|
170
186
|
# We're in a subprocess started by start_daemon_subprocess
|
|
171
187
|
# We need to write the PID file ourselves since parent didn't
|
|
172
|
-
self.logger.info("Running in subprocess daemon mode
|
|
188
|
+
self.logger.info(f"Running in subprocess daemon mode on port {self.port}")
|
|
189
|
+
self.logger.info(f"Subprocess PID: {os.getpid()}")
|
|
190
|
+
self.logger.info(f"PID file path: {self.daemon_manager.pid_file}")
|
|
173
191
|
self.daemon_manager.write_pid_file()
|
|
192
|
+
self.logger.info("PID file written successfully")
|
|
174
193
|
|
|
175
194
|
# Setup signal handlers for graceful shutdown
|
|
176
195
|
self._setup_signal_handlers()
|
|
196
|
+
self.logger.info("Signal handlers configured")
|
|
177
197
|
|
|
178
198
|
# Start the server (this will run until shutdown)
|
|
199
|
+
self.logger.info("Starting server in subprocess mode...")
|
|
179
200
|
try:
|
|
180
201
|
result = self._run_server()
|
|
181
202
|
if not result:
|
|
182
203
|
self.logger.error("Failed to start server in subprocess mode")
|
|
204
|
+
return result
|
|
205
|
+
self.logger.info("Server started successfully in subprocess mode")
|
|
183
206
|
return result
|
|
184
207
|
except Exception as e:
|
|
185
|
-
self.logger.error(
|
|
208
|
+
self.logger.error(
|
|
209
|
+
f"Server startup exception in subprocess: {e}", exc_info=True
|
|
210
|
+
)
|
|
186
211
|
raise
|
|
187
212
|
else:
|
|
188
213
|
# Legacy fork approach (kept for compatibility but not used by default)
|
|
@@ -490,7 +515,9 @@ class UnifiedMonitorDaemon:
|
|
|
490
515
|
|
|
491
516
|
# Recreate the server and health monitor after stop() sets them to None
|
|
492
517
|
self.logger.info(f"Recreating server components for {self.host}:{self.port}")
|
|
493
|
-
self.server = UnifiedMonitorServer(
|
|
518
|
+
self.server = UnifiedMonitorServer(
|
|
519
|
+
host=self.host, port=self.port, enable_hot_reload=self.enable_hot_reload
|
|
520
|
+
)
|
|
494
521
|
self.health_monitor = HealthMonitor(port=self.port)
|
|
495
522
|
|
|
496
523
|
# Reset the shutdown event for the new run
|
|
@@ -34,6 +34,11 @@ from typing import Optional, Tuple
|
|
|
34
34
|
from ...core.enums import OperationResult
|
|
35
35
|
from ...core.logging_config import get_logger
|
|
36
36
|
|
|
37
|
+
# Exit code constants for signal handling
|
|
38
|
+
EXIT_NORMAL = 0
|
|
39
|
+
EXIT_SIGKILL = 137 # 128 + SIGKILL(9) - forced termination
|
|
40
|
+
EXIT_SIGTERM = 143 # 128 + SIGTERM(15) - graceful shutdown
|
|
41
|
+
|
|
37
42
|
|
|
38
43
|
class DaemonManager:
|
|
39
44
|
"""Centralized manager for all daemon lifecycle operations.
|
|
@@ -80,18 +85,20 @@ class DaemonManager:
|
|
|
80
85
|
self.startup_status_file = None
|
|
81
86
|
|
|
82
87
|
def _get_default_pid_file(self) -> Path:
|
|
83
|
-
"""Get default PID file path."""
|
|
88
|
+
"""Get default PID file path with port number to support multiple daemons."""
|
|
84
89
|
project_root = Path.cwd()
|
|
85
90
|
claude_mpm_dir = project_root / ".claude-mpm"
|
|
86
91
|
claude_mpm_dir.mkdir(exist_ok=True)
|
|
87
|
-
|
|
92
|
+
# Include port in filename to support multiple daemon instances
|
|
93
|
+
return claude_mpm_dir / f"monitor-daemon-{self.port}.pid"
|
|
88
94
|
|
|
89
95
|
def _get_default_log_file(self) -> Path:
|
|
90
|
-
"""Get default log file path."""
|
|
96
|
+
"""Get default log file path with port number to support multiple daemons."""
|
|
91
97
|
project_root = Path.cwd()
|
|
92
98
|
claude_mpm_dir = project_root / ".claude-mpm"
|
|
93
99
|
claude_mpm_dir.mkdir(exist_ok=True)
|
|
94
|
-
|
|
100
|
+
# Include port in filename to support multiple daemon instances
|
|
101
|
+
return claude_mpm_dir / f"monitor-daemon-{self.port}.log"
|
|
95
102
|
|
|
96
103
|
def cleanup_port_conflicts(self, max_retries: int = 3) -> bool:
|
|
97
104
|
"""Clean up any processes using the daemon port.
|
|
@@ -471,6 +478,55 @@ class DaemonManager:
|
|
|
471
478
|
|
|
472
479
|
return None
|
|
473
480
|
|
|
481
|
+
def _verify_daemon_health(self, max_attempts: int = 3) -> bool:
|
|
482
|
+
"""Verify daemon is healthy by checking HTTP health endpoint.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
max_attempts: Maximum number of connection attempts
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
True if health check passes, False otherwise
|
|
489
|
+
"""
|
|
490
|
+
try:
|
|
491
|
+
import requests
|
|
492
|
+
|
|
493
|
+
for attempt in range(max_attempts):
|
|
494
|
+
try:
|
|
495
|
+
# Try to connect to health endpoint
|
|
496
|
+
response = requests.get(
|
|
497
|
+
f"http://{self.host}:{self.port}/health", timeout=2
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
if response.status_code == 200:
|
|
501
|
+
self.logger.debug(
|
|
502
|
+
f"Health check passed on attempt {attempt + 1}/{max_attempts}"
|
|
503
|
+
)
|
|
504
|
+
return True
|
|
505
|
+
|
|
506
|
+
self.logger.debug(
|
|
507
|
+
f"Health check returned status {response.status_code} on attempt {attempt + 1}/{max_attempts}"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
except requests.exceptions.RequestException as e:
|
|
511
|
+
self.logger.debug(
|
|
512
|
+
f"Health check attempt {attempt + 1}/{max_attempts} failed: {e}"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Wait before retry (except on last attempt)
|
|
516
|
+
if attempt < max_attempts - 1:
|
|
517
|
+
time.sleep(1)
|
|
518
|
+
|
|
519
|
+
self.logger.debug(f"Health check failed after {max_attempts} attempts")
|
|
520
|
+
return False
|
|
521
|
+
|
|
522
|
+
except ImportError:
|
|
523
|
+
# requests not available, skip health check
|
|
524
|
+
self.logger.debug("requests library not available, skipping health check")
|
|
525
|
+
return True
|
|
526
|
+
except Exception as e:
|
|
527
|
+
self.logger.debug(f"Health check error: {e}")
|
|
528
|
+
return False
|
|
529
|
+
|
|
474
530
|
def start_daemon(self, force_restart: bool = False) -> bool:
|
|
475
531
|
"""Start the daemon with automatic cleanup and retry.
|
|
476
532
|
|
|
@@ -505,7 +561,7 @@ class DaemonManager:
|
|
|
505
561
|
# Use subprocess for clean daemon startup (v4.2.40)
|
|
506
562
|
# This avoids fork() issues with Python threading
|
|
507
563
|
if self.use_subprocess_daemon():
|
|
508
|
-
return self.start_daemon_subprocess()
|
|
564
|
+
return self.start_daemon_subprocess(force_restart=force_restart)
|
|
509
565
|
# Fallback to traditional fork (kept for compatibility)
|
|
510
566
|
return self.daemonize()
|
|
511
567
|
|
|
@@ -523,12 +579,15 @@ class DaemonManager:
|
|
|
523
579
|
# Otherwise, use subprocess for monitor daemon to avoid threading issues
|
|
524
580
|
return True
|
|
525
581
|
|
|
526
|
-
def start_daemon_subprocess(self) -> bool:
|
|
582
|
+
def start_daemon_subprocess(self, force_restart: bool = False) -> bool:
|
|
527
583
|
"""Start daemon using subprocess.Popen for clean process isolation.
|
|
528
584
|
|
|
529
585
|
This avoids all the fork() + threading issues by starting the monitor
|
|
530
586
|
in a completely fresh process with no inherited threads or locks.
|
|
531
587
|
|
|
588
|
+
Args:
|
|
589
|
+
force_restart: Whether this is a force restart (helps interpret exit codes)
|
|
590
|
+
|
|
532
591
|
Returns:
|
|
533
592
|
True if daemon started successfully
|
|
534
593
|
"""
|
|
@@ -588,36 +647,90 @@ class DaemonManager:
|
|
|
588
647
|
pid = process.pid
|
|
589
648
|
self.logger.info(f"Monitor subprocess started with PID {pid}")
|
|
590
649
|
|
|
591
|
-
# Wait for the subprocess to write its PID file
|
|
650
|
+
# Wait for the subprocess to write its PID file and bind to port
|
|
592
651
|
# The subprocess will write the PID file after it starts successfully
|
|
593
652
|
max_wait = 10 # seconds
|
|
594
653
|
start_time = time.time()
|
|
654
|
+
pid_file_found = False
|
|
655
|
+
port_bound = False
|
|
656
|
+
|
|
657
|
+
self.logger.debug(f"Waiting up to {max_wait}s for daemon to start...")
|
|
595
658
|
|
|
596
659
|
while time.time() - start_time < max_wait:
|
|
597
660
|
# Check if process is still running
|
|
598
|
-
|
|
599
|
-
|
|
661
|
+
returncode = process.poll()
|
|
662
|
+
if returncode is not None:
|
|
663
|
+
# Process exited - interpret exit code with context
|
|
664
|
+
# Exit codes 137 (SIGKILL) and 143 (SIGTERM) are common during daemon replacement
|
|
665
|
+
if returncode == EXIT_SIGKILL:
|
|
666
|
+
# SIGKILL - process was forcefully terminated
|
|
667
|
+
if force_restart:
|
|
668
|
+
# This is expected during force restart - old daemon was killed
|
|
669
|
+
self.logger.info(
|
|
670
|
+
f"Previous monitor instance replaced (exit {EXIT_SIGKILL}: SIGKILL during force restart)"
|
|
671
|
+
)
|
|
672
|
+
else:
|
|
673
|
+
# Unexpected SIGKILL - something else killed our new daemon
|
|
674
|
+
self.logger.warning(
|
|
675
|
+
f"Monitor subprocess terminated unexpectedly (exit {EXIT_SIGKILL}: SIGKILL). "
|
|
676
|
+
f"Check {self.log_file} for details."
|
|
677
|
+
)
|
|
678
|
+
return False
|
|
679
|
+
if returncode == EXIT_SIGTERM:
|
|
680
|
+
# SIGTERM - graceful shutdown requested
|
|
681
|
+
self.logger.info(
|
|
682
|
+
f"Monitor subprocess cleanly terminated (exit {EXIT_SIGTERM}: SIGTERM, graceful shutdown)"
|
|
683
|
+
)
|
|
684
|
+
return False
|
|
685
|
+
if returncode == EXIT_NORMAL:
|
|
686
|
+
# Normal exit
|
|
687
|
+
self.logger.info(
|
|
688
|
+
f"Monitor subprocess exited normally (exit code {EXIT_NORMAL})"
|
|
689
|
+
)
|
|
690
|
+
return False
|
|
691
|
+
# Unexpected exit code - this IS an error
|
|
692
|
+
self.logger.error(
|
|
693
|
+
f"Monitor daemon subprocess exited prematurely with code {returncode}"
|
|
694
|
+
)
|
|
600
695
|
self.logger.error(
|
|
601
|
-
f"
|
|
696
|
+
f"Port {self.port} daemon failed to start. Check {self.log_file} for details."
|
|
602
697
|
)
|
|
603
698
|
return False
|
|
604
699
|
|
|
605
700
|
# Check if PID file was written
|
|
606
|
-
if self.pid_file.exists():
|
|
701
|
+
if not pid_file_found and self.pid_file.exists():
|
|
607
702
|
try:
|
|
608
703
|
with self.pid_file.open() as f:
|
|
609
704
|
written_pid = int(f.read().strip())
|
|
610
705
|
if written_pid == pid:
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
706
|
+
pid_file_found = True
|
|
707
|
+
self.logger.debug(
|
|
708
|
+
f"PID file found with correct PID {pid}"
|
|
709
|
+
)
|
|
710
|
+
except Exception as e:
|
|
711
|
+
self.logger.debug(f"Error reading PID file: {e}")
|
|
712
|
+
|
|
713
|
+
# Check if port is bound (health check)
|
|
714
|
+
if not port_bound and not self._is_port_available():
|
|
715
|
+
# Port NOT available means it's in use (good!)
|
|
716
|
+
port_bound = True
|
|
717
|
+
self.logger.debug(f"Port {self.port} is now bound")
|
|
718
|
+
|
|
719
|
+
# Success criteria: both PID file exists and port is bound
|
|
720
|
+
if pid_file_found and port_bound:
|
|
721
|
+
self.logger.info(
|
|
722
|
+
f"Monitor daemon successfully started on port {self.port} (PID: {pid})"
|
|
723
|
+
)
|
|
724
|
+
# Additional health check: verify we can connect
|
|
725
|
+
if self._verify_daemon_health(max_attempts=3):
|
|
726
|
+
self.logger.info("Daemon health check passed")
|
|
727
|
+
return True
|
|
728
|
+
self.logger.warning(
|
|
729
|
+
"Daemon started but health check failed - may still be initializing"
|
|
730
|
+
)
|
|
731
|
+
return (
|
|
732
|
+
True # Return success anyway if PID file and port are good
|
|
733
|
+
)
|
|
621
734
|
|
|
622
735
|
time.sleep(0.5)
|
|
623
736
|
|