refactor: restructure plugins per MTSS standard, consolidate assets to res/
Per docs/plugin-audit-report.md (plugins normalized to Trigger(handlers) → Service(services) → Model(repository/models) + utils): - Split monolithic __init__.py into handlers/services/utils across dailywife, deer_pipe, dice, galgame_card, helldivers_tools, huoziyinshua, learning_chat, makeaquote, mc_server_status, ncm_saying, picfinder_take, picstatus, random_jm_code, regif, steam_info, video_analysis, group_tools - Move static assets under res/: deer_pipe font/img, makeaquote font, helldivers img/templates, huoziyinshua HuoZiYinShua - Add config.py + register_config_items to ncm_saying, random_jm_code, group_tools; learning_chat unified config bridge - Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp - Disable brash_general_supercredits_tools (stub comment only) - bot.py: optional stdout/stderr redirect to log file for Web log viewer, force ANSI colorize on non-TTY sinks - Move runtime data (jm_code.json) out of plugin dir into hexi/data - Docs: plugin-audit-report.md; README reflects removed plugins Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# picstatus —— Service 层。
|
||||
@@ -0,0 +1,226 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from collections import deque
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Any, Generic, TypeVar
|
||||
from typing_extensions import override
|
||||
|
||||
from nonebot import logger
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
from ...config import config
|
||||
|
||||
T = TypeVar("T")
|
||||
TI = TypeVar("TI")
|
||||
TR = TypeVar("TR")
|
||||
TC = TypeVar("TC", bound="Collector")
|
||||
TCF = TypeVar("TCF", bound=Callable[[], Awaitable[Any]])
|
||||
R = TypeVar("R")
|
||||
|
||||
Undefined = type("Undefined", (), {})
|
||||
|
||||
|
||||
class SkipCollectError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Collector(Generic[TI, TR]):
|
||||
@abstractmethod
|
||||
async def _get(self) -> TI: ...
|
||||
|
||||
@abstractmethod
|
||||
async def get(self) -> TR: ...
|
||||
|
||||
|
||||
class BaseNormalCollector(Collector[T, T], Generic[T]):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
@override
|
||||
async def get(self) -> T:
|
||||
return await self._get()
|
||||
|
||||
|
||||
class BaseFirstTimeCollector(Collector[T, T], Generic[T]):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._cached: T | Undefined = Undefined()
|
||||
|
||||
@override
|
||||
async def get(self) -> T:
|
||||
if not isinstance(self._cached, Undefined):
|
||||
return self._cached
|
||||
data = await self._get()
|
||||
self._cached = data
|
||||
return data
|
||||
|
||||
|
||||
class BasePeriodicCollector(Collector[T, deque[T]], Generic[T]):
|
||||
def __init__(self, size: int = config.ps_default_collect_cache_size) -> None:
|
||||
super().__init__()
|
||||
self.data = deque(maxlen=size)
|
||||
|
||||
@override
|
||||
async def get(self) -> deque[T]:
|
||||
return self.data
|
||||
|
||||
async def collect(self):
|
||||
try:
|
||||
data = await self._get()
|
||||
except SkipCollectError:
|
||||
return
|
||||
except Exception:
|
||||
logger.exception("Error occurred while collecting data")
|
||||
else:
|
||||
self.data.append(data)
|
||||
|
||||
|
||||
registered_collectors: dict[str, type[Collector]] = {}
|
||||
enabled_collectors: dict[str, Collector] = {}
|
||||
|
||||
|
||||
def collector(name: str):
|
||||
def deco(cls: type[TC]) -> type[TC]:
|
||||
if name in registered_collectors:
|
||||
raise ValueError(f"Collector {name} already exists")
|
||||
registered_collectors[name] = cls
|
||||
logger.debug(f"Registered collector {name}")
|
||||
return cls
|
||||
|
||||
return deco
|
||||
|
||||
|
||||
def _enable_collector(name: str):
|
||||
if name not in registered_collectors:
|
||||
raise ValueError(f"Collector {name} not found")
|
||||
cls = registered_collectors[name]
|
||||
if issubclass(cls, BasePeriodicCollector) and name in config.ps_collect_cache_size:
|
||||
instance = cls(size=config.ps_collect_cache_size[name])
|
||||
else:
|
||||
instance = cls()
|
||||
enabled_collectors[name] = instance
|
||||
|
||||
|
||||
async def enable_collectors(*names: str):
|
||||
for name in names:
|
||||
_enable_collector(name)
|
||||
await init_first_time_collectors()
|
||||
await setup_periodic_collectors_update_job()
|
||||
|
||||
|
||||
def functional_collector(cls: type[Collector], name: str | None = None):
|
||||
def deco(func: TCF) -> TCF:
|
||||
collector_name = name or func.__name__
|
||||
if not collector_name:
|
||||
raise ValueError("name must be provided")
|
||||
|
||||
class Collector(cls):
|
||||
async def _get(self) -> Any:
|
||||
return await func()
|
||||
|
||||
collector(collector_name)(Collector)
|
||||
return func
|
||||
|
||||
return deco
|
||||
|
||||
|
||||
def normal_collector(name: str | None = None):
|
||||
return functional_collector(BaseNormalCollector, name)
|
||||
|
||||
|
||||
def first_time_collector(name: str | None = None):
|
||||
return functional_collector(BaseFirstTimeCollector, name)
|
||||
|
||||
|
||||
def periodic_collector(name: str | None = None):
|
||||
return functional_collector(BasePeriodicCollector, name)
|
||||
|
||||
|
||||
class BaseTimeBasedCounterCollector(Collector[R, Any], Generic[T, R]):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.last_obj: Undefined | T = Undefined()
|
||||
self.last_time: float = 0
|
||||
self.normal_delay: float = 1
|
||||
|
||||
@abstractmethod
|
||||
async def _calc(self, past: T, now: T, time_passed: float) -> R: ...
|
||||
|
||||
@abstractmethod
|
||||
async def _get_obj(self) -> T: ...
|
||||
|
||||
@override
|
||||
async def _get(self) -> R:
|
||||
past = self.last_obj
|
||||
past_time = self.last_time
|
||||
time_now = time.time()
|
||||
time_passed = time_now - past_time
|
||||
|
||||
self.last_time = time_now
|
||||
self.last_obj = await self._get_obj()
|
||||
if isinstance(past, Undefined):
|
||||
raise SkipCollectError
|
||||
return await self._calc(past, self.last_obj, time_passed)
|
||||
|
||||
|
||||
class NormalTimeBasedCounterCollector(
|
||||
BaseTimeBasedCounterCollector[T, R],
|
||||
BaseNormalCollector[R],
|
||||
Generic[T, R],
|
||||
):
|
||||
@override
|
||||
async def get(self) -> R:
|
||||
with suppress(SkipCollectError):
|
||||
await self._get()
|
||||
await asyncio.sleep(self.normal_delay)
|
||||
return await self._get()
|
||||
|
||||
|
||||
class PeriodicTimeBasedCounterCollector(
|
||||
BaseTimeBasedCounterCollector[T, R],
|
||||
BasePeriodicCollector[R],
|
||||
Generic[T, R],
|
||||
): ...
|
||||
|
||||
|
||||
async def collect_all() -> dict[str, Any]:
|
||||
async def get(name: str):
|
||||
return name, await enabled_collectors[name].get()
|
||||
|
||||
res = await asyncio.gather(*(get(name) for name in enabled_collectors))
|
||||
return dict(res)
|
||||
|
||||
|
||||
def load_builtin_collectors():
|
||||
for module in Path(__file__).parent.iterdir():
|
||||
if not module.name.startswith("_"):
|
||||
importlib.import_module(f".{module.stem}", __package__)
|
||||
|
||||
|
||||
async def init_first_time_collectors():
|
||||
await asyncio.gather(
|
||||
*(
|
||||
x.get()
|
||||
for x in enabled_collectors.values()
|
||||
if isinstance(x, BaseFirstTimeCollector)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def setup_periodic_collectors_update_job():
|
||||
collectors = [
|
||||
x for x in enabled_collectors.values() if isinstance(x, BasePeriodicCollector)
|
||||
]
|
||||
if not collectors:
|
||||
return
|
||||
logger.debug("Setting up periodic collectors")
|
||||
|
||||
@scheduler.scheduled_job("interval", seconds=config.ps_collect_interval)
|
||||
async def _do():
|
||||
await asyncio.gather(*(x.collect() for x in collectors))
|
||||
|
||||
await _do()
|
||||
@@ -0,0 +1,93 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from nonebot import get_bots, logger
|
||||
from nonebot.matcher import current_bot
|
||||
|
||||
from ...config import config
|
||||
from ...misc_statistics import bot_connect_time, bot_info_cache, recv_num, send_num
|
||||
from ...util import format_timedelta
|
||||
from . import normal_collector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.adapters import Bot as BaseBot
|
||||
|
||||
try:
|
||||
from nonebot.adapters.onebot.v11 import Bot as OBV11Bot
|
||||
except ImportError:
|
||||
OBV11Bot = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BotStatus:
|
||||
self_id: str
|
||||
adapter: str
|
||||
nick: str
|
||||
bot_connected: str
|
||||
msg_rec: str
|
||||
msg_sent: str
|
||||
|
||||
|
||||
async def get_ob11_msg_num(bot: "BaseBot") -> tuple[int | None, int | None]:
|
||||
if not (config.ps_ob_v11_use_get_status and OBV11Bot and isinstance(bot, OBV11Bot)):
|
||||
return None, None
|
||||
|
||||
try:
|
||||
bot_stat = (await bot.get_status()).get("stat")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Error when getting bot status: {e.__class__.__name__}: {e}",
|
||||
)
|
||||
return None, None
|
||||
if not bot_stat:
|
||||
return None, None
|
||||
|
||||
msg_rec = bot_stat.get("message_received") or bot_stat.get(
|
||||
"MessageReceived",
|
||||
)
|
||||
msg_sent = bot_stat.get("message_sent") or bot_stat.get("MessageSent")
|
||||
return msg_rec, msg_sent
|
||||
|
||||
|
||||
async def get_bot_status(bot: "BaseBot", now_time: datetime) -> BotStatus:
|
||||
nick = (
|
||||
((info := bot_info_cache[bot.self_id]).nick or info.name or info.id)
|
||||
if (not config.ps_use_env_nick) and (bot.self_id in bot_info_cache)
|
||||
else next(iter(config.nickname), None)
|
||||
) or "Bot"
|
||||
bot_connected = (
|
||||
format_timedelta(now_time - t)
|
||||
if (t := bot_connect_time.get(bot.self_id))
|
||||
else "未知"
|
||||
)
|
||||
|
||||
msg_rec, msg_sent = await get_ob11_msg_num(bot)
|
||||
if msg_rec is None:
|
||||
msg_rec = recv_num.get(bot.self_id)
|
||||
if msg_sent is None:
|
||||
msg_sent = send_num.get(bot.self_id)
|
||||
msg_rec = "未知" if (msg_rec is None) else str(msg_rec)
|
||||
msg_sent = "未知" if (msg_sent is None) else str(msg_sent)
|
||||
|
||||
return BotStatus(
|
||||
self_id=bot.self_id,
|
||||
adapter=bot.adapter.get_name(),
|
||||
nick=nick,
|
||||
bot_connected=bot_connected,
|
||||
msg_rec=msg_rec,
|
||||
msg_sent=msg_sent,
|
||||
)
|
||||
|
||||
|
||||
@normal_collector()
|
||||
async def bots() -> list[BotStatus]:
|
||||
now_time = datetime.now().astimezone()
|
||||
return (
|
||||
[await get_bot_status(current_bot.get(), now_time)]
|
||||
if config.ps_show_current_bot_only
|
||||
else await asyncio.gather(
|
||||
*(get_bot_status(bot, now_time) for bot in get_bots().values()),
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
import psutil
|
||||
from cpuinfo import get_cpu_info
|
||||
from nonebot import logger
|
||||
|
||||
from . import first_time_collector, normal_collector, periodic_collector
|
||||
|
||||
|
||||
@dataclass
|
||||
class CpuFreq:
|
||||
current: float | None
|
||||
min: float | None # noqa: A003
|
||||
max: float | None # noqa: A003
|
||||
|
||||
|
||||
@first_time_collector()
|
||||
async def cpu_brand() -> str:
|
||||
try:
|
||||
brand = (
|
||||
cast("str", get_cpu_info().get("brand_raw", ""))
|
||||
.split("@", maxsplit=1)[0]
|
||||
.strip()
|
||||
)
|
||||
if brand.lower().endswith(("cpu", "processor")):
|
||||
brand = brand.rsplit(maxsplit=1)[0].strip()
|
||||
except Exception:
|
||||
logger.exception("Error when getting CPU brand")
|
||||
return "未知型号"
|
||||
else:
|
||||
return brand
|
||||
|
||||
|
||||
@first_time_collector()
|
||||
async def cpu_count_logical() -> int | None:
|
||||
return psutil.cpu_count()
|
||||
|
||||
|
||||
@first_time_collector()
|
||||
async def cpu_count() -> int | None:
|
||||
return psutil.cpu_count(logical=False)
|
||||
|
||||
|
||||
async def get_cpu_percent() -> float:
|
||||
"""实时采样 0.5s,避免 psutil 首次调用返回 0.0"""
|
||||
return await asyncio.to_thread(psutil.cpu_percent, 0.5)
|
||||
|
||||
|
||||
async def get_cpu_percent_periodic() -> float:
|
||||
"""周期采样:两次调用之间的间隔即为采样区间,直接取瞬时差值即可"""
|
||||
return psutil.cpu_percent()
|
||||
|
||||
|
||||
normal_collector("cpu_percent")(get_cpu_percent)
|
||||
periodic_collector("cpu_percent_periodic")(get_cpu_percent_periodic)
|
||||
|
||||
|
||||
async def get_cpu_freq() -> CpuFreq:
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
return CpuFreq(
|
||||
current=getattr(cpu_freq, "current", None),
|
||||
min=getattr(cpu_freq, "min", None),
|
||||
max=getattr(cpu_freq, "max", None),
|
||||
)
|
||||
|
||||
|
||||
normal_collector("cpu_freq")(get_cpu_freq)
|
||||
periodic_collector("cpu_freq_periodic")(get_cpu_freq)
|
||||
@@ -0,0 +1,127 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import TypeAlias
|
||||
|
||||
import psutil
|
||||
from psutil._common import sdiskpart, sdiskio
|
||||
|
||||
from ...config import config
|
||||
from ...util import match_list_regexp
|
||||
from . import (
|
||||
BaseTimeBasedCounterCollector,
|
||||
NormalTimeBasedCounterCollector,
|
||||
PeriodicTimeBasedCounterCollector,
|
||||
collector,
|
||||
normal_collector,
|
||||
periodic_collector,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiskUsageNormal:
|
||||
name: str
|
||||
percent: float
|
||||
used: int
|
||||
total: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiskUsageWithExc:
|
||||
name: str
|
||||
exception: str
|
||||
|
||||
|
||||
DiskUsageType: TypeAlias = DiskUsageNormal | DiskUsageWithExc
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiskIO:
|
||||
name: str
|
||||
read: float
|
||||
write: float
|
||||
|
||||
|
||||
@periodic_collector()
|
||||
async def get_disk_usage() -> list[DiskUsageType]:
|
||||
def get_one(disk: sdiskpart) -> DiskUsageType | None:
|
||||
mountpoint = disk.mountpoint
|
||||
|
||||
if match_list_regexp(config.ps_ignore_parts, mountpoint):
|
||||
# logger.info(f"空间读取 分区 {mountpoint} 匹配 {regex.re.pattern},忽略")
|
||||
return None
|
||||
|
||||
try:
|
||||
usage = psutil.disk_usage(mountpoint)
|
||||
except Exception as e:
|
||||
# logger.exception(f"读取 {mountpoint} 占用失败")
|
||||
return (
|
||||
None
|
||||
if config.ps_ignore_bad_parts
|
||||
else DiskUsageWithExc(name=mountpoint, exception=str(e))
|
||||
)
|
||||
|
||||
return DiskUsageNormal(
|
||||
name=mountpoint,
|
||||
percent=usage.percent,
|
||||
used=usage.used,
|
||||
total=usage.total,
|
||||
)
|
||||
|
||||
usage = [x for x in map(get_one, psutil.disk_partitions()) if x]
|
||||
if config.ps_sort_parts:
|
||||
usage.sort(
|
||||
key=lambda x: x.percent if isinstance(x, DiskUsageNormal) else -1,
|
||||
reverse=not config.ps_sort_parts_reverse,
|
||||
)
|
||||
|
||||
return usage
|
||||
|
||||
|
||||
normal_collector("disk_usage")(get_disk_usage)
|
||||
periodic_collector("disk_usage_periodic")(get_disk_usage)
|
||||
|
||||
|
||||
class BaseDiskIOCollector(
|
||||
BaseTimeBasedCounterCollector[dict[str, sdiskio], list[DiskIO]],
|
||||
):
|
||||
async def _calc(
|
||||
self,
|
||||
past: dict[str, sdiskio],
|
||||
now: dict[str, sdiskio],
|
||||
time_passed: float,
|
||||
) -> list[DiskIO]:
|
||||
def calc_one(name: str, past_it: sdiskio, now_it: sdiskio) -> DiskIO | None:
|
||||
if match_list_regexp(config.ps_ignore_disk_ios, name):
|
||||
# logger.info(f"IO统计 磁盘 {name} 匹配 {regex.re.pattern},忽略")
|
||||
return None
|
||||
|
||||
read = (now_it.read_bytes - past_it.read_bytes) / time_passed
|
||||
write = (now_it.write_bytes - past_it.write_bytes) / time_passed
|
||||
|
||||
if read == 0 and write == 0 and config.ps_ignore_no_io_disk:
|
||||
# logger.info(f"IO统计 忽略无IO磁盘 {name}")
|
||||
return None
|
||||
|
||||
return DiskIO(name=name, read=read, write=write)
|
||||
|
||||
res = [calc_one(name, past[name], now[name]) for name in past if name in now]
|
||||
res = [x for x in res if x]
|
||||
if config.ps_sort_disk_ios:
|
||||
res.sort(key=lambda x: x.read + x.write, reverse=True)
|
||||
return res
|
||||
|
||||
async def _get_obj(self) -> dict[str, sdiskio]:
|
||||
return psutil.disk_io_counters(perdisk=True)
|
||||
|
||||
|
||||
@collector("disk_io")
|
||||
class NormalDiskIOCollector(
|
||||
BaseDiskIOCollector,
|
||||
NormalTimeBasedCounterCollector[dict[str, sdiskio], list[DiskIO]],
|
||||
): ...
|
||||
|
||||
|
||||
@collector("disk_io_periodic")
|
||||
class PeriodicDiskIOCollector(
|
||||
BaseDiskIOCollector,
|
||||
PeriodicTimeBasedCounterCollector[dict[str, sdiskio], list[DiskIO]],
|
||||
): ...
|
||||
@@ -0,0 +1,30 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import psutil
|
||||
|
||||
from . import normal_collector, periodic_collector
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryStat:
|
||||
percent: float
|
||||
used: int
|
||||
total: int
|
||||
|
||||
|
||||
async def get_memory_stat() -> MemoryStat:
|
||||
mem = psutil.virtual_memory()
|
||||
return MemoryStat(percent=mem.percent, used=mem.used, total=mem.total)
|
||||
|
||||
|
||||
normal_collector("memory_stat")(get_memory_stat)
|
||||
periodic_collector("memory_stat_periodic")(get_memory_stat)
|
||||
|
||||
|
||||
async def get_swap_stat() -> MemoryStat:
|
||||
swap = psutil.swap_memory()
|
||||
return MemoryStat(percent=swap.percent, used=swap.used, total=swap.total)
|
||||
|
||||
|
||||
normal_collector("swap_stat")(get_swap_stat)
|
||||
periodic_collector("swap_stat_periodic")(get_swap_stat)
|
||||
@@ -0,0 +1,130 @@
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import nonebot
|
||||
import psutil
|
||||
|
||||
from ...misc_statistics import nonebot_run_time
|
||||
from ...util import format_time_delta_ps
|
||||
from . import first_time_collector, normal_collector, periodic_collector
|
||||
|
||||
|
||||
def parse_env(env: str) -> dict[str, str | None]:
|
||||
env_lines = env.strip().splitlines()
|
||||
env_dict: dict[str, str | None] = {}
|
||||
|
||||
for line in env_lines:
|
||||
if "=" not in line:
|
||||
env_dict[line.upper()] = None
|
||||
continue
|
||||
|
||||
key, value = line.split("=", 1)
|
||||
env_dict[key.upper()] = value.strip("\"'").strip()
|
||||
|
||||
return env_dict
|
||||
|
||||
|
||||
def parse_env_file(env_file: str | Path) -> dict[str, str | None] | None:
|
||||
if not isinstance(env_file, Path):
|
||||
env_file = Path(env_file)
|
||||
if not env_file.exists():
|
||||
return None
|
||||
content = env_file.read_text(encoding="u8")
|
||||
return parse_env(content)
|
||||
|
||||
|
||||
# Thanks to https://github.com/nonedesktop/nonebot-plugin-guestool/blob/main/nonebot_plugin_guestool/info.py
|
||||
def get_linux_name_version() -> tuple[str, str] | None:
|
||||
env = parse_env_file("/etc/os-release")
|
||||
if env and (name := env.get("NAME")) and (version_id := env.get("VERSION_ID")):
|
||||
return name, version_id
|
||||
|
||||
env = parse_env_file("/etc/lsb-release")
|
||||
if (
|
||||
env
|
||||
and (name := env.get("DISTRIB_ID"))
|
||||
and (version_id := env.get("DISTRIB_RELEASE"))
|
||||
):
|
||||
return name, version_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@normal_collector("nonebot_run_time")
|
||||
async def nonebot_run_time_str() -> str:
|
||||
now_time = datetime.now().astimezone()
|
||||
return (
|
||||
format_time_delta_ps(now_time - nonebot_run_time)
|
||||
if nonebot_run_time
|
||||
else "未知"
|
||||
)
|
||||
|
||||
|
||||
@normal_collector()
|
||||
async def system_run_time() -> str:
|
||||
now_time = datetime.now().astimezone()
|
||||
return format_time_delta_ps(
|
||||
now_time - datetime.fromtimestamp(psutil.boot_time()).astimezone(),
|
||||
)
|
||||
|
||||
|
||||
@first_time_collector()
|
||||
async def nonebot_version() -> str:
|
||||
return nonebot.__version__
|
||||
|
||||
|
||||
@first_time_collector()
|
||||
async def ps_version() -> str:
|
||||
from ... import __version__
|
||||
|
||||
return __version__
|
||||
|
||||
|
||||
async def get_time_str() -> str:
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
normal_collector("time")(get_time_str)
|
||||
periodic_collector("time_periodic")(get_time_str)
|
||||
|
||||
|
||||
@first_time_collector()
|
||||
async def python_version() -> str:
|
||||
return f"{platform.python_implementation()} {platform.python_version()}"
|
||||
|
||||
|
||||
@first_time_collector()
|
||||
async def system_name():
|
||||
system, _, release, version, machine, _ = platform.uname()
|
||||
system, release, version = platform.system_alias(system, release, version)
|
||||
|
||||
if system == "Java":
|
||||
_, _, _, (system, release, machine) = platform.java_ver()
|
||||
|
||||
if system == "Darwin":
|
||||
return f"MacOS {platform.mac_ver()[0]} {machine}"
|
||||
|
||||
if system == "Windows":
|
||||
return f"Windows {release} {platform.win32_edition()} {machine}"
|
||||
|
||||
if system == "Linux":
|
||||
if (pfx := os.getenv("PREFIX")) and "termux" in pfx:
|
||||
system = f"Termux (Android) {release}" # a strange platform
|
||||
|
||||
elif os.getenv("ANDROID_ROOT") == "/system":
|
||||
system = f"Linux (Android) {release}"
|
||||
|
||||
elif ver := get_linux_name_version():
|
||||
name, version_id = ver
|
||||
version = release if version_id.lower() == "rolling" else version_id
|
||||
system = f"{name} {version}"
|
||||
|
||||
else:
|
||||
system = f"未知 Linux {release}"
|
||||
|
||||
return f"{system} {machine}"
|
||||
|
||||
return f"{system} {release}"
|
||||
@@ -0,0 +1,124 @@
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import TypeAlias
|
||||
|
||||
import psutil
|
||||
from httpx import AsyncClient, ReadTimeout
|
||||
from psutil._common import snetio
|
||||
|
||||
from ...config import TestSiteCfg, config
|
||||
from ...util import match_list_regexp
|
||||
from . import (
|
||||
BaseTimeBasedCounterCollector,
|
||||
NormalTimeBasedCounterCollector,
|
||||
PeriodicTimeBasedCounterCollector,
|
||||
collector,
|
||||
normal_collector,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetworkIO:
|
||||
name: str
|
||||
sent: float
|
||||
recv: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetworkConnectionOK:
|
||||
name: str
|
||||
status: int
|
||||
reason: str
|
||||
delay: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetworkConnectionError:
|
||||
name: str
|
||||
error: str
|
||||
|
||||
|
||||
NetworkConnectionType: TypeAlias = NetworkConnectionOK | NetworkConnectionError
|
||||
|
||||
|
||||
class BaseNetworkIOCollector(
|
||||
BaseTimeBasedCounterCollector[dict[str, snetio], list[NetworkIO]],
|
||||
):
|
||||
async def _calc(
|
||||
self,
|
||||
past: dict[str, snetio],
|
||||
now: dict[str, snetio],
|
||||
time_passed: float,
|
||||
) -> list[NetworkIO]:
|
||||
def calc_one(name: str, past_it: snetio, now_it: snetio) -> NetworkIO | None:
|
||||
if match_list_regexp(config.ps_ignore_nets, name):
|
||||
# logger.info(f"网卡IO统计 {name} 匹配 {regex.re.pattern},忽略")
|
||||
return None
|
||||
|
||||
sent = (now_it.bytes_sent - past_it.bytes_sent) / time_passed
|
||||
recv = (now_it.bytes_recv - past_it.bytes_recv) / time_passed
|
||||
|
||||
if sent == 0 and recv == 0 and config.ps_ignore_0b_net:
|
||||
# logger.info(f"网卡IO统计 忽略无IO网卡 {name}")
|
||||
return None
|
||||
|
||||
return NetworkIO(name=name, sent=sent, recv=recv)
|
||||
|
||||
res = [calc_one(name, past[name], now[name]) for name in past if name in now]
|
||||
res = [x for x in res if x]
|
||||
if config.ps_sort_nets:
|
||||
res.sort(key=lambda x: x.sent + x.recv, reverse=True)
|
||||
return res
|
||||
|
||||
async def _get_obj(self) -> dict[str, snetio]:
|
||||
return psutil.net_io_counters(pernic=True)
|
||||
|
||||
|
||||
@collector("network_io")
|
||||
class NormalDiskIOCollector(
|
||||
BaseNetworkIOCollector,
|
||||
NormalTimeBasedCounterCollector[dict[str, snetio], list[NetworkIO]],
|
||||
): ...
|
||||
|
||||
|
||||
@collector("network_io_periodic")
|
||||
class PeriodicDiskIOCollector(
|
||||
BaseNetworkIOCollector,
|
||||
PeriodicTimeBasedCounterCollector[dict[str, snetio], list[NetworkIO]],
|
||||
): ...
|
||||
|
||||
|
||||
@normal_collector()
|
||||
async def network_connection() -> list[NetworkConnectionType]:
|
||||
def format_conn_error(error: Exception) -> str:
|
||||
if isinstance(error, ReadTimeout):
|
||||
return "超时"
|
||||
return error.__class__.__name__
|
||||
|
||||
async def test_one(site: TestSiteCfg) -> NetworkConnectionType:
|
||||
try:
|
||||
async with AsyncClient(
|
||||
timeout=config.ps_test_timeout,
|
||||
proxy=config.proxy if site.use_proxy else None,
|
||||
follow_redirects=True,
|
||||
) as client:
|
||||
start = time.time()
|
||||
resp = await client.get(str(site.url))
|
||||
delay = (time.time() - start) * 1000
|
||||
|
||||
except Exception as e:
|
||||
return NetworkConnectionError(name=site.name, error=format_conn_error(e))
|
||||
|
||||
return NetworkConnectionOK(
|
||||
name=site.name,
|
||||
status=resp.status_code,
|
||||
reason=resp.reason_phrase,
|
||||
delay=delay,
|
||||
)
|
||||
|
||||
res = await asyncio.gather(*map(test_one, config.ps_test_sites))
|
||||
if config.ps_sort_sites:
|
||||
res.sort(key=lambda x: x.delay if isinstance(x, NetworkConnectionOK) else -1)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,61 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
import psutil
|
||||
|
||||
from ...config import config
|
||||
from ...util import match_list_regexp
|
||||
from . import normal_collector, periodic_collector
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessStatus:
|
||||
name: str
|
||||
cpu: float
|
||||
mem: int
|
||||
|
||||
|
||||
async def get_process_status() -> list[ProcessStatus]:
|
||||
if not config.ps_proc_len:
|
||||
return []
|
||||
|
||||
async def parse_one(proc: psutil.Process) -> ProcessStatus | None:
|
||||
name = proc.name()
|
||||
if match_list_regexp(config.ps_ignore_procs, name):
|
||||
# logger.info(f"进程 {name} 匹配 {regex.re.pattern},忽略")
|
||||
return None
|
||||
|
||||
# proc.cpu_percent()
|
||||
# await asyncio.sleep(1)
|
||||
cpu_count = psutil.cpu_count()
|
||||
with proc.oneshot():
|
||||
cpu = proc.cpu_percent()
|
||||
cpu = (
|
||||
(cpu / cpu_count) if config.ps_proc_cpu_max_100p and cpu_count else cpu
|
||||
)
|
||||
mem: int = proc.memory_info().rss
|
||||
|
||||
return ProcessStatus(name=name, cpu=cpu, mem=mem)
|
||||
|
||||
def sorter(x: ProcessStatus):
|
||||
sort_by = config.ps_proc_sort_by
|
||||
if sort_by == "mem":
|
||||
return x.mem
|
||||
# if sort_by == "cpu":
|
||||
return x.cpu
|
||||
|
||||
proc_list = cast(
|
||||
"list[ProcessStatus | None | Exception]",
|
||||
await asyncio.gather(
|
||||
*(parse_one(proc) for proc in psutil.process_iter()),
|
||||
return_exceptions=True,
|
||||
),
|
||||
)
|
||||
proc_list = [x for x in proc_list if x and (not isinstance(x, Exception))]
|
||||
proc_list.sort(key=sorter, reverse=True)
|
||||
return proc_list[: config.ps_proc_len]
|
||||
|
||||
|
||||
normal_collector("process_status")(get_process_status)
|
||||
periodic_collector("process_status_periodic")(get_process_status)
|
||||
Reference in New Issue
Block a user