fix: sanitize settings JSON, asyncio get_running_loop, Defender exclusion, _btn cleanup, ASCII Save label
Made-with: Cursor
This commit is contained in:
@@ -111,7 +111,9 @@ def main() -> None:
|
||||
# HELPERS
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
def _btn(parent: Any, label: str, cmd: Any, w: int = 80, h: int = 28, **kw: Any) -> ctk.CTkButton:
|
||||
# Pop styling keys so they are never duplicated in **kw (CustomTkinter raises on duplicate font, etc.).
|
||||
# Pull known keys out first so **kw never duplicates CTkButton parameters (font, width, …).
|
||||
kw.pop("text", None)
|
||||
kw.pop("command", None)
|
||||
font = kw.pop("font", (FONT, 11))
|
||||
fg_color = kw.pop("fg_color", ACCENT)
|
||||
hover_color = kw.pop("hover_color", ACCENT2)
|
||||
@@ -669,7 +671,7 @@ def main() -> None:
|
||||
).pack(side="left")
|
||||
|
||||
ctk.CTkFrame(sf_outer, fg_color="transparent", height=4).pack()
|
||||
_btn(sf_outer, "💾 Save All Settings",
|
||||
_btn(sf_outer, "Save All Settings",
|
||||
lambda: _save_settings(verbose=True), w=220, h=36,
|
||||
font=(FONT, 13)).pack(anchor="w", padx=6, pady=8)
|
||||
|
||||
|
||||
@@ -75,6 +75,63 @@ class Settings:
|
||||
return app_data_dir() / "settings.json"
|
||||
|
||||
|
||||
def sanitize_settings(s: Settings) -> tuple[Settings, bool]:
|
||||
"""Clamp invalid values from hand-edited JSON. Returns (settings, changed)."""
|
||||
changed = False
|
||||
try:
|
||||
cl = int(s.chain_length)
|
||||
if not (1 <= cl <= 8):
|
||||
s.chain_length = max(1, min(8, cl))
|
||||
changed = True
|
||||
except (TypeError, ValueError):
|
||||
s.chain_length = 3
|
||||
changed = True
|
||||
try:
|
||||
p = int(s.local_port)
|
||||
if not (1 <= p <= 65535):
|
||||
s.local_port = max(1, min(65535, p))
|
||||
changed = True
|
||||
except (TypeError, ValueError):
|
||||
s.local_port = 18888
|
||||
changed = True
|
||||
try:
|
||||
if int(s.health_check_seconds) < 10:
|
||||
s.health_check_seconds = 10
|
||||
changed = True
|
||||
except (TypeError, ValueError):
|
||||
s.health_check_seconds = 180
|
||||
changed = True
|
||||
try:
|
||||
if int(s.full_refresh_seconds) < 60:
|
||||
s.full_refresh_seconds = 60
|
||||
changed = True
|
||||
except (TypeError, ValueError):
|
||||
s.full_refresh_seconds = 1800
|
||||
changed = True
|
||||
try:
|
||||
if int(s.validation_concurrency) < 1:
|
||||
s.validation_concurrency = 1
|
||||
changed = True
|
||||
except (TypeError, ValueError):
|
||||
s.validation_concurrency = 64
|
||||
changed = True
|
||||
try:
|
||||
if int(s.max_candidates) < 10:
|
||||
s.max_candidates = 10
|
||||
changed = True
|
||||
except (TypeError, ValueError):
|
||||
s.max_candidates = 400
|
||||
changed = True
|
||||
try:
|
||||
if float(s.validation_timeout_seconds) < 2.0:
|
||||
s.validation_timeout_seconds = 2.0
|
||||
changed = True
|
||||
except (TypeError, ValueError):
|
||||
s.validation_timeout_seconds = 12.0
|
||||
changed = True
|
||||
return s, changed
|
||||
|
||||
|
||||
def normalize_listen_host(s: Settings) -> tuple[Settings, bool]:
|
||||
"""Force loopback bind so LAN/DHCP IP changes (e.g. cloned VMs) never break the listener."""
|
||||
if s.local_host == LISTEN_HOST:
|
||||
@@ -103,7 +160,9 @@ def load_settings() -> Settings:
|
||||
s.pinned_chain = []
|
||||
if not hasattr(s, "manual_exit_proxy") or not isinstance(s.manual_exit_proxy, str):
|
||||
s.manual_exit_proxy = ""
|
||||
s, changed = normalize_listen_host(s)
|
||||
s, ch1 = sanitize_settings(s)
|
||||
s, ch2 = normalize_listen_host(s)
|
||||
changed = ch1 or ch2
|
||||
if changed:
|
||||
try:
|
||||
p.write_text(json.dumps(asdict(s), indent=2), encoding="utf-8")
|
||||
@@ -113,6 +172,7 @@ def load_settings() -> Settings:
|
||||
|
||||
|
||||
def save_settings(s: Settings) -> None:
|
||||
s, _ = sanitize_settings(s)
|
||||
s, _ = normalize_listen_host(s)
|
||||
p = app_data_dir() / "settings.json"
|
||||
p.write_text(json.dumps(asdict(s), indent=2), encoding="utf-8")
|
||||
|
||||
@@ -22,9 +22,13 @@ GOST_RELEASE_ZIP = (
|
||||
def _add_defender_exclusion(path: Path) -> None:
|
||||
"""Add Windows Defender exclusion so GOST is not quarantined."""
|
||||
try:
|
||||
# ExclusionPath covers gost.exe under this folder; avoid invalid combined params on older builds.
|
||||
folder = str(path.parent).replace("'", "''")
|
||||
subprocess.run(
|
||||
["powershell", "-NoProfile", "-NonInteractive", "-Command",
|
||||
f"Add-MpPreference -ExclusionPath '{path.parent}' -ExclusionProcess 'gost.exe' -ErrorAction SilentlyContinue"],
|
||||
[
|
||||
"powershell", "-NoProfile", "-NonInteractive", "-Command",
|
||||
f"Add-MpPreference -ExclusionPath '{folder}' -ErrorAction SilentlyContinue",
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
|
||||
@@ -375,7 +375,7 @@ class ChainService:
|
||||
async def _build_pool(self) -> list[str]:
|
||||
"""Fetch and validate proxies. Runs blocking I/O in thread pool."""
|
||||
s = self._settings
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Fetch all sources concurrently in thread pool (they are blocking)
|
||||
async def _fetch_one(url: str) -> list[str]:
|
||||
|
||||
Reference in New Issue
Block a user