release / release (push) Failing after 1m11s
产物是架构无关的 JS tarball(约 400 KB)而不是 bun 单文件: 单文件 94 MB 里几乎全是运行时,静态文件还得外置。架构差异 由安装侧的 bun 运行时吸收,版本钉死,和 CI 构建版本一起改。
391 lines
15 KiB
Bash
391 lines
15 KiB
Bash
#!/usr/bin/env bash
|
||
#
|
||
# install.sh — Patchbay 的安装 / 更新 / 卸载脚本
|
||
#
|
||
# 它管四样东西:
|
||
# 1. bun 运行时(私有副本,版本钉死) 默认 /opt/patchbay/bun/bun
|
||
# 2. 应用本体(来自 Gitea Release) 默认 /opt/patchbay/app/
|
||
# 3. 环境变量文件 /etc/patchbay/env
|
||
# 4. systemd 服务 /etc/systemd/system/patchbay.service
|
||
#
|
||
# bash <(curl -fsSL https://git.im0o.top/im0o/mihomo-patchbay/raw/branch/main/install.sh)
|
||
# └─ 安装;已装过就更新到最新 Release(环境变量文件保持不动)
|
||
# install.sh check # 只查询有没有新版,不改动任何东西,退出码见 --help
|
||
# install.sh uninstall # 卸载;--purge 连环境变量文件和访问令牌一起删除
|
||
#
|
||
# 前提:机器上已经用 mihomo-up.sh 装好了 mihomo(Patchbay 要读它的配置文件、
|
||
# 调它的二进制做校验、连它的管理接口)。还没装先去装那个。
|
||
#
|
||
set -Eeuo pipefail
|
||
|
||
SELF_VERSION="1.0.0"
|
||
|
||
REPO_URL="https://git.im0o.top/im0o/mihomo-patchbay"
|
||
# bun 版本钉死,避免运行时行为漂移;和 .gitea/workflows/release.yml 的构建版本一起改
|
||
BUN_VERSION="${PATCHBAY_BUN_VERSION:-1.3.14}"
|
||
|
||
PREFIX="${PATCHBAY_PREFIX:-/opt/patchbay}"
|
||
ENV_FILE="/etc/patchbay/env"
|
||
SERVICE="${PATCHBAY_SERVICE:-patchbay}"
|
||
UNIT_FILE="/etc/systemd/system/${SERVICE}.service"
|
||
|
||
# mihomo-up.sh 装出来的默认布局;mihomo 装在别处时用这两个环境变量覆盖
|
||
MIHOMO_BIN="${MIHOMO_BIN:-/usr/local/bin/mihomo}"
|
||
MIHOMO_CONF_DIR="${MIHOMO_CONF_DIR:-/etc/mihomo}"
|
||
|
||
# GitHub 下载前缀(bun 从 GitHub 下载)。直连不通时填个反代,例如 https://ghfast.top/
|
||
MIRROR="${MIHOMO_MIRROR:-}"
|
||
|
||
WANT_VERSION=""; FORCE=0; ASSUME_YES=0; PURGE=0
|
||
TMPDIR_=""
|
||
|
||
# ───────────────────────── 输出 ─────────────────────────
|
||
if [[ -t 1 ]]; then
|
||
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; W=$'\033[1m'; D=$'\033[2m'; N=$'\033[0m'
|
||
else
|
||
R=""; G=""; Y=""; W=""; D=""; N=""
|
||
fi
|
||
|
||
log() { 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'
|
||
install.sh — 安装 / 更新 / 卸载 Patchbay
|
||
|
||
install.sh 安装;已装过就更新到最新 Release
|
||
install.sh check 只查询有没有新版,不改动任何东西
|
||
install.sh uninstall [--purge]
|
||
|
||
选项:
|
||
-y, --yes 非交互确认,所有询问都当作同意(用默认值)
|
||
--force 版本相同也重新安装
|
||
--version <v> 装指定版本的 Release,如 v0.1.0;也用于回滚
|
||
--mirror <url> GitHub 下载前缀(下载 bun 用),如 https://ghfast.top/
|
||
--purge uninstall 时连环境变量文件和访问令牌一起删除
|
||
-v 打印本脚本自身的版本号
|
||
|
||
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_default() { # ask_default "提示" "默认值" varname
|
||
local __adf_def="$2" __adf_var="$3" __adf_ans=""
|
||
if (( ASSUME_YES )); then printf -v "$__adf_var" '%s' "$__adf_def"; return 0; fi
|
||
printf '%s %s[%s]%s ' "$1" "$D" "$__adf_def" "$N" >&2
|
||
read -r __adf_ans < "$TTY_IN" || __adf_ans=""
|
||
[[ -z "$__adf_ans" ]] && __adf_ans="$__adf_def"
|
||
printf -v "$__adf_var" '%s' "$__adf_ans"
|
||
}
|
||
|
||
# ───────────────────────── 基础能力 ─────────────────────────
|
||
need() { command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1"; }
|
||
|
||
require_root() {
|
||
[[ ${EUID:-$(id -u)} -eq 0 ]] || die "$1 需要 root 权限,请用 sudo 重新运行"
|
||
}
|
||
|
||
gh_url() { # gh_url "owner/repo/releases/..." -> 完整下载地址
|
||
local u="https://github.com/$1"
|
||
[[ -n "$MIRROR" ]] && u="${MIRROR%/}/$u"
|
||
printf '%s' "$u"
|
||
}
|
||
|
||
release_url() { # release_url <asset> -> Gitea Release 下载地址
|
||
if [[ -n "$WANT_VERSION" ]]; then
|
||
printf '%s/releases/download/%s/%s' "$REPO_URL" "$WANT_VERSION" "$1"
|
||
else
|
||
printf '%s/releases/latest/download/%s' "$REPO_URL" "$1"
|
||
fi
|
||
}
|
||
|
||
# 读 config.yaml 顶层的简单键(形如 `secret: "xxx"`),套路取自 mihomo-up.sh 的 cfg_get
|
||
conf_get() {
|
||
local key="$1" f="$MIHOMO_CONF_DIR/config.yaml"
|
||
[[ -f "$f" ]] || return 0
|
||
sed -n -E "s/^${key}:[[:space:]]*//p" "$f" | sed -n '1p' \
|
||
| sed -E 's/[[:space:]]*#.*$//; s/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'
|
||
}
|
||
|
||
installed_version() {
|
||
[[ -f "$PREFIX/app/version.txt" ]] && tr -d '[:space:]' < "$PREFIX/app/version.txt" || printf ''
|
||
}
|
||
|
||
latest_version() {
|
||
local v
|
||
v="$(curl -fsSL --retry 2 --connect-timeout 5 --max-time 15 "$(release_url version.txt)" \
|
||
| tr -d '[:space:]')" \
|
||
|| die "拉取版本号失败: $(release_url version.txt)(仓库还没发布过 Release?网络不通?)"
|
||
[[ "$v" =~ ^v[0-9]+\.[0-9]+ ]] || die "拿到的版本号格式不对: $v"
|
||
printf '%s' "$v"
|
||
}
|
||
|
||
svc_running() { systemctl is-active --quiet "$SERVICE" 2>/dev/null; }
|
||
|
||
# ───────────────────────── bun 运行时 ─────────────────────────
|
||
bun_asset() {
|
||
case "$(uname -m)" in
|
||
x86_64|amd64)
|
||
# bun 的常规 x64 构建要求 CPU 支持 AVX2,老机器要用 baseline 构建
|
||
if grep -qw avx2 /proc/cpuinfo 2>/dev/null; then echo "bun-linux-x64"
|
||
else echo "bun-linux-x64-baseline"; fi ;;
|
||
aarch64|arm64) echo "bun-linux-aarch64" ;;
|
||
*) die "未适配的架构: $(uname -m)(bun 只发布 linux x64 / aarch64)" ;;
|
||
esac
|
||
}
|
||
|
||
install_bun() {
|
||
local bun="$PREFIX/bun/bun"
|
||
if [[ -x "$bun" && "$("$bun" --version 2>/dev/null || true)" == "$BUN_VERSION" ]]; then
|
||
info "bun $BUN_VERSION 已就位,跳过"
|
||
return 0
|
||
fi
|
||
need unzip
|
||
local asset url
|
||
asset="$(bun_asset)"
|
||
url="$(gh_url "oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${asset}.zip")"
|
||
|
||
info "下载 ${asset}.zip(约 30 MB)"
|
||
curl -fL -# --retry 3 --connect-timeout 15 --max-time 600 -o "$TMPDIR_/bun.zip" "$url" \
|
||
|| die "下载失败: $url(GitHub 不通就加 --mirror 换个下载源)"
|
||
unzip -q -o "$TMPDIR_/bun.zip" -d "$TMPDIR_" || die "解压失败,下载到的不是有效的 zip"
|
||
|
||
# 闸门: 新 bun 能运行起来,且自报版本号和目标一致(也能暴露架构选错)
|
||
local got
|
||
got="$("$TMPDIR_/$asset/bun" --version 2>/dev/null || true)"
|
||
[[ "$got" == "$BUN_VERSION" ]] \
|
||
|| die "bun 校验失败: 期望 $BUN_VERSION,实际 ${got:-执行不了}(多半是架构不对)"
|
||
|
||
mkdir -p "$PREFIX/bun"
|
||
install -m 0755 "$TMPDIR_/$asset/bun" "$bun"
|
||
ok "bun $BUN_VERSION → $bun"
|
||
}
|
||
|
||
# ───────────────────────── 应用本体 ─────────────────────────
|
||
install_app() { # $1 = 目标版本
|
||
local target="$1" url
|
||
url="$(release_url patchbay.tar.gz)"
|
||
|
||
info "下载 patchbay.tar.gz"
|
||
curl -fL -# --retry 3 --connect-timeout 15 --max-time 300 -o "$TMPDIR_/app.tar.gz" "$url" \
|
||
|| die "下载失败: $url"
|
||
mkdir -p "$TMPDIR_/unpack"
|
||
tar -xzf "$TMPDIR_/app.tar.gz" -C "$TMPDIR_/unpack" || die "解压失败,下载到的不是有效的 tar.gz"
|
||
|
||
# 闸门: 包里的东西齐不齐。server/ 和 web-dist/ 必须平级,服务端靠相对路径找前端
|
||
[[ -f "$TMPDIR_/unpack/patchbay/server/index.js" && -d "$TMPDIR_/unpack/patchbay/web-dist" ]] \
|
||
|| die "包内布局和预期不符(应有 patchbay/server/index.js 和 patchbay/web-dist/),已放弃"
|
||
|
||
# 应用目录无状态(配置和令牌都在别处),留一份旧版用于出问题时手动换回
|
||
rm -rf "$PREFIX/app.prev"
|
||
[[ -d "$PREFIX/app" ]] && mv "$PREFIX/app" "$PREFIX/app.prev"
|
||
mv "$TMPDIR_/unpack/patchbay" "$PREFIX/app"
|
||
ok "已安装 $target → $PREFIX/app"
|
||
}
|
||
|
||
# ───────────────────────── 环境变量文件 ─────────────────────────
|
||
# 各项的含义见仓库里的 server/env.ts;这里从 mihomo 的现有配置直接推导,
|
||
# 装完即可用,不用人工填一遍已经存在的信息。
|
||
write_env() {
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
info "环境变量文件已存在,保持不动: $ENV_FILE"
|
||
return 0
|
||
fi
|
||
|
||
local conf="$MIHOMO_CONF_DIR/config.yaml"
|
||
[[ -f "$conf" ]] || die "找不到 $conf。先用 mihomo-up.sh 装好 mihomo 再来装 Patchbay"
|
||
[[ -x "$MIHOMO_BIN" ]] || die "找不到 mihomo 二进制 $MIHOMO_BIN(装在别处就用 MIHOMO_BIN=路径 覆盖)"
|
||
|
||
local ctrl secret
|
||
ctrl="$(conf_get 'external-controller')"
|
||
secret="$(conf_get 'secret')"
|
||
[[ -n "$ctrl" && "$ctrl" != '""' ]] \
|
||
|| die "配置里没开管理接口(external-controller),Patchbay 靠它读线路和重载。先用 mihomo-up.sh 的配置向导开起来"
|
||
|
||
# Patchbay 和内核在同一台机器上,管理接口绑了 0.0.0.0 也从环回地址连
|
||
local api_host="${ctrl%:*}" api_port="${ctrl##*:}"
|
||
[[ "$api_host" == "0.0.0.0" || -z "$api_host" ]] && api_host="127.0.0.1"
|
||
|
||
local port
|
||
ask_default " Patchbay 监听端口" "7580" port
|
||
[[ "$port" =~ ^[0-9]+$ ]] || die "端口要是数字,收到的是「$port」"
|
||
|
||
mkdir -p "$(dirname "$ENV_FILE")"
|
||
cat > "$ENV_FILE" <<EOF
|
||
# Patchbay 环境变量 — 由 install.sh v${SELF_VERSION} 生成于 $(date '+%F %T')
|
||
# 各项含义见 ${REPO_URL}(server/env.ts)。改完 systemctl restart ${SERVICE} 生效。
|
||
MIHOMO_API=http://${api_host}:${api_port}
|
||
MIHOMO_SECRET=${secret}
|
||
PATCHBAY_CONFIG=${conf}
|
||
PATCHBAY_MIHOMO_BIN=${MIHOMO_BIN}
|
||
PATCHBAY_MIHOMO_HOME=${MIHOMO_CONF_DIR}
|
||
PATCHBAY_HOST=127.0.0.1
|
||
PATCHBAY_PORT=${port}
|
||
EOF
|
||
# 里面有管理接口的密钥,不让其它用户读
|
||
chmod 0640 "$ENV_FILE"
|
||
ok "已生成 $ENV_FILE ${D}(权限 0640,密钥在里面)${N}"
|
||
}
|
||
|
||
# ───────────────────────── systemd 服务 ─────────────────────────
|
||
write_unit() {
|
||
cat > "$1" <<EOF
|
||
[Unit]
|
||
Description=Patchbay — mihomo 配置的网页界面
|
||
Documentation=${REPO_URL}
|
||
# 只是排序不是依赖: 内核连不上时 Patchbay 照样能起来编辑配置
|
||
After=network-online.target mihomo.service
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
EnvironmentFile=${ENV_FILE}
|
||
ExecStart=${PREFIX}/bun/bun ${PREFIX}/app/server/index.js
|
||
WorkingDirectory=${PREFIX}/app
|
||
Restart=always
|
||
RestartSec=3
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
}
|
||
|
||
install_unit() {
|
||
command -v systemctl >/dev/null 2>&1 \
|
||
|| die "这台机器没有 systemd。手动运行: $PREFIX/bun/bun $PREFIX/app/server/index.js(环境变量见 $ENV_FILE)"
|
||
local tmp; tmp="$(mktemp)"
|
||
write_unit "$tmp"
|
||
if [[ -f "$UNIT_FILE" ]] && cmp -s "$tmp" "$UNIT_FILE"; then
|
||
rm -f "$tmp"; info "服务文件已是最新,没改动"
|
||
else
|
||
install -m 0644 "$tmp" "$UNIT_FILE"; rm -f "$tmp"
|
||
ok "已写入 $UNIT_FILE"
|
||
fi
|
||
systemctl daemon-reload
|
||
}
|
||
|
||
start_service() {
|
||
systemctl enable "$SERVICE" >/dev/null 2>&1 && info "已设为开机自启"
|
||
systemctl restart "$SERVICE" || true
|
||
sleep 2
|
||
if svc_running; then
|
||
ok "$SERVICE 已启动"
|
||
else
|
||
warn "$SERVICE 没有启动成功。看具体原因: journalctl -u $SERVICE -n 50 --no-pager"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
show_access() {
|
||
local port token=""
|
||
port="$(sed -n -E 's/^PATCHBAY_PORT=//p' "$ENV_FILE" 2>/dev/null | sed -n '1p')"
|
||
# 访问令牌由服务端首次启动时生成,存在配置文件旁边(见 server/auth.ts)
|
||
[[ -f "$MIHOMO_CONF_DIR/.patchbay-token" ]] && token="$(tr -d '[:space:]' < "$MIHOMO_CONF_DIR/.patchbay-token")"
|
||
|
||
log ""
|
||
log " ${D}只监听本机。从你自己的电脑访问,先开一条 SSH 隧道:${N}"
|
||
log " ${W}ssh -N -L ${port:-7580}:127.0.0.1:${port:-7580} 用户名@这台机器${N}"
|
||
log " ${D}然后浏览器打开:${N}"
|
||
log " ${W}http://127.0.0.1:${port:-7580}${N}"
|
||
if [[ -n "$token" ]]; then
|
||
log " ${D}访问令牌${N} ${W}${token}${N}"
|
||
log " ${D}令牌存在 $MIHOMO_CONF_DIR/.patchbay-token,忘了随时回来看。${N}"
|
||
else
|
||
log " ${D}访问令牌在服务首次启动时生成: journalctl -u $SERVICE | grep 令牌${N}"
|
||
fi
|
||
}
|
||
|
||
# ───────────────────────── 命令 ─────────────────────────
|
||
do_install() {
|
||
require_root "安装到 $PREFIX 并写入 systemd 服务"
|
||
need curl; need tar
|
||
TMPDIR_="$(mktemp -d)"
|
||
|
||
local target cur
|
||
target="${WANT_VERSION:-$(latest_version)}"
|
||
cur="$(installed_version)"
|
||
if [[ "$cur" == "$target" && $FORCE -eq 0 ]]; then
|
||
ok "已是 $cur,无需操作(--force 可重装)"
|
||
return 0
|
||
fi
|
||
|
||
log "${W}Patchbay ${target}${N} ${D}${cur:+(当前 $cur)}→ $PREFIX${N}"
|
||
install_bun
|
||
install_app "$target"
|
||
write_env
|
||
install_unit
|
||
start_service || return 1
|
||
show_access
|
||
}
|
||
|
||
do_check() {
|
||
local cur latest
|
||
cur="$(installed_version)"
|
||
latest="$(latest_version)"
|
||
if [[ -z "$cur" ]]; then
|
||
log "未安装(最新 Release 是 $latest)"; exit 10
|
||
elif [[ "$cur" == "$latest" ]]; then
|
||
log "已是最新 ($cur)"; exit 0
|
||
else
|
||
log "有新版: $cur → $latest"; exit 10
|
||
fi
|
||
}
|
||
|
||
do_uninstall() {
|
||
require_root "卸载 Patchbay"
|
||
if command -v systemctl >/dev/null 2>&1; then
|
||
systemctl disable --now "$SERVICE" >/dev/null 2>&1 || true
|
||
rm -f "$UNIT_FILE"
|
||
systemctl daemon-reload
|
||
fi
|
||
rm -rf "$PREFIX"
|
||
ok "已删除 $PREFIX 和 systemd 服务"
|
||
if (( PURGE )); then
|
||
rm -f "$ENV_FILE" "$MIHOMO_CONF_DIR/.patchbay-token"
|
||
rmdir "$(dirname "$ENV_FILE")" 2>/dev/null || true
|
||
ok "已删除环境变量文件和访问令牌"
|
||
else
|
||
info "留下了 $ENV_FILE 和访问令牌,重装可以继续用;不要了加 --purge"
|
||
fi
|
||
}
|
||
|
||
# ───────────────────────── 入口 ─────────────────────────
|
||
CMD="install"
|
||
while (( $# )); do
|
||
case "$1" in
|
||
install|check|uninstall) CMD="$1" ;;
|
||
-y|--yes) ASSUME_YES=1 ;;
|
||
--force) FORCE=1 ;;
|
||
--purge) PURGE=1 ;;
|
||
--version) shift; WANT_VERSION="${1:-}"; [[ -n "$WANT_VERSION" ]] || die "--version 后面要跟版本号" ;;
|
||
--mirror) shift; MIRROR="${1:-}"; [[ -n "$MIRROR" ]] || die "--mirror 后面要跟地址" ;;
|
||
-h|--help) usage ;;
|
||
-v) printf '%s\n' "$SELF_VERSION"; exit 0 ;;
|
||
*) die "不认识的参数: $1(--help 看用法)" ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
case "$CMD" in
|
||
install) do_install ;;
|
||
check) do_check ;;
|
||
uninstall) do_uninstall ;;
|
||
esac
|