crackerjack 0.33.0__py3-none-any.whl → 0.33.1__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 crackerjack might be problematic. Click here for more details.
- crackerjack/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +4 -13
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +104 -204
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +171 -174
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +44 -8
- crackerjack/managers/test_command_builder.py +1 -15
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +98 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +17 -16
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +173 -32
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +8 -10
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +0 -2
- crackerjack/mixins/error_handling.py +1 -70
- crackerjack/models/config.py +12 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +122 -122
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +6 -35
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +15 -9
- crackerjack/services/config_merge.py +19 -80
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +27 -25
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +8 -25
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +11 -30
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +19 -87
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +9 -67
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +18 -59
- crackerjack/services/performance_cache.py +20 -81
- crackerjack/services/performance_monitor.py +27 -95
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +605 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +9 -41
- crackerjack/services/security_logger.py +12 -24
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +196 -25
- crackerjack-0.33.1.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.33.0.dist-info/RECORD +0 -187
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Service watchdog with timeout protection and automatic recovery.
|
|
3
|
-
|
|
4
|
-
This module provides comprehensive monitoring of crackerjack services
|
|
5
|
-
with automatic restart capabilities and hanging prevention.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
1
|
import asyncio
|
|
9
2
|
import contextlib
|
|
10
3
|
import logging
|
|
@@ -24,8 +17,6 @@ logger = logging.getLogger("crackerjack.service_watchdog")
|
|
|
24
17
|
|
|
25
18
|
|
|
26
19
|
class ServiceState(Enum):
|
|
27
|
-
"""Service states."""
|
|
28
|
-
|
|
29
20
|
STOPPED = "stopped"
|
|
30
21
|
STARTING = "starting"
|
|
31
22
|
RUNNING = "running"
|
|
@@ -36,8 +27,6 @@ class ServiceState(Enum):
|
|
|
36
27
|
|
|
37
28
|
@dataclass
|
|
38
29
|
class ServiceConfig:
|
|
39
|
-
"""Configuration for a monitored service."""
|
|
40
|
-
|
|
41
30
|
name: str
|
|
42
31
|
command: list[str]
|
|
43
32
|
health_check_url: str | None = None
|
|
@@ -52,8 +41,6 @@ class ServiceConfig:
|
|
|
52
41
|
|
|
53
42
|
@dataclass
|
|
54
43
|
class ServiceStatus:
|
|
55
|
-
"""Status of a monitored service."""
|
|
56
|
-
|
|
57
44
|
config: ServiceConfig
|
|
58
45
|
state: ServiceState = ServiceState.STOPPED
|
|
59
46
|
process: subprocess.Popen[bytes] | None = None
|
|
@@ -66,14 +53,12 @@ class ServiceStatus:
|
|
|
66
53
|
|
|
67
54
|
@property
|
|
68
55
|
def uptime(self) -> float:
|
|
69
|
-
"""Get service uptime in seconds."""
|
|
70
56
|
if self.state == ServiceState.RUNNING and self.last_start_time > 0:
|
|
71
57
|
return time.time() - self.last_start_time
|
|
72
58
|
return 0.0
|
|
73
59
|
|
|
74
60
|
@property
|
|
75
61
|
def is_healthy(self) -> bool:
|
|
76
|
-
"""Check if service is healthy."""
|
|
77
62
|
return (
|
|
78
63
|
self.state == ServiceState.RUNNING
|
|
79
64
|
and self.process is not None
|
|
@@ -83,8 +68,6 @@ class ServiceStatus:
|
|
|
83
68
|
|
|
84
69
|
|
|
85
70
|
class ServiceWatchdog:
|
|
86
|
-
"""Watchdog for monitoring and managing services with timeout protection."""
|
|
87
|
-
|
|
88
71
|
def __init__(self, console: Console | None = None) -> None:
|
|
89
72
|
self.console = console or Console()
|
|
90
73
|
self.timeout_manager = get_timeout_manager()
|
|
@@ -92,7 +75,6 @@ class ServiceWatchdog:
|
|
|
92
75
|
self.is_running = False
|
|
93
76
|
self.monitor_task: asyncio.Task[None] | None = None
|
|
94
77
|
|
|
95
|
-
# Default service configurations
|
|
96
78
|
self.default_configs = {
|
|
97
79
|
"mcp_server": ServiceConfig(
|
|
98
80
|
name="MCP Server",
|
|
@@ -103,53 +85,55 @@ class ServiceWatchdog:
|
|
|
103
85
|
"websocket_server": ServiceConfig(
|
|
104
86
|
name="WebSocket Server",
|
|
105
87
|
command=["python", "-m", "crackerjack", "--start-websocket-server"],
|
|
106
|
-
health_check_url="http
|
|
88
|
+
health_check_url="http: //localhost: 8675/",
|
|
107
89
|
health_check_timeout=3.0,
|
|
108
90
|
startup_timeout=20.0,
|
|
109
91
|
shutdown_timeout=10.0,
|
|
110
92
|
),
|
|
93
|
+
"zuban_lsp": ServiceConfig(
|
|
94
|
+
name="Zuban LSP Server",
|
|
95
|
+
command=["uv", "run", "zuban", "server"],
|
|
96
|
+
startup_timeout=15.0,
|
|
97
|
+
shutdown_timeout=10.0,
|
|
98
|
+
max_restarts=5,
|
|
99
|
+
restart_delay=5.0,
|
|
100
|
+
restart_backoff_multiplier=2.0,
|
|
101
|
+
max_restart_delay=300.0,
|
|
102
|
+
),
|
|
111
103
|
}
|
|
112
104
|
|
|
113
105
|
def add_service(self, service_id: str, config: ServiceConfig) -> None:
|
|
114
|
-
"""Add a service to monitor."""
|
|
115
106
|
self.services[service_id] = ServiceStatus(config=config)
|
|
116
107
|
logger.info(f"Added service {service_id} to watchdog")
|
|
117
108
|
|
|
118
109
|
def remove_service(self, service_id: str) -> None:
|
|
119
|
-
"""Remove a service from monitoring."""
|
|
120
110
|
if service_id in self.services:
|
|
121
111
|
asyncio.create_task(self.stop_service(service_id))
|
|
122
112
|
del self.services[service_id]
|
|
123
113
|
logger.info(f"Removed service {service_id} from watchdog")
|
|
124
114
|
|
|
125
115
|
async def start_watchdog(self) -> None:
|
|
126
|
-
"""Start the watchdog monitoring."""
|
|
127
116
|
if self.is_running:
|
|
128
117
|
return
|
|
129
118
|
|
|
130
119
|
self.is_running = True
|
|
131
120
|
|
|
132
|
-
# Add default services
|
|
133
121
|
for service_id, config in self.default_configs.items():
|
|
134
122
|
self.add_service(service_id, config)
|
|
135
123
|
|
|
136
|
-
# Start monitoring task with timeout protection
|
|
137
124
|
self.monitor_task = asyncio.create_task(self._monitor_services())
|
|
138
125
|
|
|
139
|
-
# Setup signal handlers for graceful shutdown
|
|
140
126
|
self._setup_signal_handlers()
|
|
141
127
|
|
|
142
128
|
self.console.print("[green]🐕 Service Watchdog started[/green]")
|
|
143
129
|
logger.info("Service watchdog started")
|
|
144
130
|
|
|
145
131
|
async def stop_watchdog(self) -> None:
|
|
146
|
-
"""Stop the watchdog and all monitored services."""
|
|
147
132
|
if not self.is_running:
|
|
148
133
|
return
|
|
149
134
|
|
|
150
135
|
self.is_running = False
|
|
151
136
|
|
|
152
|
-
# Cancel monitoring task
|
|
153
137
|
if self.monitor_task and not self.monitor_task.done():
|
|
154
138
|
self.monitor_task.cancel()
|
|
155
139
|
try:
|
|
@@ -157,7 +141,6 @@ class ServiceWatchdog:
|
|
|
157
141
|
except asyncio.CancelledError:
|
|
158
142
|
pass
|
|
159
143
|
|
|
160
|
-
# Stop all services
|
|
161
144
|
stop_tasks = [
|
|
162
145
|
self.stop_service(service_id) for service_id in self.services.keys()
|
|
163
146
|
]
|
|
@@ -168,7 +151,6 @@ class ServiceWatchdog:
|
|
|
168
151
|
logger.info("Service watchdog stopped")
|
|
169
152
|
|
|
170
153
|
async def start_service(self, service_id: str) -> bool:
|
|
171
|
-
"""Start a specific service with timeout protection."""
|
|
172
154
|
if not self._validate_service_start_request(service_id):
|
|
173
155
|
return False
|
|
174
156
|
|
|
@@ -180,7 +162,6 @@ class ServiceWatchdog:
|
|
|
180
162
|
return self._handle_service_start_failure(service, service_id, e)
|
|
181
163
|
|
|
182
164
|
def _validate_service_start_request(self, service_id: str) -> bool:
|
|
183
|
-
"""Validate if service can be started."""
|
|
184
165
|
if service_id not in self.services:
|
|
185
166
|
return False
|
|
186
167
|
|
|
@@ -190,7 +171,6 @@ class ServiceWatchdog:
|
|
|
190
171
|
async def _execute_service_startup(
|
|
191
172
|
self, service_id: str, service: ServiceStatus
|
|
192
173
|
) -> bool:
|
|
193
|
-
"""Execute the service startup process with timeout protection."""
|
|
194
174
|
async with self.timeout_manager.timeout_context(
|
|
195
175
|
f"start_service_{service_id}",
|
|
196
176
|
timeout=service.config.startup_timeout,
|
|
@@ -208,13 +188,10 @@ class ServiceWatchdog:
|
|
|
208
188
|
return True
|
|
209
189
|
|
|
210
190
|
def _prepare_service_startup(self, service: ServiceStatus) -> None:
|
|
211
|
-
"""Prepare service for startup."""
|
|
212
191
|
service.state = ServiceState.STARTING
|
|
213
192
|
service.last_start_time = time.time()
|
|
214
193
|
|
|
215
194
|
async def _start_service_process(self, service: ServiceStatus) -> bool:
|
|
216
|
-
"""Start the service process and verify it's running."""
|
|
217
|
-
# Start the service process with security logging
|
|
218
195
|
security_logger = get_security_logger()
|
|
219
196
|
security_logger.log_subprocess_execution(
|
|
220
197
|
command=service.config.command,
|
|
@@ -228,10 +205,8 @@ class ServiceWatchdog:
|
|
|
228
205
|
start_new_session=True,
|
|
229
206
|
)
|
|
230
207
|
|
|
231
|
-
# Wait for process to stabilize
|
|
232
208
|
await asyncio.sleep(2)
|
|
233
209
|
|
|
234
|
-
# Check if process is still running
|
|
235
210
|
if service.process.poll() is not None:
|
|
236
211
|
service.state = ServiceState.FAILED
|
|
237
212
|
service.last_error = "Process exited immediately"
|
|
@@ -240,7 +215,6 @@ class ServiceWatchdog:
|
|
|
240
215
|
return True
|
|
241
216
|
|
|
242
217
|
async def _verify_service_health(self, service: ServiceStatus) -> bool:
|
|
243
|
-
"""Verify service health if health check is configured."""
|
|
244
218
|
if not service.config.health_check_url:
|
|
245
219
|
return True
|
|
246
220
|
|
|
@@ -256,7 +230,6 @@ class ServiceWatchdog:
|
|
|
256
230
|
def _finalize_successful_startup(
|
|
257
231
|
self, service: ServiceStatus, service_id: str
|
|
258
232
|
) -> None:
|
|
259
|
-
"""Finalize successful service startup."""
|
|
260
233
|
service.state = ServiceState.RUNNING
|
|
261
234
|
service.consecutive_failures = 0
|
|
262
235
|
service.health_check_failures = 0
|
|
@@ -267,7 +240,6 @@ class ServiceWatchdog:
|
|
|
267
240
|
def _handle_service_start_failure(
|
|
268
241
|
self, service: ServiceStatus, service_id: str, error: Exception
|
|
269
242
|
) -> bool:
|
|
270
|
-
"""Handle service startup failure."""
|
|
271
243
|
service.state = ServiceState.FAILED
|
|
272
244
|
service.last_error = str(error)
|
|
273
245
|
service.consecutive_failures += 1
|
|
@@ -282,7 +254,6 @@ class ServiceWatchdog:
|
|
|
282
254
|
return False
|
|
283
255
|
|
|
284
256
|
async def stop_service(self, service_id: str) -> bool:
|
|
285
|
-
"""Stop a specific service with timeout protection."""
|
|
286
257
|
if service_id not in self.services:
|
|
287
258
|
return False
|
|
288
259
|
|
|
@@ -320,17 +291,15 @@ class ServiceWatchdog:
|
|
|
320
291
|
return False
|
|
321
292
|
|
|
322
293
|
async def _monitor_services(self) -> None:
|
|
323
|
-
"""Main monitoring loop with timeout protection."""
|
|
324
294
|
while self.is_running:
|
|
325
295
|
try:
|
|
326
296
|
async with self.timeout_manager.timeout_context(
|
|
327
297
|
"monitor_services",
|
|
328
|
-
timeout=30.0,
|
|
298
|
+
timeout=30.0,
|
|
329
299
|
strategy=TimeoutStrategy.GRACEFUL_DEGRADATION,
|
|
330
300
|
):
|
|
331
|
-
# Check each service
|
|
332
301
|
for service_id, service in self.services.items():
|
|
333
|
-
if not self.is_running:
|
|
302
|
+
if not self.is_running:
|
|
334
303
|
break
|
|
335
304
|
|
|
336
305
|
try:
|
|
@@ -338,19 +307,16 @@ class ServiceWatchdog:
|
|
|
338
307
|
except Exception as e:
|
|
339
308
|
logger.error(f"Error checking service {service_id}: {e}")
|
|
340
309
|
|
|
341
|
-
|
|
342
|
-
await asyncio.sleep(10) # Check every 10 seconds
|
|
310
|
+
await asyncio.sleep(10)
|
|
343
311
|
|
|
344
312
|
except Exception as e:
|
|
345
313
|
logger.error(f"Monitor services error: {e}")
|
|
346
|
-
await asyncio.sleep(30)
|
|
314
|
+
await asyncio.sleep(30)
|
|
347
315
|
|
|
348
316
|
async def _check_service_health(
|
|
349
317
|
self, service_id: str, service: ServiceStatus
|
|
350
318
|
) -> None:
|
|
351
|
-
"""Check health of a single service."""
|
|
352
319
|
if service.state == ServiceState.RUNNING:
|
|
353
|
-
# Check if process is still alive
|
|
354
320
|
if service.process and service.process.poll() is not None:
|
|
355
321
|
service.state = ServiceState.FAILED
|
|
356
322
|
service.last_error = (
|
|
@@ -362,7 +328,6 @@ class ServiceWatchdog:
|
|
|
362
328
|
return
|
|
363
329
|
|
|
364
330
|
async def _perform_health_check(self, service: ServiceStatus) -> bool:
|
|
365
|
-
"""Perform HTTP health check with timeout protection."""
|
|
366
331
|
if not service.config.health_check_url:
|
|
367
332
|
return True
|
|
368
333
|
|
|
@@ -382,21 +347,17 @@ class ServiceWatchdog:
|
|
|
382
347
|
return False
|
|
383
348
|
|
|
384
349
|
async def _terminate_process(self, service: ServiceStatus) -> None:
|
|
385
|
-
"""Terminate service process gracefully with timeout."""
|
|
386
350
|
if not service.process:
|
|
387
351
|
return
|
|
388
352
|
|
|
389
353
|
try:
|
|
390
|
-
# Try graceful termination first
|
|
391
354
|
service.process.terminate()
|
|
392
355
|
|
|
393
|
-
# Wait for graceful shutdown
|
|
394
356
|
try:
|
|
395
357
|
await asyncio.wait_for(
|
|
396
358
|
self._wait_for_process_exit(service.process), timeout=5.0
|
|
397
359
|
)
|
|
398
360
|
except TimeoutError:
|
|
399
|
-
# Force kill if graceful shutdown fails
|
|
400
361
|
service.process.kill()
|
|
401
362
|
await asyncio.wait_for(
|
|
402
363
|
self._wait_for_process_exit(service.process), timeout=2.0
|
|
@@ -404,21 +365,17 @@ class ServiceWatchdog:
|
|
|
404
365
|
|
|
405
366
|
except Exception as e:
|
|
406
367
|
logger.warning(f"Error terminating process: {e}")
|
|
407
|
-
|
|
368
|
+
|
|
408
369
|
with contextlib.suppress(Exception):
|
|
409
370
|
service.process.kill()
|
|
410
371
|
|
|
411
372
|
async def _wait_for_process_exit(self, process: subprocess.Popen[bytes]) -> None:
|
|
412
|
-
"""Wait for process to exit."""
|
|
413
373
|
while process.poll() is None:
|
|
414
374
|
await asyncio.sleep(0.1)
|
|
415
375
|
|
|
416
376
|
def _setup_signal_handlers(self) -> None:
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def signal_handler(signum: int, frame: object) -> None: # noqa: ARG001
|
|
420
|
-
"""Handle termination signals."""
|
|
421
|
-
_ = frame # Signal handler frame - required by signal API
|
|
377
|
+
def signal_handler(signum: int, frame: object) -> None:
|
|
378
|
+
_ = frame
|
|
422
379
|
logger.info(f"Received signal {signum}, stopping watchdog...")
|
|
423
380
|
asyncio.create_task(self.stop_watchdog())
|
|
424
381
|
|
|
@@ -426,64 +383,74 @@ class ServiceWatchdog:
|
|
|
426
383
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
427
384
|
|
|
428
385
|
def get_service_status(self, service_id: str) -> ServiceStatus | None:
|
|
429
|
-
"""Get status of a specific service."""
|
|
430
386
|
return self.services.get(service_id)
|
|
431
387
|
|
|
432
388
|
def get_all_services_status(self) -> dict[str, ServiceStatus]:
|
|
433
|
-
"""Get status of all services."""
|
|
434
389
|
return self.services.copy()
|
|
435
390
|
|
|
436
391
|
def print_status_report(self) -> None:
|
|
437
|
-
"""Print
|
|
438
|
-
self.
|
|
439
|
-
self.console.print("=" * 50)
|
|
392
|
+
"""Print detailed status report for all services."""
|
|
393
|
+
self._print_report_header()
|
|
440
394
|
|
|
441
395
|
if not self.services:
|
|
442
396
|
self.console.print("[dim]No services configured[/dim]")
|
|
443
397
|
return
|
|
444
398
|
|
|
399
|
+
table = self._create_status_table()
|
|
400
|
+
self.console.print(table)
|
|
401
|
+
|
|
402
|
+
def _print_report_header(self) -> None:
|
|
403
|
+
"""Print the status report header."""
|
|
404
|
+
self.console.print("\n[bold blue]🐕 Service Watchdog Status[/bold blue]")
|
|
405
|
+
self.console.print("=" * 50)
|
|
406
|
+
|
|
407
|
+
def _create_status_table(self) -> Table:
|
|
408
|
+
"""Create and populate the status table."""
|
|
445
409
|
table = Table()
|
|
446
410
|
table.add_column("Service")
|
|
447
411
|
table.add_column("Status")
|
|
448
412
|
table.add_column("Uptime")
|
|
449
413
|
|
|
450
414
|
for service in self.services.values():
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
uptime = service.uptime
|
|
467
|
-
if uptime > 3600:
|
|
468
|
-
uptime_str = f"{uptime / 3600:.1f}h"
|
|
469
|
-
elif uptime > 60:
|
|
470
|
-
uptime_str = f"{uptime / 60:.1f}m"
|
|
471
|
-
elif uptime > 0:
|
|
472
|
-
uptime_str = f"{uptime:.0f}s"
|
|
473
|
-
else:
|
|
474
|
-
uptime_str = "-"
|
|
475
|
-
|
|
476
|
-
table.add_row(service.config.name, status, uptime_str)
|
|
415
|
+
status_display = self._get_service_status_display(service)
|
|
416
|
+
uptime_display = self._format_uptime(service.uptime)
|
|
417
|
+
table.add_row(service.config.name, status_display, uptime_display)
|
|
418
|
+
|
|
419
|
+
return table
|
|
420
|
+
|
|
421
|
+
def _get_service_status_display(self, service: ServiceStatus) -> str:
|
|
422
|
+
"""Get formatted status display for a service."""
|
|
423
|
+
status_map = {
|
|
424
|
+
(ServiceState.RUNNING, True): "[green]🟢 Running[/green]",
|
|
425
|
+
(ServiceState.STARTING, None): "[yellow]🟡 Starting[/yellow]",
|
|
426
|
+
(ServiceState.STOPPING, None): "[yellow]🟡 Stopping[/yellow]",
|
|
427
|
+
(ServiceState.FAILED, None): "[red]🔴 Failed[/red]",
|
|
428
|
+
(ServiceState.TIMEOUT, None): "[red]⏰ Timeout[/red]",
|
|
429
|
+
}
|
|
477
430
|
|
|
478
|
-
|
|
431
|
+
# Check for running with healthy status first
|
|
432
|
+
if service.state == ServiceState.RUNNING and service.is_healthy:
|
|
433
|
+
return status_map[(ServiceState.RUNNING, True)]
|
|
434
|
+
|
|
435
|
+
# Check other states
|
|
436
|
+
status_key = (service.state, None)
|
|
437
|
+
return status_map.get(status_key, "[dim]⚫ Stopped[/dim]")
|
|
438
|
+
|
|
439
|
+
def _format_uptime(self, uptime: float) -> str:
|
|
440
|
+
"""Format uptime duration for display."""
|
|
441
|
+
if uptime > 3600:
|
|
442
|
+
return f"{uptime / 3600: .1f}h"
|
|
443
|
+
elif uptime > 60:
|
|
444
|
+
return f"{uptime / 60: .1f}m"
|
|
445
|
+
elif uptime > 0:
|
|
446
|
+
return f"{uptime: .0f}s"
|
|
447
|
+
return "-"
|
|
479
448
|
|
|
480
449
|
|
|
481
|
-
# Global service watchdog instance
|
|
482
450
|
_global_watchdog: ServiceWatchdog | None = None
|
|
483
451
|
|
|
484
452
|
|
|
485
453
|
def get_service_watchdog(console: Console | None = None) -> ServiceWatchdog:
|
|
486
|
-
"""Get global service watchdog instance."""
|
|
487
454
|
global _global_watchdog
|
|
488
455
|
if _global_watchdog is None:
|
|
489
456
|
_global_watchdog = ServiceWatchdog(console)
|
|
@@ -45,6 +45,10 @@ class SessionCoordinator:
|
|
|
45
45
|
def end_session(self, success: bool = True) -> None:
|
|
46
46
|
self.success = success
|
|
47
47
|
self.end_time = time.time()
|
|
48
|
+
|
|
49
|
+
# Capture quality metrics at session end
|
|
50
|
+
self._capture_quality_metrics()
|
|
51
|
+
|
|
48
52
|
if success:
|
|
49
53
|
self.complete_task("session", "Session completed successfully")
|
|
50
54
|
else:
|
|
@@ -283,3 +287,148 @@ class SessionCoordinator:
|
|
|
283
287
|
def update_stage(self, stage: str, status: str) -> None:
|
|
284
288
|
if self.web_job_id:
|
|
285
289
|
self._update_websocket_progress(status, f"{stage}: {status}")
|
|
290
|
+
|
|
291
|
+
def _capture_quality_metrics(self) -> None:
|
|
292
|
+
"""Capture quality metrics at the end of the session."""
|
|
293
|
+
try:
|
|
294
|
+
quality_service = self._initialize_quality_service()
|
|
295
|
+
metrics = self._extract_session_metrics()
|
|
296
|
+
|
|
297
|
+
if metrics:
|
|
298
|
+
self._record_quality_baseline(quality_service, metrics)
|
|
299
|
+
report = quality_service.generate_comprehensive_report(metrics)
|
|
300
|
+
self._display_quality_report(report)
|
|
301
|
+
except Exception as e:
|
|
302
|
+
self._handle_quality_tracking_error(e)
|
|
303
|
+
|
|
304
|
+
def _initialize_quality_service(self) -> t.Any:
|
|
305
|
+
"""Initialize the quality baseline service."""
|
|
306
|
+
from crackerjack.services.quality_baseline_enhanced import (
|
|
307
|
+
EnhancedQualityBaselineService,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return EnhancedQualityBaselineService()
|
|
311
|
+
|
|
312
|
+
def _record_quality_baseline(
|
|
313
|
+
self, quality_service: t.Any, metrics: dict[str, t.Any]
|
|
314
|
+
) -> None:
|
|
315
|
+
"""Record quality baseline with metrics."""
|
|
316
|
+
quality_service.record_baseline(
|
|
317
|
+
coverage_percent=metrics.get("coverage_percent", 0.0),
|
|
318
|
+
test_count=metrics.get("test_count", 0),
|
|
319
|
+
test_pass_rate=metrics.get("test_pass_rate", 100.0),
|
|
320
|
+
hook_failures=metrics.get("hook_failures", 0),
|
|
321
|
+
complexity_violations=metrics.get("complexity_violations", 0),
|
|
322
|
+
security_issues=metrics.get("security_issues", 0),
|
|
323
|
+
type_errors=metrics.get("type_errors", 0),
|
|
324
|
+
linting_issues=metrics.get("linting_issues", 0),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def _handle_quality_tracking_error(self, error: Exception) -> None:
|
|
328
|
+
"""Handle quality tracking errors without failing the session."""
|
|
329
|
+
self.console.print(
|
|
330
|
+
f"[dim yellow]Warning: Quality tracking failed: {error}[/dim yellow]"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def _extract_session_metrics(self) -> dict[str, t.Any] | None:
|
|
334
|
+
"""Extract quality metrics from the current session."""
|
|
335
|
+
with suppress(Exception):
|
|
336
|
+
metrics: dict[str, t.Any] = {}
|
|
337
|
+
self._extract_test_metrics(metrics)
|
|
338
|
+
self._extract_hook_metrics(metrics)
|
|
339
|
+
self._set_default_metrics(metrics)
|
|
340
|
+
return metrics or None
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
def _extract_test_metrics(self, metrics: dict[str, t.Any]) -> None:
|
|
344
|
+
"""Extract test-related metrics from tasks."""
|
|
345
|
+
if "testing" not in self.tasks:
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
test_task = self.tasks["testing"]
|
|
349
|
+
|
|
350
|
+
if hasattr(test_task, "coverage_percent"):
|
|
351
|
+
metrics["coverage_percent"] = getattr(test_task, "coverage_percent", 0.0)
|
|
352
|
+
if hasattr(test_task, "test_count"):
|
|
353
|
+
metrics["test_count"] = getattr(test_task, "test_count", 0)
|
|
354
|
+
if hasattr(test_task, "test_pass_rate"):
|
|
355
|
+
metrics["test_pass_rate"] = getattr(test_task, "test_pass_rate", 0.0)
|
|
356
|
+
|
|
357
|
+
def _extract_hook_metrics(self, metrics: dict[str, t.Any]) -> None:
|
|
358
|
+
"""Extract hook failure metrics from tasks."""
|
|
359
|
+
hook_failures = 0
|
|
360
|
+
for task_name, task in self.tasks.items():
|
|
361
|
+
if "hooks" in task_name and hasattr(task, "status"):
|
|
362
|
+
if getattr(task, "status") == "failed":
|
|
363
|
+
hook_failures += 1
|
|
364
|
+
metrics["hook_failures"] = hook_failures
|
|
365
|
+
|
|
366
|
+
def _set_default_metrics(self, metrics: dict[str, t.Any]) -> None:
|
|
367
|
+
"""Set default values for metrics we don't have direct access to."""
|
|
368
|
+
defaults = {
|
|
369
|
+
"coverage_percent": 0.0,
|
|
370
|
+
"test_count": 0,
|
|
371
|
+
"test_pass_rate": 100.0 if self.success else 0.0,
|
|
372
|
+
"complexity_violations": 0,
|
|
373
|
+
"security_issues": 0,
|
|
374
|
+
"type_errors": 0,
|
|
375
|
+
"linting_issues": 0,
|
|
376
|
+
}
|
|
377
|
+
for key, default_value in defaults.items():
|
|
378
|
+
metrics.setdefault(key, default_value)
|
|
379
|
+
|
|
380
|
+
def _display_quality_report(self, report: t.Any) -> None:
|
|
381
|
+
"""Display a summary of the quality report."""
|
|
382
|
+
with suppress(Exception):
|
|
383
|
+
if not report.current_metrics:
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
self._display_quality_score(report)
|
|
387
|
+
self._display_quality_trend(report)
|
|
388
|
+
self._display_critical_alerts(report)
|
|
389
|
+
self._display_top_recommendations(report)
|
|
390
|
+
|
|
391
|
+
def _display_quality_score(self, report: t.Any) -> None:
|
|
392
|
+
"""Display the quality score."""
|
|
393
|
+
score = report.current_metrics.quality_score
|
|
394
|
+
self.console.print(f"\n[cyan]📊 Quality Score: {score}/100[/cyan]")
|
|
395
|
+
|
|
396
|
+
def _display_quality_trend(self, report: t.Any) -> None:
|
|
397
|
+
"""Display quality trend information."""
|
|
398
|
+
if not report.trend:
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
trend_emoji = self._get_trend_emoji(report.trend.direction.value)
|
|
402
|
+
self.console.print(
|
|
403
|
+
f"[dim]{trend_emoji} Trend: {report.trend.direction.value} "
|
|
404
|
+
f"({report.trend.confidence:.1%} confidence)[/dim]"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def _get_trend_emoji(self, direction: str) -> str:
|
|
408
|
+
"""Get emoji for trend direction."""
|
|
409
|
+
trend_emojis = {
|
|
410
|
+
"improving": "📈",
|
|
411
|
+
"declining": "📉",
|
|
412
|
+
"stable": "📊",
|
|
413
|
+
"volatile": "⚠️",
|
|
414
|
+
}
|
|
415
|
+
return trend_emojis.get(direction, "📊")
|
|
416
|
+
|
|
417
|
+
def _display_critical_alerts(self, report: t.Any) -> None:
|
|
418
|
+
"""Display critical quality alerts."""
|
|
419
|
+
critical_alerts = [a for a in report.alerts if a.severity.value == "critical"]
|
|
420
|
+
if critical_alerts:
|
|
421
|
+
self.console.print(
|
|
422
|
+
f"[red]🚨 {len(critical_alerts)} critical quality issues[/red]"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def _display_top_recommendations(self, report: t.Any) -> None:
|
|
426
|
+
"""Display top quality recommendations."""
|
|
427
|
+
if not report.recommendations:
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
self.console.print("\n[yellow]💡 Top Recommendations:[/yellow]")
|
|
431
|
+
for rec in report.recommendations[:2]: # Show top 2
|
|
432
|
+
self.console.print(f" {rec}")
|
|
433
|
+
|
|
434
|
+
# Silently fail for display issues using suppress above
|