Skip to content

Commit 0475a25

Browse files
committed
fix(agent,teacher,common): async cmd dispatch, errno import, atomic yaml writes, treeview diff refresh, recv body deadline, subprocess timeouts, hostname validation
1 parent f1e1430 commit 0475a25

8 files changed

Lines changed: 349 additions & 217 deletions

File tree

agent/firewall_manager.py

Lines changed: 52 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import subprocess
1212
from typing import Any, Dict, List, Optional, Tuple
1313
from common.utils import is_admin, get_subprocess_flags
14+
from common.clash_config import config_to_yaml
1415

1516
_script_dir = os.path.dirname(os.path.abspath(sys.executable)) if getattr(sys, "frozen", False) else os.path.dirname(os.path.abspath(__file__))
1617
CLASH_CONFIG_DIR = os.path.join(_script_dir, "configs")
@@ -46,7 +47,7 @@ def _build_clash_config(mode: str, rules: List[Dict[str, str]]) -> Dict[str, Any
4647
"socks-port": 0,
4748
"mixed-port": 0,
4849
"allow-lan": False,
49-
"bind-address": "\"*\"",
50+
"bind-address": "*",
5051
"mode": "rule",
5152
"log-level": "silent",
5253
"ipv6": False,
@@ -150,89 +151,66 @@ def _is_ip(s: str) -> bool:
150151
def _save_config(config: Dict[str, Any]) -> Tuple[bool, str]:
151152
_ensure_config_dir()
152153
try:
153-
with open(CLASH_CONFIG_FILE, "w", encoding="utf-8") as f:
154-
yaml_content = _dict_to_yaml(config)
154+
yaml_content = config_to_yaml(config)
155+
tmp_path = CLASH_CONFIG_FILE + ".tmp"
156+
with open(tmp_path, "w", encoding="utf-8") as f:
155157
f.write(yaml_content)
158+
os.replace(tmp_path, CLASH_CONFIG_FILE)
156159
return True, "config saved"
157160
except Exception as e:
158161
return False, str(e)
159162

160163

161-
def _dict_to_yaml(data: Any, indent: int = 0) -> str:
162-
result = []
163-
spaces = " " * indent
164-
if isinstance(data, dict):
165-
for key, value in data.items():
166-
if isinstance(value, (dict, list)):
167-
result.append(f"{spaces}{key}:")
168-
result.append(_dict_to_yaml(value, indent + 1))
169-
elif isinstance(value, str) and ("#" in value or "\n" in value or value == ""):
170-
result.append(f"{spaces}{key}: |")
171-
for line in value.split("\n"):
172-
result.append(f"{spaces} {line}")
173-
elif isinstance(value, bool):
174-
result.append(f"{spaces}{key}: {'true' if value else 'false'}")
175-
elif value is None:
176-
result.append(f"{spaces}{key}: null")
177-
elif isinstance(value, list):
178-
if len(value) == 0:
179-
result.append(f"{spaces}{key}: []")
180-
else:
181-
first = value[0]
182-
if isinstance(first, (dict, list)):
183-
result.append(f"{spaces}{key}:")
184-
for item in value:
185-
result.append(f"{spaces} -")
186-
result.append(_dict_to_yaml(item, indent + 2))
187-
else:
188-
items = ", ".join(str(v) for v in value)
189-
result.append(f"{spaces}{key}: [{items}]")
190-
else:
191-
result.append(f"{spaces}{key}: {value}")
192-
elif isinstance(data, list):
193-
for item in data:
194-
if isinstance(item, dict):
195-
result.append(f"{spaces}-")
196-
result.append(_dict_to_yaml(item, indent + 1))
197-
else:
198-
result.append(f"{spaces}- {item}")
199-
else:
200-
result.append(f"{spaces}{data}")
201-
return "\n".join(result)
164+
def _stop_clash_process_locked() -> None:
165+
"""Caller MUST hold CLASH_PROCESS_LOCK."""
166+
global _clash_process
167+
if _clash_process is None:
168+
return
169+
proc = _clash_process
170+
_clash_process = None
171+
try:
172+
proc.terminate()
173+
try:
174+
proc.wait(timeout=5)
175+
return
176+
except subprocess.TimeoutExpired:
177+
pass
178+
proc.kill()
179+
try:
180+
proc.wait(timeout=5)
181+
except subprocess.TimeoutExpired:
182+
# mihomo (TUN driver) 偶尔卸载慢;不再等待,但前面已发送 SIGKILL,
183+
# 句柄一旦 Python 端释放,Windows 会回收。
184+
pass
185+
except OSError:
186+
pass
202187

203188

204189
def _stop_clash_process() -> None:
205-
global _clash_process
206190
with CLASH_PROCESS_LOCK:
207-
if _clash_process is not None:
208-
try:
209-
_clash_process.terminate()
210-
_clash_process.wait(timeout=5)
211-
except Exception:
212-
try:
213-
_clash_process.kill()
214-
except Exception:
215-
pass
216-
_clash_process = None
191+
_stop_clash_process_locked()
217192

218193

219194
def _start_clash_process(clash_path: str) -> Tuple[bool, str]:
220195
global _clash_process
221-
_stop_clash_process()
222-
223-
try:
224-
_clash_process = subprocess.Popen(
225-
[clash_path, "-f", CLASH_CONFIG_FILE, "-d", CLASH_CONFIG_DIR],
226-
stdout=subprocess.DEVNULL,
227-
stderr=subprocess.DEVNULL,
228-
creationflags=get_subprocess_flags(),
229-
)
230-
time.sleep(1.5)
196+
with CLASH_PROCESS_LOCK:
197+
_stop_clash_process_locked()
198+
try:
199+
_clash_process = subprocess.Popen(
200+
[clash_path, "-f", CLASH_CONFIG_FILE, "-d", CLASH_CONFIG_DIR],
201+
stdout=subprocess.DEVNULL,
202+
stderr=subprocess.DEVNULL,
203+
creationflags=get_subprocess_flags(),
204+
)
205+
except Exception as e:
206+
return False, f"启动 mihomo 失败: {e}"
207+
time.sleep(1.5)
208+
with CLASH_PROCESS_LOCK:
209+
if _clash_process is None:
210+
return False, "mihomo 进程已被外部停止"
231211
if _clash_process.poll() is not None:
232212
return False, "mihomo 进程立即退出"
233-
return True, "mihomo 已启动"
234-
except Exception as e:
235-
return False, f"启动 mihomo 失败: {e}"
213+
return True, "mihomo 已启动"
236214

237215

238216
def _is_clash_running() -> bool:
@@ -309,19 +287,12 @@ def disable_restrictions() -> Tuple[bool, str]:
309287
return False, "需要管理员权限"
310288

311289
with CLASH_PROCESS_LOCK:
312-
global _clash_process
313-
if _clash_process is not None:
314-
try:
315-
_clash_process.terminate()
316-
_clash_process.wait(timeout=5)
317-
except Exception:
318-
try:
319-
_clash_process.kill()
320-
except Exception:
321-
pass
322-
_clash_process = None
290+
_stop_clash_process_locked()
323291
if os.path.exists(CLASH_CONFIG_FILE):
324-
os.remove(CLASH_CONFIG_FILE)
292+
try:
293+
os.remove(CLASH_CONFIG_FILE)
294+
except OSError as e:
295+
return False, f"删除配置失败: {e}"
325296

326297
return True, "已解除网络限制"
327298

@@ -344,33 +315,4 @@ def start_clash_if_config_exists() -> Tuple[bool, str]:
344315
if not ok:
345316
return False, f"启动 mihomo 失败: {msg}"
346317

347-
return True, "mihomo 已启动"
348-
349-
350-
def get_current_restriction_status() -> Dict[str, Any]:
351-
status: Dict[str, Any] = {
352-
"has_restrictions": _is_clash_running(),
353-
"mode": "disabled",
354-
"rule_count": 0,
355-
"rules": [],
356-
}
357-
358-
if not os.path.exists(CLASH_CONFIG_FILE):
359-
return status
360-
361-
try:
362-
with open(CLASH_CONFIG_FILE, "r", encoding="utf-8") as f:
363-
content = f.read()
364-
if "MATCH,REJECT" in content:
365-
if "DOMAIN-SUFFIX" in content or "DOMAIN," in content or "IP-CIDR" in content:
366-
if "DOMAIN-SUFFIX" in content or "DOMAIN," in content:
367-
status["mode"] = "blacklist"
368-
else:
369-
status["mode"] = "block_all"
370-
else:
371-
status["mode"] = "block_all"
372-
status["has_restrictions"] = True
373-
except Exception:
374-
pass
375-
376-
return status
318+
return True, "mihomo 已启动"

0 commit comments

Comments
 (0)