This commit is contained in:
eric
2026-03-28 18:10:43 -05:00
parent 513c2b7b8c
commit 8001baabf8
15 changed files with 227 additions and 489 deletions

View File

@@ -352,8 +352,6 @@ def _default_service_status(spec: ServiceSpec, detail: str) -> dict:
async def query_service(spec: ServiceSpec) -> dict:
if sys.platform == "win32":
return _default_service_status(spec, "Linux-only stack integration")
if not SERVICE_CTL.is_file():
return _default_service_status(spec, "Service controller script is missing")
code, output = await _run_command(["bash", str(SERVICE_CTL), spec.service_id, "status"])
@@ -385,8 +383,6 @@ async def service_action(service_id: str, action: str) -> dict:
spec = next((item for item in SERVICE_SPECS if item.service_id == service_id), None)
if not spec:
raise KeyError(f"Unknown service: {service_id}")
if sys.platform == "win32":
return _default_service_status(spec, "Linux-only stack integration")
if not SERVICE_CTL.is_file():
return _default_service_status(spec, "Service controller script is missing")
@@ -516,7 +512,7 @@ def network_iface_stats() -> dict[str, Any]:
"""Linux /proc/net/dev 累计流量(自开机或计数器复位以来)。"""
dev = Path("/proc/net/dev")
interfaces: list[dict[str, int | str]] = []
if sys.platform == "win32" or not dev.is_file():
if not dev.is_file():
return {"interfaces": interfaces}
skip_prefixes = ("lo", "docker", "veth", "br-", "virbr", "tun", "tap")
try:
@@ -699,7 +695,7 @@ def _read_uptime_seconds() -> Optional[float]:
def system_snapshot() -> dict:
disk = shutil.disk_usage(BASE_DIR.anchor if sys.platform == "win32" else "/")
disk = shutil.disk_usage("/")
meminfo = _read_meminfo()
load = os.getloadavg() if hasattr(os, "getloadavg") else (0.0, 0.0, 0.0)
snapshot = {
@@ -713,17 +709,16 @@ def system_snapshot() -> dict:
"uptime_seconds": _read_uptime_seconds(),
"netdata": {"available": False},
}
if sys.platform != "win32":
try:
with urlopen("http://127.0.0.1:19999/api/v1/info", timeout=1.5) as response:
payload = json.loads(response.read().decode("utf-8", "replace"))
snapshot["netdata"] = {
"available": True,
"version": payload.get("version", ""),
"mirrored_hosts": payload.get("mirrored_hosts", []),
}
except Exception:
pass
try:
with urlopen("http://127.0.0.1:19999/api/v1/info", timeout=1.5) as response:
payload = json.loads(response.read().decode("utf-8", "replace"))
snapshot["netdata"] = {
"available": True,
"version": payload.get("version", ""),
"mirrored_hosts": payload.get("mirrored_hosts", []),
}
except Exception:
pass
return snapshot

View File

