crackerjack 0.30.3__py3-none-any.whl → 0.31.4__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/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +652 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +401 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +370 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import socket
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
watchdog_event_queue: asyncio.Queue[dict[str, Any]] | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ServiceConfig:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
name: str,
|
|
22
|
+
command: list[str],
|
|
23
|
+
health_check_url: str | None = None,
|
|
24
|
+
health_check_interval: float = 30.0,
|
|
25
|
+
restart_delay: float = 5.0,
|
|
26
|
+
max_restarts: int = 10,
|
|
27
|
+
restart_window: float = 300.0,
|
|
28
|
+
) -> None:
|
|
29
|
+
self.name = name
|
|
30
|
+
self.command = command
|
|
31
|
+
self.health_check_url = health_check_url
|
|
32
|
+
self.health_check_interval = health_check_interval
|
|
33
|
+
self.restart_delay = restart_delay
|
|
34
|
+
self.max_restarts = max_restarts
|
|
35
|
+
self.restart_window = restart_window
|
|
36
|
+
|
|
37
|
+
self.process: subprocess.Popen | None = None
|
|
38
|
+
self.restart_count = 0
|
|
39
|
+
self.restart_timestamps: list[float] = []
|
|
40
|
+
self.last_health_check = 0.0
|
|
41
|
+
self.is_healthy = False
|
|
42
|
+
self.last_error: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ServiceWatchdog:
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
services: list[ServiceConfig],
|
|
49
|
+
event_queue: asyncio.Queue[dict[str, Any]] | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.services = services
|
|
52
|
+
self.is_running = True
|
|
53
|
+
self.session: aiohttp.ClientSession | None = None
|
|
54
|
+
self.event_queue = event_queue
|
|
55
|
+
|
|
56
|
+
global watchdog_event_queue
|
|
57
|
+
if event_queue:
|
|
58
|
+
watchdog_event_queue = event_queue
|
|
59
|
+
|
|
60
|
+
async def start(self) -> None:
|
|
61
|
+
self.session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10.0))
|
|
62
|
+
|
|
63
|
+
for service in self.services:
|
|
64
|
+
await self._start_service(service)
|
|
65
|
+
|
|
66
|
+
monitor_tasks = [
|
|
67
|
+
asyncio.create_task(self._monitor_service(service))
|
|
68
|
+
for service in self.services
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
status_task = asyncio.create_task(self._display_status())
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
await asyncio.gather(*monitor_tasks, status_task)
|
|
75
|
+
finally:
|
|
76
|
+
await self._cleanup()
|
|
77
|
+
|
|
78
|
+
async def stop(self) -> None:
|
|
79
|
+
self.is_running = False
|
|
80
|
+
|
|
81
|
+
for service in self.services:
|
|
82
|
+
if service.process:
|
|
83
|
+
console.print(f"[yellow]🛑 Stopping {service.name}...[/yellow]")
|
|
84
|
+
service.process.terminate()
|
|
85
|
+
try:
|
|
86
|
+
service.process.wait(timeout=10)
|
|
87
|
+
except subprocess.TimeoutExpired:
|
|
88
|
+
service.process.kill()
|
|
89
|
+
|
|
90
|
+
if self.session:
|
|
91
|
+
await self.session.close()
|
|
92
|
+
|
|
93
|
+
def _is_port_in_use(self, port: int) -> bool:
|
|
94
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
95
|
+
try:
|
|
96
|
+
s.bind(("127.0.0.1", port))
|
|
97
|
+
return False
|
|
98
|
+
except OSError:
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
async def _start_service(self, service: ServiceConfig) -> bool:
|
|
102
|
+
try:
|
|
103
|
+
if await self._check_websocket_server_running(service):
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
if not await self._launch_service_process(service):
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
return await self._finalize_service_startup(service)
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
return await self._handle_service_start_error(service, e)
|
|
113
|
+
|
|
114
|
+
async def _check_websocket_server_running(self, service: ServiceConfig) -> bool:
|
|
115
|
+
if "websocket - server" in " ".join(service.command):
|
|
116
|
+
if self._is_port_in_use(8675):
|
|
117
|
+
await self._emit_event(
|
|
118
|
+
"port_in_use",
|
|
119
|
+
service.name,
|
|
120
|
+
"Port 8675 already in use (server already running)",
|
|
121
|
+
)
|
|
122
|
+
service.is_healthy = True
|
|
123
|
+
return True
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
async def _launch_service_process(self, service: ServiceConfig) -> bool:
|
|
127
|
+
service.process = subprocess.Popen(
|
|
128
|
+
service.command,
|
|
129
|
+
stdout=subprocess.PIPE,
|
|
130
|
+
stderr=subprocess.PIPE,
|
|
131
|
+
text=True,
|
|
132
|
+
)
|
|
133
|
+
service.last_error = None
|
|
134
|
+
await asyncio.sleep(2)
|
|
135
|
+
|
|
136
|
+
return await self._check_process_startup_success(service)
|
|
137
|
+
|
|
138
|
+
async def _check_process_startup_success(self, service: ServiceConfig) -> bool:
|
|
139
|
+
exit_code = service.process.poll()
|
|
140
|
+
if exit_code is not None:
|
|
141
|
+
return await self._handle_process_died(service, exit_code)
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
async def _handle_process_died(
|
|
145
|
+
self,
|
|
146
|
+
service: ServiceConfig,
|
|
147
|
+
exit_code: int,
|
|
148
|
+
) -> bool:
|
|
149
|
+
stdout, stderr = service.process.communicate()
|
|
150
|
+
error_msg = f"Process died (exit: {exit_code})"
|
|
151
|
+
if stderr and stderr.strip():
|
|
152
|
+
error_msg += f" - {stderr.strip()[:50]}"
|
|
153
|
+
service.last_error = error_msg
|
|
154
|
+
await self._emit_event("process_died", service.name, error_msg)
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
async def _finalize_service_startup(self, service: ServiceConfig) -> bool:
|
|
158
|
+
if service.health_check_url:
|
|
159
|
+
service.is_healthy = await self._health_check(service)
|
|
160
|
+
else:
|
|
161
|
+
service.is_healthy = True
|
|
162
|
+
|
|
163
|
+
if service.is_healthy:
|
|
164
|
+
await self._emit_event("started", service.name, "Running")
|
|
165
|
+
|
|
166
|
+
return service.is_healthy
|
|
167
|
+
|
|
168
|
+
async def _handle_service_start_error(
|
|
169
|
+
self,
|
|
170
|
+
service: ServiceConfig,
|
|
171
|
+
error: Exception,
|
|
172
|
+
) -> bool:
|
|
173
|
+
service.last_error = str(error)
|
|
174
|
+
await self._emit_event(
|
|
175
|
+
"start_error",
|
|
176
|
+
service.name,
|
|
177
|
+
f"Failed: {str(error)[:30]}",
|
|
178
|
+
)
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
async def _handle_websocket_server_monitoring(self, service: ServiceConfig) -> bool:
|
|
182
|
+
if "websocket - server" not in " ".join(service.command):
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
if self._is_port_in_use(8675):
|
|
186
|
+
if await self._verify_websocket_server_health():
|
|
187
|
+
service.is_healthy = True
|
|
188
|
+
if (
|
|
189
|
+
not hasattr(service, "_port_acknowledged")
|
|
190
|
+
or not service._port_acknowledged
|
|
191
|
+
):
|
|
192
|
+
service._port_acknowledged = True
|
|
193
|
+
await self._emit_event(
|
|
194
|
+
"port_healthy",
|
|
195
|
+
service.name,
|
|
196
|
+
"WebSocket server verified healthy",
|
|
197
|
+
)
|
|
198
|
+
await asyncio.sleep(30)
|
|
199
|
+
return True
|
|
200
|
+
service.is_healthy = False
|
|
201
|
+
await self._emit_event(
|
|
202
|
+
"port_hijacked",
|
|
203
|
+
service.name,
|
|
204
|
+
"Port 8675 occupied by different service",
|
|
205
|
+
)
|
|
206
|
+
await self._restart_service(service)
|
|
207
|
+
return False
|
|
208
|
+
service._port_acknowledged = False
|
|
209
|
+
if service.process and service.process.poll() is None:
|
|
210
|
+
await self._emit_event(
|
|
211
|
+
"port_unavailable",
|
|
212
|
+
service.name,
|
|
213
|
+
"Process running but port 8675 not available",
|
|
214
|
+
)
|
|
215
|
+
await self._restart_service(service)
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
async def _check_process_health(self, service: ServiceConfig) -> bool:
|
|
219
|
+
process_running = service.process and service.process.poll() is None
|
|
220
|
+
if not process_running:
|
|
221
|
+
if service.process:
|
|
222
|
+
exit_code = service.process.poll()
|
|
223
|
+
await self._emit_event(
|
|
224
|
+
"died",
|
|
225
|
+
service.name,
|
|
226
|
+
f"Process died (exit: {exit_code})",
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
await self._emit_event("not_started", service.name, "Not started")
|
|
230
|
+
await self._restart_service(service)
|
|
231
|
+
return False
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
async def _perform_health_check_if_needed(self, service: ServiceConfig) -> bool:
|
|
235
|
+
if not service.health_check_url:
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
current_time = time.time()
|
|
239
|
+
if current_time - service.last_health_check >= service.health_check_interval:
|
|
240
|
+
service.is_healthy = await self._health_check(service)
|
|
241
|
+
service.last_health_check = current_time
|
|
242
|
+
|
|
243
|
+
if not service.is_healthy:
|
|
244
|
+
await self._emit_event(
|
|
245
|
+
"health_fail",
|
|
246
|
+
service.name,
|
|
247
|
+
"Health check failed",
|
|
248
|
+
)
|
|
249
|
+
await self._restart_service(service)
|
|
250
|
+
return False
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
async def _monitor_service(self, service: ServiceConfig) -> None:
|
|
254
|
+
while self.is_running:
|
|
255
|
+
try:
|
|
256
|
+
if await self._execute_monitoring_cycle(service):
|
|
257
|
+
await asyncio.sleep(5.0)
|
|
258
|
+
else:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
await self._handle_monitoring_error(service, e)
|
|
263
|
+
|
|
264
|
+
async def _execute_monitoring_cycle(self, service: ServiceConfig) -> bool:
|
|
265
|
+
if await self._handle_websocket_server_monitoring(service):
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
if not await self._check_process_health(service):
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
return await self._perform_health_check_if_needed(service)
|
|
272
|
+
|
|
273
|
+
async def _handle_monitoring_error(
|
|
274
|
+
self,
|
|
275
|
+
service: ServiceConfig,
|
|
276
|
+
error: Exception,
|
|
277
|
+
) -> None:
|
|
278
|
+
service.last_error = str(error)
|
|
279
|
+
console.print(f"[red]❌ Error monitoring {service.name}: {error}[/red]")
|
|
280
|
+
await asyncio.sleep(10.0)
|
|
281
|
+
|
|
282
|
+
async def _health_check(self, service: ServiceConfig) -> bool:
|
|
283
|
+
if not service.health_check_url or not self.session:
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
async with self.session.get(service.health_check_url) as response:
|
|
288
|
+
return response.status == 200
|
|
289
|
+
except Exception:
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
async def _verify_websocket_server_health(self) -> bool:
|
|
293
|
+
if not self.session:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
with suppress(Exception):
|
|
297
|
+
async with self.session.get("http: // localhost: 8675 / ") as response:
|
|
298
|
+
if response.status == 200:
|
|
299
|
+
data = await response.json()
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
"message" in data
|
|
303
|
+
and "Crackerjack" in data.get("message", "")
|
|
304
|
+
and "progress_dir" in data
|
|
305
|
+
)
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
async def _restart_service(self, service: ServiceConfig) -> None:
|
|
309
|
+
current_time = time.time()
|
|
310
|
+
reason = self._determine_restart_reason(service)
|
|
311
|
+
|
|
312
|
+
await self._emit_event("restarting", service.name, f"Restarting - {reason}")
|
|
313
|
+
|
|
314
|
+
if not await self._check_restart_rate_limit(service, current_time):
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
await self._terminate_existing_process(service)
|
|
318
|
+
await self._wait_before_restart(service)
|
|
319
|
+
await self._execute_service_restart(service, current_time)
|
|
320
|
+
|
|
321
|
+
def _determine_restart_reason(self, service: ServiceConfig) -> str:
|
|
322
|
+
return (
|
|
323
|
+
"Process died"
|
|
324
|
+
if not service.process or service.process.poll() is not None
|
|
325
|
+
else "Health failed"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
async def _check_restart_rate_limit(
|
|
329
|
+
self,
|
|
330
|
+
service: ServiceConfig,
|
|
331
|
+
current_time: float,
|
|
332
|
+
) -> bool:
|
|
333
|
+
service.restart_timestamps = [
|
|
334
|
+
ts
|
|
335
|
+
for ts in service.restart_timestamps
|
|
336
|
+
if current_time - ts < service.restart_window
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
if len(service.restart_timestamps) >= service.max_restarts:
|
|
340
|
+
console.print(
|
|
341
|
+
f"[red]🚨 {service.name} exceeded restart limit ({service.max_restarts} in {service.restart_window}s)[/red]",
|
|
342
|
+
)
|
|
343
|
+
service.last_error = "Restart rate limit exceeded"
|
|
344
|
+
await asyncio.sleep(60)
|
|
345
|
+
return False
|
|
346
|
+
return True
|
|
347
|
+
|
|
348
|
+
async def _terminate_existing_process(self, service: ServiceConfig) -> None:
|
|
349
|
+
if not service.process:
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
console.print(
|
|
354
|
+
f"[yellow]🔪 Terminating existing {service.name} process (PID: {service.process.pid})[/yellow]",
|
|
355
|
+
)
|
|
356
|
+
service.process.terminate()
|
|
357
|
+
service.process.wait(timeout=10)
|
|
358
|
+
except subprocess.TimeoutExpired:
|
|
359
|
+
console.print(f"[red]💀 Force killing {service.name} process[/red]")
|
|
360
|
+
service.process.kill()
|
|
361
|
+
except Exception as e:
|
|
362
|
+
console.print(f"[yellow]⚠️ Error terminating {service.name}: {e}[/yellow]")
|
|
363
|
+
|
|
364
|
+
async def _wait_before_restart(self, service: ServiceConfig) -> None:
|
|
365
|
+
console.print(
|
|
366
|
+
f"[yellow]⏳ Waiting {service.restart_delay}s before restarting {service.name}...[/yellow]",
|
|
367
|
+
)
|
|
368
|
+
await asyncio.sleep(service.restart_delay)
|
|
369
|
+
|
|
370
|
+
async def _execute_service_restart(
|
|
371
|
+
self,
|
|
372
|
+
service: ServiceConfig,
|
|
373
|
+
current_time: float,
|
|
374
|
+
) -> None:
|
|
375
|
+
service.restart_timestamps.append(current_time)
|
|
376
|
+
service.restart_count += 1
|
|
377
|
+
|
|
378
|
+
success = await self._start_service(service)
|
|
379
|
+
if not success:
|
|
380
|
+
await self._emit_event("restart_failed", service.name, "Restart failed")
|
|
381
|
+
|
|
382
|
+
async def _display_status(self) -> None:
|
|
383
|
+
while self.is_running:
|
|
384
|
+
try:
|
|
385
|
+
await self._update_status_display()
|
|
386
|
+
await asyncio.sleep(10.0)
|
|
387
|
+
|
|
388
|
+
except Exception as e:
|
|
389
|
+
console.print(f"[red]Error updating display: {e}[/red]")
|
|
390
|
+
await asyncio.sleep(5.0)
|
|
391
|
+
|
|
392
|
+
async def _update_status_display(self) -> None:
|
|
393
|
+
console.clear()
|
|
394
|
+
table = self._create_status_table()
|
|
395
|
+
|
|
396
|
+
for service in self.services:
|
|
397
|
+
status = self._get_service_status(service)
|
|
398
|
+
health = self._get_service_health(service)
|
|
399
|
+
restarts = str(service.restart_count)
|
|
400
|
+
error = self._format_error_message(service.last_error)
|
|
401
|
+
|
|
402
|
+
table.add_row(service.name, status, health, restarts, error)
|
|
403
|
+
|
|
404
|
+
console.print(table)
|
|
405
|
+
console.print("\n[dim]Press Ctrl + C to stop monitoring[/dim]")
|
|
406
|
+
|
|
407
|
+
def _create_status_table(self) -> Table:
|
|
408
|
+
table = Table(title="🔍 Crackerjack Service Watchdog")
|
|
409
|
+
table.add_column("Service", style="cyan", no_wrap=True)
|
|
410
|
+
table.add_column("Status", style="white")
|
|
411
|
+
table.add_column("Health", style="white")
|
|
412
|
+
table.add_column("Restarts", style="white")
|
|
413
|
+
table.add_column("Last Error", style="red")
|
|
414
|
+
return table
|
|
415
|
+
|
|
416
|
+
def _get_service_status(self, service: ServiceConfig) -> str:
|
|
417
|
+
if service.process and service.process.poll() is None:
|
|
418
|
+
return "[green]✅ Running[/green]"
|
|
419
|
+
return "[red]❌ Stopped[/red]"
|
|
420
|
+
|
|
421
|
+
def _get_service_health(self, service: ServiceConfig) -> str:
|
|
422
|
+
if service.health_check_url:
|
|
423
|
+
return (
|
|
424
|
+
"[green]🟢 Healthy[/green]"
|
|
425
|
+
if service.is_healthy
|
|
426
|
+
else "[red]🔴 Unhealthy[/red]"
|
|
427
|
+
)
|
|
428
|
+
return "[dim]N / A[/dim]"
|
|
429
|
+
|
|
430
|
+
def _format_error_message(self, error_message: str | None) -> str:
|
|
431
|
+
error = error_message or "[dim]None[/dim]"
|
|
432
|
+
if len(error) > 30:
|
|
433
|
+
error = error[:27] + "..."
|
|
434
|
+
return error
|
|
435
|
+
|
|
436
|
+
async def _emit_event(
|
|
437
|
+
self,
|
|
438
|
+
event_type: str,
|
|
439
|
+
service_name: str,
|
|
440
|
+
message: str,
|
|
441
|
+
) -> None:
|
|
442
|
+
if self.event_queue:
|
|
443
|
+
from contextlib import suppress
|
|
444
|
+
|
|
445
|
+
with suppress(Exception):
|
|
446
|
+
event = {
|
|
447
|
+
"type": event_type,
|
|
448
|
+
"service": service_name,
|
|
449
|
+
"message": message,
|
|
450
|
+
"timestamp": time.time(),
|
|
451
|
+
}
|
|
452
|
+
await self.event_queue.put(event)
|
|
453
|
+
|
|
454
|
+
async def _cleanup(self) -> None:
|
|
455
|
+
if self.session and not self.session.closed:
|
|
456
|
+
await self.session.close()
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
async def create_default_watchdog(
|
|
460
|
+
event_queue: asyncio.Queue[dict[str, Any]] | None = None,
|
|
461
|
+
) -> ServiceWatchdog:
|
|
462
|
+
python_path = sys.executable
|
|
463
|
+
|
|
464
|
+
services = [
|
|
465
|
+
ServiceConfig(
|
|
466
|
+
name="MCP Server",
|
|
467
|
+
command=[
|
|
468
|
+
python_path,
|
|
469
|
+
" - m",
|
|
470
|
+
"crackerjack",
|
|
471
|
+
" -- start - mcp - server",
|
|
472
|
+
],
|
|
473
|
+
),
|
|
474
|
+
ServiceConfig(
|
|
475
|
+
name="WebSocket Server",
|
|
476
|
+
command=[
|
|
477
|
+
python_path,
|
|
478
|
+
" - m",
|
|
479
|
+
"crackerjack",
|
|
480
|
+
" -- websocket - server",
|
|
481
|
+
],
|
|
482
|
+
health_check_url="http: // localhost: 8675 / ",
|
|
483
|
+
),
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
return ServiceWatchdog(services, event_queue)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
async def main() -> None:
|
|
490
|
+
watchdog = await create_default_watchdog()
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
await watchdog.start()
|
|
494
|
+
except KeyboardInterrupt:
|
|
495
|
+
console.print("\n[yellow]🛑 Shutting down watchdog...[/yellow]")
|
|
496
|
+
finally:
|
|
497
|
+
await watchdog.stop()
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
if __name__ == "__main__":
|
|
501
|
+
asyncio.run(main())
|