388 lines
14 KiB
Bash
388 lines
14 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# mihomo-up.sh — mihomo 内核管理脚本 (交互菜单 + 命令行双模式)
|
|
#
|
|
# bash <(curl -fsSL https://你的地址/mihomo-up.sh) # 交互菜单
|
|
# ./mihomo-up.sh check # 非交互, 给 cron/CI 用
|
|
#
|
|
set -Eeuo pipefail
|
|
|
|
REPO="MetaCubeX/mihomo"
|
|
BIN="${MIHOMO_BIN:-/usr/local/bin/mihomo}"
|
|
CONF_DIR="${MIHOMO_CONF_DIR:-/etc/mihomo}"
|
|
SERVICE="${MIHOMO_SERVICE:-mihomo}"
|
|
BACKUP_DIR="${MIHOMO_BACKUP_DIR:-/var/backups/mihomo}"
|
|
KEEP=3
|
|
|
|
ARCH_OVERRIDE=""; VARIANT=""; WANT_VERSION=""
|
|
FORCE=0; DRY_RUN=0; NO_RESTART=0; JSON=0; ASSUME_YES=0
|
|
TMPDIR_=""
|
|
|
|
# ───────────────────────── 输出 ─────────────────────────
|
|
if [[ -t 1 ]]; then
|
|
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[34m'
|
|
C=$'\033[36m'; W=$'\033[1m'; D=$'\033[2m'; N=$'\033[0m'
|
|
else
|
|
R=""; G=""; Y=""; B=""; C=""; W=""; D=""; N=""
|
|
fi
|
|
|
|
log() { (( JSON )) || printf '%s\n' "$*" >&2; }
|
|
info() { log "${D}··${N} $*"; }
|
|
ok() { log "${G}✓${N} $*"; }
|
|
warn() { log "${Y}!${N} $*"; }
|
|
die() { printf '%s✗%s %s\n' "$R" "$N" "$*" >&2; exit 1; }
|
|
|
|
cleanup() {
|
|
local rc=$?
|
|
trap - ERR
|
|
[[ -n "${TMPDIR_:-}" && -d "$TMPDIR_" ]] && rm -rf "$TMPDIR_"
|
|
return $rc
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'die "第 $LINENO 行出错, 已中止"' ERR
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
mihomo-up.sh — 检查 / 安装 / 升级 / 回滚 mihomo 内核
|
|
|
|
mihomo-up.sh 进入交互菜单 (终端下默认)
|
|
mihomo-up.sh check 只查, 不改任何东西
|
|
mihomo-up.sh update [--force] 升级内核
|
|
mihomo-up.sh rollback 回滚到上一个备份
|
|
mihomo-up.sh list 列出本地备份
|
|
|
|
选项:
|
|
-y, --yes 非交互确认, 全部默认同意
|
|
-n, --dry-run 只打印会做什么
|
|
--version <v> 指定版本, 如 v1.19.28
|
|
--no-restart 装完不启动服务
|
|
--json check 时额外输出一行 JSON
|
|
--arch <a> amd64 | arm64 | armv7
|
|
--variant <v> amd64 微架构: v1 | v2 | v3 | compatible
|
|
--bin <path> 默认 /usr/local/bin/mihomo
|
|
--conf-dir <dir> 默认 /etc/mihomo
|
|
|
|
check 退出码: 0 = 已是最新 10 = 有新版 1 = 出错
|
|
EOF
|
|
exit "${1:-0}"
|
|
}
|
|
|
|
# ───────────────────────── 交互输入 ─────────────────────────
|
|
# 关键: 从 /dev/tty 读, 这样 bash <(curl ...) 和管道场景都不会把 stdin 抢走
|
|
TTY_IN="/dev/stdin"
|
|
[[ -r /dev/tty ]] && TTY_IN="/dev/tty"
|
|
|
|
ask() { # ask "提示" varname
|
|
local prompt="$1" __var="$2" __ans=""
|
|
printf '%s' "$prompt" >&2
|
|
read -r __ans < "$TTY_IN" || __ans=""
|
|
printf -v "$__var" '%s' "$__ans"
|
|
}
|
|
|
|
confirm() { # confirm "问题?" -> 0/1
|
|
(( ASSUME_YES )) && return 0
|
|
local a; ask "$1 ${D}[y/N]${N} " a
|
|
[[ "$a" =~ ^[Yy]$ ]]
|
|
}
|
|
|
|
pause() { local _x; ask "$(printf '\n%s按回车继续…%s' "$D" "$N")" _x; }
|
|
|
|
# ───────────────────────── 基础能力 ─────────────────────────
|
|
need() { command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1"; }
|
|
|
|
detect_arch() {
|
|
[[ -n "$ARCH_OVERRIDE" ]] && { echo "$ARCH_OVERRIDE"; return; }
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) echo "amd64" ;;
|
|
aarch64|arm64) echo "arm64" ;;
|
|
armv7l|armv7) echo "armv7" ;;
|
|
*) die "未适配的架构: $(uname -m) (可用 --arch 指定)" ;;
|
|
esac
|
|
}
|
|
|
|
asset_name() {
|
|
local arch suffix=""; arch="$(detect_arch)"
|
|
[[ "$arch" == "amd64" && -n "$VARIANT" ]] && suffix="-${VARIANT}"
|
|
echo "mihomo-linux-${arch}${suffix}-${1}.gz"
|
|
}
|
|
|
|
version_line() { # 不用 head, 避免 SIGPIPE 触发 ERR trap
|
|
local out; out="$("$1" -v 2>/dev/null || true)"
|
|
printf '%s' "${out%%$'\n'*}"
|
|
}
|
|
|
|
current_version() {
|
|
[[ -x "$BIN" ]] || { printf ''; return; }
|
|
local line; line="$(version_line "$BIN")"
|
|
[[ -n "$line" ]] && awk '{print $3}' <<<"$line" || printf ''
|
|
}
|
|
|
|
latest_version() {
|
|
local v
|
|
v="$(curl -fsSL --retry 3 --connect-timeout 10 \
|
|
"https://github.com/${REPO}/releases/latest/download/version.txt" | tr -d '[:space:]')" \
|
|
|| die "拉取版本号失败 (GitHub 不通?)"
|
|
[[ "$v" =~ ^v[0-9]+\.[0-9]+ ]] || die "版本号格式异常: $v"
|
|
echo "$v"
|
|
}
|
|
|
|
svc_state() {
|
|
command -v systemctl >/dev/null 2>&1 || { echo "无 systemd"; return; }
|
|
if systemctl is-active --quiet "$SERVICE" 2>/dev/null; then echo "running"
|
|
elif systemctl list-unit-files 2>/dev/null | grep -q "^${SERVICE}\.service"; then echo "stopped"
|
|
else echo "未安装"; fi
|
|
}
|
|
|
|
# ───────────────────────── 动作 ─────────────────────────
|
|
do_update() { # $1 = 目标版本
|
|
local target="$1" cur; cur="$(current_version)"
|
|
|
|
if [[ "$cur" == "$target" && $FORCE -eq 0 ]]; then
|
|
ok "已是最新 ($cur), 无需操作"; return 0
|
|
fi
|
|
[[ ${EUID:-$(id -u)} -eq 0 ]] || die "需要 root (要写 $BIN 并重启服务)"
|
|
|
|
TMPDIR_="$(mktemp -d)"
|
|
local asset url; asset="$(asset_name "$target")"
|
|
url="https://github.com/${REPO}/releases/download/${target}/${asset}"
|
|
|
|
info "下载 $asset"
|
|
(( DRY_RUN )) && { ok "[dry-run] 会下载 $url"; return 0; }
|
|
curl -fL -# --retry 3 --connect-timeout 15 -o "$TMPDIR_/m.gz" "$url" || die "下载失败: $url"
|
|
gzip -dc "$TMPDIR_/m.gz" > "$TMPDIR_/mihomo" || die "解压失败"
|
|
chmod +x "$TMPDIR_/mihomo"
|
|
|
|
# 闸门 1: 新二进制能跑, 版本号对得上
|
|
local newline newver
|
|
newline="$(version_line "$TMPDIR_/mihomo")"
|
|
[[ -n "$newline" ]] || die "新二进制无法执行 (架构选错?)"
|
|
newver="$(awk '{print $3}' <<<"$newline")"
|
|
[[ "$newver" == "$target" ]] || die "版本对不上: 期望 $target, 实际 $newver"
|
|
ok "校验通过: $newline"
|
|
|
|
# 闸门 2: 用新二进制验现有配置
|
|
if [[ -f "$CONF_DIR/config.yaml" ]]; then
|
|
if "$TMPDIR_/mihomo" -t -d "$CONF_DIR" >"$TMPDIR_/test.log" 2>&1; then
|
|
ok "配置在新版本下校验通过"
|
|
else
|
|
warn "新版本校验现有配置失败:"; sed 's/^/ /' "$TMPDIR_/test.log" >&2
|
|
die "已中止, 未替换任何文件"
|
|
fi
|
|
fi
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
if [[ -x "$BIN" && -n "$cur" ]]; then
|
|
cp -a "$BIN" "$BACKUP_DIR/mihomo-$cur"; info "已备份旧版 → $BACKUP_DIR/mihomo-$cur"
|
|
fi
|
|
|
|
local was_active=0
|
|
[[ "$(svc_state)" == "running" ]] && was_active=1
|
|
|
|
trap - ERR # 下面自行处理失败并回滚
|
|
(( was_active )) && { info "停止 $SERVICE"; systemctl stop "$SERVICE"; }
|
|
install -m 0755 "$TMPDIR_/mihomo" "$BIN"
|
|
ok "已安装 $target → $BIN"
|
|
|
|
if (( NO_RESTART )); then
|
|
warn "--no-restart: 记得手动 systemctl start $SERVICE"
|
|
elif (( was_active )); then
|
|
systemctl start "$SERVICE" || true
|
|
sleep 2
|
|
if [[ "$(svc_state)" == "running" ]]; then
|
|
ok "$SERVICE 运行中"
|
|
else
|
|
warn "$SERVICE 启动失败, 自动回滚"
|
|
[[ -f "$BACKUP_DIR/mihomo-$cur" ]] && {
|
|
install -m 0755 "$BACKUP_DIR/mihomo-$cur" "$BIN"
|
|
systemctl start "$SERVICE" || true
|
|
warn "已回滚到 $cur; 查日志: journalctl -u $SERVICE -n 50"
|
|
}
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
compgen -G "$BACKUP_DIR/mihomo-*" >/dev/null && \
|
|
ls -1t "$BACKUP_DIR"/mihomo-* | tail -n +$((KEEP + 1)) | xargs -r rm -f
|
|
ok "完成: ${cur:-无} → $target"
|
|
trap 'die "第 $LINENO 行出错, 已中止"' ERR
|
|
}
|
|
|
|
do_rollback() {
|
|
local b; b="$(ls -1t "$BACKUP_DIR"/mihomo-* 2>/dev/null | head -n1 || true)"
|
|
[[ -n "$b" ]] || { warn "没有可回滚的备份"; return 1; }
|
|
info "回滚到 $(basename "$b")"
|
|
(( DRY_RUN )) && { ok "[dry-run] 到此为止"; return 0; }
|
|
systemctl stop "$SERVICE" 2>/dev/null || true
|
|
install -m 0755 "$b" "$BIN"
|
|
systemctl start "$SERVICE" 2>/dev/null || true
|
|
ok "已回滚: $(version_line "$BIN")"
|
|
}
|
|
|
|
do_list() {
|
|
if compgen -G "$BACKUP_DIR/mihomo-*" >/dev/null 2>&1; then
|
|
ls -1t "$BACKUP_DIR"/mihomo-* | while read -r f; do
|
|
printf ' %s %s%s%s\n' "$(basename "$f")" "$D" "$(date -r "$f" '+%F %T')" "$N"
|
|
done
|
|
else
|
|
log " ${D}没有备份 ($BACKUP_DIR)${N}"
|
|
fi
|
|
}
|
|
|
|
do_test_conf() {
|
|
[[ -x "$BIN" ]] || { warn "mihomo 还没装"; return 1; }
|
|
[[ -f "$CONF_DIR/config.yaml" ]] || { warn "找不到 $CONF_DIR/config.yaml"; return 1; }
|
|
if "$BIN" -t -d "$CONF_DIR"; then ok "配置有效"; else warn "配置有问题, 见上面报错"; fi
|
|
}
|
|
|
|
# ───────────────────────── 菜单 ─────────────────────────
|
|
banner() {
|
|
printf '%s\n' "" >&2
|
|
cat >&2 <<EOF
|
|
${C} ╭──────────────────────────────────────────╮
|
|
│ ${W}mihomo 内核管理${N}${C} ╭( ・ㅂ・)و ✧ │
|
|
╰──────────────────────────────────────────╯${N}
|
|
EOF
|
|
}
|
|
|
|
status_panel() {
|
|
local cur latest svc conf
|
|
cur="$(current_version)"; svc="$(svc_state)"
|
|
latest="$(latest_version 2>/dev/null || echo '查询失败')"
|
|
|
|
if [[ -x "$BIN" && -f "$CONF_DIR/config.yaml" ]]; then
|
|
"$BIN" -t -d "$CONF_DIR" >/dev/null 2>&1 && conf="${G}有效${N}" || conf="${R}有错${N}"
|
|
else
|
|
conf="${D}无配置${N}"
|
|
fi
|
|
|
|
local cur_s="${cur:-${Y}未安装${N}}" latest_s="$latest"
|
|
[[ -n "$cur" && "$cur" != "$latest" && "$latest" != "查询失败" ]] && latest_s="${Y}${latest} ← 有新版${N}"
|
|
[[ -n "$cur" && "$cur" == "$latest" ]] && latest_s="${G}${latest}${N}"
|
|
|
|
case "$svc" in
|
|
running) svc="${G}● 运行中${N}" ;;
|
|
stopped) svc="${Y}○ 已停止${N}" ;;
|
|
*) svc="${D}${svc}${N}" ;;
|
|
esac
|
|
|
|
printf ' %s当前版本%s %s\n' "$D" "$N" "$cur_s" >&2
|
|
printf ' %s最新版本%s %s\n' "$D" "$N" "$latest_s" >&2
|
|
printf ' %s服务状态%s %s\n' "$D" "$N" "$svc" >&2
|
|
printf ' %s配置校验%s %s\n' "$D" "$N" "$conf" >&2
|
|
printf ' %s架构 %s %s\n' "$D" "$N" "$(detect_arch)" >&2
|
|
}
|
|
|
|
menu() {
|
|
while true; do
|
|
clear 2>/dev/null || true
|
|
banner
|
|
status_panel
|
|
cat >&2 <<EOF
|
|
|
|
${W} 选个操作吧:${N}
|
|
${C}1.${N} 🔍 检查更新
|
|
${C}2.${N} ⬆️ 升级到最新版
|
|
${C}3.${N} 📌 安装指定版本
|
|
${C}4.${N} ⏪ 回滚到上一版
|
|
${C}5.${N} 📦 查看本地备份
|
|
${C}6.${N} 🧪 校验配置文件
|
|
${C}7.${N} 🎛️ 服务控制 (启停/日志)
|
|
${C}0.${N} 👋 退出
|
|
EOF
|
|
local c; ask "$(printf '\n %s>%s ' "$W" "$N")" c
|
|
echo >&2
|
|
case "$c" in
|
|
1) local cur latest; cur="$(current_version)"; latest="$(latest_version)"
|
|
if [[ -z "$cur" ]]; then warn "还没安装, 选 2 装上"
|
|
elif [[ "$cur" == "$latest" ]]; then ok "已是最新 $cur"
|
|
else warn "有新版: $cur → $latest"
|
|
confirm " 现在升级?" && do_update "$latest"; fi
|
|
pause ;;
|
|
2) local latest; latest="$(latest_version)"
|
|
confirm " 升级/重装到 $latest ?" && { FORCE=1; do_update "$latest"; FORCE=0; }
|
|
pause ;;
|
|
3) local v; ask " 输入版本号 (如 v1.19.28): " v
|
|
[[ "$v" =~ ^v[0-9] ]] && { FORCE=1; do_update "$v"; FORCE=0; } || warn "版本号格式不对"
|
|
pause ;;
|
|
4) confirm " 确认回滚?" && do_rollback; pause ;;
|
|
5) echo >&2; do_list; pause ;;
|
|
6) do_test_conf || true; pause ;;
|
|
7) svc_menu ;;
|
|
0|q|"") log " ${D}bye~${N}"; exit 0 ;;
|
|
*) warn "没这个选项啦"; sleep 1 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
svc_menu() {
|
|
cat >&2 <<EOF
|
|
|
|
${C}1.${N} ▶️ 启动 ${C}2.${N} ⏹️ 停止 ${C}3.${N} 🔄 重启
|
|
${C}4.${N} 📊 状态 ${C}5.${N} 📜 最近日志 ${C}0.${N} ↩️ 返回
|
|
EOF
|
|
local c; ask "$(printf '\n %s>%s ' "$W" "$N")" c
|
|
case "$c" in
|
|
1) systemctl start "$SERVICE" && ok "已启动" ;;
|
|
2) systemctl stop "$SERVICE" && ok "已停止" ;;
|
|
3) systemctl restart "$SERVICE" && ok "已重启" ;;
|
|
4) systemctl status "$SERVICE" --no-pager || true ;;
|
|
5) journalctl -u "$SERVICE" -n 40 --no-pager || true ;;
|
|
*) return 0 ;;
|
|
esac
|
|
pause
|
|
}
|
|
|
|
# ───────────────────────── 参数解析 ─────────────────────────
|
|
CMD=""
|
|
if [[ $# -eq 0 ]]; then
|
|
# stdin 是终端就进菜单; 被重定向/管道 (cron、CI) 就安静地跑 check
|
|
if [[ -t 0 ]] || { [[ -t 1 ]] && [[ -r /dev/tty ]]; }; then CMD="menu"; else CMD="check"; fi
|
|
else
|
|
case "$1" in
|
|
check|update|install|rollback|list|menu) CMD="$1"; shift ;;
|
|
-h|--help|help) usage 0 ;;
|
|
-*) CMD="check" ;;
|
|
*) echo "未知子命令: $1" >&2; usage 1 ;;
|
|
esac
|
|
fi
|
|
[[ "$CMD" == install ]] && { CMD="update"; FORCE=1; }
|
|
|
|
while (( $# )); do
|
|
case "$1" in
|
|
--force) FORCE=1 ;;
|
|
-y|--yes) ASSUME_YES=1 ;;
|
|
-n|--dry-run) DRY_RUN=1 ;;
|
|
--no-restart) NO_RESTART=1 ;;
|
|
--json) JSON=1 ;;
|
|
--version) WANT_VERSION="${2:?}"; shift ;;
|
|
--arch) ARCH_OVERRIDE="${2:?}"; shift ;;
|
|
--variant) VARIANT="${2:?}"; shift ;;
|
|
--bin) BIN="${2:?}"; shift ;;
|
|
--conf-dir) CONF_DIR="${2:?}"; shift ;;
|
|
-h|--help) usage 0 ;;
|
|
*) die "未知参数: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
need curl; need gzip; need install
|
|
|
|
# ───────────────────────── 分发 ─────────────────────────
|
|
case "$CMD" in
|
|
menu) menu ;;
|
|
list) do_list ;;
|
|
rollback) do_rollback ;;
|
|
check)
|
|
CUR="$(current_version)"; TARGET="${WANT_VERSION:-$(latest_version)}"
|
|
(( JSON )) && printf '{"current":"%s","latest":"%s","outdated":%s}\n' \
|
|
"${CUR:-null}" "$TARGET" "$([[ "$CUR" != "$TARGET" ]] && echo true || echo false)"
|
|
log "当前: ${CUR:-${Y}未安装${N}} 最新: $TARGET"
|
|
[[ -z "$CUR" ]] && { log "→ 未安装"; exit 10; }
|
|
[[ "$CUR" == "$TARGET" ]] && { ok "已是最新"; exit 0; }
|
|
warn "有新版本可升级"; exit 10 ;;
|
|
update)
|
|
TARGET="${WANT_VERSION:-$(latest_version)}"
|
|
CUR="$(current_version)"
|
|
log "当前: ${CUR:-${Y}未安装${N}} 目标: $TARGET"
|
|
do_update "$TARGET" ;;
|
|
esac |