@@ -12,81 +12,10 @@ import os
import subprocess
import sys
import platform
import zipfile
from pathlib import Path
import requests
import re
import distro
from tqdm import tqdm
from .logger import logger
current_platform = platform.system()
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
current_env_path = os.environ.get('PATH')
def unzip_file(zip_path: str | Path, extract_to: str | Path, delete: bool = True) -> None:
if not os.path.exists(extract_to):
os.makedirs(extract_to)
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_to)
if delete and os.path.exists(zip_path):
os.remove(zip_path)
def install_nodejs_windows():
try:
logger.warning("Node.js is not installed.")
logger.debug("Installing the stable version of Node.js for Windows...")
response = requests.get('https://nodejs.cn/download/')
if response.status_code == 200:
match = re.search('https://npmmirror.com/mirrors/node/(v.*?)/node-(v.*?)-x64.msi',
response.text)
if match:
version = match.group(1)
system_bit = 'x64' if '32' not in platform.machine() else 'x86'
url = f'https://npmmirror.com/mirrors/node/{version}/node-{version}-win-{system_bit}.zip'
else:
logger.error("Failed to retrieve the download URL for the latest version of Node.js...")
return
full_file_name = url.rsplit('/', maxsplit=1)[-1]
zip_file_path = Path(execute_dir) / full_file_name
if Path(zip_file_path).exists():
logger.debug("Node.js installation file already exists, start install...")
else:
response = requests.get(url, stream=True)
total_size = int(response.headers.get('Content-Length', 0))
block_size = 1024
with tqdm(total=total_size, unit="B", unit_scale=True,
ncols=100, desc=f'Downloading Node.js ({version})') as t:
with open(zip_file_path, 'wb') as f:
for data in response.iter_content(block_size):
t.update(len(data))
f.write(data)
unzip_file(zip_file_path, execute_dir)
extract_dir_path = str(zip_file_path).rsplit('.', maxsplit=1)[0]
f_path, f_name = os.path.splitext(zip_file_path)
new_extract_dir_path = Path(f_path).parent / 'node'
if Path(extract_dir_path).exists() and not Path(new_extract_dir_path).exists():
os.rename(extract_dir_path, new_extract_dir_path)
os.environ['PATH'] = execute_dir + '/node' + os.pathsep + current_env_path
result = subprocess.run(["node", "-v"], capture_output=True)
if result.returncode == 0:
logger.debug('Node.js installation was successful. Restart for changes to take effect')
else:
logger.debug('Node.js installation failed')
return True
else:
logger.error("Failed to retrieve the Node.js version page")
except Exception as e:
logger.error(f"type: {type(e).__name__}, Node.js installation failed {e}")
def install_nodejs_centos():
@@ -136,23 +65,6 @@ def install_nodejs_ubuntu():
logger.error(f"type: {type(e).__name__}, Node.js installation failed, {e}")
def install_nodejs_mac():
logger.warning("Node.js is not installed.")
logger.debug("Installing the latest version of Node.js for macOS...")
try:
result = subprocess.run(["brew", "install", "node"], capture_output=True)
if result.returncode == 0:
logger.debug('Node.js installation was successful. Restart for changes to take effect.')
return True
else:
logger.error("Node.js installation failed")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to install Node.js using Homebrew. {e}")
logger.error("Please install Node.js manually or check your Homebrew installation.")
except Exception as e:
logger.error(f"An unexpected error occurred: {e}")
def get_package_manager():
dist_id = distro.id()
if dist_id in ["centos", "fedora", "rhel", "amzn", "oracle", "scientific", "opencloudos", "alinux"]:
@@ -162,20 +74,13 @@ def get_package_manager():
def install_nodejs() -> bool:
if current_platform == "Windows":
return install_nodejs_windows()
elif current_platform == "Linux":
os_type = get_package_manager()
if os_type == "RHS":
return install_nodejs_centos()
else:
return install_nodejs_ubuntu()
elif current_platform == "Darwin":
return install_nodejs_mac()
else:
logger.debug(f"Node.js auto installation is not supported on this platform: {current_platform}. "
f"Please install Node.js manually.")
if current_platform != "Linux":
logger.debug("Node.js 自动安装仅支持 Linux请手动安装 Node.js。")
return False
os_type = get_package_manager()
if os_type == "RHS":
return install_nodejs_centos()
return install_nodejs_ubuntu()
def ensure_nodejs_installed(func):

View File

@@ -1,8 +1,6 @@
import os
import sys
from enum import Enum, auto
from dataclasses import dataclass, field
from .utils import logger
class ProxyType(Enum):
@@ -25,68 +23,37 @@ class ProxyInfo:
class ProxyDetector:
def __init__(self):
if sys.platform.startswith('win'):
import winreg
self.winreg = winreg
self.__path = r'Software\Microsoft\Windows\CurrentVersion\Internet Settings'
with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as key_user:
self.__INTERNET_SETTINGS = winreg.OpenKeyEx(key_user, self.__path, 0, winreg.KEY_ALL_ACCESS)
else:
self.__is_windows = False
"""仅 Linux从环境变量读取代理http_proxy / https_proxy / ftp_proxy"""
def get_proxy_info(self) -> ProxyInfo:
if sys.platform.startswith('win'):
ip, port = self._get_proxy_info_windows()
else:
ip, port = self._get_proxy_info_linux()
ip, port = self._get_proxy_info_linux()
return ProxyInfo(ip, port)
def is_proxy_enabled(self) -> bool:
if sys.platform.startswith('win'):
return self._is_proxy_enabled_windows()
else:
return self._is_proxy_enabled_linux()
def _get_proxy_info_windows(self) -> tuple[str, str]:
ip, port = "", ""
if self._is_proxy_enabled_windows():
try:
ip_port = self.winreg.QueryValueEx(self.__INTERNET_SETTINGS, "ProxyServer")[0]
if ip_port:
ip, port = ip_port.split(":")
except FileNotFoundError as err:
logger.warning("No proxy information found: " + str(err))
except Exception as err:
logger.error("An error occurred: " + str(err))
else:
logger.debug("No proxy is enabled on the system")
return ip, port
def _is_proxy_enabled_windows(self) -> bool:
try:
if self.winreg.QueryValueEx(self.__INTERNET_SETTINGS, "ProxyEnable")[0] == 1:
return True
except FileNotFoundError as err:
print("No proxy information found: " + str(err))
except Exception as err:
print("An error occurred: " + str(err))
return False
return self._is_proxy_enabled_linux()
@staticmethod
def _get_proxy_info_linux() -> tuple[str, str]:
proxies = {
'http': os.getenv('http_proxy'),
'https': os.getenv('https_proxy'),
'ftp': os.getenv('ftp_proxy')
"http": os.getenv("http_proxy"),
"https": os.getenv("https_proxy"),
"ftp": os.getenv("ftp_proxy"),
}
ip = port = ""
for proto, proxy in proxies.items():
for _proto, proxy in proxies.items():
if proxy:
ip, port = proxy.split(':')
# 支持 http://host:port
p = proxy.strip()
if "://" in p:
p = p.split("://", 1)[1]
if "@" in p:
p = p.split("@", 1)[1]
if ":" in p:
ip, _, port = p.rpartition(":")
port = port.split("/")[0]
break
return ip, port
def _is_proxy_enabled_linux(self) -> bool:
proxies = self._get_proxy_info_linux()
return any(proxy != '' for proxy in proxies)
ip, port = self._get_proxy_info_linux()
return bool(ip and port)

