update 2026-01-05 00:28:49

This commit is contained in:
kenzok8
2026-01-05 00:28:49 +08:00
parent 9404937990
commit 33470ffc5b
14 changed files with 930 additions and 189 deletions

View File

@@ -7,13 +7,13 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=alist
PKG_VERSION:=3.55.0
PKG_WEB_VERSION:=3.55.0
PKG_VERSION:=3.56.0
PKG_WEB_VERSION:=3.56.0
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/AlistGo/alist/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=c9947567d4b2b19d63f3170ecab626a9c670a7d15965f88b14889a58e11900ab
PKG_HASH:=bd4621a15e10e5c446d7bce44c31ba1be5be653f4d255f3724a2bedb53265bd9
PKG_LICENSE:=GPL-3.0
PKG_LICENSE_FILE:=LICENSE
@@ -23,7 +23,7 @@ define Download/$(PKG_NAME)-web
FILE:=$(PKG_NAME)-web-$(PKG_WEB_VERSION).tar.gz
URL_FILE:=dist.tar.gz
URL:=https://github.com/AlistGo/alist-web/releases/download/$(PKG_WEB_VERSION)/
HASH:=1857caa4e64c0c9c2d92daea5953e8c5f2a5576e33e799b172a51e974f16fd6c
HASH:=3afcfd6488ddbc112a37858b7e2a6a8c83711cc118af0072903280dd89995a81
endef
PKG_BUILD_DEPENDS:=golang/host

View File

