claude-mpm 4.13.2__py3-none-any.whl → 4.18.2__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/agents/BASE_ENGINEER.md +286 -0
- claude_mpm/agents/BASE_PM.md +48 -17
- claude_mpm/agents/OUTPUT_STYLE.md +329 -11
- claude_mpm/agents/PM_INSTRUCTIONS.md +227 -8
- claude_mpm/agents/agent_loader.py +17 -5
- claude_mpm/agents/frontmatter_validator.py +284 -253
- claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
- claude_mpm/agents/templates/api_qa.json +7 -1
- claude_mpm/agents/templates/clerk-ops.json +8 -1
- claude_mpm/agents/templates/code_analyzer.json +4 -1
- claude_mpm/agents/templates/dart_engineer.json +11 -1
- claude_mpm/agents/templates/data_engineer.json +11 -1
- claude_mpm/agents/templates/documentation.json +6 -1
- claude_mpm/agents/templates/engineer.json +18 -1
- claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
- claude_mpm/agents/templates/golang_engineer.json +11 -1
- claude_mpm/agents/templates/java_engineer.json +12 -2
- claude_mpm/agents/templates/local_ops_agent.json +1217 -6
- claude_mpm/agents/templates/nextjs_engineer.json +11 -1
- claude_mpm/agents/templates/ops.json +8 -1
- claude_mpm/agents/templates/php-engineer.json +11 -1
- claude_mpm/agents/templates/project_organizer.json +10 -3
- claude_mpm/agents/templates/prompt-engineer.json +5 -1
- claude_mpm/agents/templates/python_engineer.json +11 -1
- claude_mpm/agents/templates/qa.json +7 -1
- claude_mpm/agents/templates/react_engineer.json +11 -1
- claude_mpm/agents/templates/refactoring_engineer.json +8 -1
- claude_mpm/agents/templates/research.json +4 -1
- claude_mpm/agents/templates/ruby-engineer.json +11 -1
- claude_mpm/agents/templates/rust_engineer.json +11 -1
- claude_mpm/agents/templates/security.json +6 -1
- claude_mpm/agents/templates/svelte-engineer.json +225 -0
- claude_mpm/agents/templates/ticketing.json +6 -1
- claude_mpm/agents/templates/typescript_engineer.json +11 -1
- claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
- claude_mpm/agents/templates/version_control.json +8 -1
- claude_mpm/agents/templates/web_qa.json +7 -1
- claude_mpm/agents/templates/web_ui.json +11 -1
- claude_mpm/cli/__init__.py +34 -706
- claude_mpm/cli/commands/agent_manager.py +25 -12
- claude_mpm/cli/commands/agent_state_manager.py +186 -0
- claude_mpm/cli/commands/agents.py +204 -148
- claude_mpm/cli/commands/aggregate.py +7 -3
- claude_mpm/cli/commands/analyze.py +9 -4
- claude_mpm/cli/commands/analyze_code.py +7 -2
- claude_mpm/cli/commands/auto_configure.py +7 -9
- claude_mpm/cli/commands/config.py +47 -13
- claude_mpm/cli/commands/configure.py +294 -1788
- claude_mpm/cli/commands/configure_agent_display.py +261 -0
- claude_mpm/cli/commands/configure_behavior_manager.py +204 -0
- claude_mpm/cli/commands/configure_hook_manager.py +225 -0
- claude_mpm/cli/commands/configure_models.py +18 -0
- claude_mpm/cli/commands/configure_navigation.py +167 -0
- claude_mpm/cli/commands/configure_paths.py +104 -0
- claude_mpm/cli/commands/configure_persistence.py +254 -0
- claude_mpm/cli/commands/configure_startup_manager.py +646 -0
- claude_mpm/cli/commands/configure_template_editor.py +497 -0
- claude_mpm/cli/commands/configure_validators.py +73 -0
- claude_mpm/cli/commands/local_deploy.py +537 -0
- claude_mpm/cli/commands/memory.py +54 -20
- claude_mpm/cli/commands/mpm_init.py +39 -25
- claude_mpm/cli/commands/mpm_init_handler.py +8 -3
- claude_mpm/cli/executor.py +202 -0
- claude_mpm/cli/helpers.py +105 -0
- claude_mpm/cli/interactive/__init__.py +3 -0
- claude_mpm/cli/interactive/skills_wizard.py +491 -0
- claude_mpm/cli/parsers/__init__.py +7 -1
- claude_mpm/cli/parsers/base_parser.py +98 -3
- claude_mpm/cli/parsers/local_deploy_parser.py +227 -0
- claude_mpm/cli/shared/output_formatters.py +28 -19
- claude_mpm/cli/startup.py +481 -0
- claude_mpm/cli/utils.py +52 -1
- claude_mpm/commands/mpm-help.md +3 -0
- claude_mpm/commands/mpm-version.md +113 -0
- claude_mpm/commands/mpm.md +1 -0
- claude_mpm/config/agent_config.py +2 -2
- claude_mpm/config/model_config.py +428 -0
- claude_mpm/core/base_service.py +13 -12
- claude_mpm/core/enums.py +452 -0
- claude_mpm/core/factories.py +1 -1
- claude_mpm/core/instruction_reinforcement_hook.py +2 -1
- claude_mpm/core/interactive_session.py +9 -3
- claude_mpm/core/logging_config.py +6 -2
- claude_mpm/core/oneshot_session.py +8 -4
- claude_mpm/core/optimized_agent_loader.py +3 -3
- claude_mpm/core/output_style_manager.py +12 -192
- claude_mpm/core/service_registry.py +5 -1
- claude_mpm/core/types.py +2 -9
- claude_mpm/core/typing_utils.py +7 -6
- claude_mpm/dashboard/static/js/dashboard.js +0 -14
- claude_mpm/dashboard/templates/index.html +3 -41
- claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
- claude_mpm/hooks/instruction_reinforcement.py +7 -2
- claude_mpm/models/resume_log.py +340 -0
- claude_mpm/services/agents/auto_config_manager.py +10 -11
- claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
- claude_mpm/services/agents/deployment/agent_validator.py +17 -1
- claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
- claude_mpm/services/agents/deployment/interface_adapter.py +3 -2
- claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +7 -6
- claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +7 -16
- claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +4 -3
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +5 -3
- claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +6 -5
- claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +9 -6
- claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
- claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
- claude_mpm/services/agents/local_template_manager.py +1 -1
- claude_mpm/services/agents/memory/agent_memory_manager.py +5 -2
- claude_mpm/services/agents/registry/modification_tracker.py +5 -2
- claude_mpm/services/command_handler_service.py +11 -5
- claude_mpm/services/core/interfaces/__init__.py +74 -2
- claude_mpm/services/core/interfaces/health.py +172 -0
- claude_mpm/services/core/interfaces/model.py +281 -0
- claude_mpm/services/core/interfaces/process.py +372 -0
- claude_mpm/services/core/interfaces/restart.py +307 -0
- claude_mpm/services/core/interfaces/stability.py +260 -0
- claude_mpm/services/core/models/__init__.py +33 -0
- claude_mpm/services/core/models/agent_config.py +12 -28
- claude_mpm/services/core/models/health.py +162 -0
- claude_mpm/services/core/models/process.py +235 -0
- claude_mpm/services/core/models/restart.py +302 -0
- claude_mpm/services/core/models/stability.py +264 -0
- claude_mpm/services/core/path_resolver.py +23 -7
- claude_mpm/services/diagnostics/__init__.py +2 -2
- claude_mpm/services/diagnostics/checks/agent_check.py +25 -24
- claude_mpm/services/diagnostics/checks/claude_code_check.py +24 -23
- claude_mpm/services/diagnostics/checks/common_issues_check.py +25 -24
- claude_mpm/services/diagnostics/checks/configuration_check.py +24 -23
- claude_mpm/services/diagnostics/checks/filesystem_check.py +18 -17
- claude_mpm/services/diagnostics/checks/installation_check.py +30 -29
- claude_mpm/services/diagnostics/checks/instructions_check.py +20 -19
- claude_mpm/services/diagnostics/checks/mcp_check.py +50 -36
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +36 -31
- claude_mpm/services/diagnostics/checks/monitor_check.py +23 -22
- claude_mpm/services/diagnostics/checks/startup_log_check.py +9 -8
- claude_mpm/services/diagnostics/diagnostic_runner.py +6 -5
- claude_mpm/services/diagnostics/doctor_reporter.py +28 -25
- claude_mpm/services/diagnostics/models.py +19 -24
- claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
- claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
- claude_mpm/services/infrastructure/monitoring/base.py +5 -13
- claude_mpm/services/infrastructure/monitoring/network.py +7 -6
- claude_mpm/services/infrastructure/monitoring/process.py +13 -12
- claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
- claude_mpm/services/infrastructure/monitoring/service.py +16 -15
- claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
- claude_mpm/services/local_ops/__init__.py +163 -0
- claude_mpm/services/local_ops/crash_detector.py +257 -0
- claude_mpm/services/local_ops/health_checks/__init__.py +28 -0
- claude_mpm/services/local_ops/health_checks/http_check.py +224 -0
- claude_mpm/services/local_ops/health_checks/process_check.py +236 -0
- claude_mpm/services/local_ops/health_checks/resource_check.py +255 -0
- claude_mpm/services/local_ops/health_manager.py +430 -0
- claude_mpm/services/local_ops/log_monitor.py +396 -0
- claude_mpm/services/local_ops/memory_leak_detector.py +294 -0
- claude_mpm/services/local_ops/process_manager.py +595 -0
- claude_mpm/services/local_ops/resource_monitor.py +331 -0
- claude_mpm/services/local_ops/restart_manager.py +401 -0
- claude_mpm/services/local_ops/restart_policy.py +387 -0
- claude_mpm/services/local_ops/state_manager.py +372 -0
- claude_mpm/services/local_ops/unified_manager.py +600 -0
- claude_mpm/services/mcp_config_manager.py +9 -4
- claude_mpm/services/mcp_gateway/core/__init__.py +1 -2
- claude_mpm/services/mcp_gateway/core/base.py +18 -31
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +71 -24
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +30 -28
- claude_mpm/services/memory_hook_service.py +4 -1
- claude_mpm/services/model/__init__.py +147 -0
- claude_mpm/services/model/base_provider.py +365 -0
- claude_mpm/services/model/claude_provider.py +412 -0
- claude_mpm/services/model/model_router.py +453 -0
- claude_mpm/services/model/ollama_provider.py +415 -0
- claude_mpm/services/monitor/daemon_manager.py +3 -2
- claude_mpm/services/monitor/handlers/dashboard.py +2 -1
- claude_mpm/services/monitor/handlers/hooks.py +2 -1
- claude_mpm/services/monitor/management/lifecycle.py +3 -2
- claude_mpm/services/monitor/server.py +2 -1
- claude_mpm/services/session_management_service.py +3 -2
- claude_mpm/services/session_manager.py +205 -1
- claude_mpm/services/shared/async_service_base.py +16 -27
- claude_mpm/services/shared/lifecycle_service_base.py +1 -14
- claude_mpm/services/socketio/handlers/__init__.py +5 -2
- claude_mpm/services/socketio/handlers/hook.py +13 -2
- claude_mpm/services/socketio/handlers/registry.py +4 -2
- claude_mpm/services/socketio/server/main.py +10 -8
- claude_mpm/services/subprocess_launcher_service.py +14 -5
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +8 -7
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +6 -5
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +8 -7
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +7 -6
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +5 -4
- claude_mpm/services/unified/config_strategies/validation_strategy.py +13 -9
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +10 -3
- claude_mpm/services/unified/deployment_strategies/local.py +6 -5
- claude_mpm/services/unified/deployment_strategies/utils.py +6 -5
- claude_mpm/services/unified/deployment_strategies/vercel.py +7 -6
- claude_mpm/services/unified/interfaces.py +3 -1
- claude_mpm/services/unified/unified_analyzer.py +14 -10
- claude_mpm/services/unified/unified_config.py +2 -1
- claude_mpm/services/unified/unified_deployment.py +9 -4
- claude_mpm/services/version_service.py +104 -1
- claude_mpm/skills/__init__.py +21 -0
- claude_mpm/skills/bundled/__init__.py +6 -0
- claude_mpm/skills/bundled/api-documentation.md +393 -0
- claude_mpm/skills/bundled/async-testing.md +571 -0
- claude_mpm/skills/bundled/code-review.md +143 -0
- claude_mpm/skills/bundled/database-migration.md +199 -0
- claude_mpm/skills/bundled/docker-containerization.md +194 -0
- claude_mpm/skills/bundled/express-local-dev.md +1429 -0
- claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
- claude_mpm/skills/bundled/git-workflow.md +414 -0
- claude_mpm/skills/bundled/imagemagick.md +204 -0
- claude_mpm/skills/bundled/json-data-handling.md +223 -0
- claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
- claude_mpm/skills/bundled/pdf.md +141 -0
- claude_mpm/skills/bundled/performance-profiling.md +567 -0
- claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
- claude_mpm/skills/bundled/security-scanning.md +327 -0
- claude_mpm/skills/bundled/systematic-debugging.md +473 -0
- claude_mpm/skills/bundled/test-driven-development.md +378 -0
- claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
- claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
- claude_mpm/skills/bundled/xlsx.md +157 -0
- claude_mpm/skills/registry.py +286 -0
- claude_mpm/skills/skill_manager.py +310 -0
- claude_mpm/tools/code_tree_analyzer.py +177 -141
- claude_mpm/tools/code_tree_events.py +4 -2
- claude_mpm/utils/agent_dependency_loader.py +2 -2
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/METADATA +117 -8
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/RECORD +238 -174
- claude_mpm/dashboard/static/css/code-tree.css +0 -1639
- claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
- claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
- claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
- claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
- claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
- claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +0 -425
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +0 -1041
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +0 -347
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +0 -575
- claude_mpm/services/project/analyzer_refactored.py +0 -450
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/WHEEL +0 -0
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local Process Manager for Claude MPM Framework
|
|
3
|
+
==============================================
|
|
4
|
+
|
|
5
|
+
WHY: Provides reliable process lifecycle management for local deployments
|
|
6
|
+
with process isolation, port conflict prevention, and graceful shutdown.
|
|
7
|
+
|
|
8
|
+
DESIGN DECISION: Uses subprocess.Popen for direct process control with
|
|
9
|
+
process groups for clean termination. Integrates with DeploymentStateManager
|
|
10
|
+
for persistent tracking.
|
|
11
|
+
|
|
12
|
+
ARCHITECTURE:
|
|
13
|
+
- Process group isolation (start_new_session=True on Unix)
|
|
14
|
+
- Port conflict detection using psutil
|
|
15
|
+
- Linear probing for alternative ports
|
|
16
|
+
- Protected port range enforcement
|
|
17
|
+
- Graceful shutdown with timeout and force kill fallback
|
|
18
|
+
|
|
19
|
+
USAGE:
|
|
20
|
+
state_manager = DeploymentStateManager(state_file_path)
|
|
21
|
+
process_manager = LocalProcessManager(state_manager)
|
|
22
|
+
|
|
23
|
+
config = StartConfig(
|
|
24
|
+
command=["npm", "run", "dev"],
|
|
25
|
+
working_directory="/path/to/project",
|
|
26
|
+
port=3000
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
deployment = process_manager.start(config)
|
|
30
|
+
process_manager.stop(deployment.deployment_id)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import platform
|
|
35
|
+
import signal
|
|
36
|
+
import subprocess
|
|
37
|
+
import time
|
|
38
|
+
from datetime import datetime, timezone
|
|
39
|
+
from hashlib import sha256
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import List, Optional
|
|
42
|
+
|
|
43
|
+
import psutil
|
|
44
|
+
|
|
45
|
+
from claude_mpm.core.enums import ServiceState
|
|
46
|
+
from claude_mpm.services.core.base import SyncBaseService
|
|
47
|
+
from claude_mpm.services.core.interfaces.process import (
|
|
48
|
+
IDeploymentStateManager,
|
|
49
|
+
ILocalProcessManager,
|
|
50
|
+
)
|
|
51
|
+
from claude_mpm.services.core.models.process import (
|
|
52
|
+
DeploymentState,
|
|
53
|
+
ProcessInfo,
|
|
54
|
+
StartConfig,
|
|
55
|
+
is_port_protected,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ProcessSpawnError(Exception):
|
|
60
|
+
"""Raised when process cannot be spawned."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PortConflictError(Exception):
|
|
64
|
+
"""Raised when requested port is unavailable and no alternative found."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class LocalProcessManager(SyncBaseService, ILocalProcessManager):
|
|
68
|
+
"""
|
|
69
|
+
Manages local process lifecycle with isolation and state tracking.
|
|
70
|
+
|
|
71
|
+
WHY: Provides high-level process management operations that handle
|
|
72
|
+
all the complexity of spawning, tracking, and terminating background
|
|
73
|
+
processes reliably.
|
|
74
|
+
|
|
75
|
+
Thread Safety: Operations are thread-safe through state manager locking.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, state_manager: IDeploymentStateManager):
|
|
79
|
+
"""
|
|
80
|
+
Initialize process manager.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
state_manager: State manager for deployment persistence
|
|
84
|
+
"""
|
|
85
|
+
super().__init__("LocalProcessManager")
|
|
86
|
+
self.state_manager = state_manager
|
|
87
|
+
self.is_windows = platform.system() == "Windows"
|
|
88
|
+
|
|
89
|
+
def initialize(self) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Initialize the process manager.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True if initialization successful
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
# Ensure state manager is initialized
|
|
98
|
+
if not self.state_manager.is_initialized:
|
|
99
|
+
if not self.state_manager.initialize():
|
|
100
|
+
self.log_error("Failed to initialize state manager")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
self._initialized = True
|
|
104
|
+
self.log_info("Process manager initialized")
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
self.log_error(f"Failed to initialize: {e}")
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def shutdown(self) -> None:
|
|
112
|
+
"""Shutdown process manager (processes continue running)."""
|
|
113
|
+
self._shutdown = True
|
|
114
|
+
self.log_info("Process manager shutdown complete")
|
|
115
|
+
|
|
116
|
+
def start(self, config: StartConfig) -> DeploymentState:
|
|
117
|
+
"""
|
|
118
|
+
Start a new background process.
|
|
119
|
+
|
|
120
|
+
WHY: Combines process spawning, port allocation, and state tracking
|
|
121
|
+
in a single atomic operation.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
config: Configuration for the process to start
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
DeploymentState with process information
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ProcessSpawnError: If process cannot be spawned
|
|
131
|
+
PortConflictError: If port unavailable and no alternative found
|
|
132
|
+
ValueError: If configuration is invalid
|
|
133
|
+
"""
|
|
134
|
+
# Validate working directory exists
|
|
135
|
+
working_dir = Path(config.working_directory)
|
|
136
|
+
if not working_dir.exists():
|
|
137
|
+
raise ValueError(f"Working directory does not exist: {working_dir}")
|
|
138
|
+
|
|
139
|
+
# Handle port allocation if needed
|
|
140
|
+
allocated_port = None
|
|
141
|
+
if config.port is not None:
|
|
142
|
+
allocated_port = self._allocate_port(config.port, config.auto_find_port)
|
|
143
|
+
|
|
144
|
+
# Generate deployment ID if not provided
|
|
145
|
+
project_name = working_dir.name
|
|
146
|
+
deployment_id = config.deployment_id or self.generate_deployment_id(
|
|
147
|
+
project_name, allocated_port
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Prepare environment variables
|
|
151
|
+
env = os.environ.copy()
|
|
152
|
+
env.update(config.environment)
|
|
153
|
+
if allocated_port is not None:
|
|
154
|
+
env["PORT"] = str(allocated_port)
|
|
155
|
+
|
|
156
|
+
# Spawn the process
|
|
157
|
+
try:
|
|
158
|
+
self.log_info(
|
|
159
|
+
f"Spawning process for {deployment_id}: {' '.join(config.command)}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Platform-specific process group creation
|
|
163
|
+
if self.is_windows:
|
|
164
|
+
# Windows: use CREATE_NEW_PROCESS_GROUP
|
|
165
|
+
process = subprocess.Popen(
|
|
166
|
+
config.command,
|
|
167
|
+
cwd=str(working_dir),
|
|
168
|
+
env=env,
|
|
169
|
+
stdout=subprocess.PIPE,
|
|
170
|
+
stderr=subprocess.PIPE,
|
|
171
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
# Unix: use start_new_session for process group isolation
|
|
175
|
+
process = subprocess.Popen(
|
|
176
|
+
config.command,
|
|
177
|
+
cwd=str(working_dir),
|
|
178
|
+
env=env,
|
|
179
|
+
stdout=subprocess.PIPE,
|
|
180
|
+
stderr=subprocess.PIPE,
|
|
181
|
+
start_new_session=True,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Give process a moment to start
|
|
185
|
+
time.sleep(0.5)
|
|
186
|
+
|
|
187
|
+
# Check if process is still running
|
|
188
|
+
if process.poll() is not None:
|
|
189
|
+
# Process died immediately
|
|
190
|
+
stdout, stderr = process.communicate()
|
|
191
|
+
error_msg = stderr.decode("utf-8", errors="replace") if stderr else ""
|
|
192
|
+
raise ProcessSpawnError(
|
|
193
|
+
f"Process died immediately. Exit code: {process.returncode}. "
|
|
194
|
+
f"Error: {error_msg}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Create deployment state
|
|
198
|
+
deployment = DeploymentState(
|
|
199
|
+
deployment_id=deployment_id,
|
|
200
|
+
process_id=process.pid,
|
|
201
|
+
command=config.command,
|
|
202
|
+
working_directory=str(working_dir),
|
|
203
|
+
environment=config.environment,
|
|
204
|
+
port=allocated_port,
|
|
205
|
+
started_at=datetime.now(tz=timezone.utc),
|
|
206
|
+
status=ServiceState.RUNNING,
|
|
207
|
+
metadata=config.metadata,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Save to state
|
|
211
|
+
self.state_manager.add_deployment(deployment)
|
|
212
|
+
|
|
213
|
+
self.log_info(
|
|
214
|
+
f"Started process {process.pid} for {deployment_id} "
|
|
215
|
+
f"on port {allocated_port or 'N/A'}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return deployment
|
|
219
|
+
|
|
220
|
+
except subprocess.SubprocessError as e:
|
|
221
|
+
raise ProcessSpawnError(f"Failed to spawn process: {e}") from e
|
|
222
|
+
|
|
223
|
+
def stop(self, deployment_id: str, timeout: int = 10, force: bool = False) -> bool:
|
|
224
|
+
"""
|
|
225
|
+
Stop a running process.
|
|
226
|
+
|
|
227
|
+
WHY: Provides graceful shutdown with configurable timeout and
|
|
228
|
+
force kill fallback for stuck processes.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
deployment_id: Unique deployment identifier
|
|
232
|
+
timeout: Seconds to wait for graceful shutdown
|
|
233
|
+
force: If True, kill immediately without waiting
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if process stopped successfully
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
ValueError: If deployment_id not found
|
|
240
|
+
"""
|
|
241
|
+
deployment = self.state_manager.get_deployment(deployment_id)
|
|
242
|
+
if not deployment:
|
|
243
|
+
raise ValueError(f"Deployment not found: {deployment_id}")
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
process = psutil.Process(deployment.process_id)
|
|
247
|
+
except psutil.NoSuchProcess:
|
|
248
|
+
# Process already dead, just update state
|
|
249
|
+
self.log_info(f"Process {deployment.process_id} already dead")
|
|
250
|
+
self.state_manager.update_deployment_status(
|
|
251
|
+
deployment_id, ServiceState.STOPPED
|
|
252
|
+
)
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
self.log_info(f"Stopping process {deployment.process_id} for {deployment_id}")
|
|
256
|
+
self.state_manager.update_deployment_status(
|
|
257
|
+
deployment_id, ServiceState.STOPPING
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
if force:
|
|
262
|
+
# Force kill immediately
|
|
263
|
+
self._kill_process_group(process)
|
|
264
|
+
self.state_manager.update_deployment_status(
|
|
265
|
+
deployment_id, ServiceState.STOPPED
|
|
266
|
+
)
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
# Try graceful shutdown first
|
|
270
|
+
self._terminate_process_group(process)
|
|
271
|
+
|
|
272
|
+
# Wait for process to die
|
|
273
|
+
start_time = time.time()
|
|
274
|
+
while time.time() - start_time < timeout:
|
|
275
|
+
if not process.is_running():
|
|
276
|
+
self.log_info(f"Process {deployment.process_id} stopped gracefully")
|
|
277
|
+
self.state_manager.update_deployment_status(
|
|
278
|
+
deployment_id, ServiceState.STOPPED
|
|
279
|
+
)
|
|
280
|
+
return True
|
|
281
|
+
time.sleep(0.1)
|
|
282
|
+
|
|
283
|
+
# Timeout exceeded, force kill
|
|
284
|
+
self.log_warning(
|
|
285
|
+
f"Graceful shutdown timeout, force killing {deployment.process_id}"
|
|
286
|
+
)
|
|
287
|
+
self._kill_process_group(process)
|
|
288
|
+
self.state_manager.update_deployment_status(
|
|
289
|
+
deployment_id, ServiceState.STOPPED
|
|
290
|
+
)
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
except psutil.NoSuchProcess:
|
|
294
|
+
# Process died during shutdown
|
|
295
|
+
self.state_manager.update_deployment_status(
|
|
296
|
+
deployment_id, ServiceState.STOPPED
|
|
297
|
+
)
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
self.log_error(f"Error stopping process: {e}")
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
def restart(self, deployment_id: str, timeout: int = 10) -> DeploymentState:
|
|
305
|
+
"""
|
|
306
|
+
Restart a process (stop then start with same config).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
deployment_id: Unique deployment identifier
|
|
310
|
+
timeout: Seconds to wait for graceful shutdown
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
New DeploymentState after restart
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
ValueError: If deployment_id not found
|
|
317
|
+
ProcessSpawnError: If restart fails
|
|
318
|
+
"""
|
|
319
|
+
# Get existing deployment config
|
|
320
|
+
deployment = self.state_manager.get_deployment(deployment_id)
|
|
321
|
+
if not deployment:
|
|
322
|
+
raise ValueError(f"Deployment not found: {deployment_id}")
|
|
323
|
+
|
|
324
|
+
# Stop the process
|
|
325
|
+
self.stop(deployment_id, timeout=timeout)
|
|
326
|
+
|
|
327
|
+
# Create new start config from existing deployment
|
|
328
|
+
config = StartConfig(
|
|
329
|
+
command=deployment.command,
|
|
330
|
+
working_directory=deployment.working_directory,
|
|
331
|
+
environment=deployment.environment,
|
|
332
|
+
port=deployment.port,
|
|
333
|
+
auto_find_port=True,
|
|
334
|
+
metadata=deployment.metadata,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Remove old deployment from state
|
|
338
|
+
self.state_manager.remove_deployment(deployment_id)
|
|
339
|
+
|
|
340
|
+
# Start new process
|
|
341
|
+
return self.start(config)
|
|
342
|
+
|
|
343
|
+
def get_status(self, deployment_id: str) -> Optional[ProcessInfo]:
|
|
344
|
+
"""
|
|
345
|
+
Get current status and runtime information for a process.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
deployment_id: Unique deployment identifier
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
ProcessInfo with current status, or None if not found
|
|
352
|
+
"""
|
|
353
|
+
deployment = self.state_manager.get_deployment(deployment_id)
|
|
354
|
+
if not deployment:
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
process = psutil.Process(deployment.process_id)
|
|
359
|
+
|
|
360
|
+
# Calculate uptime
|
|
361
|
+
create_time = process.create_time()
|
|
362
|
+
uptime = time.time() - create_time
|
|
363
|
+
|
|
364
|
+
# Get memory usage
|
|
365
|
+
memory_info = process.memory_info()
|
|
366
|
+
memory_mb = memory_info.rss / (1024 * 1024)
|
|
367
|
+
|
|
368
|
+
# Get CPU usage
|
|
369
|
+
cpu_percent = process.cpu_percent(interval=0.1)
|
|
370
|
+
|
|
371
|
+
# Determine status
|
|
372
|
+
if process.is_running():
|
|
373
|
+
status = ServiceState.RUNNING
|
|
374
|
+
else:
|
|
375
|
+
status = ServiceState.STOPPED
|
|
376
|
+
|
|
377
|
+
return ProcessInfo(
|
|
378
|
+
deployment_id=deployment_id,
|
|
379
|
+
process_id=deployment.process_id,
|
|
380
|
+
status=status,
|
|
381
|
+
port=deployment.port,
|
|
382
|
+
uptime_seconds=uptime,
|
|
383
|
+
memory_mb=memory_mb,
|
|
384
|
+
cpu_percent=cpu_percent,
|
|
385
|
+
is_responding=True, # TODO: Add actual health check
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
except psutil.NoSuchProcess:
|
|
389
|
+
return ProcessInfo(
|
|
390
|
+
deployment_id=deployment_id,
|
|
391
|
+
process_id=deployment.process_id,
|
|
392
|
+
status=ServiceState.ERROR, # CRASHED semantically maps to ERROR state
|
|
393
|
+
port=deployment.port,
|
|
394
|
+
error_message="Process no longer exists",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def list_processes(
|
|
398
|
+
self, status_filter: Optional[ServiceState] = None
|
|
399
|
+
) -> List[ProcessInfo]:
|
|
400
|
+
"""
|
|
401
|
+
List all managed processes.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
status_filter: Optional status to filter by
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
List of ProcessInfo for all matching processes
|
|
408
|
+
"""
|
|
409
|
+
if status_filter:
|
|
410
|
+
deployments = self.state_manager.get_deployments_by_status(status_filter)
|
|
411
|
+
else:
|
|
412
|
+
deployments = self.state_manager.get_all_deployments()
|
|
413
|
+
|
|
414
|
+
process_infos = []
|
|
415
|
+
for deployment in deployments:
|
|
416
|
+
info = self.get_status(deployment.deployment_id)
|
|
417
|
+
if info:
|
|
418
|
+
process_infos.append(info)
|
|
419
|
+
|
|
420
|
+
return process_infos
|
|
421
|
+
|
|
422
|
+
def is_port_available(self, port: int) -> bool:
|
|
423
|
+
"""
|
|
424
|
+
Check if a port is available for use.
|
|
425
|
+
|
|
426
|
+
WHY: Port conflict prevention is critical for reliable deployments.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
port: Port number to check
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
True if port is available
|
|
433
|
+
"""
|
|
434
|
+
# Check if port is protected
|
|
435
|
+
if is_port_protected(port):
|
|
436
|
+
return False
|
|
437
|
+
|
|
438
|
+
# Check if port is in use
|
|
439
|
+
connections = psutil.net_connections()
|
|
440
|
+
return all(conn.laddr.port != port for conn in connections)
|
|
441
|
+
|
|
442
|
+
def find_available_port(
|
|
443
|
+
self, preferred_port: int, max_attempts: int = 10
|
|
444
|
+
) -> Optional[int]:
|
|
445
|
+
"""
|
|
446
|
+
Find an available port starting from preferred_port.
|
|
447
|
+
|
|
448
|
+
WHY: Uses linear probing to find alternative ports when preferred
|
|
449
|
+
port is unavailable. Respects protected port ranges.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
preferred_port: Starting port number
|
|
453
|
+
max_attempts: Maximum number of ports to try
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Available port number, or None if none found
|
|
457
|
+
"""
|
|
458
|
+
for offset in range(max_attempts):
|
|
459
|
+
candidate_port = preferred_port + offset
|
|
460
|
+
|
|
461
|
+
# Skip ports outside valid range
|
|
462
|
+
if candidate_port > 65535:
|
|
463
|
+
break
|
|
464
|
+
|
|
465
|
+
# Check if port is available
|
|
466
|
+
if self.is_port_available(candidate_port):
|
|
467
|
+
if offset > 0:
|
|
468
|
+
self.log_info(
|
|
469
|
+
f"Port {preferred_port} unavailable, using {candidate_port}"
|
|
470
|
+
)
|
|
471
|
+
return candidate_port
|
|
472
|
+
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
def cleanup_orphans(self) -> int:
|
|
476
|
+
"""
|
|
477
|
+
Clean up orphaned process state entries.
|
|
478
|
+
|
|
479
|
+
WHY: Processes may crash or be killed externally, leaving stale state.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Number of orphaned entries cleaned up
|
|
483
|
+
"""
|
|
484
|
+
return self.state_manager.cleanup_dead_pids()
|
|
485
|
+
|
|
486
|
+
def generate_deployment_id(
|
|
487
|
+
self, project_name: str, port: Optional[int] = None
|
|
488
|
+
) -> str:
|
|
489
|
+
"""
|
|
490
|
+
Generate a unique deployment ID.
|
|
491
|
+
|
|
492
|
+
WHY: Provides consistent ID generation with optional port suffix.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
project_name: Name of the project
|
|
496
|
+
port: Optional port number to include in ID
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Unique deployment identifier
|
|
500
|
+
"""
|
|
501
|
+
# Use timestamp for uniqueness
|
|
502
|
+
timestamp = datetime.now(tz=timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
503
|
+
|
|
504
|
+
# Generate short hash from project name for readability
|
|
505
|
+
name_hash = sha256(project_name.encode()).hexdigest()[:8]
|
|
506
|
+
|
|
507
|
+
if port:
|
|
508
|
+
return f"{project_name}_{name_hash}_{timestamp}_p{port}"
|
|
509
|
+
return f"{project_name}_{name_hash}_{timestamp}"
|
|
510
|
+
|
|
511
|
+
def _allocate_port(self, preferred_port: int, auto_find: bool) -> int:
|
|
512
|
+
"""
|
|
513
|
+
Allocate a port for the deployment.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
preferred_port: Preferred port number
|
|
517
|
+
auto_find: If True, find alternative if preferred unavailable
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
Allocated port number
|
|
521
|
+
|
|
522
|
+
Raises:
|
|
523
|
+
PortConflictError: If port unavailable and auto_find is False
|
|
524
|
+
"""
|
|
525
|
+
# Check if preferred port is available
|
|
526
|
+
if self.is_port_available(preferred_port):
|
|
527
|
+
return preferred_port
|
|
528
|
+
|
|
529
|
+
# If auto_find disabled, raise error
|
|
530
|
+
if not auto_find:
|
|
531
|
+
raise PortConflictError(
|
|
532
|
+
f"Port {preferred_port} is unavailable and auto_find_port is disabled"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Find alternative port
|
|
536
|
+
alternative = self.find_available_port(preferred_port)
|
|
537
|
+
if alternative is None:
|
|
538
|
+
raise PortConflictError(
|
|
539
|
+
f"No available ports found starting from {preferred_port}"
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
return alternative
|
|
543
|
+
|
|
544
|
+
def _terminate_process_group(self, process: psutil.Process) -> None:
|
|
545
|
+
"""
|
|
546
|
+
Send SIGTERM to process group for graceful shutdown.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
process: Process to terminate
|
|
550
|
+
"""
|
|
551
|
+
if self.is_windows:
|
|
552
|
+
# Windows: terminate the process tree
|
|
553
|
+
try:
|
|
554
|
+
parent = process
|
|
555
|
+
children = parent.children(recursive=True)
|
|
556
|
+
for child in children:
|
|
557
|
+
child.terminate()
|
|
558
|
+
parent.terminate()
|
|
559
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
560
|
+
pass
|
|
561
|
+
else:
|
|
562
|
+
# Unix: send SIGTERM to process group
|
|
563
|
+
try:
|
|
564
|
+
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
|
565
|
+
except (ProcessLookupError, PermissionError):
|
|
566
|
+
# Fallback to single process
|
|
567
|
+
process.terminate()
|
|
568
|
+
|
|
569
|
+
def _kill_process_group(self, process: psutil.Process) -> None:
|
|
570
|
+
"""
|
|
571
|
+
Send SIGKILL to process group for force termination.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
process: Process to kill
|
|
575
|
+
"""
|
|
576
|
+
if self.is_windows:
|
|
577
|
+
# Windows: kill the process tree
|
|
578
|
+
try:
|
|
579
|
+
parent = process
|
|
580
|
+
children = parent.children(recursive=True)
|
|
581
|
+
for child in children:
|
|
582
|
+
child.kill()
|
|
583
|
+
parent.kill()
|
|
584
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
585
|
+
pass
|
|
586
|
+
else:
|
|
587
|
+
# Unix: send SIGKILL to process group
|
|
588
|
+
try:
|
|
589
|
+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
590
|
+
except (ProcessLookupError, PermissionError):
|
|
591
|
+
# Fallback to single process
|
|
592
|
+
process.kill()
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
__all__ = ["LocalProcessManager", "PortConflictError", "ProcessSpawnError"]
|