View File

@@ -1,5 +1,5 @@
"""
Web 控制台进程后端:优先 PM2Linux/Windows 通用),不可用时回退为本地 PID 注册表。
Web 控制台进程后端:优先 PM2不可用时回退为本地 PID 注册表(仅 Linux
"""
from __future__ import annotations
@@ -101,46 +101,8 @@ def _signal_pids(pids: Iterable[int], sig: int) -> None:
pass
def _windows_kill_stack_ffmpeg(process_name: str, project_root: Path | None) -> None:
"""仅结束命令行命中业务标记的 ffmpeg.exe避免 taskkill /IM 误杀其它 FFmpeg。"""
markers = _ffmpeg_cmdline_markers(process_name, project_root)
if not markers:
return
env = os.environ.copy()
env["DOUYIN_FFMPEG_MARKERS"] = json.dumps(markers, ensure_ascii=False)
ps = r"""
$raw = $env:DOUYIN_FFMPEG_MARKERS
if (-not $raw) { exit 0 }
$m = $raw | ConvertFrom-Json
foreach ($proc in Get-CimInstance Win32_Process -Filter "Name = 'ffmpeg.exe'") {
$cmd = $proc.CommandLine
if (-not $cmd) { continue }
foreach ($x in $m) {
if ($cmd -like ('*' + $x + '*')) {
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
break
}
}
}
"""
try:
subprocess.run(
["powershell", "-NoProfile", "-NonInteractive", "-Command", ps],
env=env,
capture_output=True,
text=True,
timeout=45,
check=False,
)
except (OSError, subprocess.TimeoutExpired):
pass
async def force_kill_ffmpeg(process_name: str, project_root: Path | None = None) -> None:
"""结束与本业务相关的 ffmpegLinux 按 /proc 命令行匹配 + 可选 pkillWindows 按命令行标记筛选"""
if sys.platform == "win32":
await asyncio.to_thread(_windows_kill_stack_ffmpeg, process_name, project_root)
return
"""结束与本业务相关的 ffmpeg按 /proc 命令行匹配 + 可选 pkill。"""
def collect() -> list[int]:
return _linux_ffmpeg_pids_for_stack(process_name, project_root)
@@ -193,18 +155,6 @@ class LocalProcessRegistry:
def _pid_alive(self, pid: int) -> bool:
if pid <= 0:
return False
if sys.platform == "win32":
try:
out = subprocess.check_output(
["tasklist", "/FI", f"PID eq {pid}", "/FO", "CSV", "/NH"],
stderr=subprocess.DEVNULL,
text=True,
encoding="utf-8",
errors="ignore",
)
return str(pid) in out
except Exception:
return False
try:
os.kill(pid, 0)
return True
@@ -214,14 +164,6 @@ class LocalProcessRegistry:
def _kill_tree(self, pid: int) -> None:
if pid <= 0:
return
if sys.platform == "win32":
subprocess.run(
["taskkill", "/PID", str(pid), "/T", "/F"],
capture_output=True,
text=True,
timeout=30,
)
return
try:
os.killpg(os.getpgid(pid), signal.SIGTERM)
except (ProcessLookupError, PermissionError, OSError):
@@ -241,8 +183,6 @@ class LocalProcessRegistry:
if not bash:
return None
return [bash, str(path)]
if script.lower().endswith((".bat", ".cmd")):
return ["cmd.exe", "/c", str(path)]
return None
async def start(self, name: str, script: str) -> str:
@@ -256,27 +196,19 @@ class LocalProcessRegistry:
cmd = self.build_command(script)
if not cmd:
return f"ERROR: 无法启动 {script}(文件不存在或缺少 bash/cmd"
return f"ERROR: 无法启动 {script}(文件不存在或缺少 bash/sh"
self.log_dir.mkdir(parents=True, exist_ok=True)
out_log = open(self.log_dir / f"{name}-out.log", "a", encoding="utf-8", errors="ignore")
err_log = open(self.log_dir / f"{name}-error.log", "a", encoding="utf-8", errors="ignore")
preexec_fn = os.setsid if sys.platform != "win32" else None
creationflags = 0
if sys.platform == "win32":
creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
try:
kw: dict[str, Any] = {
"cwd": str(self.base_dir),
"stdout": out_log,
"stderr": err_log,
"preexec_fn": os.setsid,
}
if preexec_fn is not None:
kw["preexec_fn"] = preexec_fn
if creationflags:
kw["creationflags"] = creationflags
proc = await asyncio.create_subprocess_exec(*cmd, **kw)
except Exception as e:
out_log.close()