fix: anti-stall — IP check budgets, capped timers, fetch wait_for, obfuscation auto-fallback, empty validate short-circuit, unittest suite
Made-with: Cursor
This commit is contained in:
@@ -24,16 +24,6 @@ Notify = Callable[[dict[str, Any]], None]
|
||||
_FETCH_POOL = ThreadPoolExecutor(max_workers=4, thread_name_prefix="fetcher")
|
||||
|
||||
|
||||
def _filter_by_mode(pool: list[str], mode: str) -> list[str]:
|
||||
"""Filter pool by obfuscation/protocol mode."""
|
||||
if mode == "http_only":
|
||||
return [u for u in pool if u.startswith("http://")]
|
||||
if mode == "socks5_only":
|
||||
return [u for u in pool if u.startswith("socks5://")]
|
||||
# "random_mix" and "auto" — keep all, caller will shuffle
|
||||
return list(pool)
|
||||
|
||||
|
||||
class ChainService:
|
||||
"""Background rotating proxy chain using GOST."""
|
||||
|
||||
@@ -323,24 +313,44 @@ class ChainService:
|
||||
# POOL MANAGEMENT
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _apply_mode_filter(candidates: list[str], mode: str) -> list[str]:
|
||||
if mode == "http_only":
|
||||
return [u for u in candidates if u.startswith("http://")]
|
||||
if mode == "socks5_only":
|
||||
return [u for u in candidates if u.startswith("socks5://")]
|
||||
if mode == "random_mix":
|
||||
out = list(candidates)
|
||||
random.shuffle(out)
|
||||
return out
|
||||
return list(candidates)
|
||||
|
||||
def _pick_chain(self) -> list[str]:
|
||||
"""Pick chain_length hops: optional fixed last hop + random prefix from pool."""
|
||||
s = self._settings
|
||||
chain = self._pick_chain_for_mode(s.obfuscation_mode)
|
||||
if chain:
|
||||
return chain
|
||||
if s.obfuscation_mode != "auto" and not s.use_pinned_chain:
|
||||
self._notify({
|
||||
"type": "log",
|
||||
"text": "Obfuscation filter left nothing usable — retrying this pick with ALL protocols (auto).",
|
||||
})
|
||||
return self._pick_chain_for_mode("auto")
|
||||
return []
|
||||
|
||||
def _pick_chain_for_mode(self, mode: str) -> list[str]:
|
||||
"""Build one chain using given mode (http_only / socks5_only / random_mix / auto)."""
|
||||
s = self._settings
|
||||
k = max(1, s.chain_length)
|
||||
manual = self._manual_exit_url()
|
||||
if manual and not s.use_pinned_chain:
|
||||
# Last hop is always manual; earlier hops come from pool (never duplicate manual)
|
||||
mid_need = k - 1
|
||||
candidates = [
|
||||
base = [
|
||||
u for u in self._available
|
||||
if u not in self._blacklist and u != manual
|
||||
]
|
||||
if s.obfuscation_mode == "http_only":
|
||||
candidates = [u for u in candidates if u.startswith("http://")]
|
||||
elif s.obfuscation_mode == "socks5_only":
|
||||
candidates = [u for u in candidates if u.startswith("socks5://")]
|
||||
elif s.obfuscation_mode == "random_mix":
|
||||
random.shuffle(candidates)
|
||||
candidates = self._apply_mode_filter(base, mode)
|
||||
|
||||
if mid_need == 0:
|
||||
return [manual]
|
||||
@@ -354,14 +364,8 @@ class ChainService:
|
||||
self._used.add(manual)
|
||||
return prefix + [manual]
|
||||
|
||||
# Default: all hops from rotating pool
|
||||
candidates = [u for u in self._available if u not in self._blacklist]
|
||||
if s.obfuscation_mode == "http_only":
|
||||
candidates = [u for u in candidates if u.startswith("http://")]
|
||||
elif s.obfuscation_mode == "socks5_only":
|
||||
candidates = [u for u in candidates if u.startswith("socks5://")]
|
||||
elif s.obfuscation_mode == "random_mix":
|
||||
random.shuffle(candidates)
|
||||
base = [u for u in self._available if u not in self._blacklist]
|
||||
candidates = self._apply_mode_filter(base, mode)
|
||||
|
||||
if len(candidates) < k:
|
||||
return []
|
||||
@@ -380,12 +384,19 @@ class ChainService:
|
||||
# Fetch all sources concurrently in thread pool (they are blocking)
|
||||
async def _fetch_one(url: str) -> list[str]:
|
||||
try:
|
||||
rows = await loop.run_in_executor(
|
||||
_FETCH_POOL, lambda: fetch_proxy_json(url)
|
||||
rows = await asyncio.wait_for(
|
||||
loop.run_in_executor(
|
||||
_FETCH_POOL,
|
||||
lambda u=url: fetch_proxy_json(u, timeout=50.0),
|
||||
),
|
||||
timeout=55.0,
|
||||
)
|
||||
entries = normalize_entries(rows, s.prefer_elite)
|
||||
self._notify({"type": "log", "text": f"Fetched {len(entries)} from source."})
|
||||
return entries
|
||||
except asyncio.TimeoutError:
|
||||
self._notify({"type": "log", "text": f"Fetch timed out (55s): {url[:60]}..."})
|
||||
return []
|
||||
except Exception as e:
|
||||
self._notify({"type": "log", "text": f"Fetch error: {e!s}"})
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user