clash / app /clash_manager.py
clash-linux's picture
Upload 11 files
6f17509 verified
raw
history blame
7 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Clash管理器 - 负责Clash Core进程的启动、停止和API调用
"""
import os
import time
import signal
import logging
import subprocess
import requests
import json
logger = logging.getLogger(__name__)
class ClashManager:
"""管理Clash Core进程和与其API的交互"""
def __init__(self, config_path, clash_path, api_port=9090, proxy_port=7890):
"""
初始化Clash管理器
Args:
config_path: Clash配置文件路径
clash_path: Clash可执行文件路径
api_port: Clash API监听端口
proxy_port: Clash代理监听端口
"""
self.config_path = os.path.abspath(config_path)
self.clash_path = os.path.abspath(clash_path)
self.api_port = api_port
self.proxy_port = proxy_port
self.api_base_url = f"http://127.0.0.1:{api_port}"
self.clash_process = None
# 确保Clash可执行文件存在
if not os.path.exists(clash_path):
raise FileNotFoundError(f"Clash可执行文件未找到: {clash_path}")
def start_clash(self):
"""启动Clash Core进程"""
if self.clash_process and self.clash_process.poll() is None:
logger.info("Clash Core已经在运行中")
return
# 确保配置文件存在
if not os.path.exists(self.config_path):
raise FileNotFoundError(f"Clash配置文件未找到: {self.config_path}")
# 设置Clash命令行参数 (兼容Clash Meta)
cmd = [
self.clash_path,
"-f", self.config_path,
"-d", os.path.dirname(self.config_path)
]
# 为Clash Meta添加额外参数
if "meta" in self.clash_path.lower():
# Clash Meta特有参数
cmd.extend([
"-ext-ctl", f"127.0.0.1:{self.api_port}",
# 如果需要可以添加更多Clash Meta特有参数
])
else:
# 原始Clash参数
cmd.extend([
"-ext-ctl", f"127.0.0.1:{self.api_port}",
"-ext-ui", "" # 禁用外部UI
])
# 启动Clash进程
logger.info(f"正在启动Clash Core: {' '.join(cmd)}")
self.clash_process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True
)
# 等待Clash启动
time.sleep(2)
# 检查进程是否成功启动
if self.clash_process.poll() is not None:
stderr = self.clash_process.stderr.read()
raise RuntimeError(f"Clash启动失败: {stderr}")
# 验证API是否可访问
try:
self._call_api("GET", "/version")
logger.info("Clash API已就绪")
except Exception as e:
self.stop_clash()
raise RuntimeError(f"无法连接到Clash API: {str(e)}")
def stop_clash(self):
"""停止Clash Core进程"""
if self.clash_process and self.clash_process.poll() is None:
logger.info("正在停止Clash Core...")
# 尝试优雅地终止进程
self.clash_process.terminate()
# 等待进程终止
try:
self.clash_process.wait(timeout=5)
except subprocess.TimeoutExpired:
# 如果进程没有及时终止,强制结束
logger.warning("Clash进程未响应终止信号,强制结束...")
self.clash_process.kill()
self.clash_process = None
logger.info("Clash Core已停止")
def restart_clash(self):
"""重启Clash Core进程"""
logger.info("正在重启Clash Core...")
self.stop_clash()
time.sleep(1) # 给进程一些时间完全终止
self.start_clash()
logger.info("Clash Core已重启")
def get_nodes(self):
"""
获取所有可用的代理节点名称列表
Returns:
list: 节点名称列表
"""
response = self._call_api("GET", "/proxies")
proxies = response.get("proxies", {})
# 过滤出实际的代理节点(排除DIRECT, REJECT等内置代理和策略组)
node_names = []
for name, proxy in proxies.items():
if proxy.get("type") not in ["Direct", "Reject", "Selector", "URLTest", "Fallback", "LoadBalance"]:
node_names.append(name)
return node_names
def switch_node(self, node_name):
"""
切换到指定的代理节点
Args:
node_name: 节点名称
Raises:
ValueError: 如果节点名称无效
"""
# 获取所有节点以验证目标节点存在
all_nodes = self.get_nodes()
if node_name not in all_nodes:
raise ValueError(f"无效的节点名称: {node_name}")
# 切换GLOBAL策略组到指定节点
# 注意:这里假设使用GLOBAL作为顶级策略组,你可能需要根据实际配置调整
try:
self._call_api("PUT", "/proxies/GLOBAL", json={"name": node_name})
logger.info(f"已切换到节点: {node_name}")
except Exception as e:
raise RuntimeError(f"切换节点失败: {str(e)}")
def get_current_node(self):
"""
获取当前使用的节点名称
Returns:
str: 当前节点名称
"""
# 获取GLOBAL策略组的当前选择
# 注意:这里假设使用GLOBAL作为顶级策略组,你可能需要根据实际配置调整
response = self._call_api("GET", "/proxies/GLOBAL")
return response.get("now", "unknown")
def _call_api(self, method, endpoint, **kwargs):
"""
调用Clash的API
Args:
method: HTTP方法 (GET, POST, PUT等)
endpoint: API端点路径
**kwargs: 传递给requests的其他参数
Returns:
dict: API响应的JSON数据
Raises:
RuntimeError: 如果API调用失败
"""
url = f"{self.api_base_url}{endpoint}"
logger.debug(f"调用Clash API: {method} {url}")
try:
response = requests.request(method, url, timeout=10, **kwargs)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
logger.error(f"Clash API调用失败: {str(e)}")
raise RuntimeError(f"Clash API调用失败: {str(e)}")
def __del__(self):
"""析构函数,确保进程在对象销毁时被终止"""
self.stop_clash()