Files

197 lines
6.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())