@@ -3,7 +3,7 @@
# AutoUpdate for Openwrt
# Dependences: wget-ssl/wget/uclient-fetch curl jq expr sysupgrade
Version=V6.10.5
Version=V6.10.6
function TITLE() {
clear && echo "Openwrt-AutoUpdate Script by Hyy2001 ${Version}"
@@ -22,19 +22,19 @@ function SHELL_HELP() {
-f 跳过版本号校验,并强制刷写固件 ${Red}(危险)${White} *
-F, --force-flash 强制刷写固件 ${Red}(危险)${White} *
-P, --proxy 优先开启镜像加速下载固件 *
${Next} A 自动选择镜像地址
${Next} E ghproxy.cn
${Next} F ghps.cc
${Next} G ghgo.xyz
${Next} A 自动选择镜像
${Next} E [镜像站] ghproxy.cn
${Next} F [镜像站] ghps.cc
${Next} G [镜像站] ghgo.xyz
-D <Downloader> 使用指定的下载器 <wget-ssl | wget | curl | uclient-fetch> *
--decompress 解压 img.gz 固件后再更新固件 *
--skip-verify 跳过固件 SHA256 校验 ${Red}(危险)${White} *
--path <PATH> 固件下载到用户提供的绝对路径 <PATH> *
更新程序:
-x 更新 autoupdate 程序
-x -path <PATH> 更新 autoupdate 程序 (保存程序到用户提供的绝对路径 <PATH>) *
-x -url <URL> 更新 autoupdate 程序 (使用提供的地址 <URL> 更新程序) *
-x 自动更新 autoupdate 程序
-x -path <PATH> 自动更新 autoupdate 程序 (保存程序到用户提供的绝对路径 <PATH>) *
-x -url <URL> 手动更新 autoupdate 程序 (使用用户提供的地址 <URL> 更新 autoupdate 程序) *
其他参数:
--help 打印 AutoUpdate 程序帮助信息
@@ -70,7 +70,7 @@ function SHOW_VARIABLE() {
固件版本: ${OP_VERSION} *
固件标签: ${TARGET_FLAG} *
内核版本: $(uname -r)
运行内存: Memory: $(MEMINFO Mem)M | swap: $(MEMINFO Swap)M | Total: $(MEMINFO All)M
运行内存: Memory: $(MEMINFO Mem)MB | Swap: $(MEMINFO Swap)MB | Total: $(MEMINFO All)MB
其他参数: ${TARGET_BOARD} / ${TARGET_SUBTARGET}
固件作者: ${Author} *
作者仓库: ${Github} *
@@ -89,48 +89,110 @@ EOF
}
function MEMINFO() {
[[ ! $1 || ! $* =~ (All|Mem|Swap) ]] && return 1
# 只接受 Mem / Swap / All
[[ -z "$1" || ! "$1" =~ ^(Mem|Swap|All)$ ]] && return 1
local want="$1"
local Mem Swap All Result
Mem=$(free | grep Mem: | awk '{Mem=$7/1024} {printf("%.0f\n",Mem)}' 2> /dev/null)
Swap=$(free | grep Swap: | awk '{Swap=$4/1024} {printf("%.0f\n",Swap)}' 2> /dev/null)
All=$(expr ${Mem} + ${Swap} 2> /dev/null)
Result=$(eval echo '$'$1)
if [[ ${Result} ]]
then
LOGGER "[$1] 可用运行内存: [${Result}M]"
echo ${Result}
# 只调用一次 free用 awk 同时解析:
# - Mem: 优先取最后一列(一般是 available若没有 available 列则退回 free 列
# - Swap: 取 free 列通常是第4列若列不够则取最后一列
read -r Mem Swap < <(
free 2>/dev/null | awk '
$1=="Mem:" {
# 通常 NF>=7 时最后一列是 available更符合“可用内存”概念
mem_kb = (NF>=7 ? $(NF) : $4)
next
}
$1=="Swap:" {
swap_kb = ($4 ? $4 : $(NF))
next
}
END{
# free 默认一般是 KiB这里换算成 MB与你原逻辑一致
if(mem_kb=="") mem_kb=0
if(swap_kb=="") swap_kb=0
printf "%.0f %.0f\n", mem_kb/1024, swap_kb/1024
}'
)
# 兜底,避免空值导致算术报错
[[ "$Mem" =~ ^[0-9]+$ ]] || Mem=0
[[ "$Swap" =~ ^[0-9]+$ ]] || Swap=0
All=$((Mem + Swap))
Result="${!want}"
if [[ "$Result" =~ ^[0-9]+$ ]]; then
LOGGER "[$want] 可用运行内存: [${Result}M]"
echo "$Result"
return 0
else
LOGGER "[$1] 可用内存获取失败!"
LOGGER "[$want] 可用内存获取失败!"
return 1
fi
}
function SPACEINFO() {
[[ ! $1 ]] && return 1
local Result Path
Path="$(awk -F '/' '{print $2}' <<< $1)"
Result="$(df -m /${Path} 2> /dev/null | grep -v Filesystem | awk '{print $4}')"
if [[ ${Result} ]]
then
LOGGER "[/${Path}] 可用存储空间: [${Result}M]"
echo "${Result}"
local path="$1"
local Result
# 目标:不存在就输出 0
if [[ -z "$path" || ! -e "$path" ]]; then
LOGGER "[${path:-<empty>}] 路径不存在,可用存储空间: [0M]"
echo 0
return 1
fi
# df -P 保证格式稳定;-m 输出单位 MBOpenWrt busybox 通常也支持 -m
Result="$(df -Pm -- "$path" 2>/dev/null | awk 'NR==2{print $4}')"
# 兜底df 失败/异常输出 → 0
if [[ "$Result" =~ ^[0-9]+$ ]]; then
LOGGER "[$path] 可用存储空间: [${Result}M]"
echo "$Result"
return 0
else
LOGGER "[/${Path}] 可用存储空间获取失败!"
LOGGER "[$path] 可用存储空间获取失败,输出 0"
echo 0
return 1
fi
}
function RM() {
for i in $@
do
rm -r "$i" 2> /dev/null
LOGGER "删除文件: [$i]"
local i
for i in "$@"; do
# 1) 空参数直接跳过(防止 rm -r "" 这类异常)
[[ -z "$i" ]] && { LOGGER "删除跳过: [空路径]"; continue; }
# 2) 禁止危险路径(兜底防止误删系统)
case "$i" in
/|/.|/..|.|..)
LOGGER "删除拒绝(危险路径): [$i]"
continue
;;
esac
# 3) 不存在就跳过(你的需求)
# -e: 存在(文件/目录); -L: 断链符号链接也算“存在可删”
if [[ ! -e "$i" && ! -L "$i" ]]; then
LOGGER "删除跳过(不存在): [$i]"
continue
fi
# 4) 执行删除(文件/目录都覆盖)
rm -rf -- "$i" 2>/dev/null
if [[ $? == 0 ]]; then
LOGGER "删除成功: [$i]"
else
LOGGER "删除失败: [$i]"
fi
done
}
GET_DOWNLOADER() {
function GET_DOWNLOADER() {
for i in ${DL_DEPENDS[@]}
do
if [[ $(CHECK_PKG $i) == true ]]
@@ -262,7 +324,7 @@ function LOGGER() {
fi
}
function RANDOM() {
function RANDOM_HEX() {
head -n 5 /dev/urandom | md5sum | cut -c 1-$1
}
@@ -278,55 +340,84 @@ function GET_SHA256SUM() {
}
function GET_VARIABLE() {
local Result="$(grep "$1=" $2 2> /dev/null | grep -v "#" | awk -F '=' '{print $2}')"
if [[ ${Result} && $? == 0 ]]
then
eval echo "${Result}"
return 0
else
return 1
fi
local key="$1" file="$2"
local Result
Result="$(grep -E "^${key}=" "$file" 2>/dev/null | tail -n1 | cut -d= -f2-)"
[[ -n "$Result" ]] && { echo "$Result"; return 0; }
return 1
}
function EDIT_VARIABLE() {
local Mode=$1
local Mode="$1"
shift
[[ ! -s $1 ]] && ECHO r "未检测到环境变量文件: [$1] !" && return 1
case "${Mode}" in
edit)
if [[ ! $(GET_VARIABLE $2 $1) ]]
then
LOGGER "[EDIT_VARIABLE] 新增环境变量 [$2 = $3]"
echo -e "\n$2=$3" >> $1
RETURN=0
else
sed -i "s?$(GET_VARIABLE $2 $1)?$3?g" $1 2> /dev/null
if [[ $? == 0 ]]
then
LOGGER "[EDIT_VARIABLE] 环境变量 [$2 > $3] 修改成功!"
local File="$1"
shift
[[ ! -s "$File" ]] && ECHO r "未检测到环境变量文件: [$File] !" && return 1
local Key="$1"
local Val="$2"
local RETURN=1
# key 基本校验(保守一些,避免 sed 正则炸)
[[ -z "$Key" ]] && ECHO r "环境变量 Key 不能为空!" && return 1
# sed 正则/替换转义
_escape_sed_regex() { printf '%s' "$1" | sed 's/[.[\*^$(){}+?|\\/]/\\&/g'; }
_escape_sed_repl() { printf '%s' "$1" | sed 's/[\/&|\\]/\\&/g'; }
local Key_Re="$(_escape_sed_regex "$Key")"
local Val_Repl="$(_escape_sed_repl "${Val:-}")"
case "$Mode" in
edit)
[[ -z "$Val" && "$Val" != "0" ]] && { ECHO r "环境变量 [$Key] 的值不能为空!"; return 1; }
if ! grep -q -E "^${Key_Re}=" "$File" 2>/dev/null; then
LOGGER "[EDIT_VARIABLE] 新增环境变量 [$Key = $Val]"
# 不用 echo -e避免反斜杠被解释
printf '\n%s=%s\n' "$Key" "$Val" >> "$File"
RETURN=$?
else
# 精确替换整行KEY=...
sed -i -E "s|^${Key_Re}=.*|${Key}=${Val_Repl}|" "$File" 2>/dev/null
if [[ $? == 0 ]]; then
LOGGER "[EDIT_VARIABLE] 环境变量 [$Key > $Val] 修改成功!"
RETURN=0
else
LOGGER "[EDIT_VARIABLE] 环境变量 [$Key > $Val] 修改失败!"
RETURN=1
fi
fi
;;
rm)
# 只删除以 KEY= 开头的行
sed -i -E "/^${Key_Re}=/d" "$File" 2>/dev/null
if [[ $? == 0 ]]; then
LOGGER "[EDIT_VARIABLE] 从 $File 删除环境变量 [$Key] ... 成功"
RETURN=0
else
LOGGER "[EDIT_VARIABLE] 环境变量 [$2 > $3] 修改失败!"
LOGGER "[EDIT_VARIABLE] 从 $File 删除环境变量 [$Key] ... 失败"
RETURN=1
fi
fi
;;
rm)
sed -i "/$2=/d" $1
if [[ $? == 0 ]]
then
LOGGER "[EDIT_VARIABLE] 从 $1 删除环境变量 [$2] ... 成功"
RETURN=0
else
LOGGER "[EDIT_VARIABLE] 从 $1 删除环境变量 [$2] ... 失败"
RETURN=1
fi
;;
;;
*)
ECHO r "EDIT_VARIABLE 模式错误: [$Mode] (仅支持 edit/rm)"
return 1
;;
esac
cp -a ${Config_Custom} ${Tmp_Path}/custom
grep -vE "^[[:blank:]]*$" ${Tmp_Path}/custom > ${Config_Custom}
echo >> ${Config_Custom}
return ${RETURN}
# 保留你原来的功能:把 Config_Custom 去空行并保证末尾有空行
# (即使本次修改的不是 Config_Custom,也按原行为执行)
[[ -d "$Tmp_Path" ]] || mkdir -p "$Tmp_Path" 2>/dev/null
if [[ -f "$Config_Custom" ]]; then
cp -a "$Config_Custom" "$Tmp_Path/custom" 2>/dev/null
grep -vE "^[[:blank:]]*$" "$Tmp_Path/custom" > "$Config_Custom"
echo >> "$Config_Custom"
fi
return $RETURN
}
function LOAD_CONFIG() {
@@ -522,7 +613,7 @@ function ANALYZE_API() {
exit 1
}
local API_Cache=${Tmp_Path}/API_Cache
if [[ $(CHECK_TIME ${API_File} 1) == false ]]
if [[ $(CHECK_TIME ${API_Cache} 1) == false ]]
then
DOWNLOADER --path ${Tmp_Path} --file-name API_Cache --dl ${DL_DEPENDS[@]} --url "${Github_API}@@1 $(Proxy_X ${Github_Release}/API G@@1 F@@1 E@@1)" --no-url-name --timeout 5
[[ ! $? == 0 || -z $(cat ${API_Cache} 2> /dev/null) ]] && {
@@ -1053,9 +1144,9 @@ function DOWNLOADER() {
}
function REMOVE_CACHE() {
rm -r ${Tmp_Path}/API \
${Tmp_Path}/Update_Logs.json \
${Tmp_Path}/API_Cache 2> /dev/null
RM ${Tmp_Path}/API
RM ${Tmp_Path}/Update_Logs.json
RM ${Tmp_Path}/API_Cache
}
function LOG() {
@@ -1076,7 +1167,7 @@ function LOG() {
fi
EDIT_VARIABLE rm ${Config_Custom} Log_Path
EDIT_VARIABLE edit ${Config_Custom} Log_Path $2
Log_Path=${Log_Path}
Log_Path="$2"
uci set autoupdate.@autoupdate[0].logpath=$2 2> /dev/null
uci commit autoupdate
ECHO y "AutoUpdate 日志保存路径已修改为: [$2]!"
@@ -1248,7 +1339,7 @@ function AutoUpdate_Main() {
exit $?
;;
--backup)
local Backup_File="backup-$(uname -n)-$(date +%Y-%m-%d)-$(RANDOM 5).tar.gz"
local Backup_File="backup-$(uname -n)-$(date +%Y-%m-%d)-$(RANDOM_HEX 5).tar.gz"
shift
[[ $# -gt 1 ]] && SHELL_HELP
if [[ ! $1 ]]
@@ -1373,7 +1464,7 @@ function AutoUpdate_Main() {
exit $?
;;
--reset)
rm -r ${Config_Default} ${Config_Custom}
RM ${Config_Default} ${Config_Custom}
cp -a /rom/$(dirname ${Config_Default})/* $(dirname ${Config_Default})/
cp -a /rom/etc/config/autoupdate /etc/config
REMOVE_CACHE
@@ -1388,10 +1479,18 @@ function AutoUpdate_Main() {
}
function KILL_PROCESS() {
local i ; for i in $(ps -ef 2> /dev/null | grep -v grep | grep $1 | grep -v $$ | awk '{print $1}' 2> /dev/null)
do
kill -9 ${i} 2> /dev/null &
LOGGER "结束进程: ${i}"
local pattern="$1"
local pids=""
if command -v pgrep >/dev/null 2>&1; then
pids="$(pgrep -f "$pattern" 2>/dev/null | grep -v "^$$\$" || true)"
else
pids="$(ps -ef 2>/dev/null \
| awk -v pat="$pattern" -v self="$$" '$0 ~ pat && $2 != self && $0 !~ /grep/ {print $2}')"
fi
for pid in $pids; do
kill -9 "$pid" 2>/dev/null && LOGGER "结束进程 PID: $pid (pattern=$pattern)"
done
}

View File

@@ -1,12 +1,12 @@
# Copyright (C) 2020-2023 Hyy2001X <https://github.com/Hyy2001X>
# Copyright (C) 2020-2026 Hyy2001X <https://github.com/Hyy2001X>
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for iPerf3 server
LUCI_DEPENDS:=+iperf3
LUCI_PKGARCH:=all
PKG_VERSION:=2.0
PKG_RELEASE:=4
PKG_VERSION:=3.0
PKG_RELEASE:=1
include $(TOPDIR)/feeds/luci/luci.mk

View File

@@ -1,17 +1,168 @@
module("luci.controller.iperf3-server",package.seeall)
module("luci.controller.iperf3-server", package.seeall)
function index()
if not nixio.fs.access("/etc/config/iperf3-server") then
return
end
entry({"admin", "services", "iperf3-server"}, cbi("iperf3-server"), _("iPerf3 Server"),99)
entry({"admin", "services", "iperf3-server", "status"}, call("act_status")).leaf = true
entry({"admin", "services", "iperf3-server"}, cbi("iperf3-server"), _("iPerf3 Server"), 99)
entry({"admin", "services", "iperf3-server", "status"}, call("act_status")).leaf = true
entry({"admin", "services", "iperf3-server", "start"}, call("act_start")).leaf = true
entry({"admin", "services", "iperf3-server", "stop"}, call("act_stop")).leaf = true
entry({"admin", "services", "iperf3-server", "restart"}, call("act_restart")).leaf = true
end
function act_status()
local e = {}
e.running = luci.sys.call("pgrep iperf3 > /dev/null") == 0
local function json_ok(extra)
local e = extra or {}
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function act_status()
local uci = require("luci.model.uci").cursor()
local sys = require("luci.sys")
local result = {
running = false, -- 是否任意一个 server 正常运行
servers = {} -- 每个端口的详细状态
}
-- 解析 /proc/net/tcp & /proc/net/tcp6判断端口是否 LISTEN
local function is_listening_in_proc(path, port)
local f = io.open(path, "r")
if not f then return false end
local hex = string.format("%04X", port) -- 端口 16 进制,大写,固定 4 位
-- /proc/net/tcp 列格式: sl local_address rem_address st ...
-- local_address: AAAAAAAA:PPPP
-- st = 0A 表示 LISTEN
for line in f:lines() do
-- 跳过表头
if not line:match("^%s*sl%s+") then
local st = line:match("^%s*%d+:%s+%x+:" .. hex .. "%s+%x+:%x+%s+(%x+)")
if st == "0A" then
f:close()
return true
end
end
end
f:close()
return false
end
local function is_listening(port)
-- IPv4 LISTEN
if is_listening_in_proc("/proc/net/tcp", port) then
return true
end
-- IPv6 LISTEN有的系统会只在 tcp6 里出现)
if is_listening_in_proc("/proc/net/tcp6", port) then
return true
end
return false
end
-- 用 ps 判断:是否存在 iperf3 server / delay 阶段 sleep+iperf3
local ps = sys.exec("ps w 2>/dev/null") or ""
local function has_iperf3_server_proc(port)
local p = tostring(port)
for line in ps:gmatch("[^\r\n]+") do
-- 只要这行同时包含:
-- 1) iperf3
-- 2) -sserver
-- 3) -p 端口(支持 "-p 5201" 或 "-p5201"
if line:find("iperf3", 1, true) then
local hasS = line:match("%-s") ~= nil
local hasP = (line:match("%-p%s*" .. p .. "%f[^%d]") ~= nil) or (line:match("%-p" .. p .. "%f[^%d]") ~= nil)
if hasS and hasP then
return true
end
end
end
return false
end
local function has_delay_pending_proc(port)
local p = tostring(port)
for line in ps:gmatch("[^\r\n]+") do
-- 匹配 sh -c "sleep N; exec iperf3 -s -p <port> ..."
if (line:find("/bin/sh", 1, true) or line:find("sh -c", 1, true))
and line:find("sleep", 1, true)
and line:find("iperf3", 1, true)
and (line:match("%-p%s*" .. p .. "%f[^%d]") or line:match("%-p" .. p .. "%f[^%d]"))
then
return true
end
end
return false
end
uci:foreach("iperf3-server", "servers", function(s)
local port = tonumber(s.port)
local enabled = (s.enable_server == "1")
if not port then
return
end
local listen = is_listening(port)
local iperf3_proc = has_iperf3_server_proc(port)
local delay_pending = has_delay_pending_proc(port)
local state, detail
if not enabled then
state = "disabled"
detail = "disabled in config"
else
if delay_pending and not listen then
state = "delay"
detail = "delay pending (sleep)"
elseif listen and iperf3_proc then
state = "running"
detail = "listening and iperf3 process found"
result.running = true
elseif listen and not iperf3_proc then
state = "conflict"
detail = "port is listening but not iperf3 (occupied)"
else
state = "stopped"
detail = "not listening"
end
end
result.servers[#result.servers + 1] = {
port = port,
enable = enabled,
listen = listen,
state = state,
detail = detail
}
end)
luci.http.prepare_content("application/json")
luci.http.write_json(result)
end
function act_start()
local rc = luci.sys.call("/etc/init.d/iperf3-server start >/dev/null 2>&1")
json_ok({ ok = (rc == 0) })
end
function act_stop()
local rc = luci.sys.call("/etc/init.d/iperf3-server stop >/dev/null 2>&1")
json_ok({ ok = (rc == 0) })
end
function act_restart()
local rc = luci.sys.call("/etc/init.d/iperf3-server restart >/dev/null 2>&1")
json_ok({ ok = (rc == 0) })
end

View File

@@ -1,36 +1,95 @@
m = Map("iperf3-server", translate("iPerf3 Server"), translate("iPerf3 - The ultimate speed test tool for TCP, UDP and SCTP"))
m = Map("iperf3-server",
translate("iPerf3 Server"),
translate("iPerf3 - The ultimate speed test tool for TCP, UDP and SCTP")
)
m:section(SimpleSection).template = "iperf3-server/iperf3-server_status"
s = m:section(TypedSection, "iperf3-server", "")
s.addremove = false
s.anonymous = true
-- 主配置段
local g = m:section(TypedSection, "iperf3-server", "")
g.addremove = false
g.anonymous = true
main_enable = s:option(Flag, "main_enable", translate("Enable"), translate("Enable iPerf3 Servers"))
local main_enable = g:option(Flag, "main_enable",
translate("Enable"),
translate("Enable iPerf3 Servers")
)
main_enable.default = "0"
main_enable.rmempty = false
s = m:section(TypedSection, "servers", translate("Server Settings"), translate("Set up Multi-iPerf3 Servers"))
-- servers 表格
local s = m:section(TypedSection, "servers",
translate("Server Settings"),
translate("Set up Multi-iPerf3 Servers")
)
s.anonymous = true
s.addremove = true
s.template = "cbi/tblsection"
s.template = "cbi/tblsection"
enable_server = s:option(Flag, "enable_server", translate("Enable"))
local enable_server = s:option(Flag, "enable_server", translate("Enable"))
enable_server.default = "1"
enable_server.rmempty = false
port = s:option(Value, "port", translate("Port"))
local port = s:option(Value, "port", translate("Port"))
port.datatype = "port"
port.default = "5201"
port.rmempty = false
port.default = "5201"
port.rmempty = true -- 关键:允许新增行先空着,避免“点添加无反应”
delay = s:option(Value, "delay", translate("Start delay (Seconds)"))
delay.default = "0"
function port.validate(self, value, section)
-- 新增行刚创建时可能是空值:先放行,让它能显示出来
if value == nil or value == "" then
return value
end
local v = tonumber(value)
if not v or v < 1 or v > 65535 then
return nil, translate("Invalid port.")
end
-- 端口去重:仅在有值时检查
local dup = false
m.uci:foreach("iperf3-server", "servers", function(s2)
if s2[".name"] ~= section and s2.port and tonumber(s2.port) == v then
dup = true
end
end)
if dup then
return nil, translate("Port must be unique.")
end
return tostring(v)
end
local delay = s:option(Value, "delay", translate("Start delay (Seconds)"))
delay.default = "0"
delay.datatype = "uinteger"
delay.rmempty = false
delay.rmempty = true -- 同理:新增行先允许空
extra_options = s:option(Value, "extra_options", translate("Extra Options"))
extra_options.rmempty = true
extra_options.password= false
function delay.validate(self, value, section)
if value == nil or value == "" then
return value
end
local v = tonumber(value)
if v == nil or v < 0 or v > 3600 then
return nil, translate("Delay must be between 0 and 3600 seconds.")
end
return tostring(v)
end
local extra_options = s:option(Value, "extra_options", translate("Extra Options"))
extra_options.rmempty = true
extra_options.password = false
function extra_options.validate(self, value, section)
if not value or value == "" then
return value
end
-- 简单拦截 shell 元字符(安全兜底)
if value:match("[;&|`$()<>\"']") then
return nil, translate("Invalid characters in Extra Options.")
end
return value
end
return m

View File

@@ -1,22 +1,165 @@
<script type="text/javascript">//<![CDATA[
XHR.poll(3, '<%=url([[admin]], [[services]], [[iperf3-server]], [[status]])%>', null,
function(x, data) {
var tb = document.getElementById('iperf3-server_status');
if (data && tb) {
if (data.running) {
var links = '<em><b><font color=green>iPerf3 Server <%:RUNNING%></font></b></em>';
tb.innerHTML = links;
} else {
tb.innerHTML = '<em><b><font color=red>iPerf3 Server <%:NOT RUNNING%></font></b></em>';
<fieldset class="cbi-section">
<legend><%:Status%></legend>
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<div id="iperf3_status">-</div>
<button class="btn cbi-button cbi-button-apply" id="btn_start" type="button"><%:Start%></button>
<button class="btn cbi-button cbi-button-reset" id="btn_stop" type="button"><%:Stop%></button>
<button class="btn cbi-button cbi-button-action" id="btn_restart" type="button"><%:Restart%></button>
<span id="iperf3_busy" style="display:none; color:#999;">...</span>
</div>
<div id="iperf3_list" style="margin-top:8px;"></div>
</fieldset>
<script type="text/javascript">
//<![CDATA[
(function () {
var token = '<%=token%>';
function esc(s) {
if (s === null || s === undefined) return '';
return String(s).replace(/[&<>"']/g, function (c) {
return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]);
});
}
function setBusy(b) {
document.getElementById('iperf3_busy').style.display = b ? '' : 'none';
document.getElementById('btn_start').disabled = b;
document.getElementById('btn_stop').disabled = b;
document.getElementById('btn_restart').disabled = b;
}
function badge(text, color) {
return '<span style="color:' + color + '">' + text + '</span>';
}
function stateBadge(it) {
// 兼容旧字段it.running/it.pid
// 新字段优先it.state
var st = it.state;
if (!st) {
if (it.enable === false) st = "disabled";
else st = it.running ? "running" : "stopped";
}
if (st === "running") return badge('<%:Running%>', 'green');
if (st === "delay") return badge('<%:Delay%>', '#d67f00'); // 橙色
if (st === "conflict") return badge('<%:Conflict%>', 'red');
if (st === "disabled") return badge('<%:Disabled%>', '#999');
return badge('<%:Stopped%>', '#999');
}
function overallBadge(data) {
// controller 返回 data.running=true 表示至少一个 running
// 如果没有 running但存在 delay/conflict也给更准确的总体状态
if (data && data.running) {
return badge('<%:Running%>', 'green');
}
var hasDelay = false, hasConflict = false;
if (data && data.servers && data.servers.length) {
for (var i = 0; i < data.servers.length; i++) {
var st = data.servers[i].state;
if (st === "delay") hasDelay = true;
else if (st === "conflict") hasConflict = true;
}
}
if (hasConflict) return badge('<%:Conflict%>', 'red');
if (hasDelay) return badge('<%:Delay%>', '#d67f00');
return badge('<%:Not running%>', 'red');
}
);
function renderStatus(data) {
var s = document.getElementById('iperf3_status');
s.innerHTML = overallBadge(data);
var list = document.getElementById('iperf3_list');
var html = '';
if (data && data.servers && data.servers.length) {
html += '<ul>';
for (var i = 0; i < data.servers.length; i++) {
var it = data.servers[i];
var port = esc(it.port);
var en = (it.enable !== false) && (it.enable !== "0"); // 兼容 true/false/字符串
var detail = it.detail ? (' <span style="color:#999">(' + esc(it.detail) + ')</span>') : '';
// 兼容旧字段 pid
var pidInfo = '';
if (it.pid) {
pidInfo = ' <span style="color:#999">PID ' + esc(it.pid) + '</span>';
}
// 监听信息(新字段 listen
var listenInfo = '';
if (typeof it.listen === "boolean") {
listenInfo = it.listen ? ' <span style="color:#999">[LISTEN]</span>' : ' <span style="color:#999">[NO LISTEN]</span>';
}
html += '<li>'
+ 'Port ' + port + ' : '
+ stateBadge(it)
+ pidInfo
+ listenInfo
+ detail
+ (en ? '' : ' <span style="color:#999">(disabled)</span>')
+ '</li>';
}
html += '</ul>';
// 状态说明(简短)
html += '<div style="margin-top:6px; color:#999;">'
+ '<span><%:Delay%></span>: <%:starting after configured delay%> &nbsp; '
+ '<span><%:Conflict%></span>: <%:port is occupied%>'
+ '</div>';
}
list.innerHTML = html;
}
function refresh() {
XHR.get('<%=luci.dispatcher.build_url("admin/services/iperf3-server/status")%>', null,
function (x, data) { renderStatus(data || {}); }
);
}
function doAction(url, confirmText) {
if (confirmText && !window.confirm(confirmText)) return;
setBusy(true);
XHR.post(url, { token: token }, function (x, data) {
setBusy(false);
// restart 有 delay 的情况下,立即 refresh 可能还是 delay/stopped轮询会继续更新
refresh();
});
}
document.getElementById('btn_start').addEventListener('click', function () {
doAction('<%=luci.dispatcher.build_url("admin/services/iperf3-server/start")%>', '<%:Start iPerf3 servers?%>');
});
document.getElementById('btn_stop').addEventListener('click', function () {
doAction('<%=luci.dispatcher.build_url("admin/services/iperf3-server/stop")%>', '<%:Stop iPerf3 servers?%>');
});
document.getElementById('btn_restart').addEventListener('click', function () {
doAction('<%=luci.dispatcher.build_url("admin/services/iperf3-server/restart")%>', '<%:Restart iPerf3 servers?%>');
});
// 轮询刷新
XHR.poll(2, '<%=luci.dispatcher.build_url("admin/services/iperf3-server/status")%>', null,
function (x, data) { renderStatus(data || {}); }
);
// 首次刷新
refresh();
})();
//]]>
</script>
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
<fieldset class="cbi-section">
<p id="iperf3-server_status">
<em><%:Collecting data...%></em>
</p>
</fieldset>

View File

@@ -24,3 +24,75 @@ msgstr "iPerf3 服务端监听端口"
msgid "Extra Options"
msgstr "额外参数"
msgid "Status"
msgstr "状态"
msgid "Running"
msgstr "运行中"
msgid "Not running"
msgstr "未运行"
msgid "Stopped"
msgstr "已停止"
msgid "Start"
msgstr "启动"
msgid "Stop"
msgstr "停止"
msgid "Restart"
msgstr "重启"
msgid "Delay"
msgstr "延迟中"
msgid "Conflict"
msgstr "端口冲突"
msgid "Disabled"
msgstr "已禁用"
msgid "starting after configured delay"
msgstr "将在设定的延迟时间后启动"
msgid "port is occupied"
msgstr "端口已被其他服务占用"
msgid "port is listening but not iperf3 (occupied)"
msgstr "端口正在监听,但不是 iPerf3被占用"
msgid "Delay pending (sleep)"
msgstr "延迟启动中"
msgid "listening and iperf3 process found"
msgstr "正在监听,且 iPerf3 进程已启动"
msgid "not listening"
msgstr "未监听端口"
msgid "disabled in config"
msgstr "已在配置中禁用"
msgid "PID"
msgstr "进程号"
msgid "LISTEN"
msgstr "监听中"
msgid "NO LISTEN"
msgstr "未监听"
msgid "port is listening"
msgstr "端口正在监听"
msgid "Start iPerf3 servers?"
msgstr "确认启动 iPerf3 服务?"
msgid "Stop iPerf3 servers?"
msgstr "确认停止 iPerf3 服务?"
msgid "Restart iPerf3 servers?"
msgstr "确认重启 iPerf3 服务?"

View File

@@ -24,3 +24,75 @@ msgstr "iPerf3 服务端监听端口"
msgid "Extra Options"
msgstr "额外参数"
msgid "Status"
msgstr "状态"
msgid "Running"
msgstr "运行中"
msgid "Not running"
msgstr "未运行"
msgid "Stopped"
msgstr "已停止"
msgid "Start"
msgstr "启动"
msgid "Stop"
msgstr "停止"
msgid "Restart"
msgstr "重启"
msgid "Delay"
msgstr "延迟中"
msgid "Conflict"
msgstr "端口冲突"
msgid "Disabled"
msgstr "已禁用"
msgid "starting after configured delay"
msgstr "将在设定的延迟时间后启动"
msgid "port is occupied"
msgstr "端口已被其他服务占用"
msgid "port is listening but not iperf3 (occupied)"
msgstr "端口正在监听,但不是 iPerf3被占用"
msgid "Delay pending (sleep)"
msgstr "延迟启动中"
msgid "listening and iperf3 process found"
msgstr "正在监听,且 iPerf3 进程已启动"
msgid "not listening"
msgstr "未监听端口"
msgid "disabled in config"
msgstr "已在配置中禁用"
msgid "PID"
msgstr "进程号"
msgid "LISTEN"
msgstr "监听中"
msgid "NO LISTEN"
msgstr "未监听"
msgid "port is listening"
msgstr "端口正在监听"
msgid "Start iPerf3 servers?"
msgstr "确认启动 iPerf3 服务?"
msgid "Stop iPerf3 servers?"
msgstr "确认停止 iPerf3 服务?"
msgid "Restart iPerf3 servers?"
msgstr "确认重启 iPerf3 服务?"

View File

@@ -1,4 +1,3 @@
config iperf3-server
option enable '0'
option port '5201'
config iperf3-server 'main'
option main_enable '0'

View File

@@ -1,55 +1,184 @@
#!/bin/sh /etc/rc.common
# iPerf3 Server init (OpenWrt 23.05) - procd multi-instance (UCI-only fetch)
# UCI: /etc/config/iperf3-server
#
# config iperf3-server
# option main_enable '1'
#
# config servers
# option enable_server '1'
# option port '5201'
# option delay '0'
# option extra_options ''
USE_PROCD=1
START=99
LOGGER="logger -t [iPerf3-Server]"
start() {
local basic_list="main_enable"
local server_list="port delay extra_options enable_server"
for i in $(echo $basic_list)
do
local eval $i="$(uci_get_by_type iperf3-server 0 $i)"
done ; unset i
if [ "$main_enable" == 1 ]
then
server_number=$(uci show iperf3-server 2> /dev/null | egrep '@servers\[[0-9]\]+=servers' | wc -l)
# server_number=$(uci show iperf3-server 2> /dev/null | egrep -o '@servers\[[0-9]\]+=servers' | awk 'END {print}' | egrep -o "[0-9]")
for u in $(seq 0 $((${server_number} - 1)))
do
for i in $server_list
do
eval ${i}=$(uci_get_by_type servers $u $i)
done ; unset i
if [ "$enable_server" == 1 ]
then
sleep $delay
$LOGGER "Starting iPerf3 Server [$u] with Port [$port] ..."
old_process="$(ps -efww | grep 'iperf3 -s -D -p $port' | grep -v 'grep' | awk '{print $1}')"
[ "$old_process" ] && kill -9 "$old_process" 2> /dev/null
$(command -v iperf3) -s -D -p $port $extra_options
#procd_open_instance
#procd_set_param command sleep $delay ; $(command -v iperf3) -s -D -p $port $extra_options
#procd_set_param respawn 3000 3 10
#procd_close_instance
fi
unset enable_server delay
done ; unset u
else
$LOGGER "iPerf3 Server is disabled ..."
stop_service
fi
PROG="/usr/bin/iperf3"
CONFIG="iperf3-server"
LOGGER="logger -t iPerf3-Server"
# 与 CBI 的 extra_options 校验保持一致:拦截明显 shell 元字符
is_safe_extra_opts() {
[ -z "$1" ] && return 0
case "$1" in
*";"*|*"&"*|*"|"*|*"\`"*|*"\$"*|*"<"*|*">"*|*"\""*|*"'"* )
return 1
;;
esac
return 0
}
stop() {
# 停止某端口:既杀 iperf3 server也杀 delay 阶段的 sh/sleep
kill_by_port() {
local port="$1"
local pids
# 1) 已经起来的 iperf3 server
pids="$(ps w | grep '[i]perf3' | grep ' -s' | grep " -p $port" | awk '{print $1}')"
[ -n "$pids" ] && kill $pids 2>/dev/null
# 2) delay 阶段:/bin/sh -c "sleep N; exec iperf3 -s -p PORT ..."
pids="$(ps w | grep '[s]leep' | grep '[i]perf3' | grep " -p $port" | awk '{print $1}')"
[ -n "$pids" ] && kill $pids 2>/dev/null
sleep 1
# hard kill 兜底
pids="$(ps w | grep '[i]perf3' | grep ' -s' | grep " -p $port" | awk '{print $1}')"
[ -n "$pids" ] && kill -9 $pids 2>/dev/null
pids="$(ps w | grep '[s]leep' | grep '[i]perf3' | grep " -p $port" | awk '{print $1}')"
[ -n "$pids" ] && kill -9 $pids 2>/dev/null
}
# 获取 servers 索引列表0 1 2 ...
get_server_indexes() {
# 从 uci show 里提取 @servers[NUM]
uci -q show "$CONFIG" 2>/dev/null \
| sed -n "s/^$CONFIG\.@servers\[\([0-9]\+\)\]=servers$/\1/p"
}
start_one_idx() {
local idx="$1"
local enable_server port delay extra_options
enable_server="$(uci -q get $CONFIG.@servers[$idx].enable_server)"
port="$(uci -q get $CONFIG.@servers[$idx].port)"
delay="$(uci -q get $CONFIG.@servers[$idx].delay)"
extra_options="$(uci -q get $CONFIG.@servers[$idx].extra_options)"
# 默认值
[ -n "$enable_server" ] || enable_server="1"
[ -n "$port" ] || port="5201"
[ -n "$delay" ] || delay="0"
[ "$enable_server" = "1" ] || return 0
# 端口合法性兜底
case "$port" in
*[!0-9]*|'') $LOGGER "Skip invalid port in @servers[$idx]"; return 0 ;;
esac
[ "$port" -ge 1 ] 2>/dev/null && [ "$port" -le 65535 ] 2>/dev/null || {
$LOGGER "Skip invalid port [$port] in @servers[$idx]"
return 0
}
# delay 合法性兜底
case "$delay" in
*[!0-9]*|'') delay=0 ;;
esac
# extra_options 安全兜底
if [ -n "$extra_options" ] && ! is_safe_extra_opts "$extra_options"; then
$LOGGER "Rejected unsafe extra_options on port [$port] (@servers[$idx])"
extra_options=""
fi
$LOGGER "Starting @servers[$idx] port=[$port] delay=[$delay] ..."
# procd 实例名按端口
procd_open_instance "iperf3_${port}"
# 不用 -Ddelay 用 sh -c 不阻塞
if [ "$delay" -gt 0 ] 2>/dev/null; then
if [ -n "$extra_options" ]; then
procd_set_param command /bin/sh -c "sleep $delay; exec $PROG -s -p $port $extra_options"
else
procd_set_param command /bin/sh -c "sleep $delay; exec $PROG -s -p $port"
fi
else
if [ -n "$extra_options" ]; then
procd_set_param command "$PROG" -s -p "$port" $extra_options
else
procd_set_param command "$PROG" -s -p "$port"
fi
fi
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
start_service() {
[ -x "$PROG" ] || { $LOGGER "iperf3 not found: $PROG"; return 1; }
# 主开关:直接用 uci 读取匿名段
local main_enable
main_enable="$(uci -q get $CONFIG.@iperf3-server[0].main_enable)"
[ -n "$main_enable" ] || main_enable="0"
[ "$main_enable" = "1" ] || {
$LOGGER "iPerf3 Server is disabled ..."
return 0
}
# 遍历 servers 索引
local idx
for idx in $(get_server_indexes); do
start_one_idx "$idx"
done
return 0
}
stop_one_idx() {
local idx="$1"
local enable_server port
enable_server="$(uci -q get $CONFIG.@servers[$idx].enable_server)"
[ -n "$enable_server" ] || enable_server="1"
[ "$enable_server" = "1" ] || return 0
port="$(uci -q get $CONFIG.@servers[$idx].port)"
[ -n "$port" ] || port="5201"
case "$port" in
*[!0-9]*|'') return 0 ;;
esac
kill_by_port "$port"
}
stop_service() {
$LOGGER "Stopping iPerf3 Server ..."
ps -efww | grep 'iperf3 -s -D' | grep -v 'grep' | awk '{print $1}' | xargs kill -9
# 按 servers 列表逐个端口清理(不误杀其它 iperf3
local idx
for idx in $(get_server_indexes); do
stop_one_idx "$idx"
done
return 0
}
reload_service() {
# /etc/init.d/iperf3-server reload
stop
start
}
service_triggers() {
procd_add_reload_trigger "iperf3-server"
}
uci_get_by_type() {
local ret=$(uci get iperf3-server.@$1[$2].$3 2>/dev/null)
echo ${ret:=$4}
procd_add_reload_trigger "$CONFIG"
}

View File

@@ -1,10 +1,14 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
delete ucitrack.@iperf3-server[-1]
# 已存在就不重复写
if uci -q show ucitrack | grep -q "=iperf3-server"; then
exit 0
fi
uci -q batch <<'EOF' >/dev/null
add ucitrack iperf3-server
set ucitrack.@iperf3-server[-1].init=iperf3-server
set ucitrack.@iperf3-server[-1].init='iperf3-server'
commit ucitrack
EOF
exit 0
exit 0

View File

@@ -865,7 +865,7 @@ function gen_config(var)
end
table.insert(balancers, {
tag = balancer_tag,
selector = valid_nodes,
selector = api.clone(valid_nodes),
fallbackTag = fallback_node_tag,
strategy = strategy
})
@@ -1198,6 +1198,21 @@ function gen_config(var)
end
end)
if default_outboundTag or default_balancerTag then
local rule = {
_flag = "default",
type = "field",
outboundTag = default_outboundTag,
balancerTag = default_balancerTag
}
if node.domainStrategy == "IPIfNonMatch" then
rule.ip = { "0.0.0.0/0", "::/0" }
else
rule.network = "tcp,udp"
end
table.insert(rules, rule)
end
routing = {
domainStrategy = node.domainStrategy or "AsIs",
domainMatcher = node.domainMatcher or "hybrid",

View File

@@ -19,7 +19,6 @@ PKG_BUILD_DIR:=$(BUILD_DIR)/qtbase-$(PKG_VERSION)
PKG_BUILD_DEPENDS:=qt6base/host
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
PKG_BUILD_FLAGS:=no-mips16
CMAKE_INSTALL:=1
@@ -127,6 +126,7 @@ CMAKE_OPTIONS+= \
-DFEATURE_testlib=$(if $(CONFIG_PACKAGE_libQt6Test),ON,OFF) \
-DFEATURE_itemmodeltester=OFF \
-DFEATURE_widgets=$(if $(CONFIG_PACKAGE_libQt6Widgets),ON,OFF) \
-DFEATURE_commandlinkbutton=OFF \
-DFEATURE_xml=$(if $(CONFIG_PACKAGE_libQt6Xml),ON,OFF) \
-DFEATURE_tuiotouch=$(if $(CONFIG_PACKAGE_qt6-plugin-libqtuiotouchplugin),ON,OFF)

View File

@@ -12,7 +12,6 @@ PKG_BUILD_DIR:=$(BUILD_DIR)/libtorrent-$(PKG_VERSION)
PKG_LICENSE:=BSD
PKG_LICENSE_FILES:=COPYING
PKG_USE_MIPS16:=0
PKG_BUILD_FLAGS:=no-mips16
PKG_BUILD_PARALLEL:=1
PKG_INSTALL:=1
@@ -47,9 +46,8 @@ CMAKE_OPTIONS += \
-DCMAKE_BUILD_TYPE=Release \
-Ddeprecated-functions=OFF \
-Dlogging=OFF \
-DCMAKE_CXX_STANDARD=17 \
-Dpython-bindings=$(if $(CONFIG_PACKAGE_python3-libtorrent),ON,OFF) \
-Dpython-egg-info=$(if $(CONFIG_PACKAGE_python3-libtorrent),ON,OFF)
-Dpython-bindings=OFF \
-Dpython-egg-info=OFF
define Build/InstallDev
$(INSTALL_DIR) $(1)/usr/include