zen-ai-pentest 2.0.0__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.
- agents/__init__.py +28 -0
- agents/agent_base.py +239 -0
- agents/agent_orchestrator.py +346 -0
- agents/analysis_agent.py +225 -0
- agents/cli.py +258 -0
- agents/exploit_agent.py +224 -0
- agents/integration.py +211 -0
- agents/post_scan_agent.py +937 -0
- agents/react_agent.py +384 -0
- agents/react_agent_enhanced.py +616 -0
- agents/react_agent_vm.py +298 -0
- agents/research_agent.py +176 -0
- api/__init__.py +11 -0
- api/auth.py +123 -0
- api/main.py +1027 -0
- api/schemas.py +357 -0
- api/websocket.py +97 -0
- autonomous/__init__.py +122 -0
- autonomous/agent.py +253 -0
- autonomous/agent_loop.py +1370 -0
- autonomous/exploit_validator.py +1537 -0
- autonomous/memory.py +448 -0
- autonomous/react.py +339 -0
- autonomous/tool_executor.py +488 -0
- backends/__init__.py +16 -0
- backends/chatgpt_direct.py +133 -0
- backends/claude_direct.py +130 -0
- backends/duckduckgo.py +138 -0
- backends/openrouter.py +120 -0
- benchmarks/__init__.py +149 -0
- benchmarks/benchmark_engine.py +904 -0
- benchmarks/ci_benchmark.py +785 -0
- benchmarks/comparison.py +729 -0
- benchmarks/metrics.py +553 -0
- benchmarks/run_benchmarks.py +809 -0
- ci_cd/__init__.py +2 -0
- core/__init__.py +17 -0
- core/async_pool.py +282 -0
- core/asyncio_fix.py +222 -0
- core/cache.py +472 -0
- core/container.py +277 -0
- core/database.py +114 -0
- core/input_validator.py +353 -0
- core/models.py +288 -0
- core/orchestrator.py +611 -0
- core/plugin_manager.py +571 -0
- core/rate_limiter.py +405 -0
- core/secure_config.py +328 -0
- core/shield_integration.py +296 -0
- modules/__init__.py +46 -0
- modules/cve_database.py +362 -0
- modules/exploit_assist.py +330 -0
- modules/nuclei_integration.py +480 -0
- modules/osint.py +604 -0
- modules/protonvpn.py +554 -0
- modules/recon.py +165 -0
- modules/sql_injection_db.py +826 -0
- modules/tool_orchestrator.py +498 -0
- modules/vuln_scanner.py +292 -0
- modules/wordlist_generator.py +566 -0
- risk_engine/__init__.py +99 -0
- risk_engine/business_impact.py +267 -0
- risk_engine/business_impact_calculator.py +563 -0
- risk_engine/cvss.py +156 -0
- risk_engine/epss.py +190 -0
- risk_engine/example_usage.py +294 -0
- risk_engine/false_positive_engine.py +1073 -0
- risk_engine/scorer.py +304 -0
- web_ui/backend/main.py +471 -0
- zen_ai_pentest-2.0.0.dist-info/METADATA +795 -0
- zen_ai_pentest-2.0.0.dist-info/RECORD +75 -0
- zen_ai_pentest-2.0.0.dist-info/WHEEL +5 -0
- zen_ai_pentest-2.0.0.dist-info/entry_points.txt +2 -0
- zen_ai_pentest-2.0.0.dist-info/licenses/LICENSE +21 -0
- zen_ai_pentest-2.0.0.dist-info/top_level.txt +10 -0
ci_cd/__init__.py
ADDED
core/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zen AI Pentest - Multi-LLM Penetration Testing Intelligence System
|
|
3
|
+
Author: SHAdd0WTAka
|
|
4
|
+
Version: 1.0.0
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# AsyncIO Fix für Windows Python 3.13+ (Issue #10)
|
|
8
|
+
# Muss vor allen anderen Imports geladen werden
|
|
9
|
+
try:
|
|
10
|
+
from .asyncio_fix import patch_asyncio_for_windows, safe_asyncio_run
|
|
11
|
+
patch_asyncio_for_windows()
|
|
12
|
+
except ImportError:
|
|
13
|
+
pass # Not critical
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
16
|
+
__author__ = "SHAdd0WTAka"
|
|
17
|
+
__description__ = "Multi-LLM Penetration Testing Intelligence System"
|
core/async_pool.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async Connection Pooling and Resource Management
|
|
3
|
+
Optimized HTTP client with connection reuse
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, AsyncGenerator, Dict, Optional
|
|
11
|
+
|
|
12
|
+
import aiohttp
|
|
13
|
+
|
|
14
|
+
from core.rate_limiter import RateLimitedClient, TokenBucket
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PoolConfig:
|
|
21
|
+
"""Connection pool configuration"""
|
|
22
|
+
|
|
23
|
+
limit: int = 100
|
|
24
|
+
limit_per_host: int = 10
|
|
25
|
+
ttl_dns_cache: int = 300
|
|
26
|
+
use_dns_cache: bool = True
|
|
27
|
+
timeout: aiohttp.ClientTimeout = None
|
|
28
|
+
|
|
29
|
+
def __post_init__(self):
|
|
30
|
+
if self.timeout is None:
|
|
31
|
+
self.timeout = aiohttp.ClientTimeout(total=60, connect=10)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConnectionPool:
|
|
35
|
+
"""
|
|
36
|
+
Managed HTTP connection pool with rate limiting.
|
|
37
|
+
Implements proper connection reuse and cleanup.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, config: PoolConfig = None):
|
|
41
|
+
self.config = config or PoolConfig()
|
|
42
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
43
|
+
self._connector: Optional[aiohttp.TCPConnector] = None
|
|
44
|
+
self._semaphore: Optional[asyncio.Semaphore] = None
|
|
45
|
+
self._rate_limiter: Optional[TokenBucket] = None
|
|
46
|
+
self._lock = asyncio.Lock()
|
|
47
|
+
self._closed = True
|
|
48
|
+
|
|
49
|
+
async def _create_session(self) -> aiohttp.ClientSession:
|
|
50
|
+
"""Create HTTP session with optimized connector"""
|
|
51
|
+
self._connector = aiohttp.TCPConnector(
|
|
52
|
+
limit=self.config.limit,
|
|
53
|
+
limit_per_host=self.config.limit_per_host,
|
|
54
|
+
ttl_dns_cache=self.config.ttl_dns_cache,
|
|
55
|
+
use_dns_cache=self.config.use_dns_cache,
|
|
56
|
+
enable_cleanup_closed=True,
|
|
57
|
+
force_close=False,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Global semaphore for concurrent request limiting
|
|
61
|
+
self._semaphore = asyncio.Semaphore(self.config.limit)
|
|
62
|
+
|
|
63
|
+
self._session = aiohttp.ClientSession(
|
|
64
|
+
connector=self._connector,
|
|
65
|
+
timeout=self.config.timeout,
|
|
66
|
+
headers={
|
|
67
|
+
"User-Agent": "Zen-AI-Pentest/1.0 (Security Scanner)",
|
|
68
|
+
"Accept": "application/json, text/plain, */*",
|
|
69
|
+
"Accept-Encoding": "gzip, deflate",
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
self._closed = False
|
|
74
|
+
return self._session
|
|
75
|
+
|
|
76
|
+
async def get_session(self) -> aiohttp.ClientSession:
|
|
77
|
+
"""Get or create session"""
|
|
78
|
+
if self._session is None or self._session.closed:
|
|
79
|
+
async with self._lock:
|
|
80
|
+
if self._session is None or self._session.closed:
|
|
81
|
+
await self._create_session()
|
|
82
|
+
return self._session
|
|
83
|
+
|
|
84
|
+
@asynccontextmanager
|
|
85
|
+
async def request(
|
|
86
|
+
self, method: str, url: str, rate_limit: float = None, **kwargs
|
|
87
|
+
) -> AsyncGenerator[aiohttp.ClientResponse, None]:
|
|
88
|
+
"""
|
|
89
|
+
Make rate-limited HTTP request with connection pooling.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
method: HTTP method
|
|
93
|
+
url: Request URL
|
|
94
|
+
rate_limit: Max requests per second (None = unlimited)
|
|
95
|
+
**kwargs: Additional aiohttp request arguments
|
|
96
|
+
"""
|
|
97
|
+
session = await self.get_session()
|
|
98
|
+
|
|
99
|
+
# Apply rate limiting if configured
|
|
100
|
+
if rate_limit and self._rate_limiter is None:
|
|
101
|
+
self._rate_limiter = TokenBucket(rate=rate_limit, capacity=5)
|
|
102
|
+
|
|
103
|
+
if self._rate_limiter:
|
|
104
|
+
await self._rate_limiter.wait()
|
|
105
|
+
|
|
106
|
+
# Limit concurrent requests
|
|
107
|
+
async with self._semaphore:
|
|
108
|
+
try:
|
|
109
|
+
async with session.request(method, url, **kwargs) as response:
|
|
110
|
+
yield response
|
|
111
|
+
except aiohttp.ClientError as e:
|
|
112
|
+
logger.error(f"HTTP error {method} {url}: {e}")
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
async def get(self, url: str, **kwargs) -> aiohttp.ClientResponse:
|
|
116
|
+
"""Simple GET request"""
|
|
117
|
+
async with self.request("GET", url, **kwargs) as response:
|
|
118
|
+
return await response.text()
|
|
119
|
+
|
|
120
|
+
async def post(self, url: str, **kwargs) -> aiohttp.ClientResponse:
|
|
121
|
+
"""Simple POST request"""
|
|
122
|
+
async with self.request("POST", url, **kwargs) as response:
|
|
123
|
+
return await response.text()
|
|
124
|
+
|
|
125
|
+
async def json_get(self, url: str, **kwargs) -> Dict[str, Any]:
|
|
126
|
+
"""GET request with JSON parsing"""
|
|
127
|
+
async with self.request("GET", url, **kwargs) as response:
|
|
128
|
+
return await response.json()
|
|
129
|
+
|
|
130
|
+
async def close(self):
|
|
131
|
+
"""Close all connections properly"""
|
|
132
|
+
async with self._lock:
|
|
133
|
+
if self._session and not self._session.closed:
|
|
134
|
+
await self._session.close()
|
|
135
|
+
if self._connector and not self._connector.closed:
|
|
136
|
+
await self._connector.close()
|
|
137
|
+
self._closed = True
|
|
138
|
+
logger.debug("Connection pool closed")
|
|
139
|
+
|
|
140
|
+
async def __aenter__(self):
|
|
141
|
+
await self.get_session()
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
145
|
+
await self.close()
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def closed(self) -> bool:
|
|
149
|
+
return self._closed
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SmartHTTPClient(RateLimitedClient):
|
|
153
|
+
"""
|
|
154
|
+
HTTP client with circuit breaker, retries, and connection pooling.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
name: str,
|
|
160
|
+
pool_config: PoolConfig = None,
|
|
161
|
+
rate_config: Any = None,
|
|
162
|
+
circuit_config: Any = None,
|
|
163
|
+
):
|
|
164
|
+
super().__init__(name, rate_config, circuit_config)
|
|
165
|
+
self.pool = ConnectionPool(pool_config)
|
|
166
|
+
|
|
167
|
+
async def _do_request(self, method: str, url: str, **kwargs) -> Dict[str, Any]:
|
|
168
|
+
"""Execute HTTP request with full error handling"""
|
|
169
|
+
async with self.pool.request(method, url, **kwargs) as response:
|
|
170
|
+
content_type = response.headers.get("Content-Type", "")
|
|
171
|
+
|
|
172
|
+
if "application/json" in content_type:
|
|
173
|
+
data = await response.json()
|
|
174
|
+
else:
|
|
175
|
+
data = await response.text()
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"status": response.status,
|
|
179
|
+
"headers": dict(response.headers),
|
|
180
|
+
"data": data,
|
|
181
|
+
"url": str(response.url),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async def close(self):
|
|
185
|
+
await self.pool.close()
|
|
186
|
+
await super().close()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ParallelProcessor:
|
|
190
|
+
"""
|
|
191
|
+
Execute tasks in parallel with semaphore-controlled concurrency.
|
|
192
|
+
Prevents resource exhaustion during bulk operations.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(self, max_concurrent: int = 10):
|
|
196
|
+
self.max_concurrent = max_concurrent
|
|
197
|
+
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
198
|
+
|
|
199
|
+
async def run(self, tasks: list, return_exceptions: bool = True) -> list:
|
|
200
|
+
"""
|
|
201
|
+
Run tasks with controlled concurrency.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
tasks: List of coroutines
|
|
205
|
+
return_exceptions: If True, exceptions are returned instead of raised
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
async def bounded_task(task):
|
|
209
|
+
async with self._semaphore:
|
|
210
|
+
return await task
|
|
211
|
+
|
|
212
|
+
# Create bounded tasks
|
|
213
|
+
bounded = [bounded_task(t) for t in tasks]
|
|
214
|
+
|
|
215
|
+
# Execute with exception handling
|
|
216
|
+
return await asyncio.gather(*bounded, return_exceptions=return_exceptions)
|
|
217
|
+
|
|
218
|
+
async def map(self, func: callable, items: list, *args, **kwargs) -> list:
|
|
219
|
+
"""
|
|
220
|
+
Map function over items with controlled concurrency.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
func: Async function to apply
|
|
224
|
+
items: List of items to process
|
|
225
|
+
*args, **kwargs: Additional arguments for func
|
|
226
|
+
"""
|
|
227
|
+
tasks = [func(item, *args, **kwargs) for item in items]
|
|
228
|
+
return await self.run(tasks)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# Global pool instance
|
|
232
|
+
_global_pool: Optional[ConnectionPool] = None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
async def get_global_pool() -> ConnectionPool:
|
|
236
|
+
"""Get global connection pool"""
|
|
237
|
+
global _global_pool
|
|
238
|
+
if _global_pool is None:
|
|
239
|
+
_global_pool = ConnectionPool()
|
|
240
|
+
return _global_pool
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
async def close_global_pool():
|
|
244
|
+
"""Close global connection pool"""
|
|
245
|
+
global _global_pool
|
|
246
|
+
if _global_pool:
|
|
247
|
+
await _global_pool.close()
|
|
248
|
+
_global_pool = None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# Example usage patterns
|
|
252
|
+
async def example_bulk_requests(urls: list[str]):
|
|
253
|
+
"""Example: Bulk HTTP requests with connection pooling"""
|
|
254
|
+
pool = ConnectionPool(PoolConfig(limit_per_host=5))
|
|
255
|
+
|
|
256
|
+
processor = ParallelProcessor(max_concurrent=10)
|
|
257
|
+
|
|
258
|
+
async def fetch(url):
|
|
259
|
+
async with pool.request("GET", url) as response:
|
|
260
|
+
return await response.text()
|
|
261
|
+
|
|
262
|
+
results = await processor.map(fetch, urls)
|
|
263
|
+
await pool.close()
|
|
264
|
+
return results
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def example_with_circuit_breaker():
|
|
268
|
+
"""Example: HTTP client with circuit breaker"""
|
|
269
|
+
from core.rate_limiter import CircuitBreakerConfig, RateLimitConfig
|
|
270
|
+
|
|
271
|
+
client = SmartHTTPClient(
|
|
272
|
+
name="api-client",
|
|
273
|
+
pool_config=PoolConfig(limit_per_host=5),
|
|
274
|
+
rate_config=RateLimitConfig(requests_per_second=2.0, burst_size=5),
|
|
275
|
+
circuit_config=CircuitBreakerConfig(failure_threshold=3, recovery_timeout=30.0),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
result = await client.request("GET", "https://api.example.com/data")
|
|
280
|
+
print(result)
|
|
281
|
+
finally:
|
|
282
|
+
await client.close()
|
core/asyncio_fix.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python 3.13 Windows AsyncIO Fix
|
|
3
|
+
Issue #10: Python 3.13 Windows AsyncIO Fix permanent lösen
|
|
4
|
+
|
|
5
|
+
Fix für bekannte Probleme:
|
|
6
|
+
- ProactorEventLoop issues on Windows
|
|
7
|
+
- asyncio.run() hanging
|
|
8
|
+
- Task cancellation problems
|
|
9
|
+
- Event loop policy conflicts
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import asyncio
|
|
14
|
+
import platform
|
|
15
|
+
import warnings
|
|
16
|
+
from typing import Optional
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_windows() -> bool:
|
|
23
|
+
"""Check if running on Windows"""
|
|
24
|
+
return platform.system() == "Windows"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_python_313_or_higher() -> bool:
|
|
28
|
+
"""Check if Python 3.13 or higher"""
|
|
29
|
+
return sys.version_info >= (3, 13)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def apply_windows_asyncio_fix():
|
|
33
|
+
"""
|
|
34
|
+
Wendet Windows-spezifische AsyncIO Fixes an
|
|
35
|
+
|
|
36
|
+
Muss vor der ersten asyncio Nutzung aufgerufen werden!
|
|
37
|
+
"""
|
|
38
|
+
if not is_windows():
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
logger.info("Applying Windows AsyncIO fixes...")
|
|
42
|
+
|
|
43
|
+
# Fix 1: SelectorEventLoop für Windows (statt Proactor)
|
|
44
|
+
# Python 3.13 hat Proactor-Probleme auf Windows
|
|
45
|
+
try:
|
|
46
|
+
if is_python_313_or_higher():
|
|
47
|
+
# Nutze SelectorEventLoop als Workaround
|
|
48
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
49
|
+
logger.info("Set WindowsSelectorEventLoopPolicy")
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.warning(f"Could not set SelectorEventLoopPolicy: {e}")
|
|
52
|
+
|
|
53
|
+
# Fix 2: Erhöhe Stack-Size für recursive coroutines
|
|
54
|
+
try:
|
|
55
|
+
import threading
|
|
56
|
+
threading.stack_size(2 * 1024 * 1024) # 2MB stack
|
|
57
|
+
except Exception:
|
|
58
|
+
pass # Nicht kritisch
|
|
59
|
+
|
|
60
|
+
logger.info("Windows AsyncIO fixes applied")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SafeAsyncioRunner:
|
|
64
|
+
"""
|
|
65
|
+
Sicherer AsyncIO Runner für Windows Python 3.13+
|
|
66
|
+
|
|
67
|
+
Workaround für hängende asyncio.run() Aufrufe
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, timeout: Optional[float] = None):
|
|
71
|
+
self.timeout = timeout
|
|
72
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
73
|
+
|
|
74
|
+
def run(self, coro):
|
|
75
|
+
"""
|
|
76
|
+
Führt eine Coroutine sicher aus
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
coro: Die auszuführende Coroutine
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Das Ergebnis der Coroutine
|
|
83
|
+
"""
|
|
84
|
+
# Fix anwenden falls nötig
|
|
85
|
+
if is_windows() and is_python_313_or_higher():
|
|
86
|
+
return self._run_with_fix(coro)
|
|
87
|
+
else:
|
|
88
|
+
return asyncio.run(coro)
|
|
89
|
+
|
|
90
|
+
def _run_with_fix(self, coro):
|
|
91
|
+
"""Interne Run-Methode mit Windows Fixes"""
|
|
92
|
+
# Erstelle neuen Loop statt asyncio.run()
|
|
93
|
+
loop = asyncio.new_event_loop()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
asyncio.set_event_loop(loop)
|
|
97
|
+
|
|
98
|
+
# Setze exception handler
|
|
99
|
+
def exception_handler(loop, context):
|
|
100
|
+
logger.error(f"AsyncIO Exception: {context}")
|
|
101
|
+
|
|
102
|
+
loop.set_exception_handler(exception_handler)
|
|
103
|
+
|
|
104
|
+
# Führe coroutine aus
|
|
105
|
+
if self.timeout:
|
|
106
|
+
return loop.run_until_complete(
|
|
107
|
+
asyncio.wait_for(coro, timeout=self.timeout)
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
return loop.run_until_complete(coro)
|
|
111
|
+
|
|
112
|
+
finally:
|
|
113
|
+
# Cleanup
|
|
114
|
+
try:
|
|
115
|
+
# Cancel pending tasks
|
|
116
|
+
pending = asyncio.all_tasks(loop)
|
|
117
|
+
if pending:
|
|
118
|
+
for task in pending:
|
|
119
|
+
task.cancel()
|
|
120
|
+
|
|
121
|
+
# Wait for cancellation
|
|
122
|
+
loop.run_until_complete(
|
|
123
|
+
asyncio.gather(*pending, return_exceptions=True)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
loop.run_until_complete(loop.shutdown_asyncgens())
|
|
127
|
+
loop.close()
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.warning(f"Error during loop cleanup: {e}")
|
|
131
|
+
|
|
132
|
+
asyncio.set_event_loop(None)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def safe_asyncio_run(coro, timeout: Optional[float] = None):
|
|
136
|
+
"""
|
|
137
|
+
Sicherer Ersatz für asyncio.run()
|
|
138
|
+
|
|
139
|
+
Usage:
|
|
140
|
+
result = safe_asyncio_run(my_coroutine())
|
|
141
|
+
"""
|
|
142
|
+
runner = SafeAsyncioRunner(timeout=timeout)
|
|
143
|
+
return runner.run(coro)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class AsyncIOContext:
|
|
147
|
+
"""
|
|
148
|
+
Context Manager für sichere AsyncIO Operationen
|
|
149
|
+
|
|
150
|
+
Usage:
|
|
151
|
+
async with AsyncIOContext() as ctx:
|
|
152
|
+
result = await ctx.run_task(my_task())
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, timeout: Optional[float] = None):
|
|
156
|
+
self.timeout = timeout
|
|
157
|
+
self.loop: Optional[asyncio.AbstractEventLoop] = None
|
|
158
|
+
|
|
159
|
+
async def __aenter__(self):
|
|
160
|
+
self.loop = asyncio.get_event_loop()
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
164
|
+
# Cleanup bei Bedarf
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
async def run_task(self, coro):
|
|
168
|
+
"""Führt eine Task mit Timeout aus"""
|
|
169
|
+
if self.timeout:
|
|
170
|
+
return await asyncio.wait_for(coro, timeout=self.timeout)
|
|
171
|
+
return await coro
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def patch_asyncio_for_windows():
|
|
175
|
+
"""
|
|
176
|
+
Hauptfunktion zum Patchen von AsyncIO auf Windows
|
|
177
|
+
|
|
178
|
+
Sollte ganz am Anfang der Anwendung aufgerufen werden.
|
|
179
|
+
"""
|
|
180
|
+
if not is_windows():
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# Wende Fixes an
|
|
184
|
+
apply_windows_asyncio_fix()
|
|
185
|
+
|
|
186
|
+
# Monkey-patch asyncio.run für Python 3.13+
|
|
187
|
+
if is_python_313_or_higher():
|
|
188
|
+
import asyncio
|
|
189
|
+
|
|
190
|
+
# Speichere Original
|
|
191
|
+
_original_run = asyncio.run
|
|
192
|
+
|
|
193
|
+
def _patched_run(coro, *, debug=False):
|
|
194
|
+
"""Gepatchte asyncio.run()"""
|
|
195
|
+
runner = SafeAsyncioRunner()
|
|
196
|
+
return runner.run(coro)
|
|
197
|
+
|
|
198
|
+
# Ersetze nur wenn nötig
|
|
199
|
+
if not getattr(asyncio, '_patched', False):
|
|
200
|
+
asyncio.run = _patched_run
|
|
201
|
+
asyncio._patched = True
|
|
202
|
+
logger.info("asyncio.run patched for Windows compatibility")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# Automatisch Fixes anwenden beim Import
|
|
206
|
+
patch_asyncio_for_windows()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
# Test
|
|
211
|
+
import time
|
|
212
|
+
|
|
213
|
+
async def test_coro():
|
|
214
|
+
await asyncio.sleep(0.1)
|
|
215
|
+
return "Success"
|
|
216
|
+
|
|
217
|
+
print(f"Python version: {sys.version}")
|
|
218
|
+
print(f"Windows: {is_windows()}")
|
|
219
|
+
print(f"Python >= 3.13: {is_python_313_or_higher()}")
|
|
220
|
+
|
|
221
|
+
result = safe_asyncio_run(test_coro())
|
|
222
|
+
print(f"Result: {result}")
|