crackerjack 0.31.10__py3-none-any.whl → 0.31.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +281 -94
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +343 -209
- crackerjack/dynamic_config.py +47 -6
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +17 -63
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +44 -73
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +71 -47
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +276 -428
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.10.dist-info/RECORD +0 -149
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""WebSocket and network resource lifecycle management.
|
|
2
|
+
|
|
3
|
+
Provides comprehensive cleanup patterns for WebSocket connections,
|
|
4
|
+
HTTP clients, and network resources to prevent resource leaks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import contextlib
|
|
9
|
+
import logging
|
|
10
|
+
import socket
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
import typing as t
|
|
14
|
+
|
|
15
|
+
import aiohttp
|
|
16
|
+
import websockets
|
|
17
|
+
|
|
18
|
+
from .resource_manager import ManagedResource, ResourceManager
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ManagedWebSocketConnection(ManagedResource):
|
|
22
|
+
"""WebSocket connection with automatic cleanup on error scenarios."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
websocket: t.Any, # websockets protocols have import issues
|
|
27
|
+
manager: ResourceManager | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__(manager)
|
|
30
|
+
self.websocket = websocket
|
|
31
|
+
self.logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
async def cleanup(self) -> None:
|
|
34
|
+
"""Clean up WebSocket connection."""
|
|
35
|
+
if not self._closed and not self.websocket.closed:
|
|
36
|
+
self._closed = True
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
await asyncio.wait_for(self.websocket.close(), timeout=5.0)
|
|
40
|
+
except (TimeoutError, websockets.ConnectionClosed):
|
|
41
|
+
# Expected when connection is already closed or times out
|
|
42
|
+
pass
|
|
43
|
+
except Exception as e:
|
|
44
|
+
self.logger.warning(f"Error closing WebSocket connection: {e}")
|
|
45
|
+
|
|
46
|
+
async def send_safe(self, message: str) -> bool:
|
|
47
|
+
"""Send message safely, returning True if successful."""
|
|
48
|
+
if self._closed or self.websocket.closed:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
await self.websocket.send(message)
|
|
53
|
+
return True
|
|
54
|
+
except (websockets.ConnectionClosed, websockets.InvalidState):
|
|
55
|
+
await self.cleanup()
|
|
56
|
+
return False
|
|
57
|
+
except Exception as e:
|
|
58
|
+
self.logger.warning(f"Error sending WebSocket message: {e}")
|
|
59
|
+
await self.cleanup()
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ManagedHTTPClient(ManagedResource):
|
|
64
|
+
"""HTTP client session with automatic cleanup."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
session: aiohttp.ClientSession,
|
|
69
|
+
manager: ResourceManager | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
super().__init__(manager)
|
|
72
|
+
self.session = session
|
|
73
|
+
|
|
74
|
+
async def cleanup(self) -> None:
|
|
75
|
+
"""Clean up HTTP client session."""
|
|
76
|
+
if not self._closed and not self.session.closed:
|
|
77
|
+
self._closed = True
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
await self.session.close()
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logging.getLogger(__name__).warning(f"Error closing HTTP session: {e}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ManagedWebSocketServer(ManagedResource):
|
|
86
|
+
"""WebSocket server with comprehensive lifecycle management."""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
port: int,
|
|
91
|
+
host: str = "127.0.0.1",
|
|
92
|
+
manager: ResourceManager | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
super().__init__(manager)
|
|
95
|
+
self.port = port
|
|
96
|
+
self.host = host
|
|
97
|
+
self.server: t.Any | None = None # websockets server type has import issues
|
|
98
|
+
self.connections: set[ManagedWebSocketConnection] = set()
|
|
99
|
+
self.logger = logging.getLogger(__name__)
|
|
100
|
+
self._server_task: asyncio.Task[t.Any] | None = None
|
|
101
|
+
|
|
102
|
+
async def start(
|
|
103
|
+
self,
|
|
104
|
+
handler: t.Callable[[t.Any], t.Awaitable[None]],
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Start the WebSocket server."""
|
|
107
|
+
if self.server:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Wrap handler to manage connections
|
|
111
|
+
async def managed_handler(
|
|
112
|
+
websocket: t.Any, # websocket protocol type
|
|
113
|
+
) -> None:
|
|
114
|
+
managed_conn = ManagedWebSocketConnection(websocket, self.manager)
|
|
115
|
+
self.connections.add(managed_conn)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
await handler(websocket)
|
|
119
|
+
finally:
|
|
120
|
+
self.connections.discard(managed_conn)
|
|
121
|
+
await managed_conn.cleanup()
|
|
122
|
+
|
|
123
|
+
self.server = await websockets.serve(
|
|
124
|
+
managed_handler,
|
|
125
|
+
self.host,
|
|
126
|
+
self.port,
|
|
127
|
+
# Configure timeouts and limits
|
|
128
|
+
ping_interval=20,
|
|
129
|
+
ping_timeout=10,
|
|
130
|
+
close_timeout=10,
|
|
131
|
+
max_size=2**20, # 1MB
|
|
132
|
+
max_queue=32,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self.logger.info(f"WebSocket server started on {self.host}:{self.port}")
|
|
136
|
+
|
|
137
|
+
async def cleanup(self) -> None:
|
|
138
|
+
"""Clean up WebSocket server and all connections."""
|
|
139
|
+
if self._closed:
|
|
140
|
+
return
|
|
141
|
+
self._closed = True
|
|
142
|
+
|
|
143
|
+
# Close all active connections
|
|
144
|
+
if self.connections:
|
|
145
|
+
close_tasks = [
|
|
146
|
+
asyncio.create_task(conn.cleanup()) for conn in list(self.connections)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
if close_tasks:
|
|
150
|
+
await asyncio.gather(*close_tasks, return_exceptions=True)
|
|
151
|
+
|
|
152
|
+
self.connections.clear()
|
|
153
|
+
|
|
154
|
+
# Close server
|
|
155
|
+
if self.server:
|
|
156
|
+
try:
|
|
157
|
+
self.server.close()
|
|
158
|
+
await self.server.wait_closed()
|
|
159
|
+
self.logger.info("WebSocket server closed successfully")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
self.logger.warning(f"Error closing WebSocket server: {e}")
|
|
162
|
+
finally:
|
|
163
|
+
self.server = None
|
|
164
|
+
|
|
165
|
+
def get_connection_count(self) -> int:
|
|
166
|
+
"""Get the number of active connections."""
|
|
167
|
+
return len([conn for conn in self.connections if not conn._closed])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ManagedSubprocess(ManagedResource):
|
|
171
|
+
"""Subprocess with enhanced lifecycle management and resource cleanup."""
|
|
172
|
+
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
process: subprocess.Popen[bytes],
|
|
176
|
+
timeout: float = 30.0,
|
|
177
|
+
manager: ResourceManager | None = None,
|
|
178
|
+
) -> None:
|
|
179
|
+
super().__init__(manager)
|
|
180
|
+
self.process = process
|
|
181
|
+
self.timeout = timeout
|
|
182
|
+
self.logger = logging.getLogger(__name__)
|
|
183
|
+
self._monitor_task: asyncio.Task[t.Any] | None = None
|
|
184
|
+
|
|
185
|
+
async def start_monitoring(self) -> None:
|
|
186
|
+
"""Start monitoring the process for unexpected termination."""
|
|
187
|
+
if self._monitor_task:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
self._monitor_task = asyncio.create_task(self._monitor_process())
|
|
191
|
+
|
|
192
|
+
async def _monitor_process(self) -> None:
|
|
193
|
+
"""Monitor process health and log unexpected terminations."""
|
|
194
|
+
try:
|
|
195
|
+
while self.process.poll() is None:
|
|
196
|
+
await asyncio.sleep(5.0)
|
|
197
|
+
|
|
198
|
+
# Process has terminated
|
|
199
|
+
return_code = self.process.returncode
|
|
200
|
+
if return_code != 0:
|
|
201
|
+
self.logger.warning(
|
|
202
|
+
f"Process {self.process.pid} terminated with code {return_code}"
|
|
203
|
+
)
|
|
204
|
+
except asyncio.CancelledError:
|
|
205
|
+
pass
|
|
206
|
+
except Exception as e:
|
|
207
|
+
self.logger.warning(f"Error monitoring process: {e}")
|
|
208
|
+
|
|
209
|
+
async def cleanup(self) -> None:
|
|
210
|
+
"""Clean up subprocess with graceful termination."""
|
|
211
|
+
if self._closed:
|
|
212
|
+
return
|
|
213
|
+
self._closed = True
|
|
214
|
+
|
|
215
|
+
# Cancel monitoring
|
|
216
|
+
if self._monitor_task and not self._monitor_task.done():
|
|
217
|
+
self._monitor_task.cancel()
|
|
218
|
+
try:
|
|
219
|
+
await self._monitor_task
|
|
220
|
+
except asyncio.CancelledError:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
# Clean up process
|
|
224
|
+
if self.process.poll() is None:
|
|
225
|
+
try:
|
|
226
|
+
# Try graceful termination first
|
|
227
|
+
self.process.terminate()
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
self.process.wait(timeout=5.0)
|
|
231
|
+
self.logger.debug(
|
|
232
|
+
f"Process {self.process.pid} terminated gracefully"
|
|
233
|
+
)
|
|
234
|
+
except subprocess.TimeoutExpired:
|
|
235
|
+
# Force kill if graceful termination fails
|
|
236
|
+
self.process.kill()
|
|
237
|
+
try:
|
|
238
|
+
self.process.wait(timeout=2.0)
|
|
239
|
+
self.logger.warning(f"Process {self.process.pid} force killed")
|
|
240
|
+
except subprocess.TimeoutExpired:
|
|
241
|
+
self.logger.error(
|
|
242
|
+
f"Process {self.process.pid} did not terminate after force kill"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
except ProcessLookupError:
|
|
246
|
+
# Process already terminated
|
|
247
|
+
pass
|
|
248
|
+
except Exception as e:
|
|
249
|
+
self.logger.warning(
|
|
250
|
+
f"Error cleaning up process {self.process.pid}: {e}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def is_running(self) -> bool:
|
|
254
|
+
"""Check if the process is still running."""
|
|
255
|
+
return not self._closed and self.process.poll() is None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class NetworkResourceManager:
|
|
259
|
+
"""Manager for network-related resources with health monitoring."""
|
|
260
|
+
|
|
261
|
+
def __init__(self) -> None:
|
|
262
|
+
self.resource_manager = ResourceManager()
|
|
263
|
+
self.logger = logging.getLogger(__name__)
|
|
264
|
+
|
|
265
|
+
async def create_websocket_server(
|
|
266
|
+
self,
|
|
267
|
+
port: int,
|
|
268
|
+
host: str = "127.0.0.1",
|
|
269
|
+
) -> ManagedWebSocketServer:
|
|
270
|
+
"""Create a managed WebSocket server."""
|
|
271
|
+
server = ManagedWebSocketServer(port, host, self.resource_manager)
|
|
272
|
+
return server
|
|
273
|
+
|
|
274
|
+
async def create_http_client(
|
|
275
|
+
self,
|
|
276
|
+
timeout: aiohttp.ClientTimeout | None = None,
|
|
277
|
+
**kwargs: t.Any,
|
|
278
|
+
) -> ManagedHTTPClient:
|
|
279
|
+
"""Create a managed HTTP client session."""
|
|
280
|
+
timeout = timeout or aiohttp.ClientTimeout(total=30.0)
|
|
281
|
+
session = aiohttp.ClientSession(timeout=timeout, **kwargs)
|
|
282
|
+
return ManagedHTTPClient(session, self.resource_manager)
|
|
283
|
+
|
|
284
|
+
def create_subprocess(
|
|
285
|
+
self,
|
|
286
|
+
process: subprocess.Popen[bytes],
|
|
287
|
+
timeout: float = 30.0,
|
|
288
|
+
) -> ManagedSubprocess:
|
|
289
|
+
"""Create a managed subprocess."""
|
|
290
|
+
return ManagedSubprocess(process, timeout, self.resource_manager)
|
|
291
|
+
|
|
292
|
+
async def check_port_available(self, port: int, host: str = "127.0.0.1") -> bool:
|
|
293
|
+
"""Check if a port is available for binding."""
|
|
294
|
+
try:
|
|
295
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
296
|
+
sock.settimeout(1.0)
|
|
297
|
+
result = sock.connect_ex((host, port))
|
|
298
|
+
return result != 0 # Port is available if connection fails
|
|
299
|
+
except Exception:
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
async def wait_for_port(
|
|
303
|
+
self,
|
|
304
|
+
port: int,
|
|
305
|
+
host: str = "127.0.0.1",
|
|
306
|
+
timeout: float = 30.0,
|
|
307
|
+
poll_interval: float = 0.5,
|
|
308
|
+
) -> bool:
|
|
309
|
+
"""Wait for a port to become available (service to start)."""
|
|
310
|
+
start_time = time.time()
|
|
311
|
+
|
|
312
|
+
while time.time() - start_time < timeout:
|
|
313
|
+
with contextlib.suppress(Exception):
|
|
314
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
315
|
+
sock.settimeout(1.0)
|
|
316
|
+
result = sock.connect_ex((host, port))
|
|
317
|
+
if result == 0: # Connection successful
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
await asyncio.sleep(poll_interval)
|
|
321
|
+
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
async def cleanup_all(self) -> None:
|
|
325
|
+
"""Clean up all managed network resources."""
|
|
326
|
+
await self.resource_manager.cleanup_all()
|
|
327
|
+
|
|
328
|
+
async def __aenter__(self) -> "NetworkResourceManager":
|
|
329
|
+
return self
|
|
330
|
+
|
|
331
|
+
async def __aexit__(
|
|
332
|
+
self,
|
|
333
|
+
exc_type: type[BaseException] | None,
|
|
334
|
+
exc_val: BaseException | None,
|
|
335
|
+
exc_tb: type[BaseException] | None,
|
|
336
|
+
) -> None:
|
|
337
|
+
await self.cleanup_all()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# Context manager helpers for common network resource patterns
|
|
341
|
+
@contextlib.asynccontextmanager
|
|
342
|
+
async def with_websocket_server(
|
|
343
|
+
port: int,
|
|
344
|
+
handler: t.Callable[[t.Any], t.Awaitable[None]],
|
|
345
|
+
host: str = "127.0.0.1",
|
|
346
|
+
):
|
|
347
|
+
"""Context manager for a WebSocket server with automatic cleanup."""
|
|
348
|
+
async with NetworkResourceManager() as manager:
|
|
349
|
+
server = await manager.create_websocket_server(port, host)
|
|
350
|
+
try:
|
|
351
|
+
await server.start(handler)
|
|
352
|
+
yield server
|
|
353
|
+
finally:
|
|
354
|
+
await server.cleanup()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@contextlib.asynccontextmanager
|
|
358
|
+
async def with_http_client(**kwargs: t.Any):
|
|
359
|
+
"""Context manager for an HTTP client with automatic cleanup."""
|
|
360
|
+
async with NetworkResourceManager() as manager:
|
|
361
|
+
client = await manager.create_http_client(**kwargs)
|
|
362
|
+
try:
|
|
363
|
+
yield client.session
|
|
364
|
+
finally:
|
|
365
|
+
await client.cleanup()
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@contextlib.asynccontextmanager
|
|
369
|
+
async def with_managed_subprocess(
|
|
370
|
+
command: list[str],
|
|
371
|
+
timeout: float = 30.0,
|
|
372
|
+
**popen_kwargs: t.Any,
|
|
373
|
+
):
|
|
374
|
+
"""Context manager for a subprocess with automatic cleanup."""
|
|
375
|
+
async with NetworkResourceManager() as manager:
|
|
376
|
+
# Ensure we get bytes output for consistent type handling
|
|
377
|
+
process = subprocess.Popen[bytes](command, text=False, **popen_kwargs)
|
|
378
|
+
managed_proc = manager.create_subprocess(process, timeout)
|
|
379
|
+
try:
|
|
380
|
+
await managed_proc.start_monitoring()
|
|
381
|
+
yield managed_proc
|
|
382
|
+
finally:
|
|
383
|
+
await managed_proc.cleanup()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class WebSocketHealthMonitor:
|
|
387
|
+
"""Health monitor for WebSocket connections and servers."""
|
|
388
|
+
|
|
389
|
+
def __init__(self, check_interval: float = 30.0) -> None:
|
|
390
|
+
self.check_interval = check_interval
|
|
391
|
+
self.monitored_servers: list[ManagedWebSocketServer] = []
|
|
392
|
+
self.logger = logging.getLogger(__name__)
|
|
393
|
+
self._monitor_task: asyncio.Task[t.Any] | None = None
|
|
394
|
+
|
|
395
|
+
def add_server(self, server: ManagedWebSocketServer) -> None:
|
|
396
|
+
"""Add a server to health monitoring."""
|
|
397
|
+
self.monitored_servers.append(server)
|
|
398
|
+
|
|
399
|
+
def remove_server(self, server: ManagedWebSocketServer) -> None:
|
|
400
|
+
"""Remove a server from health monitoring."""
|
|
401
|
+
if server in self.monitored_servers:
|
|
402
|
+
self.monitored_servers.remove(server)
|
|
403
|
+
|
|
404
|
+
async def start_monitoring(self) -> None:
|
|
405
|
+
"""Start health monitoring."""
|
|
406
|
+
if self._monitor_task:
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
self._monitor_task = asyncio.create_task(self._monitor_loop())
|
|
410
|
+
|
|
411
|
+
async def stop_monitoring(self) -> None:
|
|
412
|
+
"""Stop health monitoring."""
|
|
413
|
+
if self._monitor_task and not self._monitor_task.done():
|
|
414
|
+
self._monitor_task.cancel()
|
|
415
|
+
try:
|
|
416
|
+
await self._monitor_task
|
|
417
|
+
except asyncio.CancelledError:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
async def _monitor_loop(self) -> None:
|
|
421
|
+
"""Main monitoring loop."""
|
|
422
|
+
try:
|
|
423
|
+
while True:
|
|
424
|
+
for server in self.monitored_servers.copy():
|
|
425
|
+
try:
|
|
426
|
+
await self._check_server_health(server)
|
|
427
|
+
except Exception as e:
|
|
428
|
+
self.logger.warning(f"Health check failed for server: {e}")
|
|
429
|
+
|
|
430
|
+
await asyncio.sleep(self.check_interval)
|
|
431
|
+
|
|
432
|
+
except asyncio.CancelledError:
|
|
433
|
+
pass
|
|
434
|
+
|
|
435
|
+
async def _check_server_health(self, server: ManagedWebSocketServer) -> None:
|
|
436
|
+
"""Check health of a specific server."""
|
|
437
|
+
if server._closed:
|
|
438
|
+
self.remove_server(server)
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
connection_count = server.get_connection_count()
|
|
442
|
+
|
|
443
|
+
# Log health metrics
|
|
444
|
+
self.logger.debug(
|
|
445
|
+
f"Server {server.host}:{server.port} - "
|
|
446
|
+
f"Active connections: {connection_count}"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Could add more sophisticated health checks here:
|
|
450
|
+
# - Memory usage monitoring
|
|
451
|
+
# - Connection rate limiting
|
|
452
|
+
# - Error rate monitoring
|
|
453
|
+
# - Automatic restart on failure
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# Global network resource cleanup
|
|
457
|
+
_global_network_managers: list[NetworkResourceManager] = []
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def register_network_manager(manager: NetworkResourceManager) -> None:
|
|
461
|
+
"""Register a network resource manager for global cleanup."""
|
|
462
|
+
_global_network_managers.append(manager)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
async def cleanup_all_network_resources() -> None:
|
|
466
|
+
"""Clean up all globally registered network resource managers."""
|
|
467
|
+
cleanup_tasks = [
|
|
468
|
+
asyncio.create_task(manager.cleanup_all())
|
|
469
|
+
for manager in _global_network_managers
|
|
470
|
+
]
|
|
471
|
+
|
|
472
|
+
if cleanup_tasks:
|
|
473
|
+
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
|
|
474
|
+
|
|
475
|
+
_global_network_managers.clear()
|