's'
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
69
src/proxy.py
69
src/proxy.py
@@ -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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Web 控制台进程后端:优先 PM2(Linux/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:
|
||||
"""结束与本业务相关的 ffmpeg:Linux 按 /proc 命令行匹配 + 可选 pkill;Windows 按命令行标记筛选。"""
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user