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,501 @@
|
|
|
1
|
+
"""Comprehensive resource management with automatic cleanup patterns.
|
|
2
|
+
|
|
3
|
+
This module provides RAII (Resource Acquisition Is Initialization) patterns
|
|
4
|
+
and comprehensive resource lifecycle management to prevent resource leaks
|
|
5
|
+
even in error scenarios.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import contextlib
|
|
10
|
+
import logging
|
|
11
|
+
import tempfile
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
import typing as t
|
|
15
|
+
import weakref
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import TracebackType
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ResourceProtocol(t.Protocol):
|
|
22
|
+
"""Protocol for resources that can be cleaned up."""
|
|
23
|
+
|
|
24
|
+
async def cleanup(self) -> None:
|
|
25
|
+
"""Clean up the resource."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ResourceManager:
|
|
30
|
+
"""Centralized resource management with automatic cleanup on error scenarios."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, logger: logging.Logger | None = None) -> None:
|
|
33
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
34
|
+
self._resources: list[ResourceProtocol] = []
|
|
35
|
+
self._cleanup_callbacks: list[t.Callable[[], t.Awaitable[None]]] = []
|
|
36
|
+
self._lock = threading.RLock()
|
|
37
|
+
self._closed = False
|
|
38
|
+
|
|
39
|
+
def register_resource(self, resource: ResourceProtocol) -> None:
|
|
40
|
+
"""Register a resource for automatic cleanup."""
|
|
41
|
+
with self._lock:
|
|
42
|
+
if self._closed:
|
|
43
|
+
# If already closed, immediately clean up the resource
|
|
44
|
+
asyncio.create_task(resource.cleanup())
|
|
45
|
+
return
|
|
46
|
+
self._resources.append(resource)
|
|
47
|
+
|
|
48
|
+
def register_cleanup_callback(
|
|
49
|
+
self, callback: t.Callable[[], t.Awaitable[None]]
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Register a cleanup callback to be called during shutdown."""
|
|
52
|
+
with self._lock:
|
|
53
|
+
if self._closed:
|
|
54
|
+
# If already closed, immediately call the callback
|
|
55
|
+
coro = callback()
|
|
56
|
+
if asyncio.iscoroutine(coro):
|
|
57
|
+
asyncio.ensure_future(coro)
|
|
58
|
+
return
|
|
59
|
+
self._cleanup_callbacks.append(callback)
|
|
60
|
+
|
|
61
|
+
async def cleanup_all(self) -> None:
|
|
62
|
+
"""Clean up all registered resources."""
|
|
63
|
+
with self._lock:
|
|
64
|
+
if self._closed:
|
|
65
|
+
return
|
|
66
|
+
self._closed = True
|
|
67
|
+
|
|
68
|
+
resources = self._resources.copy()
|
|
69
|
+
callbacks = self._cleanup_callbacks.copy()
|
|
70
|
+
|
|
71
|
+
# Clean up resources
|
|
72
|
+
for resource in resources:
|
|
73
|
+
try:
|
|
74
|
+
await resource.cleanup()
|
|
75
|
+
except Exception as e:
|
|
76
|
+
self.logger.warning(f"Error cleaning up resource {resource}: {e}")
|
|
77
|
+
|
|
78
|
+
# Call cleanup callbacks
|
|
79
|
+
for callback in callbacks:
|
|
80
|
+
try:
|
|
81
|
+
await callback()
|
|
82
|
+
except Exception as e:
|
|
83
|
+
self.logger.warning(f"Error in cleanup callback: {e}")
|
|
84
|
+
|
|
85
|
+
with self._lock:
|
|
86
|
+
self._resources.clear()
|
|
87
|
+
self._cleanup_callbacks.clear()
|
|
88
|
+
|
|
89
|
+
async def __aenter__(self) -> "ResourceManager":
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
async def __aexit__(
|
|
93
|
+
self,
|
|
94
|
+
exc_type: type[BaseException] | None,
|
|
95
|
+
exc_val: BaseException | None,
|
|
96
|
+
exc_tb: TracebackType | None,
|
|
97
|
+
) -> None:
|
|
98
|
+
await self.cleanup_all()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ManagedResource(ABC):
|
|
102
|
+
"""Base class for managed resources with automatic cleanup."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, manager: ResourceManager | None = None) -> None:
|
|
105
|
+
self.manager = manager
|
|
106
|
+
self._closed = False
|
|
107
|
+
|
|
108
|
+
if manager:
|
|
109
|
+
manager.register_resource(self)
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
async def cleanup(self) -> None:
|
|
113
|
+
"""Clean up the resource. Must be implemented by subclasses."""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
async def close(self) -> None:
|
|
117
|
+
"""Manually close the resource."""
|
|
118
|
+
if not self._closed:
|
|
119
|
+
self._closed = True
|
|
120
|
+
await self.cleanup()
|
|
121
|
+
|
|
122
|
+
def __del__(self) -> None:
|
|
123
|
+
"""Ensure cleanup is called even if explicitly forgotten."""
|
|
124
|
+
if not self._closed:
|
|
125
|
+
# Create cleanup task if event loop is available
|
|
126
|
+
with contextlib.suppress(RuntimeError):
|
|
127
|
+
loop = asyncio.get_event_loop()
|
|
128
|
+
if loop.is_running():
|
|
129
|
+
asyncio.create_task(self.cleanup())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ManagedTemporaryFile(ManagedResource):
|
|
133
|
+
"""Temporary file with automatic cleanup on error scenarios."""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
suffix: str = "",
|
|
138
|
+
prefix: str = "crackerjack-",
|
|
139
|
+
manager: ResourceManager | None = None,
|
|
140
|
+
) -> None:
|
|
141
|
+
super().__init__(manager)
|
|
142
|
+
self.temp_file = tempfile.NamedTemporaryFile(
|
|
143
|
+
suffix=suffix,
|
|
144
|
+
prefix=prefix,
|
|
145
|
+
delete=False, # We'll handle deletion ourselves
|
|
146
|
+
)
|
|
147
|
+
self.path = Path(self.temp_file.name)
|
|
148
|
+
|
|
149
|
+
async def cleanup(self) -> None:
|
|
150
|
+
"""Clean up temporary file."""
|
|
151
|
+
if not self._closed:
|
|
152
|
+
self._closed = True
|
|
153
|
+
|
|
154
|
+
# Close file handle
|
|
155
|
+
if not self.temp_file.closed:
|
|
156
|
+
self.temp_file.close()
|
|
157
|
+
|
|
158
|
+
# Remove file if it exists
|
|
159
|
+
try:
|
|
160
|
+
if self.path.exists():
|
|
161
|
+
self.path.unlink()
|
|
162
|
+
except OSError as e:
|
|
163
|
+
# Log but don't raise - cleanup should be best effort
|
|
164
|
+
logging.getLogger(__name__).warning(
|
|
165
|
+
f"Failed to remove temporary file {self.path}: {e}"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def write_text(self, content: str, encoding: str = "utf-8") -> None:
|
|
169
|
+
"""Write text content to the temporary file."""
|
|
170
|
+
if self._closed:
|
|
171
|
+
raise RuntimeError("Cannot write to closed temporary file")
|
|
172
|
+
self.path.write_text(content, encoding=encoding)
|
|
173
|
+
|
|
174
|
+
def read_text(self, encoding: str = "utf-8") -> str:
|
|
175
|
+
"""Read text content from the temporary file."""
|
|
176
|
+
return self.path.read_text(encoding=encoding)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ManagedTemporaryDirectory(ManagedResource):
|
|
180
|
+
"""Temporary directory with automatic cleanup on error scenarios."""
|
|
181
|
+
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
suffix: str = "",
|
|
185
|
+
prefix: str = "crackerjack-",
|
|
186
|
+
manager: ResourceManager | None = None,
|
|
187
|
+
) -> None:
|
|
188
|
+
super().__init__(manager)
|
|
189
|
+
self.temp_dir = tempfile.mkdtemp(suffix=suffix, prefix=prefix)
|
|
190
|
+
self.path = Path(self.temp_dir)
|
|
191
|
+
|
|
192
|
+
async def cleanup(self) -> None:
|
|
193
|
+
"""Clean up temporary directory and all contents."""
|
|
194
|
+
if not self._closed:
|
|
195
|
+
self._closed = True
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
import shutil
|
|
199
|
+
|
|
200
|
+
if self.path.exists():
|
|
201
|
+
shutil.rmtree(self.path)
|
|
202
|
+
except OSError as e:
|
|
203
|
+
logging.getLogger(__name__).warning(
|
|
204
|
+
f"Failed to remove temporary directory {self.path}: {e}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class ManagedProcess(ManagedResource):
|
|
209
|
+
"""Process with automatic cleanup on error scenarios."""
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
process: asyncio.subprocess.Process,
|
|
214
|
+
timeout: float = 30.0,
|
|
215
|
+
manager: ResourceManager | None = None,
|
|
216
|
+
) -> None:
|
|
217
|
+
super().__init__(manager)
|
|
218
|
+
self.process = process
|
|
219
|
+
self.timeout = timeout
|
|
220
|
+
|
|
221
|
+
async def cleanup(self) -> None:
|
|
222
|
+
"""Clean up process with graceful termination."""
|
|
223
|
+
if not self._closed and self.process.returncode is None:
|
|
224
|
+
self._closed = True
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
# Try graceful termination first
|
|
228
|
+
self.process.terminate()
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
await asyncio.wait_for(self.process.wait(), timeout=5.0)
|
|
232
|
+
except TimeoutError:
|
|
233
|
+
# Force kill if graceful termination fails
|
|
234
|
+
self.process.kill()
|
|
235
|
+
try:
|
|
236
|
+
await asyncio.wait_for(self.process.wait(), timeout=2.0)
|
|
237
|
+
except TimeoutError:
|
|
238
|
+
logging.getLogger(__name__).warning(
|
|
239
|
+
f"Process {self.process.pid} did not terminate after force kill"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
except ProcessLookupError:
|
|
243
|
+
# Process already terminated
|
|
244
|
+
pass
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logging.getLogger(__name__).warning(
|
|
247
|
+
f"Error cleaning up process {self.process.pid}: {e}"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ManagedTask(ManagedResource):
|
|
252
|
+
"""Asyncio task with automatic cancellation on error scenarios."""
|
|
253
|
+
|
|
254
|
+
def __init__(
|
|
255
|
+
self,
|
|
256
|
+
task: asyncio.Task[t.Any],
|
|
257
|
+
timeout: float = 30.0,
|
|
258
|
+
manager: ResourceManager | None = None,
|
|
259
|
+
) -> None:
|
|
260
|
+
super().__init__(manager)
|
|
261
|
+
self.task = task
|
|
262
|
+
self.timeout = timeout
|
|
263
|
+
|
|
264
|
+
async def cleanup(self) -> None:
|
|
265
|
+
"""Clean up task with cancellation."""
|
|
266
|
+
if not self._closed and not self.task.done():
|
|
267
|
+
self._closed = True
|
|
268
|
+
|
|
269
|
+
self.task.cancel()
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
await asyncio.wait_for(self.task, timeout=self.timeout)
|
|
273
|
+
except (TimeoutError, asyncio.CancelledError):
|
|
274
|
+
# Expected when cancelling or timing out
|
|
275
|
+
pass
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logging.getLogger(__name__).warning(f"Error cleaning up task: {e}")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ManagedFileHandle(ManagedResource):
|
|
281
|
+
"""File handle with automatic closing on error scenarios."""
|
|
282
|
+
|
|
283
|
+
def __init__(
|
|
284
|
+
self,
|
|
285
|
+
file_handle: t.IO[t.Any],
|
|
286
|
+
manager: ResourceManager | None = None,
|
|
287
|
+
) -> None:
|
|
288
|
+
super().__init__(manager)
|
|
289
|
+
self.file_handle = file_handle
|
|
290
|
+
|
|
291
|
+
async def cleanup(self) -> None:
|
|
292
|
+
"""Clean up file handle."""
|
|
293
|
+
if not self._closed and not self.file_handle.closed:
|
|
294
|
+
self._closed = True
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
self.file_handle.close()
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logging.getLogger(__name__).warning(f"Error closing file handle: {e}")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ResourceContext:
|
|
303
|
+
"""Context manager for automatic resource management."""
|
|
304
|
+
|
|
305
|
+
def __init__(self) -> None:
|
|
306
|
+
self.resource_manager = ResourceManager()
|
|
307
|
+
|
|
308
|
+
def managed_temp_file(
|
|
309
|
+
self,
|
|
310
|
+
suffix: str = "",
|
|
311
|
+
prefix: str = "crackerjack-",
|
|
312
|
+
) -> ManagedTemporaryFile:
|
|
313
|
+
"""Create a managed temporary file."""
|
|
314
|
+
return ManagedTemporaryFile(suffix, prefix, self.resource_manager)
|
|
315
|
+
|
|
316
|
+
def managed_temp_dir(
|
|
317
|
+
self,
|
|
318
|
+
suffix: str = "",
|
|
319
|
+
prefix: str = "crackerjack-",
|
|
320
|
+
) -> ManagedTemporaryDirectory:
|
|
321
|
+
"""Create a managed temporary directory."""
|
|
322
|
+
return ManagedTemporaryDirectory(suffix, prefix, self.resource_manager)
|
|
323
|
+
|
|
324
|
+
def managed_process(
|
|
325
|
+
self,
|
|
326
|
+
process: asyncio.subprocess.Process,
|
|
327
|
+
timeout: float = 30.0,
|
|
328
|
+
) -> ManagedProcess:
|
|
329
|
+
"""Create a managed process."""
|
|
330
|
+
return ManagedProcess(process, timeout, self.resource_manager)
|
|
331
|
+
|
|
332
|
+
def managed_task(
|
|
333
|
+
self,
|
|
334
|
+
task: asyncio.Task[t.Any],
|
|
335
|
+
timeout: float = 30.0,
|
|
336
|
+
) -> ManagedTask:
|
|
337
|
+
"""Create a managed task."""
|
|
338
|
+
return ManagedTask(task, timeout, self.resource_manager)
|
|
339
|
+
|
|
340
|
+
def managed_file(
|
|
341
|
+
self,
|
|
342
|
+
file_handle: t.IO[t.Any],
|
|
343
|
+
) -> ManagedFileHandle:
|
|
344
|
+
"""Create a managed file handle."""
|
|
345
|
+
return ManagedFileHandle(file_handle, self.resource_manager)
|
|
346
|
+
|
|
347
|
+
async def __aenter__(self) -> "ResourceContext":
|
|
348
|
+
await self.resource_manager.__aenter__()
|
|
349
|
+
return self
|
|
350
|
+
|
|
351
|
+
async def __aexit__(
|
|
352
|
+
self,
|
|
353
|
+
exc_type: type[BaseException] | None,
|
|
354
|
+
exc_val: BaseException | None,
|
|
355
|
+
exc_tb: TracebackType | None,
|
|
356
|
+
) -> None:
|
|
357
|
+
await self.resource_manager.__aexit__(exc_type, exc_val, exc_tb)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# Global resource manager registry
|
|
361
|
+
_global_managers: weakref.WeakSet[ResourceManager] = weakref.WeakSet()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def register_global_resource_manager(manager: ResourceManager) -> None:
|
|
365
|
+
"""Register a resource manager for global cleanup."""
|
|
366
|
+
_global_managers.add(manager)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
async def cleanup_all_global_resources() -> None:
|
|
370
|
+
"""Clean up all globally registered resource managers."""
|
|
371
|
+
managers = list(_global_managers)
|
|
372
|
+
|
|
373
|
+
cleanup_tasks = [asyncio.create_task(manager.cleanup_all()) for manager in managers]
|
|
374
|
+
|
|
375
|
+
if cleanup_tasks:
|
|
376
|
+
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# Cleanup helper context managers
|
|
380
|
+
@contextlib.asynccontextmanager
|
|
381
|
+
async def with_resource_cleanup():
|
|
382
|
+
"""Context manager that ensures resource cleanup on any exit."""
|
|
383
|
+
async with ResourceContext() as ctx:
|
|
384
|
+
yield ctx
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@contextlib.asynccontextmanager
|
|
388
|
+
async def with_temp_file(suffix: str = "", prefix: str = "crackerjack-"):
|
|
389
|
+
"""Context manager for temporary file with automatic cleanup."""
|
|
390
|
+
async with ResourceContext() as ctx:
|
|
391
|
+
temp_file = ctx.managed_temp_file(suffix, prefix)
|
|
392
|
+
try:
|
|
393
|
+
yield temp_file
|
|
394
|
+
finally:
|
|
395
|
+
await temp_file.cleanup()
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@contextlib.asynccontextmanager
|
|
399
|
+
async def with_temp_dir(suffix: str = "", prefix: str = "crackerjack-"):
|
|
400
|
+
"""Context manager for temporary directory with automatic cleanup."""
|
|
401
|
+
async with ResourceContext() as ctx:
|
|
402
|
+
temp_dir = ctx.managed_temp_dir(suffix, prefix)
|
|
403
|
+
try:
|
|
404
|
+
yield temp_dir
|
|
405
|
+
finally:
|
|
406
|
+
await temp_dir.cleanup()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@contextlib.asynccontextmanager
|
|
410
|
+
async def with_managed_process(
|
|
411
|
+
process: asyncio.subprocess.Process,
|
|
412
|
+
timeout: float = 30.0,
|
|
413
|
+
):
|
|
414
|
+
"""Context manager for process with automatic cleanup."""
|
|
415
|
+
async with ResourceContext() as ctx:
|
|
416
|
+
managed_proc = ctx.managed_process(process, timeout)
|
|
417
|
+
try:
|
|
418
|
+
yield managed_proc
|
|
419
|
+
finally:
|
|
420
|
+
await managed_proc.cleanup()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# Enhanced error handling utilities
|
|
424
|
+
class ResourceLeakDetector:
|
|
425
|
+
"""Detector for potential resource leaks during development."""
|
|
426
|
+
|
|
427
|
+
def __init__(self) -> None:
|
|
428
|
+
self.open_files: set[str] = set()
|
|
429
|
+
self.active_processes: set[int] = set()
|
|
430
|
+
self.active_tasks: set[asyncio.Task[t.Any]] = set()
|
|
431
|
+
self._start_time = time.time()
|
|
432
|
+
|
|
433
|
+
def track_file(self, file_path: str) -> None:
|
|
434
|
+
"""Track an opened file."""
|
|
435
|
+
self.open_files.add(file_path)
|
|
436
|
+
|
|
437
|
+
def untrack_file(self, file_path: str) -> None:
|
|
438
|
+
"""Untrack a closed file."""
|
|
439
|
+
self.open_files.discard(file_path)
|
|
440
|
+
|
|
441
|
+
def track_process(self, pid: int) -> None:
|
|
442
|
+
"""Track a spawned process."""
|
|
443
|
+
self.active_processes.add(pid)
|
|
444
|
+
|
|
445
|
+
def untrack_process(self, pid: int) -> None:
|
|
446
|
+
"""Untrack a terminated process."""
|
|
447
|
+
self.active_processes.discard(pid)
|
|
448
|
+
|
|
449
|
+
def track_task(self, task: asyncio.Task[t.Any]) -> None:
|
|
450
|
+
"""Track an active task."""
|
|
451
|
+
self.active_tasks.add(task)
|
|
452
|
+
|
|
453
|
+
def untrack_task(self, task: asyncio.Task[t.Any]) -> None:
|
|
454
|
+
"""Untrack a completed task."""
|
|
455
|
+
self.active_tasks.discard(task)
|
|
456
|
+
|
|
457
|
+
def get_leak_report(self) -> dict[str, t.Any]:
|
|
458
|
+
"""Get a report of potential resource leaks."""
|
|
459
|
+
return {
|
|
460
|
+
"duration_seconds": time.time() - self._start_time,
|
|
461
|
+
"open_files": list(self.open_files),
|
|
462
|
+
"active_processes": list(self.active_processes),
|
|
463
|
+
"active_tasks": len([t for t in self.active_tasks if not t.done()]),
|
|
464
|
+
"total_tracked_files": len(self.open_files),
|
|
465
|
+
"total_tracked_processes": len(self.active_processes),
|
|
466
|
+
"total_tracked_tasks": len(self.active_tasks),
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
def has_potential_leaks(self) -> bool:
|
|
470
|
+
"""Check if there are potential resource leaks."""
|
|
471
|
+
return bool(
|
|
472
|
+
self.open_files
|
|
473
|
+
or self.active_processes
|
|
474
|
+
or any(not t.done() for t in self.active_tasks)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# Development-time resource leak detection
|
|
479
|
+
_leak_detector: ResourceLeakDetector | None = None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def enable_leak_detection() -> ResourceLeakDetector:
|
|
483
|
+
"""Enable resource leak detection for development."""
|
|
484
|
+
global _leak_detector
|
|
485
|
+
_leak_detector = ResourceLeakDetector()
|
|
486
|
+
return _leak_detector
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def get_leak_detector() -> ResourceLeakDetector | None:
|
|
490
|
+
"""Get the current resource leak detector, if enabled."""
|
|
491
|
+
return _leak_detector
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def disable_leak_detection() -> dict[str, t.Any] | None:
|
|
495
|
+
"""Disable resource leak detection and return final report."""
|
|
496
|
+
global _leak_detector
|
|
497
|
+
if _leak_detector:
|
|
498
|
+
report = _leak_detector.get_leak_report()
|
|
499
|
+
_leak_detector = None
|
|
500
|
+
return report
|
|
501
|
+
return None
|