mihomo.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import asyncio
  2. import os
  3. import subprocess
  4. import hashlib
  5. from pathlib import Path
  6. from fastapi import APIRouter, HTTPException
  7. from datetime import datetime, timedelta
  8. from pydantic import BaseModel
  9. from typing import Dict, List, Optional
  10. import httpx
  11. from sqlmodel import Session, select, or_
  12. import yaml
  13. import signal
  14. from asyncio import subprocess
  15. import multiprocessing
  16. from multiprocessing import Pipe, Process
  17. from config.logu import logger, get_logger
  18. from config.settings import settings
  19. from config.app_yaml import app_yaml, Subscription
  20. from routers.subscriptions import list_subscriptions,SubscriptionResponse
  21. from utils.mihomo_service import port_is_using,find_free_port
  22. from utils.sub import update_config
  23. from utils.processes_mgr import process_manager
  24. from database.models.subscription import SubscriptionManager,SubscriptFile,MihomoMeta
  25. # 初始化全局变量来保存进程池
  26. POOL = None
  27. mihomo_router = APIRouter()
  28. class MihomoBatchRequest(BaseModel):
  29. id: int
  30. port: Optional[int] = None
  31. class MihomoRunningStatus(MihomoBatchRequest):
  32. pid: int
  33. started_at: datetime
  34. class ProcessInfo(BaseModel):
  35. provider_name: str
  36. class MihomoResponse(MihomoBatchRequest):
  37. error: int = 0
  38. detail: Optional[Dict] = None
  39. class MihomoMetaWithURL(MihomoMeta, table=False):
  40. # 覆盖导致问题的关系字段
  41. subscript_file: Optional[int] = None # 使用外键类型替代关系对象
  42. external_controller_url: Optional[str] = None
  43. class Config:
  44. arbitrary_types_allowed = True
  45. async def request_select_proxy_name(external_ctl: str, provider_name: str, proxy_name: str, max_retries: int = 5, delay: float = 2.0) -> Optional[dict]:
  46. url = f"http://{external_ctl}/proxies/{provider_name}"
  47. payload = {"name": proxy_name}
  48. async with httpx.AsyncClient() as client:
  49. for attempt in range(max_retries):
  50. try:
  51. response = await client.put(url, json=payload)
  52. response.raise_for_status() # 如果请求失败,抛出异常
  53. return response # 该接口没有返回值,直接返回response
  54. except (httpx.HTTPError, httpx.RequestError) as e:
  55. if attempt < max_retries - 1:
  56. await asyncio.sleep(delay) # 等待一段时间后重试
  57. else:
  58. raise HTTPException(status_code=500, detail=f"Failed to select proxy after {max_retries} attempts: {str(e)}")
  59. @mihomo_router.post("/start")
  60. async def post_start_mihomo(request: MihomoBatchRequest):
  61. db = SubscriptionManager()
  62. # 获取对应的订阅文件
  63. with Session(db.engine) as session:
  64. # 查找对应的订阅文件
  65. miho_model = session.exec(
  66. select(MihomoMeta)
  67. .where(MihomoMeta.id == request.id)
  68. ).first()
  69. if not miho_model:
  70. raise HTTPException(status_code=404, detail="Provider not found")
  71. sub_file = miho_model.subscript_file
  72. # logger.info(f"miho_model.subscript_file {miho_model.subscript_file}")
  73. # return miho_model
  74. if miho_model.pid:
  75. return miho_model
  76. mixed_port = request.port
  77. # 如果端口未指定,查找可用端口
  78. if not mixed_port:
  79. mixed_port = find_free_port()
  80. external_controller_port = find_free_port((mixed_port+1, 18000))
  81. config = {}
  82. # 保存临时配置文件
  83. temp_path = settings.MIHOMO_TEMP_PATH / f"{miho_model.provider_name}_{external_controller_port}.yaml"
  84. # 更新端口配置
  85. config['mixed-port'] = mixed_port
  86. config['external-controller'] = f'127.0.0.1:{external_controller_port}'
  87. config['bind-address'] = '127.0.0.1'
  88. logger.info(f"sub_file.file_path {sub_file.file_path}")
  89. logger.info(f"temp_path {temp_path}")
  90. logger.info(f"config {config}")
  91. res = update_config(Path(sub_file.file_path), config, Path(temp_path))
  92. # 启动进程
  93. try:
  94. command = [str(settings.MIHOMO_BIN_PATH), "-f", str(temp_path)]
  95. logger.info(f"Executing command: {' '.join(command)}")
  96. pid = process_manager.start_process(command, external_controller_port)
  97. miho_model.mixed_port = mixed_port
  98. miho_model.external_controller = f'127.0.0.1:{external_controller_port}'
  99. miho_model.temp_file_path = str(temp_path)
  100. miho_model.pid = pid
  101. miho_model.running = True
  102. miho_model.updated_at = datetime.now()
  103. try:
  104. await request_select_proxy_name(miho_model.external_controller, miho_model.provider_name, miho_model.proxy_name)
  105. except Exception as e:
  106. logger.error(f"Failed to select proxy: {str(e)}")
  107. process_manager.stop_process(external_controller_port)
  108. raise HTTPException(status_code=500, detail=str(e))
  109. # 更新数据库记录
  110. session.add(miho_model)
  111. session.commit()
  112. session.refresh(miho_model)
  113. mihomo_with_url = MihomoMetaWithURL(**miho_model.model_dump())
  114. if miho_model.external_controller:
  115. host, port = miho_model.external_controller.split(":")
  116. mihomo_with_url.external_controller_url = f"https://yacd.metacubex.one/?hostname={host}&port={port}&secret=#/proxies"
  117. return mihomo_with_url
  118. except Exception as e:
  119. logger.exception(f"Failed to start mihomo: {str(e)}")
  120. raise HTTPException(status_code=500, detail=str(e))
  121. @mihomo_router.post("/stop")
  122. async def post_stop_mihomo(request: MihomoBatchRequest):
  123. db = SubscriptionManager()
  124. with Session(db.engine) as session:
  125. selected_provider = session.exec(
  126. select(MihomoMeta)
  127. .where(MihomoMeta.id == request.id)
  128. ).first()
  129. if not selected_provider:
  130. logger.error(f"Provider not found with id {request.id}")
  131. raise HTTPException(status_code=404, detail="Provider not found")
  132. if selected_provider.pid:
  133. try:
  134. process_manager.stop_process(selected_provider.external_controller)
  135. except Exception as e:
  136. logger.error(f"Failed to stop mihomo: {str(e)}")
  137. raise HTTPException(status_code=500, detail=str(e))
  138. selected_provider.pid = None
  139. selected_provider.running = False
  140. selected_provider.updated_at = datetime.now()
  141. session.add(selected_provider)
  142. session.commit()
  143. session.refresh(selected_provider)
  144. return selected_provider
  145. else:
  146. raise HTTPException(status_code=400, detail="Provider is not running")
  147. @mihomo_router.get("/")
  148. async def get_mihomo_running_status():
  149. db = SubscriptionManager()
  150. with Session(db.engine) as session:
  151. all = session.exec(
  152. select(MihomoMeta)
  153. .where(MihomoMeta.pid.is_not(None))
  154. ).all()
  155. result = []
  156. for mihomo_model in all:
  157. mihomo_with_url = MihomoMetaWithURL(**mihomo_model.model_dump())
  158. if mihomo_model.external_controller:
  159. host, port = mihomo_model.external_controller.split(":")
  160. mihomo_with_url.external_controller_url = f"https://yacd.metacubex.one/?hostname={host}&port={port}&secret=#/proxies"
  161. result.append(mihomo_with_url)
  162. return result
  163. @mihomo_router.get("/external-controller")
  164. async def get_controller_urls():
  165. running_list = await get_mihomo_running_status()
  166. logger.info(f"running_list {running_list}")
  167. # https://yacd.metacubex.one/?hostname=127.0.0.1&port=9351&secret=#/proxies
  168. urls = []
  169. for item in running_list:
  170. host, port = item.external_controller.split(":")
  171. urls.append(f"https://yacd.metacubex.one/?hostname={host}&port={port}&secret=#/proxies")
  172. return urls
  173. async def stop_all_mihomo():
  174. running_list = await get_mihomo_running_status()
  175. for item in running_list:
  176. if item.pid:
  177. logger.info(f"stop mihomo {item}")
  178. await post_stop_mihomo(MihomoBatchRequest(id=item.id))
  179. # logger.info(f"running_list {running_list}")