#!/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())