/** * 主机信息的纯解析。 * * mihomo 的管理接口只报自己的内存,CPU、主机内存、网关这些要靠 Patchbay 服务端 * 自己读 /proc 和 os 模块——服务端和内核跑在同一台机器上,这正是它能代答的原因。 * 读文件在 server/host.ts;这里只做字符串到数字的解析,让 bun test 够得着。 */ /** GET /api/host 的响应。数据由 server/host.ts 采集,类型放这里让前后端共用。 */ export interface HostInfo { /** 0-100。第一次请求没有参照快照,是 null */ cpuUsage: number | null cpuCount: number /** 内存总量。Linux 用 MemTotal,其余平台退回 totalmem */ memTotal: number /** 还能用的内存。Linux 用 MemAvailable,其余平台退回 freemem */ memAvailable: number /** 1 分钟负载。Windows 上拿不到,是 null */ load1: number | null uptimeSeconds: number platform: string release: string arch: string /** 非回环网卡上的 IPv4,含网卡名 */ lanAddresses: Array<{ iface: string; address: string }> gateway: string | null nameservers: string[] } /** GET /api/host/exit 的响应:经由某个入站端口探测到的公网 IP。 */ export interface ExitProbe { ok: boolean ip?: string country?: string countryCode?: string /** 整个探测来回的毫秒数 */ ms?: number message?: string } function meminfoBytes(meminfo: string, field: string): number | null { const match = new RegExp(`^${field}:\\s+(\\d+)\\s*kB`, 'm').exec(meminfo) if (!match) return null return Number(match[1]) * 1024 } /** * /proc/meminfo 里的 MemTotal,换算成字节。 * * 不能用 os.totalmem():它底层是 sysinfo() 系统调用,LXC 里的 lxcfs 只虚拟化 * /proc 下的文件、拦不住系统调用,所以 sysinfo() 返回的是宿主机的物理内存总量。 * 从 /proc/meminfo 读才是容器视角,且与 MemAvailable 出自同一份数据。 */ export function parseMemTotal(meminfo: string): number | null { return meminfoBytes(meminfo, 'MemTotal') } /** /proc/meminfo 里的 MemAvailable(内核估算的「还能用多少」),换算成字节。 */ export function parseMemAvailable(meminfo: string): number | null { return meminfoBytes(meminfo, 'MemAvailable') } /** * /proc/net/route 里的默认网关。 * * 格式是制表分隔的表:目的网络为 00000000 的那行是默认路由,Gateway 列是 * 小端序的十六进制 IPv4——字节序要倒回来才是人读的顺序。 */ export function parseGateway(procNetRoute: string): string | null { for (const line of procNetRoute.split('\n').slice(1)) { const cols = line.trim().split(/\s+/) if (cols.length < 3 || cols[1] !== '00000000') continue const hex = cols[2]! if (!/^[0-9a-fA-F]{8}$/.test(hex)) continue const bytes = [6, 4, 2, 0].map((i) => parseInt(hex.slice(i, i + 2), 16)) return bytes.join('.') } return null } /** /etc/resolv.conf 里的 DNS 服务器,按出现顺序。 */ export function parseNameservers(resolvConf: string): string[] { const result: string[] = [] for (const line of resolvConf.split('\n')) { const match = /^\s*nameserver\s+(\S+)/.exec(line) if (match) result.push(match[1]!) } return result } /** 一次 CPU 时间快照:忙碌和总量的累计毫秒(os.cpus() 各核求和)。 */ export interface CpuTimes { busy: number total: number } /** * 两次快照之间的 CPU 使用率,0 到 100。 * 时间没有前进(同一毫秒内连问两次,或计数回绕)时返回 null,不硬造一个数。 */ export function cpuUsageBetween(prev: CpuTimes, next: CpuTimes): number | null { const total = next.total - prev.total const busy = next.busy - prev.busy if (total <= 0 || busy < 0) return null return Math.min(100, (busy / total) * 100) }