Files
mihomo-up/mihomo-up.sh
T
im0o 8e4505391d Initial commit: mihomo 内核管理脚本
bash 脚本,用于在 Linux 服务器上检查、安装、升级、回滚 mihomo 内核,
支持交互菜单与命令行双模式,带安全验证、自动备份与回滚。
2026-08-09 12:51:57 +08:00

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