This commit is contained in:
eric
2026-05-19 06:47:07 +00:00
parent e1d7251545
commit 78e1308063
63 changed files with 2527 additions and 186 deletions

View File

@@ -29,6 +29,7 @@ BASH_BIN = "/bin/bash" if Path("/bin/bash").is_file() else (shutil.which("bash")
_PRO_URL_CONFIG_INI = re.compile(r"^URL_config\.[a-zA-Z0-9_.-]+\.ini$")
_PRO_YOUTUBE_INI = re.compile(r"^youtube\.[a-zA-Z0-9_.-]+\.ini$")
_NETWORK_RATE_CACHE: dict[str, Any] = {"timestamp": 0.0, "interfaces": {}}
@dataclass(frozen=True)
@@ -497,12 +498,10 @@ def _env_int(env: dict[str, str], key: str, default: int) -> int:
def _neko_http_ports(env: dict[str, str]) -> list[int]:
count = max(1, min(3, _env_int(env, "NEKO_INSTANCE_COUNT", 2)))
return [
_env_int(env, "NEKO_HTTP_PORT_1", _env_int(env, "NEKO_HTTP_PORT", 9200)),
_env_int(env, "NEKO_HTTP_PORT_2", 9201),
_env_int(env, "NEKO_HTTP_PORT_3", 9202),
][:count]
max_count = max(2, min(64, _env_int(env, "NEKO_MAX_INSTANCE_COUNT", 64)))
count = max(2, min(max_count, _env_int(env, "NEKO_INSTANCE_COUNT", 2)))
base = _env_int(env, "NEKO_HTTP_PORT", 9200)
return [_env_int(env, f"NEKO_HTTP_PORT_{idx}", base + idx - 1) for idx in range(1, count + 1)]
def _neko_instances(mdns_host: str, env: dict[str, str]) -> list[dict[str, Any]]:
@@ -790,16 +789,24 @@ def _iface_operstate(name: str) -> str:
def network_iface_stats() -> dict[str, Any]:
"""Linux /proc/net/dev cumulative counters plus current link bandwidth hints."""
"""Linux /proc/net/dev counters with per-refresh bandwidth rates."""
dev = Path("/proc/net/dev")
interfaces: list[dict[str, Any]] = []
if not dev.is_file():
return {"interfaces": interfaces}
return {"interfaces": interfaces, "total_rx_bytes": 0, "total_tx_bytes": 0, "total_rx_bps": 0.0, "total_tx_bps": 0.0}
skip_prefixes = ("lo", "docker", "veth", "br-", "virbr", "tun", "tap")
try:
lines = dev.read_text(encoding="utf-8", errors="replace").splitlines()
except OSError:
return {"interfaces": interfaces}
return {"interfaces": interfaces, "total_rx_bytes": 0, "total_tx_bytes": 0, "total_rx_bps": 0.0, "total_tx_bps": 0.0}
now = time.monotonic()
previous_ts = float(_NETWORK_RATE_CACHE.get("timestamp") or 0.0)
previous = _NETWORK_RATE_CACHE.get("interfaces") or {}
next_cache: dict[str, dict[str, int]] = {}
total_rx = 0
total_tx = 0
total_rx_bps = 0.0
total_tx_bps = 0.0
for line in lines[2:]:
if ":" not in line:
continue
@@ -811,20 +818,50 @@ def network_iface_stats() -> dict[str, Any]:
if len(parts) < 16:
continue
try:
rx_bytes = int(parts[0])
rx_packets = int(parts[1])
tx_bytes = int(parts[8])
tx_packets = int(parts[9])
total_rx += rx_bytes
total_tx += tx_bytes
prev = previous.get(name) if isinstance(previous, dict) else None
elapsed = max(0.0, now - previous_ts) if previous_ts else 0.0
rx_bps = 0.0
tx_bps = 0.0
if isinstance(prev, dict) and elapsed > 0:
prev_rx = int(prev.get("rx_bytes", rx_bytes))
prev_tx = int(prev.get("tx_bytes", tx_bytes))
rx_bps = max(0.0, (rx_bytes - prev_rx) / elapsed)
tx_bps = max(0.0, (tx_bytes - prev_tx) / elapsed)
total_rx_bps += rx_bps
total_tx_bps += tx_bps
next_cache[name] = {"rx_bytes": rx_bytes, "tx_bytes": tx_bytes}
interfaces.append(
{
"name": name,
"rx_bytes": int(parts[0]),
"rx_packets": int(parts[1]),
"tx_bytes": int(parts[8]),
"tx_packets": int(parts[9]),
"rx_bytes": rx_bytes,
"rx_packets": rx_packets,
"tx_bytes": tx_bytes,
"tx_packets": tx_packets,
"rx_bps": round(rx_bps, 2),
"tx_bps": round(tx_bps, 2),
"sample_interval_seconds": round(elapsed, 3),
"speed_mbps": _iface_speed_mbps(name),
"operstate": _iface_operstate(name),
}
)
except (ValueError, IndexError):
continue
return {"interfaces": interfaces}
_NETWORK_RATE_CACHE["timestamp"] = now
_NETWORK_RATE_CACHE["interfaces"] = next_cache
return {
"interfaces": interfaces,
"total_rx_bytes": total_rx,
"total_tx_bytes": total_tx,
"total_rx_bps": round(total_rx_bps, 2),
"total_tx_bps": round(total_tx_bps, 2),
"sample_interval_seconds": round(max(0.0, now - previous_ts) if previous_ts else 0.0, 3),
}
def recent_pm2_log_lines(max_lines: int = 14) -> list[str]:
@@ -878,7 +915,7 @@ async def stack_stream_probes() -> dict[str, Any]:
http = _probe_http_get(f"http://127.0.0.1:{port}/", timeout=2.5)
instances.append({"id": idx, "port": port, "tcp": tcp, "http": http})
primary = instances[0] if instances else {"port": 0, "tcp": {}, "http": {}}
required_count = max(1, min(3, _env_int(env, "NEKO_INSTANCE_COUNT", 2)))
required_count = len(neko_ports)
return {
"port": primary["port"],
"tcp": primary["tcp"],