1111import subprocess
1212from typing import Any , Dict , List , Optional , Tuple
1313from 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__ ))
1617CLASH_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:
150151def _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
204189def _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
219194def _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
238216def _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