v1.3.17 feat: add SIGHUP hot-reload support, replace restart sequences with reload

- special thanks to @starfendou for the technical guidance on hot reloading.
- add `cmd_systemctl reload` subcommand with PID detection (Alpine: pidfile → kill -HUP; systemd: systemctl kill -s HUP)
- add OpenRC `reload()` function in sing-box service via start-stop-daemon --signal HUP
- replace `restart` with `reload` for config changes (ports, hy2realm, bindinterface, uuid/sni/name)
- replace `disable+enable` with `reload` for protocol add/remove and port change flows
- replace `restart` with `reload` for custom route add/delete operations
- remove unnecessary `sync_firewall_rules` call in hy2bw bandwidth change (only modifies subscribe/proxies, no port changes)
- run `sing-box check -C` with the new binary before replacing the running one. abort upgrade early when the new binary rejects the existing config
- force base-config regeneration on upgrade to guarantee sing-box check passes

v1.3.17 feat: 新增 SIGHUP 热更支持,用 reload 替换重启流程

- 感谢 @starfendou 在热加载(Hot Reloading)技术上给予的指导与支持。
- cmd_systemctl 新增 reload 子命令,支持 PID 检测(Alpine: pidfile → kill -HUP; systemd: systemctl kill -s HUP)
- OpenRC 服务脚本新增 reload() 函数,通过 start-stop-daemon --signal HUP 实现热更
- 端口 / hy2realm / 绑定接口 / 通用配置修改改为 reload
- 协议增删和端口变更流程从 disable+enable 改为 reload
- 自定义路由增删改为 reload
- 移除 hy2bw 带宽修改中无意义的 sync_firewall_rules 调用(只改 subscribe/proxies,不涉及端口)
- 替换运行中二进制前,先用新内核执行 `sing-box check -C` 预检配置,新内核不兼容现有配置时提前中止升级
- 升级时强制重置基础配置至新版格式,保证兼容性
This commit is contained in:
fscarmen
2026-07-21 06:39:28 +00:00
parent 4f29ea5c92
commit 56997fdbee
2 changed files with 125 additions and 100 deletions
+123 -100
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# 当前脚本版本号
VERSION='v1.3.16 (2026.07.16)'
VERSION='v1.3.17 (2026.07.20)'
# Github 反代加速代理
GITHUB_PROXY=('https://hub.glowp.xyz/' 'https://proxy.vvvv.ee/')
@@ -40,8 +40,8 @@ mkdir -p "$TEMP_DIR"
E[0]="Language:\n 1. English (default) \n 2. 简体中文"
C[0]="${E[0]}"
E[1]="1. Add bind_interface option in sb -d menu to bind outbound traffic to a specific NIC; 2. Change v2rayN Hysteria2 Realm config from Finalmask field to ProtoExtraObj"
C[1]="1. sb -d 菜单新增「指定网络出口」选项,可为出站流量绑定特定网卡; 2. v2rayN 的 Hysteria2 Realm 配置从 Finalmask 字段改为 ProtoExtraObj"
E[1]="1. Add bind_interface option in sb -d menu to bind outbound traffic to a specific NIC; 2. Change v2rayN Hysteria2 Realm config from Finalmask field to ProtoExtraObj; 3. add SIGHUP hot-reload support, replace restart sequences with reload; 4. force base-config regeneration on upgrade to guarantee sing-box check passes"
C[1]="1. sb -d 菜单新增「指定网络出口」选项,可为出站流量绑定特定网卡; 2. v2rayN 的 Hysteria2 Realm 配置从 Finalmask 字段改为 ProtoExtraObj; 3. 新增 SIGHUP 热更支持,用 reload 替换重启流程; 4. 新增 SIGHUP 热更支持,用 reload 替换重启流程; 4. 升级时强制重置基础配置至新版格式,保证兼容性"
E[2]="Downloading Sing-box. Please wait a seconds ..."
C[2]="下载 Sing-box 中,请稍等 ..."
E[3]="Input errors up to 5 times.The script is aborted."
@@ -146,8 +146,8 @@ E[52]="Please set the ip \[\${WS_SERVER_IP_SHOW}] to domain \[\${TYPE_HOST_DOMAI
C[52]="请在 Cloudflare 绑定 \[\${WS_SERVER_IP_SHOW}] 的域名为 \[\${TYPE_HOST_DOMAIN}], 并设置 origin rule 为 \[\${TYPE_PORT_WS}]"
E[53]="Please select or enter the preferred address (domain / IPv4 / [IPv6], optional :port), the default is \${CDN_DOMAIN[0]}:"
C[53]="请选择或者填入优选地址(域名 / IPv4 / [IPv6],可选 :端口),默认为 \${CDN_DOMAIN[0]}:"
E[54]=""
C[54]=""
E[54]="Configuration check failed, new version \$ONLINE is incompatible with current config."
C[54]="配置文件检查失败,新版本 \$ONLINE 与当前配置不兼容"
E[55]="The script runs today: \$TODAY. Total: \$TOTAL"
C[55]="脚本当天运行次数: \$TODAY,累计运行次数: \$TOTAL"
E[56]="Process ID"
@@ -262,8 +262,8 @@ E[110]="No CDN protocol is currently in use"
C[110]="当前没有使用 CDN 的协议"
E[111]="Please select or enter a new CDN (press Enter to keep the current one):"
C[111]="请选择或输入新的 CDN (回车保持当前值):"
E[112]="Change complete, restarting service..."
C[112]="修改完成,正在重启服务..."
E[112]="Change complete"
C[112]="修改完成"
E[113]="Failed to change CDN, using random privateKey"
C[113]="privateKey 格式失败次数过多,已使用随机私钥"
E[114]="Invalid privateKey format: expected a 43-character base64url-encoded string."
@@ -382,6 +382,12 @@ E[171]="1. Default (not specified)"
C[171]="1. 默认(不指定)"
E[172]="Bound interface updated to: "
C[172]="绑定接口已更新为: "
E[173]="Hot reload successful (PID unchanged: \$MAINPID)"
C[173]="热加载成功(PID 未变: \$MAINPID"
E[174]="Failed to update configuration. Please check manually. Suggestion: reinstall script"
C[174]="更新配置后仍然无法检查成功,建议重装脚本"
E[175]="Update base configuration? (Node configs will remain unaffected; only log, outbounds, endpoints, route, experimental, dns, ntp, http_clients, etc., will be reset) [Y/n]:"
C[175]="是否更新基础配置?(不影响节点配置,仅重置 log、outbounds、endpoints、route、experimental、dns、ntp、http_clients[Y/n]:"
# 自定义字体彩色,read 函数
warning() { echo -e "\033[31m\033[01m$*\033[0m"; } # 红色
@@ -751,18 +757,16 @@ change_config() {
OLD_START_PORT=$(awk 'NR == 1 { min = $0 } { if ($0 < min) min = $0; count++ } END {print min}' <<< "$OLD_PORTS")
OLD_CONSECUTIVE_PORTS=$(awk 'END { print NR }' <<< "$OLD_PORTS")
input_start_port $OLD_CONSECUTIVE_PORTS
cmd_systemctl disable sing-box
for ((a=0; a<$OLD_CONSECUTIVE_PORTS; a++)) do
[ -s ${WORK_DIR}/conf/${CONF_FILES[a]} ] && sed -i "s/\(.*listen_port.*:\)$((OLD_START_PORT+a))/\1$((START_PORT+a))/" ${WORK_DIR}/conf/*
done
fetch_nodes_value
[ -n "$PORT_NGINX" ] && UUID_CONFIRM=$(awk '/location/ && /\// {match($0, /\/([^ \/]+)/, arr); p=arr[1]; sub(/-(vmess|vless|auto2|auto).*/, "", p); print p; exit}' ${WORK_DIR}/nginx.conf) && export_nginx_conf_file
cmd_systemctl enable sing-box
cmd_systemctl reload sing-box
[ -n "$ARGO_DOMAIN" ] && export_argo_json_file
sync_firewall_rules
sleep 2
export_list
cmd_systemctl status sing-box &>/dev/null && info " Sing-box $(text 121) $(text 37) " || error " Sing-box $(text 121) $(text 38) "
return
elif [ "$KEY" = "hy2bw" ]; then
# 修改 Hysteria2 带宽
@@ -778,7 +782,6 @@ change_config() {
warning " $(text 143) "
done
sed -i -E "s/(up: \")([0-9]+)( Mbps\")/\1${HY2_UP}\3/g; s/(down: \")([0-9]+)( Mbps\")/\1${HY2_DOWN}\3/g" ${WORK_DIR}/subscribe/proxies
sync_firewall_rules
hint " $(text 112) "
export_list
return
@@ -799,8 +802,7 @@ change_config() {
set_hy2_realm_config enable
[ "$IS_HY2_WARP" = 'is_hy2_warp' ] && sync_hy2_warp_route enable || sync_hy2_warp_route disable
fi
hint " $(text 112) "
cmd_systemctl restart sing-box
cmd_systemctl reload sing-box
export_list
return
elif [ "$KEY" = "hy2hopping" ]; then
@@ -915,9 +917,7 @@ change_config() {
warning " Invalid selection " && return
fi
cmd_systemctl restart sing-box
sleep 2
cmd_systemctl status sing-box &>/dev/null && info "\n Sing-box $(text 28) $(text 37) \n" || warning "\n Sing-box $(text 27) $(text 38) \n"
cmd_systemctl reload sing-box
export_list
return
fi
@@ -936,8 +936,6 @@ change_config() {
fi
# 批量替换,更换服务IP和指纹不需重启服务
[[ ! "$KEY" =~ ^(fingerprint|serverip|cdn)$ ]] && hint " $(text 112) "
if [[ "$KEY" =~ ^(serverip|cdn)$ ]]; then
# IP 在配置里出现的形式有多种,逐一替换
find ${WORK_DIR} -type f | xargs -P 50 sed -i -e "s|\"server\": \"${OLD}\"|\"server\": \"${NEW_VAL}\"|g; s|\"WS_SERVER_IP_SHOW\": \"${OLD}\"|\"WS_SERVER_IP_SHOW\": \"${NEW_VAL}\"|g" 2>/dev/null
@@ -946,11 +944,7 @@ change_config() {
find ${WORK_DIR}/subscribe -type f | xargs -P 50 sed -i "s|${OLD}|${NEW_VAL}|g" 2>/dev/null
else
find ${WORK_DIR} -type f | xargs -P 50 sed -i "s|${OLD}|${NEW_VAL}|g" 2>/dev/null
if [[ ! "$KEY" =~ ^(fingerprint)$ ]]; then
cmd_systemctl restart sing-box
sleep 2
cmd_systemctl status sing-box &>/dev/null && info "\n Sing-box $(text 28) $(text 37) \n" || warning "\n Sing-box $(text 27) $(text 38) \n"
fi
[[ ! "$KEY" =~ ^(fingerprint)$ ]] && cmd_systemctl reload sing-box
fi
export_list
}
@@ -1549,8 +1543,7 @@ custom_route_add() {
custom_route_compact_rules
info " $(text 157) "
hint " $(text 112) "
cmd_systemctl restart sing-box
cmd_systemctl reload sing-box
sleep 2
cmd_systemctl status sing-box &>/dev/null && \
info "\n Sing-box $(text 28) $(text 37) \n" || \
@@ -1690,8 +1683,7 @@ custom_route_delete() {
fi
info " $(text 160) "
hint " $(text 112) "
cmd_systemctl restart sing-box
cmd_systemctl reload sing-box
sleep 2
cmd_systemctl status sing-box &>/dev/null && \
info "\n Sing-box $(text 28) $(text 37) \n" || \
@@ -2094,6 +2086,15 @@ cmd_systemctl() {
restart )
rc-service "$2" restart >/dev/null 2>&1
;;
reload )
local MAINPID=$(cat /var/run/sing-box.pid 2>/dev/null)
if [ -n "$MAINPID" ] && kill -0 "$MAINPID" 2>/dev/null; then
kill -HUP "$MAINPID" 2>/dev/null
info "\n $(text 173) \n"
else
rc-service "$2" restart >/dev/null 2>&1
fi
;;
status )
rc-service "$2" status
;;
@@ -2116,6 +2117,15 @@ cmd_systemctl() {
systemctl restart "$2" >/dev/null 2>&1
[ "$IS_CENTOS" = 'CentOS7' ] && [ "$2" = 'sing-box' ] && [ -s $WORK_DIR/nginx.conf ] && nginx_run
;;
reload )
local MAINPID=$(systemctl show -p MainPID sing-box 2>/dev/null | awk -F= '{print $2}')
if [ -n "$MAINPID" ] && [ "$MAINPID" -gt 0 ] 2>/dev/null; then
systemctl kill -s HUP sing-box >/dev/null 2>&1
info "\n $(text 173) \n"
else
systemctl restart sing-box >/dev/null 2>&1
fi
;;
status )
systemctl is-active "$2"
;;
@@ -3445,20 +3455,10 @@ http {
echo "$NGINX_CONF" > ${WORK_DIR}/nginx.conf
}
# 生成 sing-box 配置文件
sing-box_json() {
local IS_CHANGE=$1
mkdir -p ${WORK_DIR}/conf ${WORK_DIR}/logs ${WORK_DIR}/subscribe
# 判断是否为新安装,不为 change 就是新安装
if [ "$IS_CHANGE" = 'change' ]; then
# 判断 sing-box 主程序所在路径
DIR=${WORK_DIR}
else
DIR=$TEMP_DIR
# 生成 log 配置
cat > ${WORK_DIR}/conf/00_log.json << EOF
# 生成 sing-box 基础配置
generate_sing_box_base_conf() {
# 生成 log 配置
cat > ${WORK_DIR}/conf/00_log.json << EOF
{
"log":{
"disabled":false,
@@ -3469,8 +3469,8 @@ sing-box_json() {
}
EOF
# 生成 outbound 配置
cat > ${WORK_DIR}/conf/01_outbounds.json << EOF
# 生成 outbound 配置
cat > ${WORK_DIR}/conf/01_outbounds.json << EOF
{
"outbounds":[
{
@@ -3482,8 +3482,8 @@ EOF
}
EOF
# 生成 endpoint 配置
cat > ${WORK_DIR}/conf/02_endpoints.json << EOF
# 生成 endpoint 配置
cat > ${WORK_DIR}/conf/02_endpoints.json << EOF
{
"endpoints":[
{
@@ -3516,8 +3516,8 @@ EOF
}
EOF
# 生成 route 配置
cat > ${WORK_DIR}/conf/03_route.json << EOF
# 生成 route 配置
cat > ${WORK_DIR}/conf/03_route.json << EOF
{
"route":{
"default_http_client": "http-client-direct",
@@ -3554,15 +3554,15 @@ EOF
"rule_set":[
"geosite-openai"
],
"outbound":"${CHATGPT_OUT}"
"outbound":"${CHATGPT_OUT:-direct}"
}
]
}
}
EOF
# 生成缓存文件
cat > ${WORK_DIR}/conf/04_experimental.json << EOF
# 生成缓存文件
cat > ${WORK_DIR}/conf/04_experimental.json << EOF
{
"experimental": {
"cache_file": {
@@ -3573,8 +3573,8 @@ EOF
}
EOF
# 生成 dns 配置文件
cat > ${WORK_DIR}/conf/05_dns.json << EOF
# 生成 dns 配置文件
cat > ${WORK_DIR}/conf/05_dns.json << EOF
{
"dns":{
"servers":[
@@ -3588,8 +3588,8 @@ EOF
}
EOF
# 内建的 NTP 客户端服务配置文件,这对于无法进行时间同步的环境很有用
cat > ${WORK_DIR}/conf/06_ntp.json << EOF
# 内建的 NTP 客户端服务配置文件,这对于无法进行时间同步的环境很有用
cat > ${WORK_DIR}/conf/06_ntp.json << EOF
{
"ntp": {
"enabled": true,
@@ -3600,8 +3600,8 @@ EOF
}
EOF
# 专门给 sing-box 内部组件发 HTTP 请求用,比如这些场景会用到它:下载远程 rule_set:.srs 规则文件,ACME 申请证书,Cloudflare Origin CA 证书提供器,DERP / Tailscale 相关 HTTP 请求
cat > ${WORK_DIR}/conf/07_http_clients.json << EOF
# 专门给 sing-box 内部组件发 HTTP 请求用,比如这些场景会用到它:下载远程 rule_set:.srs 规则文件,ACME 申请证书,Cloudflare Origin CA 证书提供器,DERP / Tailscale 相关 HTTP 请求
cat > ${WORK_DIR}/conf/07_http_clients.json << EOF
{
"http_clients": [
{
@@ -3610,6 +3610,20 @@ EOF
]
}
EOF
}
# 生成 sing-box 配置文件
sing-box_json() {
local IS_CHANGE=$1
mkdir -p ${WORK_DIR}/conf ${WORK_DIR}/logs ${WORK_DIR}/subscribe
# 判断是否为新安装,不为 change 就是新安装
if [ "$IS_CHANGE" = 'change' ]; then
# 判断 sing-box 主程序所在路径
DIR=${WORK_DIR}
else
DIR=$TEMP_DIR
generate_sing_box_base_conf
fi
# 生成 Reality 公私钥,第一次安装的时候,如有指定的私钥,则使用该私钥及生成对应的公钥;如没有指定私钥则使用新生成的;添加协议的时,使用相应数组里的第一个非空值,如全空则像第一次安装那样使用新生成的
@@ -4224,10 +4238,16 @@ depend() {
[ -n "$PORT_NGINX" ] && OPENRC_SERVICE+="
need nginx"
# 添加 start_pre 函数,确保目录存在并设置正确权限
# 添加 reload 函数,支持 SIGHUP 热更
OPENRC_SERVICE+="
}
reload() {
ebegin \"Reloading \${RC_SVCNAME}\"
start-stop-daemon --signal HUP --pidfile \$pidfile
eend \$? \"Failed to reload \${RC_SVCNAME}\"
}
start_pre() {
# 确保日志目录和PID目录存在并有正确权限
mkdir -p ${WORK_DIR}/logs
@@ -5583,9 +5603,6 @@ change_protocols() {
unset PORT_NAIVE
fi
# 停止 sing-box 服务
cmd_systemctl disable sing-box
# 生成 Nginx 配置文件
[ -n "$PORT_NGINX" ] && export_nginx_conf_file
@@ -5610,8 +5627,8 @@ change_protocols() {
# 如有需要,删除 nginx 配置文件
! ls ${ARGO_DAEMON_FILE} >/dev/null 2>&1 && [[ -s ${WORK_DIR}/nginx.conf && "$IS_SUB" = 'no_sub' ]] && IS_ARGO=no_argo && rm -f ${WORK_DIR}/nginx.conf
# 运行 sing-box
cmd_systemctl enable sing-box
# 热更 sing-box(SIGHUP 重新加载配置,PID 不变)
cmd_systemctl reload sing-box
# 打开防火墙相关端口
sync_firewall_rules
@@ -5621,17 +5638,6 @@ change_protocols() {
# 再次检测状态,运行 sing-box
check_install
case "${STATUS[0]}" in
"$(text 26)" )
error "\n Sing-box $(text 28) $(text 38) \n"
;;
"$(text 27)" )
cmd_systemctl enable sing-box
cmd_systemctl status sing-box &>/dev/null && info "\n Sing-box $(text 28) $(text 37) \n" || error "\n Sing-box $(text 28) $(text 38) \n"
;;
"$(text 28)" )
info "\n Sing-box $(text 28) $(text 37) \n"
esac
# 导出节点和订阅服务信息
export_list
@@ -5669,38 +5675,55 @@ version() {
check_system_info
wget --no-check-certificate --continue ${GH_PROXY}https://github.com/SagerNet/sing-box/releases/download/v$ONLINE/sing-box-$ONLINE-linux-$SING_BOX_ARCH.tar.gz -qO- | tar xz -C $TEMP_DIR sing-box-$ONLINE-linux-$SING_BOX_ARCH/sing-box
if [ -s $TEMP_DIR/sing-box-$ONLINE-linux-$SING_BOX_ARCH/sing-box ]; then
cmd_systemctl disable sing-box
[ -s $TEMP_DIR/sing-box-$ONLINE-linux-$SING_BOX_ARCH/sing-box ] || error "\n $(text 42) \n"
if ! $TEMP_DIR/sing-box-$ONLINE-linux-$SING_BOX_ARCH/sing-box check -C ${WORK_DIR}/conf >/dev/null; then
warning "\n $(text 54) " && reading "\n $(text 175) " UPDATE_CONFIG
[ "${UPDATE_CONFIG,,}" = 'n' ] && exit 1
# 备份旧版本
cp ${WORK_DIR}/sing-box ${WORK_DIR}/sing-box.bak
hint "\n $(text 102) \n"
# 设置基础配置参数 dns.servers.prefer_go 和 dns.strategy
local STRATEGY=$(grep -E --exclude="03_route.json" 'ipv4_only|ipv6_only|prefer_ipv4|prefer_ipv6' ${WORK_DIR}/conf/0*.json | awk -F '"' '{print $(NF-1); exit}')
STRATEGY=${STRATEGY:-prefer_ipv4}
command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet systemd-resolved && local IS_PREFER_GO=false || local IS_PREFER_GO=true
# 安装新版本
chmod +x $TEMP_DIR/sing-box-$ONLINE-linux-$SING_BOX_ARCH/sing-box && mv $TEMP_DIR/sing-box-$ONLINE-linux-$SING_BOX_ARCH/sing-box ${WORK_DIR}/sing-box
# 备份旧基础配置
for i in $(ls ${WORK_DIR}/conf/0*); do
cp $i ${i}.bak
done
generate_sing_box_base_conf
if ! $TEMP_DIR/sing-box-$ONLINE-linux-$SING_BOX_ARCH/sing-box check -C ${WORK_DIR}/conf >/dev/null; then
for i in $(ls ${WORK_DIR}/conf/0*.bak); do
mv $i ${i%%.bak}
done
error "\n $(text 174) \n"
fi
fi
cmd_systemctl disable sing-box
# 备份旧版本
cp ${WORK_DIR}/sing-box ${WORK_DIR}/sing-box.bak
hint "\n $(text 102) \n"
# 安装新版本
chmod +x $TEMP_DIR/sing-box-$ONLINE-linux-$SING_BOX_ARCH/sing-box && mv $TEMP_DIR/sing-box-$ONLINE-linux-$SING_BOX_ARCH/sing-box ${WORK_DIR}/sing-box
cmd_systemctl enable sing-box
sleep 2
# 检查新版本是否成功运行
if cmd_systemctl status sing-box &>/dev/null; then
# 新版本运行成功,删除备份
rm -f ${WORK_DIR}/sing-box.bak ${WORK_DIR}/conf/*.bak
info "\n $(text 103) \n"
else
# 新版本运行失败,恢复旧版本
warning "\n $(text 104) \n"
mv ${WORK_DIR}/sing-box.bak ${WORK_DIR}/sing-box
rm -f ${WORK_DIR}/conf/*.bak
cmd_systemctl enable sing-box
sleep 2
# 检查新版本是否成功运行
if cmd_systemctl status sing-box &>/dev/null; then
# 新版本运行成功,删除备份
rm -f ${WORK_DIR}/sing-box.bak
info "\n $(text 103) \n"
else
# 新版本运行失败,恢复旧版本
warning "\n $(text 104) \n"
mv ${WORK_DIR}/sing-box.bak ${WORK_DIR}/sing-box
cmd_systemctl enable sing-box
sleep 2
if cmd_systemctl status sing-box &>/dev/null; then
info "\n $(text 105) \n"
else
error "\n $(text 106) \n"
fi
fi
else
error "\n $(text 42) "
cmd_systemctl status sing-box &>/dev/null && info "\n $(text 105) \n" || error "\n $(text 106) \n"
fi
fi
}