mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 15:45:13 +08:00
🌈 Sync 2026-07-29 14:47:30
This commit is contained in:
1 parent
d31b90c3d6
commit
9419456fd2
48 files changed
+13901
-3399
No files matched your search
@@ -0,0 +1,183 @@
|
||||
# luci-app-pingpacket
|
||||
|
||||
`luci-app-pingpacket` 是一个面向 OpenWrt 的 LuCI 状态监控插件,用于持续观察国内直连质量和国外经代理访问质量,并提供统计、日志和快速排障能力。
|
||||
|
||||
## 界面截图
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" align="center">
|
||||
<img src="jpg/1.png" alt="Ping丢包监控总览" width="100%">
|
||||
<br>
|
||||
<sub>监控总览</sub>
|
||||
</td>
|
||||
<td width="50%" align="center">
|
||||
<img src="jpg/2.png" alt="Ping丢包监控设置" width="100%">
|
||||
<br>
|
||||
<sub>监控设置</sub>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="center">
|
||||
<img src="jpg/3.png" alt="Ping丢包监控运行日志" width="78%">
|
||||
<br>
|
||||
<sub>运行日志</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 适用平台
|
||||
|
||||
- OpenWrt 24.10.x,发布 `ipk` 安装包
|
||||
- OpenWrt 25.12.x,发布 `apk` 安装包
|
||||
- 已安装 LuCI Web 管理界面
|
||||
- 设备需具备 `ping`、`curl`、`awk`、`sed`、`grep`、`tail` 等基础命令
|
||||
|
||||
当前仓库的 GitHub Actions 工作流默认构建 `x86/64` SDK 版本;插件本身为 LuCI 页面和 shell 脚本,原则上也可移植到其他支持 LuCI 的 OpenWrt 架构。
|
||||
|
||||
## 页面位置
|
||||
|
||||
后台路径:
|
||||
|
||||
`状态 -> Ping丢包监控`
|
||||
|
||||
子页面:
|
||||
|
||||
- `监控`
|
||||
- `日志`
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 监控页
|
||||
|
||||
- 支持分别配置国内目标和国外测速 URL
|
||||
- 国内目标通过本机 `ping` 进行 ICMP 探测
|
||||
- 国外目标通过 `curl` + `socks5h` 代理进行连续测速,更接近代理实际访问质量
|
||||
- 国外测速仅将 HTTP `200/204` 视为成功
|
||||
- 页面内可直接启用或关闭监控服务
|
||||
- 点击“保存并应用”后会保存配置;如果当前已启用监控,服务会自动重启并按新配置重新开始累计统计
|
||||
- 当检测到“数据停滞”时,页面会直接显示“重新启动服务”按钮
|
||||
- 实时显示开始时间、运行时长、最近更新时间、CPU 占用、内存占用和服务状态
|
||||
|
||||
### 统计卡片
|
||||
|
||||
国内和国外分别独立统计以下数据:
|
||||
|
||||
- 平均延迟
|
||||
- 最低延迟
|
||||
- 最高延迟
|
||||
- 丢包率
|
||||
- 丢包数
|
||||
- 样本数
|
||||
|
||||
统计口径说明:
|
||||
|
||||
- 以上数据均为“本次服务启用后的累计统计值”
|
||||
- 停止服务、重新启用服务或重启服务后,统计会重新开始
|
||||
- 国内和国外分别独立累计,不合并计算
|
||||
|
||||
### 日志页
|
||||
|
||||
- 显示插件运行日志
|
||||
- 顶部支持“自动刷新”开关,默认勾选
|
||||
- 支持“清除日志”按钮
|
||||
- 显示日志最后更新时间、日志行数和日志大小
|
||||
- 默认保留最近 500 行日志
|
||||
|
||||
当前日志会记录:
|
||||
|
||||
- 服务启动
|
||||
- 服务停止
|
||||
- 单次丢包事件
|
||||
- 恢复事件
|
||||
- 国外代理测速失败原因
|
||||
|
||||
日志规则:
|
||||
|
||||
- 只要单次探测失败,就立即记录一条日志
|
||||
- 连续失败时,每次失败都会单独记录
|
||||
- 故障后的首次成功响应会记录恢复日志
|
||||
|
||||
## 当前探测方式
|
||||
|
||||
### 国内
|
||||
|
||||
- 目标类型:IP 或域名
|
||||
- 探测方式:ICMP `ping`
|
||||
- 默认目标:`www.baidu.com`
|
||||
- 默认 Ping 间隔:`1` 秒
|
||||
|
||||
### 国外
|
||||
|
||||
- 目标类型:域名或完整 URL
|
||||
- 探测方式:通过 `socks5h` 代理发起 `curl` 请求
|
||||
- 默认测速 URL:`https://www.google.com/generate_204`
|
||||
- 默认代理端口:`7891`
|
||||
- 默认测速间隔:`5` 秒
|
||||
- 代理主机固定为 OpenWrt 本机 `127.0.0.1`
|
||||
|
||||
## 配置规则
|
||||
|
||||
- 国内和国外目标可以只填其中一个
|
||||
- 两个目标都为空时,不能“保存并应用”
|
||||
- 两个目标都为空时,也不能通过开关启用服务
|
||||
- 若填写国外测速 URL,则必须同时填写代理端口
|
||||
- 代理端口必须为 `1-65535` 的数字
|
||||
- 外国测速 URL 未填写协议时,系统会自动按 `https://` 处理
|
||||
- 页面轮询刷新时,不会覆盖用户正在编辑但尚未保存的输入内容
|
||||
|
||||
## 默认配置
|
||||
|
||||
安装或补齐配置时,默认值如下:
|
||||
|
||||
- 国内目标:`www.baidu.com`
|
||||
- 国外测速 URL:`https://www.google.com/generate_204`
|
||||
- 国外代理类型:`socks5h`
|
||||
- 国外代理端口:`7891`
|
||||
- 国内 Ping 间隔:`1`
|
||||
- 国外测速间隔:`5`
|
||||
|
||||
默认 UCI 配置示例:
|
||||
|
||||
```uci
|
||||
config pingpacket 'config'
|
||||
option enabled '0'
|
||||
option domestic_target 'www.baidu.com'
|
||||
option foreign_target 'https://www.google.com/generate_204'
|
||||
option foreign_proxy_type 'socks5h'
|
||||
option foreign_proxy_host '127.0.0.1'
|
||||
option foreign_proxy_port '7891'
|
||||
option domestic_interval '1'
|
||||
option foreign_interval '5'
|
||||
```
|
||||
|
||||
## 依赖
|
||||
|
||||
- `luci-base`
|
||||
- `luci-lua-runtime`
|
||||
- `luci-compat`
|
||||
- `curl`
|
||||
|
||||
## 安装
|
||||
|
||||
### OpenWrt 24.10.x
|
||||
|
||||
上传 `ipk` 安装包后执行:
|
||||
|
||||
```sh
|
||||
opkg install luci-app-pingpacket_v1.0.22_all.ipk
|
||||
```
|
||||
|
||||
### OpenWrt 25.12.x
|
||||
|
||||
上传 `apk` 安装包后执行:
|
||||
|
||||
```sh
|
||||
apk add --allow-untrusted ./luci-app-pingpacket_v1.0.22_all.apk
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 升级时会尽量保留已有 `/etc/config/pingpacket`
|
||||
- 若系统中存在历史遗留的 `/etc/config/pingpacket-opkg`,安装脚本会尽量兼容处理
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -0,0 +1,55 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI web interface for Pingpacket monitoring
|
||||
LUCI_DEPENDS:=+luci-base +luci-lua-runtime +luci-compat +curl
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_NAME:=luci-app-pingpacket
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=20
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller
|
||||
$(INSTALL_DATA) ./luasrc/controller/pingpacket.lua $(1)/usr/lib/lua/luci/controller/pingpacket.lua
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/view/pingpacket
|
||||
$(INSTALL_DATA) ./luasrc/view/pingpacket/status.htm $(1)/usr/lib/lua/luci/view/pingpacket/status.htm
|
||||
$(INSTALL_DATA) ./luasrc/view/pingpacket/logs.htm $(1)/usr/lib/lua/luci/view/pingpacket/logs.htm
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./root/etc/config/pingpacket $(1)/etc/config/pingpacket
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./root/etc/init.d/pingpacket $(1)/etc/init.d/pingpacket
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) ./root/usr/bin/pingpacket.sh $(1)/usr/bin/pingpacket.sh
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-pingpacket.json \
|
||||
$(1)/usr/share/rpcd/acl.d/luci-app-pingpacket.json
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
rm -f /tmp/luci-indexcache.*
|
||||
rm -rf /tmp/luci-modulecache/
|
||||
/etc/init.d/rpcd reload 2>/dev/null
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/prerm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
[ -f /etc/init.d/pingpacket ] && {
|
||||
/etc/init.d/pingpacket disable 2>/dev/null
|
||||
/etc/init.d/pingpacket stop 2>/dev/null
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,518 @@
|
||||
module("luci.controller.pingpacket", package.seeall)
|
||||
|
||||
local LOG_FILE = "/tmp/pingpacket.log"
|
||||
local CPU_STATE_FILE = "/tmp/pingpacket_cpu_state"
|
||||
local DEFAULT_PROXY_HOST = "127.0.0.1"
|
||||
local function trim(value)
|
||||
return (value or ""):gsub("%c", ""):match("^%s*(.-)%s*$") or ""
|
||||
end
|
||||
|
||||
local function normalize_enabled(value)
|
||||
return value == "1" and "1" or "0"
|
||||
end
|
||||
|
||||
local function normalize_proxy_type(value)
|
||||
return "socks5h"
|
||||
end
|
||||
|
||||
local function normalize_proxy_port(value)
|
||||
value = trim(value)
|
||||
|
||||
if value == "" then
|
||||
return ""
|
||||
end
|
||||
|
||||
if not value:match("^%d+$") then
|
||||
return nil
|
||||
end
|
||||
|
||||
local port = tonumber(value)
|
||||
if not port or port < 1 or port > 65535 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return tostring(port)
|
||||
end
|
||||
|
||||
local function normalize_proxy_host(value)
|
||||
value = trim(value)
|
||||
|
||||
if value ~= "" then
|
||||
return value
|
||||
end
|
||||
|
||||
return DEFAULT_PROXY_HOST
|
||||
end
|
||||
|
||||
local function normalize_probe_interval(value)
|
||||
value = trim(value)
|
||||
|
||||
if value == "" then
|
||||
return "1"
|
||||
end
|
||||
|
||||
if not value:match("^%d+$") then
|
||||
return nil
|
||||
end
|
||||
|
||||
local interval = tonumber(value)
|
||||
if not interval or interval < 1 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return tostring(interval)
|
||||
end
|
||||
|
||||
local function normalize_foreign_probe_interval(value)
|
||||
value = trim(value)
|
||||
|
||||
if value == "" then
|
||||
return "5"
|
||||
end
|
||||
|
||||
return normalize_probe_interval(value)
|
||||
end
|
||||
|
||||
local function write_json(payload)
|
||||
local http = require("luci.http")
|
||||
local jsonc = require("luci.jsonc")
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write(jsonc.stringify(payload))
|
||||
end
|
||||
|
||||
local function read_start_time()
|
||||
local start_time_file = io.open("/var/run/pingpacket/start_time", "r")
|
||||
if not start_time_file then
|
||||
return ""
|
||||
end
|
||||
|
||||
local value = trim(start_time_file:read("*a"))
|
||||
start_time_file:close()
|
||||
return value
|
||||
end
|
||||
|
||||
local function should_require_target(request_source, enabled)
|
||||
if request_source == "save" then
|
||||
return true
|
||||
end
|
||||
|
||||
return enabled == "1"
|
||||
end
|
||||
|
||||
local function has_any_target(uci)
|
||||
local domestic_target = trim(uci:get("pingpacket", "config", "domestic_target"))
|
||||
local foreign_target = trim(uci:get("pingpacket", "config", "foreign_target"))
|
||||
|
||||
return domestic_target ~= "" or foreign_target ~= ""
|
||||
end
|
||||
|
||||
local function has_valid_foreign_proxy(uci)
|
||||
local foreign_target = trim(uci:get("pingpacket", "config", "foreign_target"))
|
||||
local proxy_port = trim(uci:get("pingpacket", "config", "foreign_proxy_port"))
|
||||
|
||||
if foreign_target == "" then
|
||||
return true
|
||||
end
|
||||
|
||||
return proxy_port:match("^%d+$") and tonumber(proxy_port) and tonumber(proxy_port) >= 1 and tonumber(proxy_port) <= 65535
|
||||
end
|
||||
|
||||
local function read_file_content(path)
|
||||
local file = io.open(path, "r")
|
||||
if not file then
|
||||
return ""
|
||||
end
|
||||
|
||||
local content = file:read("*a") or ""
|
||||
file:close()
|
||||
return content
|
||||
end
|
||||
|
||||
local function read_log_payload()
|
||||
local nixio_fs = require("nixio.fs")
|
||||
local stat = nixio_fs.stat(LOG_FILE)
|
||||
|
||||
return {
|
||||
content = read_file_content(LOG_FILE),
|
||||
updated_at = (stat and stat.mtime) and os.date("%Y-%m-%d %H:%M:%S", stat.mtime) or "",
|
||||
size = (stat and stat.size) or 0
|
||||
}
|
||||
end
|
||||
|
||||
local function read_cpu_sample()
|
||||
local file = io.open("/proc/stat", "r")
|
||||
if not file then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local line = file:read("*l") or ""
|
||||
file:close()
|
||||
|
||||
local total = 0
|
||||
local idle = 0
|
||||
local index = 0
|
||||
|
||||
for token in line:gmatch("%S+") do
|
||||
if token ~= "cpu" then
|
||||
index = index + 1
|
||||
if index > 8 then
|
||||
break
|
||||
end
|
||||
|
||||
local value = tonumber(token) or 0
|
||||
total = total + value
|
||||
|
||||
if index == 4 or index == 5 then
|
||||
idle = idle + value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if total <= 0 then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
return total, idle
|
||||
end
|
||||
|
||||
local function read_saved_cpu_sample()
|
||||
local content = trim(read_file_content(CPU_STATE_FILE))
|
||||
local total, idle, timestamp = content:match("^(%d+)%s+(%d+)%s*(%d*)$")
|
||||
|
||||
if not total or not idle then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
timestamp = tonumber(timestamp)
|
||||
|
||||
return tonumber(total), tonumber(idle), timestamp
|
||||
end
|
||||
|
||||
local function write_cpu_sample(total, idle)
|
||||
local file = io.open(CPU_STATE_FILE, "w")
|
||||
if not file then
|
||||
return
|
||||
end
|
||||
|
||||
file:write(string.format("%d %d %d\n", total, idle, os.time()))
|
||||
file:close()
|
||||
end
|
||||
|
||||
local function format_percent(value)
|
||||
value = tonumber(value) or 0
|
||||
|
||||
if value < 0 then
|
||||
value = 0
|
||||
elseif value > 100 then
|
||||
value = 100
|
||||
end
|
||||
|
||||
return string.format("%.1f", value)
|
||||
end
|
||||
|
||||
local function read_memory_usage()
|
||||
local file = io.open("/proc/meminfo", "r")
|
||||
if not file then
|
||||
return "0.0"
|
||||
end
|
||||
|
||||
local mem_total
|
||||
local mem_available
|
||||
local mem_free = 0
|
||||
local buffers = 0
|
||||
local cached = 0
|
||||
|
||||
for line in file:lines() do
|
||||
local key, value = line:match("^(%w+):%s+(%d+)")
|
||||
value = tonumber(value)
|
||||
|
||||
if key == "MemTotal" then
|
||||
mem_total = value
|
||||
elseif key == "MemAvailable" then
|
||||
mem_available = value
|
||||
elseif key == "MemFree" then
|
||||
mem_free = value or 0
|
||||
elseif key == "Buffers" then
|
||||
buffers = value or 0
|
||||
elseif key == "Cached" then
|
||||
cached = value or 0
|
||||
end
|
||||
end
|
||||
|
||||
file:close()
|
||||
|
||||
if not mem_total or mem_total <= 0 then
|
||||
return "0.0"
|
||||
end
|
||||
|
||||
if not mem_available then
|
||||
mem_available = mem_free + buffers + cached
|
||||
end
|
||||
|
||||
return format_percent(((mem_total - mem_available) * 100) / mem_total)
|
||||
end
|
||||
|
||||
local function read_system_metrics()
|
||||
local current_total, current_idle = read_cpu_sample()
|
||||
local previous_total, previous_idle, previous_timestamp = read_saved_cpu_sample()
|
||||
local cpu_usage = "0.0"
|
||||
|
||||
if current_total and current_idle then
|
||||
if not previous_timestamp or (os.time() - previous_timestamp) > 10 then
|
||||
previous_total = nil
|
||||
previous_idle = nil
|
||||
end
|
||||
|
||||
if previous_total and previous_idle and current_total > previous_total then
|
||||
local total_delta = current_total - previous_total
|
||||
local idle_delta = current_idle - previous_idle
|
||||
|
||||
if total_delta > 0 then
|
||||
cpu_usage = format_percent(((total_delta - idle_delta) * 100) / total_delta)
|
||||
end
|
||||
end
|
||||
|
||||
write_cpu_sample(current_total, current_idle)
|
||||
end
|
||||
|
||||
return {
|
||||
cpu_usage = cpu_usage,
|
||||
memory_usage = read_memory_usage()
|
||||
}
|
||||
end
|
||||
|
||||
local function validate_config(request_source, enabled, domestic, foreign, proxy_port)
|
||||
if should_require_target(request_source, enabled) and domestic == "" and foreign == "" then
|
||||
return false, "请至少填写一个监控目标。"
|
||||
end
|
||||
|
||||
if foreign ~= "" and proxy_port == "" then
|
||||
return false, "已填写国外目标时,请同时填写代理端口。"
|
||||
end
|
||||
|
||||
return true, ""
|
||||
end
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/pingpacket") then
|
||||
return
|
||||
end
|
||||
|
||||
local page = entry(
|
||||
{"admin", "status", "pingpacket"},
|
||||
alias("admin", "status", "pingpacket", "monitor"),
|
||||
"Ping丢包监控",
|
||||
40
|
||||
)
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-pingpacket" }
|
||||
|
||||
entry({"admin", "status", "pingpacket", "monitor"}, template("pingpacket/status"), "监控", 1).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "logs"}, template("pingpacket/logs"), "日志", 2).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "status"}, alias("admin", "status", "pingpacket", "monitor")).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "get_data"}, call("action_get_data")).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "get_logs"}, call("action_get_logs")).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "clear_logs"}, call("action_clear_logs")).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "save_config"}, call("action_save_config")).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "restart_service"}, call("action_restart_service")).leaf = true
|
||||
end
|
||||
|
||||
function action_get_data()
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local sys = require("luci.sys")
|
||||
local jsonc = require("luci.jsonc")
|
||||
|
||||
local enabled = normalize_enabled(uci:get("pingpacket", "config", "enabled"))
|
||||
local domestic_target = trim(uci:get("pingpacket", "config", "domestic_target"))
|
||||
local foreign_target = trim(uci:get("pingpacket", "config", "foreign_target"))
|
||||
local foreign_proxy_type = normalize_proxy_type(uci:get("pingpacket", "config", "foreign_proxy_type"))
|
||||
local foreign_proxy_host = normalize_proxy_host(uci:get("pingpacket", "config", "foreign_proxy_host"))
|
||||
local foreign_proxy_port = trim(uci:get("pingpacket", "config", "foreign_proxy_port"))
|
||||
local domestic_interval = normalize_probe_interval(uci:get("pingpacket", "config", "domestic_interval")) or "1"
|
||||
local foreign_interval = normalize_foreign_probe_interval(uci:get("pingpacket", "config", "foreign_interval")) or "5"
|
||||
|
||||
local result = {
|
||||
enabled = enabled,
|
||||
domestic_target = domestic_target,
|
||||
foreign_target = foreign_target,
|
||||
foreign_proxy_type = foreign_proxy_type,
|
||||
foreign_proxy_host = foreign_proxy_host,
|
||||
foreign_proxy_port = foreign_proxy_port,
|
||||
domestic_interval = domestic_interval,
|
||||
foreign_interval = foreign_interval,
|
||||
server_time = os.date("%Y-%m-%d %H:%M:%S"),
|
||||
start_time = read_start_time(),
|
||||
updated_at = "",
|
||||
service_running = (sys.call("[ -s /var/run/pingpacket/start_time ]") == 0),
|
||||
system = read_system_metrics(),
|
||||
domestic = {
|
||||
avg = "0.0",
|
||||
min = "0.0",
|
||||
max = "0.0",
|
||||
loss_rate = "0.0",
|
||||
loss_count = 0,
|
||||
count = 0,
|
||||
samples = 0
|
||||
},
|
||||
foreign = {
|
||||
avg = "0.0",
|
||||
min = "0.0",
|
||||
max = "0.0",
|
||||
loss_rate = "0.0",
|
||||
loss_count = 0,
|
||||
count = 0,
|
||||
samples = 0
|
||||
}
|
||||
}
|
||||
|
||||
local status_file = io.open("/tmp/pingpacket_status.json", "r")
|
||||
if status_file then
|
||||
local content = status_file:read("*a")
|
||||
status_file:close()
|
||||
|
||||
local ok, data = pcall(jsonc.parse, content)
|
||||
if ok and data then
|
||||
if data.start_time then
|
||||
result.start_time = data.start_time
|
||||
end
|
||||
if data.updated_at then
|
||||
result.updated_at = data.updated_at
|
||||
end
|
||||
if data.domestic then
|
||||
result.domestic = data.domestic
|
||||
end
|
||||
if data.foreign then
|
||||
result.foreign = data.foreign
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
write_json(result)
|
||||
end
|
||||
|
||||
function action_get_logs()
|
||||
local payload = read_log_payload()
|
||||
payload.server_time = os.date("%Y-%m-%d %H:%M:%S")
|
||||
write_json(payload)
|
||||
end
|
||||
|
||||
function action_clear_logs()
|
||||
local nixio_fs = require("nixio.fs")
|
||||
nixio_fs.remove(LOG_FILE)
|
||||
|
||||
write_json({
|
||||
success = true,
|
||||
message = ""
|
||||
})
|
||||
end
|
||||
|
||||
function action_save_config()
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local sys = require("luci.sys")
|
||||
local http = require("luci.http")
|
||||
|
||||
local enabled = normalize_enabled(http.formvalue("enabled"))
|
||||
local domestic = trim(http.formvalue("domestic_target"))
|
||||
local foreign = trim(http.formvalue("foreign_target"))
|
||||
local proxy_type = normalize_proxy_type(http.formvalue("foreign_proxy_type"))
|
||||
local proxy_host = DEFAULT_PROXY_HOST
|
||||
local proxy_port = normalize_proxy_port(http.formvalue("foreign_proxy_port"))
|
||||
local domestic_interval = normalize_probe_interval(http.formvalue("domestic_interval"))
|
||||
local foreign_interval = normalize_foreign_probe_interval(http.formvalue("foreign_interval"))
|
||||
local request_source = trim(http.formvalue("request_source"))
|
||||
|
||||
if proxy_port == nil then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "代理端口必须是 1 到 65535 之间的数字。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
if domestic_interval == nil then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "国内 Ping间隔必须是大于 0 的整数秒。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
if foreign_interval == nil then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "国外 Ping间隔必须是大于 0 的整数秒。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local valid, message = validate_config(request_source, enabled, domestic, foreign, proxy_port)
|
||||
if not valid then
|
||||
write_json({
|
||||
success = false,
|
||||
message = message
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
uci:set("pingpacket", "config", "enabled", enabled)
|
||||
uci:set("pingpacket", "config", "domestic_target", domestic)
|
||||
uci:set("pingpacket", "config", "foreign_target", foreign)
|
||||
uci:set("pingpacket", "config", "foreign_proxy_type", proxy_type)
|
||||
uci:set("pingpacket", "config", "foreign_proxy_host", proxy_host)
|
||||
uci:set("pingpacket", "config", "foreign_proxy_port", proxy_port)
|
||||
uci:set("pingpacket", "config", "domestic_interval", domestic_interval)
|
||||
uci:set("pingpacket", "config", "foreign_interval", foreign_interval)
|
||||
uci:commit("pingpacket")
|
||||
|
||||
sys.call("rm -f /tmp/luci-indexcache*")
|
||||
|
||||
local rc
|
||||
if enabled == "1" then
|
||||
rc = sys.call("/bin/sh /etc/init.d/pingpacket restart >/dev/null 2>&1")
|
||||
else
|
||||
rc = sys.call("/bin/sh /etc/init.d/pingpacket stop >/dev/null 2>&1")
|
||||
end
|
||||
|
||||
write_json({
|
||||
success = (rc == 0),
|
||||
message = (rc == 0) and "" or "服务状态更新失败。"
|
||||
})
|
||||
end
|
||||
|
||||
function action_restart_service()
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local sys = require("luci.sys")
|
||||
|
||||
local enabled = normalize_enabled(uci:get("pingpacket", "config", "enabled"))
|
||||
|
||||
if enabled ~= "1" then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "当前监控未启用。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
if not has_any_target(uci) then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "请至少填写一个监控目标。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
if not has_valid_foreign_proxy(uci) then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "国外目标已填写,但代理端口无效。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local rc = sys.call("/bin/sh /etc/init.d/pingpacket restart >/dev/null 2>&1")
|
||||
write_json({
|
||||
success = (rc == 0),
|
||||
message = (rc == 0) and "" or "重新启动监控服务失败。"
|
||||
})
|
||||
end
|
||||
@@ -0,0 +1,477 @@
|
||||
<%+header%>
|
||||
|
||||
<style>
|
||||
.pp-log-page {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
padding: 8px 10px 20px;
|
||||
}
|
||||
|
||||
.pp-log-hero {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 24px 26px;
|
||||
margin: 12px 0 18px;
|
||||
border-radius: 18px;
|
||||
color: #fff;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(255, 255, 255, 0.2), transparent 34%),
|
||||
linear-gradient(135deg, #10615b, #0f766e 60%, #164e63);
|
||||
box-shadow: 0 18px 38px rgba(15, 118, 110, 0.18);
|
||||
}
|
||||
|
||||
.pp-log-copy {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.pp-log-kicker {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.pp-log-title {
|
||||
margin: 0;
|
||||
font-size: 30px;
|
||||
line-height: 1.15;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pp-log-subtitle {
|
||||
margin: 10px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.pp-log-summary {
|
||||
min-width: 250px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(6px);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.pp-log-summary-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.pp-log-summary-value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: "JetBrains Mono", "Consolas", "Menlo", monospace;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.pp-log-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.pp-log-meta,
|
||||
.pp-log-panel {
|
||||
background: linear-gradient(180deg, #ffffff, #fbfdff);
|
||||
border: 1px solid #dde8f6;
|
||||
box-shadow: 0 10px 28px rgba(27, 73, 124, 0.08);
|
||||
}
|
||||
|
||||
.pp-log-meta {
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.pp-log-meta-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #6d7d92;
|
||||
}
|
||||
|
||||
.pp-log-meta-value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #24364d;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: "JetBrains Mono", "Consolas", "Menlo", monospace;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.pp-log-panel {
|
||||
padding: 22px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.pp-log-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.pp-log-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pp-log-refresh {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #344a63;
|
||||
}
|
||||
|
||||
.pp-log-refresh input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pp-log-note {
|
||||
font-size: 13px;
|
||||
color: #6f8198;
|
||||
}
|
||||
|
||||
.pp-log-button {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #a34717;
|
||||
background: linear-gradient(135deg, #ffe0c7, #ffd2b0);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 24px rgba(240, 123, 45, 0.14);
|
||||
transition: transform 0.16s ease, box-shadow 0.16s ease, opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.pp-log-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 28px rgba(240, 123, 45, 0.18);
|
||||
}
|
||||
|
||||
.pp-log-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pp-log-message {
|
||||
min-height: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pp-log-message.success {
|
||||
color: #1f8f52;
|
||||
}
|
||||
|
||||
.pp-log-message.error {
|
||||
color: #d13d38;
|
||||
}
|
||||
|
||||
.pp-log-output {
|
||||
min-height: 420px;
|
||||
max-height: 620px;
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #dce7f5;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0)),
|
||||
#102132;
|
||||
color: #d5e5f6;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
font-family: "JetBrains Mono", "Consolas", "Menlo", monospace;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.pp-log-hero,
|
||||
.pp-log-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pp-log-summary {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.pp-log-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pp-log-page {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.pp-log-hero,
|
||||
.pp-log-panel {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="pp-log-page">
|
||||
<div class="pp-log-hero">
|
||||
<div class="pp-log-copy">
|
||||
<div class="pp-log-kicker">OpenWrt Status</div>
|
||||
<h2 class="pp-log-title">运行日志</h2>
|
||||
<p class="pp-log-subtitle">这里会持续记录监控服务的启动、停止、单次丢包、恢复事件,以及国外代理 curl 探测失败时的原因,便于快速排查最近一次异常发生的时间和内容。</p>
|
||||
</div>
|
||||
|
||||
<div class="pp-log-summary">
|
||||
<span class="pp-log-summary-label">日志说明</span>
|
||||
<div class="pp-log-summary-value">默认保留最近 500 行事件记录,适合排查服务重启、连续失败、单次丢包、代理配置问题和恢复过程。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pp-log-grid">
|
||||
<div class="pp-log-meta">
|
||||
<span class="pp-log-meta-label">最后更新时间</span>
|
||||
<div class="pp-log-meta-value" id="logUpdatedAt">--</div>
|
||||
</div>
|
||||
<div class="pp-log-meta">
|
||||
<span class="pp-log-meta-label">日志行数</span>
|
||||
<div class="pp-log-meta-value" id="logLineCount">0</div>
|
||||
</div>
|
||||
<div class="pp-log-meta">
|
||||
<span class="pp-log-meta-label">日志大小</span>
|
||||
<div class="pp-log-meta-value" id="logSize">0 B</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pp-log-panel">
|
||||
<div class="pp-log-toolbar">
|
||||
<div class="pp-log-tools">
|
||||
<label class="pp-log-refresh">
|
||||
<input type="checkbox" id="autoRefresh" checked>
|
||||
<span>自动刷新</span>
|
||||
</label>
|
||||
<button class="pp-log-button" id="clearLogsButton" type="button">清除日志</button>
|
||||
<span class="pp-log-message" id="logMessage"></span>
|
||||
</div>
|
||||
<div class="pp-log-note">取消勾选后会停止自动轮询,当前页面内容会保持不变。</div>
|
||||
</div>
|
||||
|
||||
<pre class="pp-log-output" id="logContent">正在加载日志...</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var apiBase = '<%=luci.dispatcher.build_url("admin", "status", "pingpacket")%>';
|
||||
var token = (window.L && L.env && L.env.token) ? L.env.token : '<%=token%>';
|
||||
var logsUrl = apiBase + '/get_logs';
|
||||
var clearUrl = apiBase + '/clear_logs';
|
||||
var refreshTimer = null;
|
||||
var fetchInFlight = false;
|
||||
var clearInFlight = false;
|
||||
var refreshInterval = 2000;
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function setMessage(text, type) {
|
||||
var node = $('logMessage');
|
||||
node.textContent = text || '';
|
||||
node.className = type ? 'pp-log-message ' + type : 'pp-log-message';
|
||||
}
|
||||
|
||||
function scheduleRefresh(delay) {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
}
|
||||
|
||||
if (!$('autoRefresh').checked) {
|
||||
refreshTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
refreshTimer = setTimeout(fetchLogs, typeof delay === 'number' ? delay : refreshInterval);
|
||||
}
|
||||
|
||||
function formatBytes(size) {
|
||||
var value = Number(size) || 0;
|
||||
if (value < 1024) {
|
||||
return value + ' B';
|
||||
}
|
||||
if (value < 1024 * 1024) {
|
||||
return (value / 1024).toFixed(1) + ' KB';
|
||||
}
|
||||
return (value / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function countLines(content) {
|
||||
var normalized;
|
||||
|
||||
if (!content) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
normalized = content.replace(/\n+$/, '');
|
||||
if (!normalized) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return normalized.split('\n').length;
|
||||
}
|
||||
|
||||
function updateView(payload) {
|
||||
var content = payload.content || '';
|
||||
var output = $('logContent');
|
||||
|
||||
output.textContent = content || '当前暂无日志记录。';
|
||||
$('logUpdatedAt').textContent = payload.updated_at || '--';
|
||||
$('logLineCount').textContent = String(countLines(content));
|
||||
$('logSize').textContent = formatBytes(payload.size || 0);
|
||||
if ($('autoRefresh').checked) {
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchLogs() {
|
||||
var xhr;
|
||||
|
||||
if (fetchInFlight || clearInFlight) {
|
||||
scheduleRefresh(refreshInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchInFlight = true;
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', logsUrl, true);
|
||||
xhr.onload = function() {
|
||||
var response = null;
|
||||
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
response = JSON.parse(xhr.responseText);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (response) {
|
||||
updateView(response);
|
||||
} else {
|
||||
setMessage('日志读取失败,请稍后重试。', 'error');
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
setMessage('日志读取失败,请稍后重试。', 'error');
|
||||
};
|
||||
xhr.onloadend = function() {
|
||||
fetchInFlight = false;
|
||||
scheduleRefresh(refreshInterval);
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
var xhr;
|
||||
|
||||
if (fetchInFlight || clearInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInFlight = true;
|
||||
$('clearLogsButton').disabled = true;
|
||||
setMessage('正在清除日志...', '');
|
||||
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', clearUrl, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = function() {
|
||||
var response = null;
|
||||
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
response = JSON.parse(xhr.responseText);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (response && response.success) {
|
||||
updateView({
|
||||
content: '',
|
||||
updated_at: '',
|
||||
size: 0
|
||||
});
|
||||
setMessage('日志已清除。', 'success');
|
||||
} else {
|
||||
setMessage('清除日志失败,请稍后重试。', 'error');
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
setMessage('清除日志失败,请稍后重试。', 'error');
|
||||
};
|
||||
xhr.onloadend = function() {
|
||||
clearInFlight = false;
|
||||
$('clearLogsButton').disabled = false;
|
||||
if ($('autoRefresh').checked) {
|
||||
scheduleRefresh(refreshInterval);
|
||||
}
|
||||
};
|
||||
xhr.send('token=' + encodeURIComponent(token));
|
||||
}
|
||||
|
||||
$('autoRefresh').addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
setMessage('已恢复自动刷新。', 'success');
|
||||
fetchLogs();
|
||||
return;
|
||||
}
|
||||
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
|
||||
setMessage('已暂停自动刷新。', '');
|
||||
});
|
||||
|
||||
$('clearLogsButton').addEventListener('click', clearLogs);
|
||||
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (!document.hidden && $('autoRefresh').checked) {
|
||||
fetchLogs();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('focus', function() {
|
||||
if ($('autoRefresh').checked) {
|
||||
fetchLogs();
|
||||
}
|
||||
});
|
||||
|
||||
fetchLogs();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<%+footer%>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,81 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
msgid "Ping Packet Loss Monitor"
|
||||
msgstr "Ping丢包监控"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "已关闭"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "已启用"
|
||||
|
||||
msgid "Start Time"
|
||||
msgstr "开始时间"
|
||||
|
||||
msgid "Duration"
|
||||
msgstr "运行时长"
|
||||
|
||||
msgid "Domestic"
|
||||
msgstr "国内"
|
||||
|
||||
msgid "Foreign"
|
||||
msgstr "国外"
|
||||
|
||||
msgid "Average"
|
||||
msgstr "平均延迟"
|
||||
|
||||
msgid "Minimum"
|
||||
msgstr "最低延迟"
|
||||
|
||||
msgid "Maximum"
|
||||
msgstr "最高延迟"
|
||||
|
||||
msgid "Packet Loss"
|
||||
msgstr "丢包率"
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "未配置"
|
||||
|
||||
msgid "Monitoring Settings"
|
||||
msgstr "监控设置"
|
||||
|
||||
msgid "Ping Domestic IP or Domain"
|
||||
msgstr "Ping 国内 IP 或域名"
|
||||
|
||||
msgid "e.g. 114.114.114.114 or baidu.com"
|
||||
msgstr "例如:114.114.114.114 或 baidu.com"
|
||||
|
||||
msgid "Leave empty to disable domestic monitoring."
|
||||
msgstr "留空则不启用国内监控。"
|
||||
|
||||
msgid "Ping Foreign IP or Domain"
|
||||
msgstr "Ping 国外 IP 或域名"
|
||||
|
||||
msgid "e.g. 8.8.8.8 or google.com"
|
||||
msgstr "例如:8.8.8.8 或 google.com"
|
||||
|
||||
msgid "Leave empty to disable foreign monitoring."
|
||||
msgstr "留空则不启用国外监控。"
|
||||
|
||||
msgid "Save & Apply"
|
||||
msgstr "保存并应用"
|
||||
|
||||
msgid "Saved successfully."
|
||||
msgstr "保存成功。"
|
||||
|
||||
msgid "Save failed."
|
||||
msgstr "保存失败。"
|
||||
|
||||
msgid "Ping Packet Loss"
|
||||
msgstr "Ping丢包监控"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
msgid "Please configure at least one target."
|
||||
msgstr "请至少配置一个目标。"
|
||||
|
||||
msgid "Failed to update service state."
|
||||
msgstr "服务状态更新失败。"
|
||||
@@ -0,0 +1,9 @@
|
||||
config pingpacket 'config'
|
||||
option enabled '0'
|
||||
option domestic_target 'www.baidu.com'
|
||||
option foreign_target 'https://www.google.com/generate_204'
|
||||
option foreign_proxy_type 'socks5h'
|
||||
option foreign_proxy_host '127.0.0.1'
|
||||
option foreign_proxy_port '7891'
|
||||
option domestic_interval '1'
|
||||
option foreign_interval '5'
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=90
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/bin/pingpacket.sh
|
||||
RUN_DIR=/var/run/pingpacket
|
||||
STATUS_FILE=/tmp/pingpacket_status.json
|
||||
CONFIG_FILE=$RUN_DIR/config
|
||||
LOG_FILE=/tmp/pingpacket.log
|
||||
LOG_MAX_LINES=500
|
||||
|
||||
log_rotate() {
|
||||
[ -f "$LOG_FILE" ] || return 0
|
||||
tail -n "$LOG_MAX_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp" 2>/dev/null && \
|
||||
mv -f "${LOG_FILE}.tmp" "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_event() {
|
||||
local event_type="$1"
|
||||
local message="$2"
|
||||
local timestamp
|
||||
|
||||
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
printf '[%s] [%s] %s\n' "$timestamp" "$event_type" "$message" >> "$LOG_FILE"
|
||||
log_rotate
|
||||
}
|
||||
|
||||
format_foreign_summary() {
|
||||
local target="$1"
|
||||
local proxy_type="$2"
|
||||
local proxy_host="$3"
|
||||
local proxy_port="$4"
|
||||
|
||||
[ -n "$target" ] || {
|
||||
echo "未配置"
|
||||
return
|
||||
}
|
||||
|
||||
if [ -n "$proxy_port" ]; then
|
||||
echo "${target}(代理 ${proxy_type} 本机端口 ${proxy_port})"
|
||||
else
|
||||
echo "${target}(代理端口未配置)"
|
||||
fi
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled domestic foreign proxy_type proxy_host proxy_port domestic_interval foreign_interval foreign_summary
|
||||
|
||||
config_load pingpacket
|
||||
config_get enabled config enabled "0"
|
||||
config_get domestic config domestic_target ""
|
||||
config_get foreign config foreign_target ""
|
||||
config_get proxy_type config foreign_proxy_type "socks5h"
|
||||
config_get proxy_host config foreign_proxy_host "127.0.0.1"
|
||||
config_get proxy_port config foreign_proxy_port "7891"
|
||||
config_get domestic_interval config domestic_interval "1"
|
||||
config_get foreign_interval config foreign_interval "5"
|
||||
proxy_type="socks5h"
|
||||
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
[ -n "$domestic" ] || [ -n "$foreign" ] || return 0
|
||||
|
||||
rm -rf "$RUN_DIR"
|
||||
rm -f "$STATUS_FILE"
|
||||
mkdir -p "$RUN_DIR"
|
||||
printf '%s\n' "$domestic" > "$CONFIG_FILE"
|
||||
printf '%s\n' "$foreign" >> "$CONFIG_FILE"
|
||||
printf '%s\n' "$proxy_type" >> "$CONFIG_FILE"
|
||||
printf '%s\n' "${proxy_host:-127.0.0.1}" >> "$CONFIG_FILE"
|
||||
printf '%s\n' "$proxy_port" >> "$CONFIG_FILE"
|
||||
printf '%s\n' "$domestic_interval" >> "$CONFIG_FILE"
|
||||
printf '%s\n' "$foreign_interval" >> "$CONFIG_FILE"
|
||||
|
||||
foreign_summary="$(format_foreign_summary "$foreign" "$proxy_type" "$proxy_host" "$proxy_port")"
|
||||
log_event "启动" "监控服务已启动,国内目标:${domestic:-未配置},国外目标:${foreign_summary}"
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command /bin/sh "$PROG"
|
||||
procd_set_param respawn 3600 10 5
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
if [ -d "$RUN_DIR" ] || [ -f "$STATUS_FILE" ]; then
|
||||
log_event "停止" "监控服务已停止。"
|
||||
fi
|
||||
|
||||
rm -rf "$RUN_DIR"
|
||||
rm -f "$STATUS_FILE"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "pingpacket"
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
#!/bin/sh
|
||||
|
||||
STATUS_FILE="/tmp/pingpacket_status.json"
|
||||
STATUS_TMP="${STATUS_FILE}.tmp"
|
||||
LOG_FILE="/tmp/pingpacket.log"
|
||||
LOG_MAX_LINES=500
|
||||
RUN_DIR="/var/run/pingpacket"
|
||||
START_TIME_FILE="$RUN_DIR/start_time"
|
||||
child_pids=""
|
||||
domestic_fail_count=0
|
||||
foreign_fail_count=0
|
||||
domestic_fault_open=0
|
||||
foreign_fault_open=0
|
||||
domestic_total_samples=0
|
||||
foreign_total_samples=0
|
||||
domestic_success_count=0
|
||||
foreign_success_count=0
|
||||
domestic_loss_count=0
|
||||
foreign_loss_count=0
|
||||
domestic_rtt_sum="0"
|
||||
foreign_rtt_sum="0"
|
||||
domestic_min_rtt=""
|
||||
foreign_min_rtt=""
|
||||
domestic_max_rtt=""
|
||||
foreign_max_rtt=""
|
||||
domestic_last_probe_at=0
|
||||
foreign_last_probe_at=0
|
||||
domestic_last_fault_probe_at=0
|
||||
foreign_last_fault_probe_at=0
|
||||
domestic_last_stats_probe_at=0
|
||||
foreign_last_stats_probe_at=0
|
||||
|
||||
log_rotate() {
|
||||
[ -f "$LOG_FILE" ] || return 0
|
||||
tail -n "$LOG_MAX_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp" 2>/dev/null && \
|
||||
mv -f "${LOG_FILE}.tmp" "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_event() {
|
||||
local event_type="$1"
|
||||
local scope="$2"
|
||||
local message="$3"
|
||||
local timestamp
|
||||
|
||||
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
if [ -n "$scope" ]; then
|
||||
printf '[%s] [%s] [%s] %s\n' "$timestamp" "$event_type" "$scope" "$message" >> "$LOG_FILE"
|
||||
else
|
||||
printf '[%s] [%s] %s\n' "$timestamp" "$event_type" "$message" >> "$LOG_FILE"
|
||||
fi
|
||||
log_rotate
|
||||
}
|
||||
|
||||
kill_children() {
|
||||
[ -n "$child_pids" ] || return 0
|
||||
kill $child_pids 2>/dev/null
|
||||
wait $child_pids 2>/dev/null
|
||||
child_pids=""
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
kill_children
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap cleanup INT TERM
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
date '+%Y-%m-%d %H:%M:%S' > "$START_TIME_FILE"
|
||||
|
||||
write_result_file() {
|
||||
local path="$1"
|
||||
local success="$2"
|
||||
local rtt="$3"
|
||||
local detail="$4"
|
||||
local probe_at="$5"
|
||||
|
||||
[ -n "$probe_at" ] || probe_at="$(date '+%s' 2>/dev/null || echo 0)"
|
||||
|
||||
mkdir -p "$(dirname "$path")"
|
||||
printf '%s\n%s\n%s\n%s\n' "$success" "$rtt" "$detail" "$probe_at" > "$path"
|
||||
}
|
||||
|
||||
normalize_foreign_url() {
|
||||
case "$1" in
|
||||
http://*|https://*)
|
||||
printf '%s\n' "$1"
|
||||
;;
|
||||
*)
|
||||
printf 'https://%s\n' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_runtime_state() {
|
||||
local domestic
|
||||
local foreign
|
||||
local proxy_type
|
||||
local proxy_host
|
||||
local proxy_port
|
||||
local domestic_interval
|
||||
local foreign_interval
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
if [ ! -s "$START_TIME_FILE" ]; then
|
||||
date '+%Y-%m-%d %H:%M:%S' > "$START_TIME_FILE"
|
||||
fi
|
||||
|
||||
if [ -f "$RUN_DIR/config" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! command -v uci >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
domestic="$(uci -q get pingpacket.config.domestic_target)"
|
||||
foreign="$(uci -q get pingpacket.config.foreign_target)"
|
||||
proxy_type="$(uci -q get pingpacket.config.foreign_proxy_type)"
|
||||
proxy_host="$(uci -q get pingpacket.config.foreign_proxy_host)"
|
||||
proxy_port="$(uci -q get pingpacket.config.foreign_proxy_port)"
|
||||
domestic_interval="$(uci -q get pingpacket.config.domestic_interval)"
|
||||
foreign_interval="$(uci -q get pingpacket.config.foreign_interval)"
|
||||
|
||||
proxy_type="socks5h"
|
||||
[ -n "$proxy_host" ] || proxy_host="127.0.0.1"
|
||||
[ -n "$proxy_port" ] || proxy_port="7891"
|
||||
[ -n "$domestic_interval" ] || domestic_interval="1"
|
||||
[ -n "$foreign_interval" ] || foreign_interval="5"
|
||||
|
||||
printf '%s\n' "$domestic" > "$RUN_DIR/config"
|
||||
printf '%s\n' "$foreign" >> "$RUN_DIR/config"
|
||||
printf '%s\n' "$proxy_type" >> "$RUN_DIR/config"
|
||||
printf '%s\n' "$proxy_host" >> "$RUN_DIR/config"
|
||||
printf '%s\n' "$proxy_port" >> "$RUN_DIR/config"
|
||||
printf '%s\n' "$domestic_interval" >> "$RUN_DIR/config"
|
||||
printf '%s\n' "$foreign_interval" >> "$RUN_DIR/config"
|
||||
}
|
||||
|
||||
normalize_interval() {
|
||||
local value="$1"
|
||||
|
||||
case "$value" in
|
||||
""|*[!0-9]*)
|
||||
printf '1\n'
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$value" -ge 1 ] 2>/dev/null; then
|
||||
printf '%s\n' "$value"
|
||||
else
|
||||
printf '1\n'
|
||||
fi
|
||||
}
|
||||
|
||||
should_probe_now() {
|
||||
local last_probe_at="${1:-0}"
|
||||
local interval="${2:-1}"
|
||||
local now_ts="${3:-0}"
|
||||
|
||||
if [ "$last_probe_at" -le 0 ] 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ $((now_ts - last_probe_at)) -ge "$interval" ] 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_ip_literal() {
|
||||
case "$1" in
|
||||
*:*|*.*)
|
||||
printf '%s\n' "$1" | awk '
|
||||
/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { found=1 }
|
||||
/^[0-9A-Fa-f:]+$/ { found=1 }
|
||||
END { exit(found ? 0 : 1) }
|
||||
'
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_target_ip() {
|
||||
local target="$1"
|
||||
local name="$2"
|
||||
local cache_target_file="$RUN_DIR/${name}_resolved_target"
|
||||
local cache_ip_file="$RUN_DIR/${name}_resolved_ip"
|
||||
local cache_time_file="$RUN_DIR/${name}_resolved_at"
|
||||
local cached_target
|
||||
local cached_ip
|
||||
local cached_time
|
||||
local now
|
||||
local resolved_ip=""
|
||||
|
||||
if is_ip_literal "$target"; then
|
||||
printf '%s\n' "$target"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cached_target="$(cat "$cache_target_file" 2>/dev/null || echo "")"
|
||||
cached_ip="$(cat "$cache_ip_file" 2>/dev/null || echo "")"
|
||||
cached_time="$(cat "$cache_time_file" 2>/dev/null || echo "")"
|
||||
now="$(date '+%s' 2>/dev/null || echo 0)"
|
||||
|
||||
if [ "$cached_target" = "$target" ] && [ -n "$cached_ip" ] && [ -n "$cached_time" ]; then
|
||||
if [ $((now - cached_time)) -lt 300 ] 2>/dev/null; then
|
||||
printf '%s\n' "$cached_ip"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v resolveip >/dev/null 2>&1; then
|
||||
resolved_ip="$(
|
||||
resolveip -4 -t 3 "$target" 2>/dev/null | awk '
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
|
||||
print $i
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
)"
|
||||
fi
|
||||
|
||||
if [ -z "$resolved_ip" ] && command -v nslookup >/dev/null 2>&1; then
|
||||
resolved_ip="$(
|
||||
nslookup "$target" 2>/dev/null | awk '
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
|
||||
print $i
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
)"
|
||||
fi
|
||||
|
||||
if [ -n "$resolved_ip" ]; then
|
||||
printf '%s\n' "$target" > "$cache_target_file"
|
||||
printf '%s\n' "$resolved_ip" > "$cache_ip_file"
|
||||
printf '%s\n' "$now" > "$cache_time_file"
|
||||
printf '%s\n' "$resolved_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$cached_target" = "$target" ] && [ -n "$cached_ip" ]; then
|
||||
printf '%s\n' "$cached_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "$target"
|
||||
}
|
||||
|
||||
do_ping() {
|
||||
local target="$1"
|
||||
local name="$2"
|
||||
local result_file="$RUN_DIR/${name}_last_result"
|
||||
local ping_target
|
||||
local result
|
||||
local rtt
|
||||
|
||||
ping_target="$(resolve_target_ip "$target" "$name")"
|
||||
result="$(ping -n -c 1 -W 2 "$ping_target" 2>/dev/null)"
|
||||
if echo "$result" | grep -q "bytes from"; then
|
||||
rtt="$(echo "$result" | sed -n 's/.*time=\([0-9.]*\).*/\1/p')"
|
||||
write_result_file "$result_file" "1" "${rtt:-0}" ""
|
||||
else
|
||||
write_result_file "$result_file" "0" "" "ping 无响应"
|
||||
fi
|
||||
}
|
||||
|
||||
do_proxy_curl() {
|
||||
local target="$1"
|
||||
local proxy_type="$2"
|
||||
local proxy_host="${3:-127.0.0.1}"
|
||||
local proxy_port="$4"
|
||||
local name="$5"
|
||||
local result_file="$RUN_DIR/${name}_last_result"
|
||||
local error_file="$RUN_DIR/${name}_curl_error"
|
||||
local url
|
||||
local proxy_addr
|
||||
local curl_result
|
||||
local http_code
|
||||
local time_pretransfer
|
||||
local rtt_ms
|
||||
local detail
|
||||
local rc
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
write_result_file "$result_file" "0" "" "系统未安装 curl"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "$proxy_port" ]; then
|
||||
write_result_file "$result_file" "0" "" "代理端口未配置"
|
||||
return 0
|
||||
fi
|
||||
|
||||
url="$(normalize_foreign_url "$target")"
|
||||
proxy_addr="${proxy_host}:${proxy_port}"
|
||||
|
||||
curl_result="$(
|
||||
curl -k -I -L -sS --noproxy "" \
|
||||
--connect-timeout 5 --max-time 10 \
|
||||
--socks5-hostname "$proxy_addr" \
|
||||
-o /dev/null -w '%{http_code}:%{time_pretransfer}' \
|
||||
"$url" 2>"$error_file"
|
||||
)"
|
||||
rc=$?
|
||||
|
||||
http_code="$(printf '%s' "$curl_result" | awk -F ':' 'NR==1 { print $1 }')"
|
||||
time_pretransfer="$(printf '%s' "$curl_result" | awk -F ':' 'NR==1 { print $2 }')"
|
||||
|
||||
if [ "$rc" -eq 0 ] && [ -n "$http_code" ] && [ -n "$time_pretransfer" ] && { [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; }; then
|
||||
rtt_ms="$(awk -v sec="$time_pretransfer" 'BEGIN { printf "%.1f", (sec + 0) * 1000 }')"
|
||||
write_result_file "$result_file" "1" "$rtt_ms" "curl 连接探测成功,HTTP ${http_code}"
|
||||
else
|
||||
detail="$(sed -n '1p' "$error_file" 2>/dev/null)"
|
||||
[ -n "$detail" ] || detail="curl 退出码 ${rc},HTTP ${http_code:-000}"
|
||||
write_result_file "$result_file" "0" "" "$detail"
|
||||
fi
|
||||
|
||||
rm -f "$error_file"
|
||||
}
|
||||
|
||||
get_target_state() {
|
||||
local name="$1"
|
||||
|
||||
case "$name" in
|
||||
domestic)
|
||||
fail_count="$domestic_fail_count"
|
||||
fault_open="$domestic_fault_open"
|
||||
;;
|
||||
foreign)
|
||||
fail_count="$foreign_fail_count"
|
||||
fault_open="$foreign_fault_open"
|
||||
;;
|
||||
*)
|
||||
fail_count=0
|
||||
fault_open=0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
set_target_state() {
|
||||
local name="$1"
|
||||
local next_fail_count="$2"
|
||||
local next_fault_open="$3"
|
||||
|
||||
case "$name" in
|
||||
domestic)
|
||||
domestic_fail_count="$next_fail_count"
|
||||
domestic_fault_open="$next_fault_open"
|
||||
;;
|
||||
foreign)
|
||||
foreign_fail_count="$next_fail_count"
|
||||
foreign_fault_open="$next_fault_open"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_processed_probe_at() {
|
||||
local name="$1"
|
||||
local kind="$2"
|
||||
|
||||
case "${name}:${kind}" in
|
||||
domestic:fault)
|
||||
processed_probe_at="$domestic_last_fault_probe_at"
|
||||
;;
|
||||
foreign:fault)
|
||||
processed_probe_at="$foreign_last_fault_probe_at"
|
||||
;;
|
||||
domestic:stats)
|
||||
processed_probe_at="$domestic_last_stats_probe_at"
|
||||
;;
|
||||
foreign:stats)
|
||||
processed_probe_at="$foreign_last_stats_probe_at"
|
||||
;;
|
||||
*)
|
||||
processed_probe_at=0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
set_processed_probe_at() {
|
||||
local name="$1"
|
||||
local kind="$2"
|
||||
local value="${3:-0}"
|
||||
|
||||
case "${name}:${kind}" in
|
||||
domestic:fault)
|
||||
domestic_last_fault_probe_at="$value"
|
||||
;;
|
||||
foreign:fault)
|
||||
foreign_last_fault_probe_at="$value"
|
||||
;;
|
||||
domestic:stats)
|
||||
domestic_last_stats_probe_at="$value"
|
||||
;;
|
||||
foreign:stats)
|
||||
foreign_last_stats_probe_at="$value"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
reset_target_state() {
|
||||
local name="$1"
|
||||
|
||||
rm -f "$RUN_DIR/${name}_last_result"
|
||||
set_target_state "$name" 0 0
|
||||
set_processed_probe_at "$name" "fault" 0
|
||||
set_processed_probe_at "$name" "stats" 0
|
||||
}
|
||||
|
||||
update_target_faults() {
|
||||
local name="$1"
|
||||
local label="$2"
|
||||
local target="$3"
|
||||
local result_file="$RUN_DIR/${name}_last_result"
|
||||
local success="0"
|
||||
local rtt=""
|
||||
local detail=""
|
||||
local probe_at=""
|
||||
|
||||
[ -n "$target" ] || {
|
||||
reset_target_state "$name"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_target_state "$name"
|
||||
|
||||
if [ -f "$result_file" ]; then
|
||||
success="$(sed -n '1p' "$result_file" 2>/dev/null)"
|
||||
rtt="$(sed -n '2p' "$result_file" 2>/dev/null)"
|
||||
detail="$(sed -n '3p' "$result_file" 2>/dev/null)"
|
||||
probe_at="$(sed -n '4p' "$result_file" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
get_processed_probe_at "$name" "fault"
|
||||
if [ -n "$probe_at" ] && [ "$probe_at" = "$processed_probe_at" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$success" = "1" ]; then
|
||||
if [ "$fault_open" = "1" ]; then
|
||||
log_event "恢复" "$label" "目标 ${target} 已恢复响应,当前延迟 ${rtt:-0} ms"
|
||||
fi
|
||||
|
||||
set_target_state "$name" 0 0
|
||||
set_processed_probe_at "$name" "fault" "${probe_at:-0}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
fail_count=$((fail_count + 1))
|
||||
if [ -n "$detail" ]; then
|
||||
log_event "丢包" "$label" "目标 ${target} 本次探测失败(连续第 ${fail_count} 次):${detail}"
|
||||
else
|
||||
log_event "丢包" "$label" "目标 ${target} 本次探测失败(连续第 ${fail_count} 次)"
|
||||
fi
|
||||
fault_open=1
|
||||
|
||||
set_target_state "$name" "$fail_count" "$fault_open"
|
||||
set_processed_probe_at "$name" "fault" "${probe_at:-0}"
|
||||
}
|
||||
|
||||
calc_stats() {
|
||||
local name="$1"
|
||||
local total_samples=0
|
||||
local success_count=0
|
||||
local loss_count=0
|
||||
local rtt_sum="0"
|
||||
local min_rtt=""
|
||||
local max_rtt=""
|
||||
local avg="0.0"
|
||||
local min="0.0"
|
||||
local max="0.0"
|
||||
local loss_rate="0.0"
|
||||
|
||||
case "$name" in
|
||||
domestic)
|
||||
total_samples="$domestic_total_samples"
|
||||
success_count="$domestic_success_count"
|
||||
loss_count="$domestic_loss_count"
|
||||
rtt_sum="$domestic_rtt_sum"
|
||||
min_rtt="$domestic_min_rtt"
|
||||
max_rtt="$domestic_max_rtt"
|
||||
;;
|
||||
foreign)
|
||||
total_samples="$foreign_total_samples"
|
||||
success_count="$foreign_success_count"
|
||||
loss_count="$foreign_loss_count"
|
||||
rtt_sum="$foreign_rtt_sum"
|
||||
min_rtt="$foreign_min_rtt"
|
||||
max_rtt="$foreign_max_rtt"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$success_count" -gt 0 ]; then
|
||||
avg="$(awk -v sum="$rtt_sum" -v count="$success_count" 'BEGIN { printf "%.1f", (sum + 0) / count }')"
|
||||
min="$(awk -v value="${min_rtt:-0}" 'BEGIN { printf "%.1f", value + 0 }')"
|
||||
max="$(awk -v value="${max_rtt:-0}" 'BEGIN { printf "%.1f", value + 0 }')"
|
||||
fi
|
||||
|
||||
if [ "$total_samples" -gt 0 ]; then
|
||||
loss_rate="$(awk -v loss="$loss_count" -v total="$total_samples" 'BEGIN { printf "%.1f", loss * 100.0 / total }')"
|
||||
fi
|
||||
|
||||
printf '{"avg":"%s","min":"%s","max":"%s","loss_rate":"%s","loss_count":%d,"count":%d,"samples":%d}' \
|
||||
"$avg" "$min" "$max" "$loss_rate" "$loss_count" "$success_count" "$total_samples"
|
||||
}
|
||||
|
||||
update_cumulative_stats() {
|
||||
local name="$1"
|
||||
local target="$2"
|
||||
local result_file="$RUN_DIR/${name}_last_result"
|
||||
local success="0"
|
||||
local rtt=""
|
||||
local probe_at=""
|
||||
local total_samples=0
|
||||
local success_count=0
|
||||
local loss_count=0
|
||||
local rtt_sum="0"
|
||||
local min_rtt=""
|
||||
local max_rtt=""
|
||||
|
||||
[ -n "$target" ] || return 0
|
||||
|
||||
case "$name" in
|
||||
domestic)
|
||||
total_samples="$domestic_total_samples"
|
||||
success_count="$domestic_success_count"
|
||||
loss_count="$domestic_loss_count"
|
||||
rtt_sum="$domestic_rtt_sum"
|
||||
min_rtt="$domestic_min_rtt"
|
||||
max_rtt="$domestic_max_rtt"
|
||||
;;
|
||||
foreign)
|
||||
total_samples="$foreign_total_samples"
|
||||
success_count="$foreign_success_count"
|
||||
loss_count="$foreign_loss_count"
|
||||
rtt_sum="$foreign_rtt_sum"
|
||||
min_rtt="$foreign_min_rtt"
|
||||
max_rtt="$foreign_max_rtt"
|
||||
;;
|
||||
esac
|
||||
|
||||
total_samples=$((total_samples + 1))
|
||||
|
||||
if [ -f "$result_file" ]; then
|
||||
success="$(sed -n '1p' "$result_file" 2>/dev/null)"
|
||||
rtt="$(sed -n '2p' "$result_file" 2>/dev/null)"
|
||||
probe_at="$(sed -n '4p' "$result_file" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
get_processed_probe_at "$name" "stats"
|
||||
if [ -n "$probe_at" ] && [ "$probe_at" = "$processed_probe_at" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$success" = "1" ]; then
|
||||
rtt="${rtt:-0}"
|
||||
success_count=$((success_count + 1))
|
||||
rtt_sum="$(awk -v sum="$rtt_sum" -v value="$rtt" 'BEGIN { printf "%.3f", (sum + 0) + (value + 0) }')"
|
||||
|
||||
if [ -z "$min_rtt" ]; then
|
||||
min_rtt="$rtt"
|
||||
else
|
||||
min_rtt="$(awk -v current="$min_rtt" -v value="$rtt" 'BEGIN { printf "%.3f", ((value + 0) < (current + 0)) ? (value + 0) : (current + 0) }')"
|
||||
fi
|
||||
|
||||
if [ -z "$max_rtt" ]; then
|
||||
max_rtt="$rtt"
|
||||
else
|
||||
max_rtt="$(awk -v current="$max_rtt" -v value="$rtt" 'BEGIN { printf "%.3f", ((value + 0) > (current + 0)) ? (value + 0) : (current + 0) }')"
|
||||
fi
|
||||
else
|
||||
loss_count=$((loss_count + 1))
|
||||
fi
|
||||
|
||||
case "$name" in
|
||||
domestic)
|
||||
domestic_total_samples="$total_samples"
|
||||
domestic_success_count="$success_count"
|
||||
domestic_loss_count="$loss_count"
|
||||
domestic_rtt_sum="$rtt_sum"
|
||||
domestic_min_rtt="$min_rtt"
|
||||
domestic_max_rtt="$max_rtt"
|
||||
;;
|
||||
foreign)
|
||||
foreign_total_samples="$total_samples"
|
||||
foreign_success_count="$success_count"
|
||||
foreign_loss_count="$loss_count"
|
||||
foreign_rtt_sum="$rtt_sum"
|
||||
foreign_min_rtt="$min_rtt"
|
||||
foreign_max_rtt="$max_rtt"
|
||||
;;
|
||||
esac
|
||||
|
||||
set_processed_probe_at "$name" "stats" "${probe_at:-0}"
|
||||
}
|
||||
|
||||
while true; do
|
||||
ensure_runtime_state
|
||||
START_TIME="$(cat "$START_TIME_FILE" 2>/dev/null || echo "")"
|
||||
UPDATED_AT="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
NOW_TS="$(date '+%s' 2>/dev/null || echo 0)"
|
||||
DOMESTIC_TARGET=""
|
||||
FOREIGN_TARGET=""
|
||||
FOREIGN_PROXY_TYPE="socks5h"
|
||||
FOREIGN_PROXY_HOST="127.0.0.1"
|
||||
FOREIGN_PROXY_PORT="7891"
|
||||
DOMESTIC_INTERVAL="1"
|
||||
FOREIGN_INTERVAL="5"
|
||||
|
||||
if [ -f "$RUN_DIR/config" ]; then
|
||||
DOMESTIC_TARGET="$(sed -n '1p' "$RUN_DIR/config")"
|
||||
FOREIGN_TARGET="$(sed -n '2p' "$RUN_DIR/config")"
|
||||
FOREIGN_PROXY_TYPE="$(sed -n '3p' "$RUN_DIR/config")"
|
||||
FOREIGN_PROXY_HOST="$(sed -n '4p' "$RUN_DIR/config")"
|
||||
FOREIGN_PROXY_PORT="$(sed -n '5p' "$RUN_DIR/config")"
|
||||
DOMESTIC_INTERVAL="$(sed -n '6p' "$RUN_DIR/config")"
|
||||
FOREIGN_INTERVAL="$(sed -n '7p' "$RUN_DIR/config")"
|
||||
fi
|
||||
|
||||
FOREIGN_PROXY_TYPE="socks5h"
|
||||
[ -n "$FOREIGN_PROXY_HOST" ] || FOREIGN_PROXY_HOST="127.0.0.1"
|
||||
[ -n "$FOREIGN_PROXY_PORT" ] || FOREIGN_PROXY_PORT="7891"
|
||||
[ -n "$FOREIGN_INTERVAL" ] || FOREIGN_INTERVAL="5"
|
||||
DOMESTIC_INTERVAL="$(normalize_interval "$DOMESTIC_INTERVAL")"
|
||||
FOREIGN_INTERVAL="$(normalize_interval "$FOREIGN_INTERVAL")"
|
||||
|
||||
child_pids=""
|
||||
if [ -n "$DOMESTIC_TARGET" ]; then
|
||||
if should_probe_now "$domestic_last_probe_at" "$DOMESTIC_INTERVAL" "$NOW_TS"; then
|
||||
do_ping "$DOMESTIC_TARGET" "domestic" &
|
||||
child_pids="$!"
|
||||
domestic_last_probe_at="$NOW_TS"
|
||||
fi
|
||||
else
|
||||
domestic_last_probe_at=0
|
||||
reset_target_state "domestic"
|
||||
fi
|
||||
|
||||
if [ -n "$FOREIGN_TARGET" ]; then
|
||||
if should_probe_now "$foreign_last_probe_at" "$FOREIGN_INTERVAL" "$NOW_TS"; then
|
||||
do_proxy_curl "$FOREIGN_TARGET" "$FOREIGN_PROXY_TYPE" "$FOREIGN_PROXY_HOST" "$FOREIGN_PROXY_PORT" "foreign" &
|
||||
child_pids="${child_pids:+$child_pids }$!"
|
||||
foreign_last_probe_at="$NOW_TS"
|
||||
fi
|
||||
else
|
||||
foreign_last_probe_at=0
|
||||
reset_target_state "foreign"
|
||||
fi
|
||||
|
||||
[ -n "$child_pids" ] && wait $child_pids 2>/dev/null
|
||||
child_pids=""
|
||||
|
||||
update_target_faults "domestic" "国内" "$DOMESTIC_TARGET"
|
||||
update_target_faults "foreign" "国外" "$FOREIGN_TARGET"
|
||||
update_cumulative_stats "domestic" "$DOMESTIC_TARGET"
|
||||
update_cumulative_stats "foreign" "$FOREIGN_TARGET"
|
||||
|
||||
DOMESTIC_STATS="$(calc_stats "domestic")"
|
||||
FOREIGN_STATS="$(calc_stats "foreign")"
|
||||
|
||||
printf '{"start_time":"%s","updated_at":"%s","domestic":%s,"foreign":%s}\n' \
|
||||
"$START_TIME" "$UPDATED_AT" "$DOMESTIC_STATS" "$FOREIGN_STATS" > "$STATUS_TMP" && \
|
||||
mv -f "$STATUS_TMP" "$STATUS_FILE"
|
||||
|
||||
sleep 1
|
||||
done
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"luci-app-pingpacket": {
|
||||
"description": "Grant access to luci-app-pingpacket",
|
||||
"read": {
|
||||
"uci": [
|
||||
"pingpacket"
|
||||
]
|
||||
},
|
||||
"write": {
|
||||
"uci": [
|
||||
"pingpacket"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user