197 lines
6.7 KiB
Python
197 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
||
"""Server Monitor entry point.
|
||
|
||
Usage:
|
||
python3 monitor.py --bind 0.0.0.0 --port 8787
|
||
python3 monitor.py --demo
|
||
python3 monitor.py --once --demo
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import logging
|
||
import os
|
||
import signal
|
||
import socket
|
||
import sys
|
||
import threading
|
||
import time
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
from servermon import __version__
|
||
from servermon.demo import DemoSource
|
||
from servermon.metrics import SystemCollector
|
||
from servermon.service import MonitorService
|
||
from servermon.web import AuthConfig, build_server
|
||
|
||
logger = logging.getLogger("servermon")
|
||
|
||
|
||
def _build_parser() -> argparse.ArgumentParser:
|
||
parser = argparse.ArgumentParser(
|
||
prog="server-monitor",
|
||
description="Linux 服务器监控展示牌:CPU / 内存 / 负载 / 磁盘 / 网络,1024x768 自适应面板。",
|
||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||
)
|
||
parser.add_argument("--bind", default="0.0.0.0", help="监听地址")
|
||
parser.add_argument("--port", type=int, default=8787, help="监听端口")
|
||
parser.add_argument("--interval", type=float, default=1.0, help="采样间隔(秒,最小 0.2)")
|
||
parser.add_argument("--demo", action="store_true", help="使用内置演示数据(可在非 Linux 上运行)")
|
||
parser.add_argument("--once", action="store_true", help="打印一次 JSON 快照后退出")
|
||
parser.add_argument(
|
||
"--auth",
|
||
default=None,
|
||
metavar="USER:PASSWORD",
|
||
help="可选 HTTP Basic 认证;也可用环境变量 SERVER_MONITOR_AUTH",
|
||
)
|
||
parser.add_argument("--web-dir", default=None, help="前端静态文件目录")
|
||
parser.add_argument("--verbose", action="store_true", help="输出 HTTP 访问日志")
|
||
parser.add_argument("--version", action="version", version=f"server-monitor {__version__}")
|
||
return parser
|
||
|
||
|
||
def _local_ips() -> list:
|
||
ips = []
|
||
try:
|
||
host = socket.gethostname()
|
||
for info in socket.getaddrinfo(host, None, socket.AF_INET):
|
||
ip = info[4][0]
|
||
if ip not in ips and not ip.startswith("127."):
|
||
ips.append(ip)
|
||
except OSError:
|
||
pass
|
||
try:
|
||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
sock.settimeout(0.2)
|
||
sock.connect(("1.1.1.1", 80))
|
||
ip = sock.getsockname()[0]
|
||
sock.close()
|
||
if ip not in ips:
|
||
ips.append(ip)
|
||
except OSError:
|
||
pass
|
||
return ips
|
||
|
||
|
||
def _print_banner(bind: str, port: int, demo: bool, auth: Optional[AuthConfig]) -> None:
|
||
print()
|
||
print(" ╭──────────────────────────────────────────────────────────╮")
|
||
print(" │ SERVER MONITOR · 1024x768 · realtime metrics │")
|
||
print(" ╰──────────────────────────────────────────────────────────╯")
|
||
print(f" 模式 : {'DEMO 演示数据' if demo else 'LIVE 系统指标'}")
|
||
print(f" 监听 : {bind}:{port}")
|
||
if bind in ("0.0.0.0", "::", ""):
|
||
print(f" 本机访问 : http://127.0.0.1:{port}/")
|
||
for ip in _local_ips():
|
||
print(f" 局域网 : http://{ip}:{port}/")
|
||
elif ":" in bind:
|
||
print(f" 访问 : http://[{bind}]:{port}/")
|
||
else:
|
||
print(f" 访问 : http://{bind}:{port}/")
|
||
print(f" 认证 : {'HTTP Basic 已启用' if auth else '未启用(建议仅暴露在可信网络)'}")
|
||
print(" 快捷键 : SPACE 暂停轮播 · ←/→ 切换 · F 全屏")
|
||
print()
|
||
|
||
|
||
def _run_once(args: argparse.Namespace) -> int:
|
||
source = DemoSource() if args.demo else SystemCollector()
|
||
service = MonitorService(source, interval=args.interval)
|
||
service.bootstrap()
|
||
if not args.demo:
|
||
time.sleep(max(0.3, min(2.0, args.interval)))
|
||
service.collect_once()
|
||
snapshot = service.snapshot()
|
||
if snapshot is None:
|
||
print("无法生成快照", file=sys.stderr)
|
||
return 1
|
||
json.dump(snapshot, sys.stdout, ensure_ascii=False, indent=2)
|
||
sys.stdout.write("\n")
|
||
return 0
|
||
|
||
|
||
def main(argv: Optional[list] = None) -> int:
|
||
args = _build_parser().parse_args(argv)
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
||
datefmt="%H:%M:%S",
|
||
)
|
||
|
||
if args.interval < 0.2:
|
||
logger.error("--interval 不能小于 0.2 秒")
|
||
return 2
|
||
if not 1 <= args.port <= 65535:
|
||
logger.error("--port 必须在 1-65535 之间")
|
||
return 2
|
||
|
||
try:
|
||
auth_raw = args.auth if args.auth is not None else os.environ.get("SERVER_MONITOR_AUTH")
|
||
auth = AuthConfig.parse(auth_raw)
|
||
except ValueError as exc:
|
||
logger.error("认证配置错误: %s", exc)
|
||
return 2
|
||
|
||
if args.once:
|
||
return _run_once(args)
|
||
|
||
if not args.demo and not sys.platform.startswith("linux"):
|
||
logger.error("实时模式仅支持 Linux。开发预览请加 --demo。")
|
||
return 2
|
||
|
||
source = DemoSource() if args.demo else SystemCollector()
|
||
service = MonitorService(source, interval=args.interval)
|
||
service.bootstrap()
|
||
|
||
stop_event = threading.Event()
|
||
web_dir = Path(args.web_dir) if args.web_dir else None
|
||
try:
|
||
server = build_server(
|
||
args.bind,
|
||
args.port,
|
||
service,
|
||
auth=auth,
|
||
web_dir=web_dir,
|
||
stop_event=stop_event,
|
||
verbose=args.verbose,
|
||
)
|
||
except (OSError, ValueError, FileNotFoundError) as exc:
|
||
logger.error("无法启动 HTTP 服务: %s", exc)
|
||
return 1
|
||
|
||
def shutdown(signum: int, _frame: object) -> None:
|
||
logger.info("收到信号 %s,正在停止…", signum)
|
||
stop_event.set()
|
||
|
||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||
try:
|
||
signal.signal(sig, shutdown)
|
||
except (ValueError, OSError):
|
||
pass
|
||
|
||
server_thread = threading.Thread(target=server.serve_forever, name="http", daemon=True)
|
||
server_thread.start()
|
||
service.start()
|
||
|
||
_print_banner(args.bind, args.port, args.demo, auth)
|
||
if auth is None and args.bind not in ("127.0.0.1", "::1", "localhost"):
|
||
logger.warning("当前未启用认证且监听非回环地址,请用防火墙限制访问范围。")
|
||
|
||
try:
|
||
while not stop_event.wait(0.5):
|
||
pass
|
||
except KeyboardInterrupt:
|
||
stop_event.set()
|
||
finally:
|
||
service.stop()
|
||
server.shutdown()
|
||
server.server_close()
|
||
logger.info("已停止")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|