Files
linkmaster-node/install.sh
yoyo bb73e0f384 feat: 更新打包和安装逻辑,支持新格式发布包
- 在 all-upload-release.sh 中添加临时打包目录,复制二进制文件及必要的脚本和配置文件。
- 修改 install.sh 以支持新格式发布包的提取,简化安装流程,无需从 Git 克隆。
- 更新 INSTALL.md 和 README.md,说明新格式发布包的优点和安装步骤。
- 确保安装脚本能够处理旧格式发布包,保持向后兼容性。
2025-12-24 01:31:30 +08:00

1581 lines
59 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================
# LinkMaster 节点端一键安装脚本
# 使用方法: curl -fsSL https://gitee.nas.cpolar.cn/yoyo/linkmaster-node/raw/branch/main/install.sh | bash -s -- <后端地址>
# 示例: curl -fsSL https://gitee.nas.cpolar.cn/yoyo/linkmaster-node/raw/branch/main/install.sh | bash -s -- http://192.168.1.100:8080
# ============================================
set -e
# 错误处理函数
error_handler() {
local line_number=$1
local command=$2
echo ""
echo -e "${RED}========================================${NC}"
echo -e "${RED} 脚本执行出错!${NC}"
echo -e "${RED}========================================${NC}"
echo -e "${YELLOW}错误位置: 第 ${line_number}${NC}"
echo -e "${YELLOW}失败命令: ${command}${NC}"
echo ""
echo -e "${YELLOW}故障排查建议:${NC}"
echo " 1. 检查网络连接是否正常"
echo " 2. 检查后端地址是否正确: ${BACKEND_URL:-未设置}"
echo " 3. 检查是否有足够的磁盘空间和权限"
echo " 4. 查看上面的详细错误信息"
echo ""
echo -e "${YELLOW}查看服务日志: sudo journalctl -u ${SERVICE_NAME:-linkmaster-node} -n 50${NC}"
exit 1
}
# 设置错误陷阱
trap 'error_handler ${LINENO} "${BASH_COMMAND}"' ERR
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# 配置
# 尝试从脚本URL自动提取仓库信息如果通过curl下载
SCRIPT_URL="${SCRIPT_URL:-}"
if [ -z "$SCRIPT_URL" ] && [ -n "${BASH_SOURCE[0]}" ]; then
# 如果脚本是通过 curl 下载的,尝试从环境变量获取
SCRIPT_URL="${SCRIPT_URL:-}"
fi
# 默认配置(如果无法自动提取,使用这些默认值)
GITHUB_REPO="${GITHUB_REPO:-yoyo/linkmaster-node}" # 默认仓库(独立的 node 项目)
GITHUB_BRANCH="${GITHUB_BRANCH:-main}" # 默认分支
SOURCE_DIR="/opt/linkmaster-node" # 源码目录
BINARY_NAME="linkmaster-node"
INSTALL_DIR="/usr/local/bin"
SERVICE_NAME="linkmaster-node"
# 获取后端地址参数
BACKEND_URL="${1:-}"
if [ -z "$BACKEND_URL" ]; then
echo -e "${RED}错误: 请提供后端服务器地址${NC}"
echo -e "${YELLOW}使用方法:${NC}"
echo " curl -fsSL https://gitee.nas.cpolar.cn/${GITHUB_REPO}/raw/branch/${GITHUB_BRANCH}/install.sh | bash -s -- http://your-backend-server:8080"
echo ""
echo -e "${YELLOW}注意:${NC}"
echo " - 节点端需要直接连接后端服务器,不是前端地址"
echo " - 后端默认端口: 8080"
echo " - 如果节点和后端在同一服务器: http://localhost:8080"
echo " - 如果节点和后端在不同服务器: http://backend-ip:8080 或 http://backend-domain:8080"
exit 1
fi
# 检测系统类型和架构
detect_system() {
# 检测操作系统类型linux/darwin/windows
OS_TYPE=$(uname -s | tr '[:upper:]' '[:lower:]')
case $OS_TYPE in
linux)
OS_TYPE="linux"
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
OS_VERSION=$VERSION_ID
else
OS="linux"
OS_VERSION=""
fi
;;
darwin)
OS_TYPE="darwin"
OS="darwin"
OS_VERSION=$(sw_vers -productVersion 2>/dev/null || echo "")
;;
*)
echo -e "${RED}不支持的操作系统: $OS_TYPE${NC}"
exit 1
;;
esac
ARCH=$(uname -m)
case $ARCH in
x86_64)
ARCH="amd64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
echo -e "${RED}不支持的架构: $ARCH${NC}"
exit 1
;;
esac
echo -e "${BLUE}检测到系统: $OS_TYPE $OS_VERSION ($ARCH)${NC}"
}
# 检测并选择最快的镜像源
detect_fastest_mirror() {
echo -e "${BLUE}检测最快的镜像源...${NC}"
# Ubuntu/Debian 镜像源列表
UBUNTU_MIRRORS=(
"mirrors.huaweicloud.com"
"mirrors.163.com"
"archive.ubuntu.com"
)
# CentOS/RHEL 镜像源列表
CENTOS_MIRRORS=(
"mirrors.huaweicloud.com"
)
FASTEST_MIRROR=""
FASTEST_TIME=999999
# 根据系统类型选择镜像源列表
if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
MIRRORS=("${UBUNTU_MIRRORS[@]}")
MIRROR_TYPE="ubuntu"
elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ] || [ "$OS" = "rocky" ] || [ "$OS" = "almalinux" ]; then
MIRRORS=("${CENTOS_MIRRORS[@]}")
MIRROR_TYPE="centos"
else
echo -e "${YELLOW}⚠ 未知系统类型,跳过镜像源检测${NC}"
return
fi
# 检查是否有测试工具
if ! command -v ping > /dev/null 2>&1 && ! command -v curl > /dev/null 2>&1; then
echo -e "${YELLOW}⚠ 未找到 ping 或 curl 命令,跳过镜像源检测,使用默认源${NC}"
return
fi
# 测试每个镜像源的速度
echo -e "${BLUE}正在测试镜像源速度...${NC}"
for mirror in "${MIRRORS[@]}"; do
echo -n " 测试 ${mirror}... "
# 优先使用 ping 测试延迟(取平均值)
if command -v ping > /dev/null 2>&1; then
# ping 3次取平均时间macOS 使用 -WLinux 使用 -w
if [[ "$OSTYPE" == "darwin"* ]]; then
PING_RESULT=$(ping -c 3 -W 2000 "$mirror" 2>/dev/null | grep "avg" | awk -F'/' '{print $5}')
else
PING_RESULT=$(ping -c 3 -w 2 "$mirror" 2>/dev/null | grep "avg" | awk -F'/' '{print $5}')
fi
if [ -n "$PING_RESULT" ] && [ "$PING_RESULT" != "0" ]; then
TIME=$(echo "$PING_RESULT" | awk '{print int($1)}')
echo -e "${GREEN}${TIME}ms${NC}"
if [ "$TIME" -lt "$FASTEST_TIME" ] 2>/dev/null; then
FASTEST_TIME=$TIME
FASTEST_MIRROR=$mirror
fi
else
echo -e "${RED}超时${NC}"
fi
elif command -v curl > /dev/null 2>&1; then
# 如果没有 ping 命令,使用 curl 测试连接时间
TIME=$(curl -o /dev/null -s -w "%{time_total}" --connect-timeout 3 "http://${mirror}" 2>/dev/null | awk '{print int($1*1000)}')
if [ -n "$TIME" ] && [ "$TIME" -gt 0 ] && [ "$TIME" -lt 10000 ]; then
echo -e "${GREEN}${TIME}ms${NC}"
if [ "$TIME" -lt "$FASTEST_TIME" ]; then
FASTEST_TIME=$TIME
FASTEST_MIRROR=$mirror
fi
else
echo -e "${RED}超时${NC}"
fi
fi
done
if [ -z "$FASTEST_MIRROR" ]; then
echo -e "${YELLOW}⚠ 无法检测到可用镜像源,使用默认源${NC}"
return
fi
echo -e "${GREEN}✓ 最快镜像源: ${FASTEST_MIRROR} (${FASTEST_TIME}ms)${NC}"
# 配置镜像源
if [ "$MIRROR_TYPE" = "ubuntu" ]; then
configure_ubuntu_mirror "$FASTEST_MIRROR"
elif [ "$MIRROR_TYPE" = "centos" ]; then
configure_centos_mirror "$FASTEST_MIRROR"
fi
}
# 配置 Ubuntu/Debian 镜像源
configure_ubuntu_mirror() {
local mirror="$1"
local sources_file="/etc/apt/sources.list"
local sources_backup="/etc/apt/sources.list.backup.$(date +%Y%m%d_%H%M%S)"
# 备份原始源配置
if [ -f "$sources_file" ] && [ ! -f "$sources_backup" ]; then
echo -e "${BLUE}备份原始源配置...${NC}"
sudo cp "$sources_file" "$sources_backup"
fi
# 检测 Ubuntu 版本代号
if [ -f /etc/os-release ]; then
. /etc/os-release
CODENAME=$(echo "$VERSION_CODENAME" | tr '[:upper:]' '[:lower:]')
if [ -z "$CODENAME" ]; then
# 尝试从版本号推断
case "$VERSION_ID" in
"22.04") CODENAME="jammy" ;;
"20.04") CODENAME="focal" ;;
"18.04") CODENAME="bionic" ;;
*) CODENAME="jammy" ;; # 默认使用 jammy
esac
fi
else
CODENAME="jammy" # 默认
fi
# 检测是 Ubuntu 还是 Debian
if [ "$OS" = "ubuntu" ]; then
echo -e "${BLUE}配置 Ubuntu 镜像源: ${mirror}${NC}"
sudo tee "$sources_file" > /dev/null <<EOF
# LinkMaster Node 自动配置的镜像源
deb https://${mirror}/ubuntu/ ${CODENAME} main restricted universe multiverse
deb https://${mirror}/ubuntu/ ${CODENAME}-updates main restricted universe multiverse
deb https://${mirror}/ubuntu/ ${CODENAME}-backports main restricted universe multiverse
deb https://${mirror}/ubuntu/ ${CODENAME}-security main restricted universe multiverse
EOF
elif [ "$OS" = "debian" ]; then
# Debian 版本检测
if [ -f /etc/debian_version ]; then
DEBIAN_VERSION=$(cat /etc/debian_version | cut -d'.' -f1)
case "$DEBIAN_VERSION" in
"12") CODENAME="bookworm" ;;
"11") CODENAME="bullseye" ;;
"10") CODENAME="buster" ;;
*) CODENAME="bookworm" ;;
esac
else
CODENAME="bookworm"
fi
echo -e "${BLUE}配置 Debian 镜像源: ${mirror}${NC}"
sudo tee "$sources_file" > /dev/null <<EOF
# LinkMaster Node 自动配置的镜像源
deb https://${mirror}/debian/ ${CODENAME} main contrib non-free
deb https://${mirror}/debian/ ${CODENAME}-updates main contrib non-free
deb https://${mirror}/debian-security ${CODENAME}-security main contrib non-free
EOF
fi
echo -e "${GREEN}✓ 镜像源配置完成${NC}"
}
# 配置 CentOS/RHEL 镜像源
configure_centos_mirror() {
local mirror="$1"
local repo_dir="/etc/yum.repos.d"
local backup_dir="/etc/yum.repos.d/backup.$(date +%Y%m%d_%H%M%S)"
# 备份原始源配置
if [ -d "$repo_dir" ] && [ ! -d "$backup_dir" ]; then
echo -e "${BLUE}备份原始源配置...${NC}"
sudo mkdir -p "$backup_dir"
sudo cp -r "$repo_dir"/*.repo "$backup_dir/" 2>/dev/null || true
fi
# 检测系统类型和版本
local release_file="/etc/redhat-release"
local os_version=""
local os_name="centos"
if [ -f "$release_file" ]; then
RELEASE_CONTENT=$(cat "$release_file")
os_version=$(echo "$RELEASE_CONTENT" | grep -oE '[0-9]+' | head -1)
# 检测系统类型
if echo "$RELEASE_CONTENT" | grep -qi "rocky"; then
os_name="rocky"
elif echo "$RELEASE_CONTENT" | grep -qi "almalinux"; then
os_name="almalinux"
elif echo "$RELEASE_CONTENT" | grep -qi "centos"; then
os_name="centos"
fi
else
os_version="7"
fi
echo -e "${BLUE}配置 ${os_name} ${os_version} 镜像源: ${mirror}${NC}"
# 备份并禁用所有现有仓库
sudo find "$repo_dir" -name "*.repo" -not -name "*.backup" -exec sudo mv {} {}.backup \; 2>/dev/null || true
# 根据系统类型创建新的仓库配置
if [ "$os_name" = "rocky" ]; then
# Rocky Linux
sudo tee "$repo_dir/Rocky-Base.repo" > /dev/null <<EOF
[baseos]
name=Rocky Linux \$releasever - BaseOS - ${mirror}
baseurl=https://${mirror}/rocky/\$releasever/BaseOS/\$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rocky
[appstream]
name=Rocky Linux \$releasever - AppStream - ${mirror}
baseurl=https://${mirror}/rocky/\$releasever/AppStream/\$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rocky
EOF
elif [ "$os_name" = "almalinux" ]; then
# AlmaLinux
sudo tee "$repo_dir/AlmaLinux-Base.repo" > /dev/null <<EOF
[baseos]
name=AlmaLinux \$releasever - BaseOS - ${mirror}
baseurl=https://${mirror}/almalinux/\$releasever/BaseOS/\$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux
[appstream]
name=AlmaLinux \$releasever - AppStream - ${mirror}
baseurl=https://${mirror}/almalinux/\$releasever/AppStream/\$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux
EOF
elif [ "$os_version" = "7" ]; then
# CentOS 7
sudo tee "$repo_dir/CentOS-Base.repo" > /dev/null <<EOF
[base]
name=CentOS-\$releasever - Base - ${mirror}
baseurl=https://${mirror}/centos/\$releasever/os/\$basearch/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
[updates]
name=CentOS-\$releasever - Updates - ${mirror}
baseurl=https://${mirror}/centos/\$releasever/updates/\$basearch/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
[extras]
name=CentOS-\$releasever - Extras - ${mirror}
baseurl=https://${mirror}/centos/\$releasever/extras/\$basearch/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
EOF
else
# CentOS Stream 8/9 或其他版本
sudo tee "$repo_dir/CentOS-Base.repo" > /dev/null <<EOF
[baseos]
name=CentOS-\$releasever - BaseOS - ${mirror}
baseurl=https://${mirror}/centos-stream/\$stream/BaseOS/\$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial
[appstream]
name=CentOS-\$releasever - AppStream - ${mirror}
baseurl=https://${mirror}/centos-stream/\$stream/AppStream/\$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial
EOF
fi
echo -e "${GREEN}✓ 镜像源配置完成${NC}"
}
# 安装系统依赖
install_dependencies() {
echo -e "${BLUE}安装系统依赖...${NC}"
if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
echo -e "${BLUE}更新 apt 软件包列表...${NC}"
sudo apt-get update
echo -e "${BLUE}安装系统依赖包...${NC}"
sudo apt-get install -y curl wget ping traceroute dnsutils git
elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ] || [ "$OS" = "rocky" ] || [ "$OS" = "almalinux" ]; then
echo -e "${BLUE}安装系统依赖包...${NC}"
sudo yum install -y curl wget iputils traceroute bind-utils git
else
echo -e "${YELLOW}警告: 未知系统类型,跳过依赖安装${NC}"
fi
echo -e "${GREEN}✓ 系统依赖安装完成${NC}"
}
# 从官网下载安装 Go
install_go_from_official() {
echo -e "${BLUE}从 Go 官网下载安装...${NC}"
# 检测架构
local arch=""
case "$ARCH" in
amd64)
arch="amd64"
;;
arm64)
arch="arm64"
;;
*)
echo -e "${RED}不支持的架构: $ARCH${NC}"
return 1
;;
esac
# Go 版本(可以根据需要修改)
local go_version="1.21.5"
local go_tar="go${go_version}.linux-${arch}.tar.gz"
local go_url="https://golang.google.cn/dl/${go_tar}"
# 如果国内镜像访问失败,尝试官方源
local download_success=false
# 尝试从国内镜像下载
echo -e "${BLUE}尝试从 Go 国内镜像下载...${NC}"
if curl -fsSL -o "/tmp/${go_tar}" "${go_url}" 2>/dev/null; then
download_success=true
else
# 尝试从官方源下载
echo -e "${BLUE}尝试从 Go 官方源下载...${NC}"
go_url="https://go.dev/dl/${go_tar}"
if curl -fsSL -o "/tmp/${go_tar}" "${go_url}" 2>/dev/null; then
download_success=true
fi
fi
if [ "$download_success" = false ]; then
echo -e "${RED}下载 Go 失败,请检查网络连接${NC}"
return 1
fi
# 删除旧版本(如果存在)
if [ -d "/usr/local/go" ]; then
echo -e "${BLUE}删除旧版本 Go...${NC}"
sudo rm -rf /usr/local/go
fi
# 解压安装
echo -e "${BLUE}解压并安装 Go...${NC}"
sudo tar -C /usr/local -xzf "/tmp/${go_tar}"
rm -f "/tmp/${go_tar}"
echo -e "${GREEN}✓ Go 解压完成${NC}"
# 添加到 PATH如果还没有
if ! grep -q "/usr/local/go/bin" /etc/profile 2>/dev/null; then
echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee -a /etc/profile > /dev/null
fi
# 设置当前会话的 PATH
export PATH=$PATH:/usr/local/go/bin
# 验证安装
if command -v go > /dev/null 2>&1; then
GO_VERSION=$(go version 2>/dev/null | head -1)
echo -e "${GREEN}✓ Go 安装完成: ${GO_VERSION}${NC}"
return 0
else
echo -e "${YELLOW}⚠ Go 已安装但未在 PATH 中,请重新登录或执行: export PATH=\$PATH:/usr/local/go/bin${NC}"
# 临时添加到 PATH 继续执行
export PATH=$PATH:/usr/local/go/bin
if command -v go > /dev/null 2>&1; then
GO_VERSION=$(go version 2>/dev/null | head -1)
echo -e "${GREEN}✓ Go 安装完成: ${GO_VERSION}${NC}"
return 0
fi
return 1
fi
}
# 安装 Go 环境
install_go() {
echo -e "${BLUE}检查 Go 环境...${NC}"
# 先检查是否已安装且可用
if command -v go > /dev/null 2>&1; then
# 确保 PATH 包含 Go如果从官网安装的
if [ -d "/usr/local/go/bin" ] && ! echo "$PATH" | grep -q "/usr/local/go/bin"; then
export PATH=$PATH:/usr/local/go/bin
fi
# 尝试获取 Go 版本
GO_VERSION=$(go version 2>/dev/null | head -1 || echo "")
if [ -n "$GO_VERSION" ]; then
echo -e "${GREEN}✓ Go 已安装: ${GO_VERSION}${NC}"
# 再次验证 Go 是否可用
if go version > /dev/null 2>&1; then
echo -e "${BLUE}Go 环境正常,跳过安装流程${NC}"
return 0
else
echo -e "${YELLOW}⚠ Go 已安装但无法正常运行,尝试重新安装...${NC}"
fi
else
# 如果 command -v go 存在但无法获取版本,可能是 PATH 问题
# 尝试添加 /usr/local/go/bin 到 PATH
if [ -d "/usr/local/go/bin" ]; then
export PATH=/usr/local/go/bin:$PATH
GO_VERSION=$(go version 2>/dev/null | head -1 || echo "")
if [ -n "$GO_VERSION" ]; then
echo -e "${GREEN}✓ Go 已安装: ${GO_VERSION}${NC}"
echo -e "${BLUE}Go 环境正常,跳过安装流程${NC}"
return 0
fi
fi
echo -e "${YELLOW}⚠ Go 已安装但无法获取版本信息,尝试重新安装...${NC}"
fi
fi
# 如果 Go 未安装或不可用,开始安装流程
echo -e "${BLUE}开始安装 Go 环境...${NC}"
# 尝试从系统包管理器安装
local install_success=false
if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
echo -e "${BLUE}尝试从 apt 安装 Go...${NC}"
sudo apt-get update
if sudo apt-get install -y golang-go; then
install_success=true
else
echo -e "${YELLOW}⚠ apt 安装 Go 失败${NC}"
fi
elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ] || [ "$OS" = "rocky" ] || [ "$OS" = "almalinux" ]; then
echo -e "${BLUE}尝试从 yum 安装 Go...${NC}"
# 检查 yum 源中是否有 golang 包
if yum list available golang > /dev/null 2>&1; then
if sudo yum install -y golang; then
install_success=true
else
echo -e "${YELLOW}⚠ yum 安装 Go 失败${NC}"
fi
else
echo -e "${YELLOW}⚠ yum 源中未找到 golang 包,尝试从官网下载安装${NC}"
fi
fi
# 检查安装是否成功
if [ "$install_success" = true ] && command -v go > /dev/null 2>&1; then
GO_VERSION=$(go version 2>/dev/null | head -1)
echo -e "${GREEN}✓ Go 安装完成: ${GO_VERSION}${NC}"
return 0
fi
# 如果包管理器安装失败,从官网下载安装
echo -e "${BLUE}包管理器安装失败,尝试从官网下载安装...${NC}"
if install_go_from_official; then
return 0
fi
# 所有方法都失败
echo -e "${RED}Go 安装失败${NC}"
show_build_alternatives
exit 1
}
# 显示替代方案
show_build_alternatives() {
echo ""
echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}"
echo -e "${YELLOW} 安装失败,请使用以下替代方案:${NC}"
echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}"
echo ""
echo -e "${GREEN}手动编译安装:${NC}"
echo " git clone https://gitee.nas.cpolar.cn/${GITHUB_REPO}.git ${SOURCE_DIR}"
echo " cd ${SOURCE_DIR}"
echo " go build -o agent ./cmd/agent"
echo " sudo cp agent /usr/local/bin/linkmaster-node"
echo " sudo chmod +x /usr/local/bin/linkmaster-node"
echo ""
}
# 检查是否已安装
check_installed() {
# 检查服务文件是否存在
if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then
return 0
fi
# 检查二进制文件是否存在
if [ -f "$INSTALL_DIR/$BINARY_NAME" ]; then
return 0
fi
# 检查源码目录是否存在
if [ -d "$SOURCE_DIR" ]; then
return 0
fi
return 1
}
# 卸载已安装的服务
uninstall_service() {
echo -e "${BLUE}检测到已安装的服务,开始卸载...${NC}"
# 停止服务
if systemctl is-active --quiet ${SERVICE_NAME} 2>/dev/null; then
echo -e "${BLUE}停止服务...${NC}"
sudo systemctl stop ${SERVICE_NAME} 2>/dev/null || true
sleep 2
fi
# 禁用服务
if systemctl is-enabled --quiet ${SERVICE_NAME} 2>/dev/null; then
echo -e "${BLUE}禁用服务...${NC}"
sudo systemctl disable ${SERVICE_NAME} 2>/dev/null || true
fi
# 删除 systemd 服务文件
if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then
echo -e "${BLUE}删除服务文件...${NC}"
sudo rm -f /etc/systemd/system/${SERVICE_NAME}.service
fi
# 删除可能的 override 配置目录(包含 Environment 等配置)
if [ -d "/etc/systemd/system/${SERVICE_NAME}.service.d" ]; then
echo -e "${BLUE}删除服务配置目录...${NC}"
sudo rm -rf /etc/systemd/system/${SERVICE_NAME}.service.d
fi
# 重新加载 systemd daemon
sudo systemctl daemon-reload
# 删除二进制文件
if [ -f "$INSTALL_DIR/$BINARY_NAME" ]; then
echo -e "${BLUE}删除二进制文件...${NC}"
sudo rm -f "$INSTALL_DIR/$BINARY_NAME"
fi
# 删除源码目录
if [ -d "$SOURCE_DIR" ]; then
echo -e "${BLUE}删除源码目录...${NC}"
sudo rm -rf "$SOURCE_DIR"
fi
# 清理进程(如果还在运行)
if pgrep -f "$BINARY_NAME" > /dev/null 2>&1; then
echo -e "${BLUE}清理残留进程...${NC}"
sudo pkill -f "$BINARY_NAME" 2>/dev/null || true
sleep 1
fi
echo -e "${GREEN}✓ 卸载完成${NC}"
echo ""
}
# 尝试从 Releases 下载二进制文件
download_binary_from_releases() {
echo -e "${BLUE}尝试从 Releases 下载预编译二进制文件...${NC}"
# Gitea API 地址
local api_base="https://gitee.nas.cpolar.cn/api/v1"
local repo_api="${api_base}/repos/${GITHUB_REPO}"
# 获取所有 releases按创建时间排序最新的在前
echo -e "${BLUE}获取最新 release 信息...${NC}"
local releases_response=$(curl -s -X GET "${repo_api}/releases?limit=10" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$releases_response" ]; then
echo -e "${YELLOW}⚠ 无法获取 releases 信息,将使用源码编译${NC}"
return 1
fi
# 解析所有 releases找到最新的按创建时间或版本号
# Gitea API 返回的 releases 通常已经按创建时间倒序排列
# 但我们还是需要解析并验证
# 提取所有 tag 和对应的 release_id、创建时间
local latest_tag=""
local latest_release_id=""
local latest_created_at=""
# 使用更健壮的方式解析 JSON虽然简单但能工作
# 查找第一个有效的 release非 draft非 prerelease
local tag_line=$(echo "$releases_response" | grep -o '"tag_name":"[^"]*"' | head -1)
local id_line=$(echo "$releases_response" | grep -o '"id":[0-9]*' | head -1)
local created_line=$(echo "$releases_response" | grep -o '"created_at":"[^"]*"' | head -1)
local draft_line=$(echo "$releases_response" | grep -o '"draft":[^,}]*' | head -1)
local prerelease_line=$(echo "$releases_response" | grep -o '"prerelease":[^,}]*' | head -1)
# 检查是否是 draft 或 prerelease
if echo "$draft_line" | grep -q "true" || echo "$prerelease_line" | grep -q "true"; then
# 如果是 draft 或 prerelease尝试找下一个
echo -e "${YELLOW}⚠ 第一个 release 是草稿或预发布版本,查找正式版本...${NC}"
# 简化处理:如果第一个是预发布,仍然使用它(因为可能是最新的)
fi
latest_tag=$(echo "$tag_line" | cut -d'"' -f4)
latest_release_id=$(echo "$id_line" | cut -d':' -f2)
latest_created_at=$(echo "$created_line" | cut -d'"' -f4)
if [ -z "$latest_tag" ] || [ -z "$latest_release_id" ]; then
echo -e "${YELLOW}⚠ 无法解析 release 信息,将使用源码编译${NC}"
return 1
fi
# 显示找到的版本信息
echo -e "${GREEN}✓ 找到最新版本: ${latest_tag}${NC}"
if [ -n "$latest_created_at" ]; then
echo -e "${BLUE} 发布日期: ${latest_created_at}${NC}"
fi
# 获取 release 的详细信息(包含 commit hash
local release_detail=$(curl -s -X GET "${repo_api}/releases/${latest_release_id}" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$release_detail" ]; then
echo -e "${YELLOW}⚠ 无法获取 release 详细信息,将使用源码编译${NC}"
return 1
fi
# 解析 release 对应的 commit hashtarget_commitish
local release_commit=$(echo "$release_detail" | grep -o '"target_commitish":"[^"]*"' | head -1 | cut -d'"' -f4)
# 如果 target_commitish 是分支名(如 "main"),需要通过 API 获取该分支的 commit hash
if [ -n "$release_commit" ] && [ "${#release_commit}" -lt 40 ]; then
# 可能是分支名,尝试获取分支的最新 commit hash
local branch_info=$(curl -s -X GET "${repo_api}/git/commits/${release_commit}" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$branch_info" ]; then
local branch_commit=$(echo "$branch_info" | grep -o '"sha":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -n "$branch_commit" ] && [ "${#branch_commit}" -eq 40 ]; then
release_commit="$branch_commit"
fi
fi
fi
# 如果 release_detail 中没有 target_commitish 或获取失败,尝试通过 tag 获取 commit hash
if [ -z "$release_commit" ] || [ "${#release_commit}" -lt 40 ]; then
# 通过 tag API 获取 commit hash
local tag_info=$(curl -s -X GET "${repo_api}/git/tags/${latest_tag}" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$tag_info" ]; then
local tag_commit=$(echo "$tag_info" | grep -o '"sha":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -n "$tag_commit" ] && [ "${#tag_commit}" -eq 40 ]; then
release_commit="$tag_commit"
fi
fi
fi
# 构建文件名(根据系统类型)
# 处理 tag 可能带 v 前缀的情况(如 v1.0.0
local version_in_filename="${latest_tag}"
# 如果 tag 以 v 开头,同时尝试带 v 和不带 v 的文件名
local file_ext="tar.gz"
if [ "$OS_TYPE" = "windows" ]; then
file_ext="zip"
fi
# 先尝试使用 tag 的原始格式(可能带 v
local file_name_with_v="agent-${OS_TYPE}-${ARCH}-${latest_tag}"
local full_file_name_with_v="${file_name_with_v}.${file_ext}"
# 如果 tag 以 v 开头,也尝试不带 v 的版本
local file_name_without_v=""
local full_file_name_without_v=""
if [ "${latest_tag#v}" != "${latest_tag}" ]; then
# tag 以 v 开头,去掉 v 前缀
version_in_filename="${latest_tag#v}"
file_name_without_v="agent-${OS_TYPE}-${ARCH}-${version_in_filename}"
full_file_name_without_v="${file_name_without_v}.${file_ext}"
fi
# 查找匹配的二进制文件(优先尝试带 v 的,如果找不到再尝试不带 v 的)
local download_url=""
local full_file_name=""
# 先尝试带 v 的文件名
download_url=$(echo "$release_detail" | grep -o "\"browser_download_url\":\"[^\"]*${full_file_name_with_v}[^\"]*\"" | head -1 | cut -d'"' -f4)
if [ -n "$download_url" ]; then
full_file_name="$full_file_name_with_v"
echo -e "${BLUE}找到文件: ${full_file_name}${NC}"
elif [ -n "$full_file_name_without_v" ]; then
# 如果带 v 的找不到,尝试不带 v 的
download_url=$(echo "$release_detail" | grep -o "\"browser_download_url\":\"[^\"]*${full_file_name_without_v}[^\"]*\"" | head -1 | cut -d'"' -f4)
if [ -n "$download_url" ]; then
full_file_name="$full_file_name_without_v"
echo -e "${BLUE}找到文件: ${full_file_name} (tag: ${latest_tag})${NC}"
fi
fi
if [ -z "$download_url" ] || [ -z "$full_file_name" ]; then
echo -e "${YELLOW}⚠ 未找到匹配的二进制文件${NC}"
echo -e "${YELLOW} 尝试的文件名:${NC}"
echo -e "${YELLOW} - ${full_file_name_with_v}${NC}"
if [ -n "$full_file_name_without_v" ]; then
echo -e "${YELLOW} - ${full_file_name_without_v}${NC}"
fi
echo -e "${YELLOW} 将使用源码编译${NC}"
return 1
fi
echo -e "${BLUE}下载二进制文件: ${full_file_name}...${NC}"
# 创建临时目录
local temp_dir=$(mktemp -d)
local download_file="${temp_dir}/${full_file_name}"
# 下载文件
if ! curl -fsSL -o "$download_file" "$download_url" 2>/dev/null; then
echo -e "${YELLOW}⚠ 下载失败,将使用源码编译${NC}"
rm -rf "$temp_dir"
return 1
fi
# 检查文件大小(至少应该大于 1MB
local file_size=$(stat -f%z "$download_file" 2>/dev/null || stat -c%s "$download_file" 2>/dev/null || echo "0")
if [ "$file_size" -lt 1048576 ]; then
echo -e "${YELLOW}⚠ 下载的文件大小异常,将使用源码编译${NC}"
rm -rf "$temp_dir"
return 1
fi
# 解压文件
echo -e "${BLUE}解压发布包...${NC}"
cd "$temp_dir"
local extracted_dir=""
if [ "$file_ext" = "tar.gz" ]; then
if ! tar -xzf "$download_file" 2>/dev/null; then
echo -e "${YELLOW}⚠ 解压失败,将使用源码编译${NC}"
rm -rf "$temp_dir"
return 1
fi
# 查找解压后的目录(可能是直接解压到当前目录,也可能是在子目录中)
extracted_dir=$(find . -maxdepth 1 -type d ! -name "." ! -name ".." | head -1)
if [ -z "$extracted_dir" ]; then
extracted_dir="."
fi
else
# Windows zip 文件(虽然脚本主要在 Linux 上运行,但保留兼容性)
if ! unzip -q "$download_file" 2>/dev/null; then
echo -e "${YELLOW}⚠ 解压失败,将使用源码编译${NC}"
rm -rf "$temp_dir"
return 1
fi
extracted_dir=$(find . -maxdepth 1 -type d ! -name "." ! -name ".." | head -1)
if [ -z "$extracted_dir" ]; then
extracted_dir="."
fi
fi
cd "$extracted_dir" || {
echo -e "${YELLOW}⚠ 无法进入解压目录,将使用源码编译${NC}"
rm -rf "$temp_dir"
return 1
}
# 显示解压后的目录内容(用于调试)
echo -e "${BLUE}解压目录内容:${NC}"
ls -la . 2>/dev/null || true
echo ""
# 查找二进制文件(先检查当前目录,再递归查找)
local binary_file=""
if [ "$OS_TYPE" = "windows" ]; then
if [ -f "./agent.exe" ]; then
binary_file="./agent.exe"
elif [ -f "agent.exe" ]; then
binary_file="agent.exe"
else
# 递归查找所有 .exe 文件
binary_file=$(find . -type f -name "*.exe" ! -name "*.tar.gz" ! -name "*.zip" 2>/dev/null | grep -i agent | head -1)
fi
else
# Linux/macOS: 先检查常见位置
if [ -f "./agent" ]; then
binary_file="./agent"
elif [ -f "agent" ]; then
binary_file="agent"
else
# 递归查找所有文件,排除压缩包和目录
# 查找名为 agent 的文件(不是目录)
binary_file=$(find . -type f \( -name "agent" -o -name "agent-*" \) ! -name "*.tar.gz" ! -name "*.zip" 2>/dev/null | head -1)
# 如果还是找不到,尝试查找所有可执行文件
if [ -z "$binary_file" ]; then
binary_file=$(find . -type f -perm +111 ! -name "*.tar.gz" ! -name "*.zip" ! -name "*.sh" 2>/dev/null | head -1)
fi
fi
fi
if [ -z "$binary_file" ] || [ ! -f "$binary_file" ]; then
echo -e "${YELLOW}⚠ 未找到解压后的二进制文件,将使用源码编译${NC}"
echo -e "${YELLOW} 当前目录: $(pwd)${NC}"
echo -e "${YELLOW} 查找的文件: agent 或 agent-*${NC}"
echo -e "${YELLOW} 所有文件列表:${NC}"
find . -type f 2>/dev/null | head -20 || true
rm -rf "$temp_dir"
return 1
fi
# 确保使用绝对路径
local binary_path=""
if [[ "$binary_file" == /* ]]; then
binary_path="$binary_file"
else
# 转换为绝对路径
binary_path="$(cd "$(dirname "$binary_file")" && pwd)/$(basename "$binary_file")"
fi
echo -e "${GREEN}✓ 找到二进制文件: ${binary_path}${NC}"
# 验证二进制文件是否可执行
if [ ! -x "$binary_path" ]; then
chmod +x "$binary_path" 2>/dev/null || true
fi
# 验证二进制文件类型Linux 应该是 ELF 文件)
if [ "$OS_TYPE" = "linux" ]; then
if command -v file > /dev/null 2>&1; then
local file_type=$(file "$binary_path" 2>/dev/null || echo "")
if [ -n "$file_type" ] && ! echo "$file_type" | grep -qi "ELF"; then
echo -e "${YELLOW}⚠ 二进制文件类型异常: ${file_type}${NC}"
echo -e "${YELLOW} 将使用源码编译${NC}"
rm -rf "$temp_dir"
return 1
fi
fi
fi
# 保存当前目录extracted_dir
local extracted_path="$(pwd)"
# 检查是否是新格式的发布包(包含脚本文件)
local has_scripts=false
if [ -f "$extracted_path/install.sh" ] || [ -f "$extracted_path/run.sh" ] || [ -f "$extracted_path/start-systemd.sh" ]; then
has_scripts=true
echo -e "${GREEN}✓ 检测到新格式发布包(包含脚本文件)${NC}"
fi
# 创建源码目录
if [ -d "$SOURCE_DIR" ]; then
sudo rm -rf "$SOURCE_DIR"
fi
sudo mkdir -p "$SOURCE_DIR"
if [ "$has_scripts" = true ]; then
# 新格式:从压缩包提取所有文件
echo -e "${BLUE}从发布包提取所有文件...${NC}"
# 复制二进制文件
sudo cp "$binary_path" "$SOURCE_DIR/agent"
sudo chmod +x "$SOURCE_DIR/agent"
echo -e "${GREEN}✓ 已提取二进制文件${NC}"
# 复制脚本文件
local scripts=("install.sh" "run.sh" "start-systemd.sh" "uninstall.sh")
for script in "${scripts[@]}"; do
if [ -f "$extracted_path/$script" ]; then
sudo cp "$extracted_path/$script" "$SOURCE_DIR/"
sudo chmod +x "$SOURCE_DIR/$script"
echo -e "${GREEN}✓ 已提取 $script${NC}"
fi
done
# 复制示例配置文件
if [ -f "$extracted_path/config.yaml.example" ]; then
sudo cp "$extracted_path/config.yaml.example" "$SOURCE_DIR/config.yaml.example"
echo -e "${GREEN}✓ 已提取 config.yaml.example${NC}"
fi
echo -e "${GREEN}✓ 所有文件已从发布包提取,无需克隆 Git 仓库${NC}"
else
# 旧格式:只有二进制文件,需要克隆 Git 仓库获取脚本
echo -e "${BLUE}检测到旧格式发布包(仅包含二进制文件)${NC}"
echo -e "${BLUE}克隆仓库以获取启动脚本...${NC}"
if ! sudo git clone --branch "${GITHUB_BRANCH}" "https://gitee.nas.cpolar.cn/${GITHUB_REPO}.git" "$SOURCE_DIR" 2>&1; then
echo -e "${YELLOW}⚠ 克隆仓库失败,将使用源码编译${NC}"
rm -rf "$temp_dir"
return 1
fi
# 对比 Git commit hash仅旧格式需要
if [ -n "$release_commit" ]; then
echo -e "${BLUE}验证 Git commit 版本...${NC}"
if [ -d "$SOURCE_DIR/.git" ]; then
cd "$SOURCE_DIR" || {
echo -e "${YELLOW}⚠ 无法切换到源码目录,跳过验证${NC}"
cd /tmp || true
}
local current_commit=$(git rev-parse HEAD 2>/dev/null || echo "")
if [ -n "$current_commit" ] && [ "${#release_commit}" -eq 40 ] && [ "${#current_commit}" -eq 40 ]; then
local release_commit_short=$(echo "$release_commit" | cut -c1-7)
local current_commit_short=$(echo "$current_commit" | cut -c1-7)
echo -e "${BLUE} Release commit: ${release_commit_short}${NC}"
echo -e "${BLUE} 当前代码 commit: ${current_commit_short}${NC}"
if [ "$release_commit" != "$current_commit" ]; then
echo -e "${YELLOW}⚠ Commit hash 不匹配,二进制文件可能不是最新代码编译的${NC}"
echo -e "${YELLOW} Release 基于较旧的代码,将使用源码编译最新版本${NC}"
cd /tmp || true
rm -rf "$temp_dir"
return 1
else
echo -e "${GREEN}✓ Commit hash 匹配${NC}"
fi
fi
cd /tmp || true
fi
fi
# 用下载的二进制文件覆盖克隆目录中的文件
sudo cp "$binary_path" "$SOURCE_DIR/agent"
sudo chmod +x "$SOURCE_DIR/agent"
fi
# 复制到安装目录
sudo mkdir -p "$INSTALL_DIR"
sudo cp "$SOURCE_DIR/agent" "$INSTALL_DIR/$BINARY_NAME"
sudo chmod +x "$INSTALL_DIR/$BINARY_NAME"
# 清理临时文件
rm -rf "$temp_dir"
# 显示文件信息
local binary_size=$(du -h "$SOURCE_DIR/agent" | cut -f1)
echo -e "${GREEN}✓ 安装文件准备完成 (文件大小: ${binary_size})${NC}"
echo -e "${BLUE}版本: ${latest_tag}${NC}"
echo -e "${BLUE}安装目录: ${SOURCE_DIR}${NC}"
return 0
}
# 从源码编译安装
build_from_source() {
echo -e "${BLUE}从源码编译安装节点端...${NC}"
# 检查 Go 环境(如果已安装则跳过安装流程)
if ! command -v go > /dev/null 2>&1; then
echo -e "${BLUE}未检测到 Go 环境,开始安装...${NC}"
install_go
else
# Go 已安装,先确保 PATH 正确
if [ -d "/usr/local/go/bin" ] && ! echo "$PATH" | grep -q "/usr/local/go/bin"; then
export PATH=/usr/local/go/bin:$PATH
fi
# 验证是否可用
GO_VERSION=$(go version 2>/dev/null | head -1 || echo "")
if [ -n "$GO_VERSION" ] && go version > /dev/null 2>&1; then
echo -e "${GREEN}✓ Go 已安装: ${GO_VERSION}${NC}"
echo -e "${BLUE}跳过 Go 安装流程,直接使用现有环境${NC}"
else
echo -e "${YELLOW}⚠ Go 已安装但无法正常运行,尝试重新安装...${NC}"
install_go
fi
fi
# 确保 Go 在 PATH 中(如果从官网安装的)
if [ -d "/usr/local/go/bin" ] && ! echo "$PATH" | grep -q "/usr/local/go/bin"; then
export PATH=$PATH:/usr/local/go/bin
fi
# 再次检查 Go 版本(确保安装成功)
GO_VERSION=$(go version 2>/dev/null | head -1 || echo "")
if [ -z "$GO_VERSION" ]; then
echo -e "${RED}无法获取 Go 版本信息${NC}"
show_build_alternatives
exit 1
fi
echo -e "${BLUE}使用 Go 版本: ${GO_VERSION}${NC}"
# 确定 Go 的完整路径(用于 sudo 执行)
GO_BIN=$(command -v go)
if [ -z "$GO_BIN" ]; then
echo -e "${RED}无法找到 Go 可执行文件${NC}"
show_build_alternatives
exit 1
fi
# 检查源码目录是否已存在(可能由 download_binary_from_releases 已克隆)
if [ -d "$SOURCE_DIR" ] && [ -d "$SOURCE_DIR/.git" ]; then
echo -e "${BLUE}检测到已存在的仓库目录,复用现有目录...${NC}"
cd "$SOURCE_DIR"
# 确保是最新代码
echo -e "${BLUE}更新代码到最新版本...${NC}"
sudo git fetch origin 2>/dev/null || true
sudo git checkout "${GITHUB_BRANCH}" 2>/dev/null || true
sudo git pull origin "${GITHUB_BRANCH}" 2>/dev/null || true
else
# 如果源码目录已存在但不是 git 仓库,删除它
if [ -d "$SOURCE_DIR" ]; then
echo -e "${YELLOW}清理旧的源码目录...${NC}"
sudo rm -rf "$SOURCE_DIR"
fi
# 克隆仓库到源码目录
echo -e "${BLUE}克隆仓库到 ${SOURCE_DIR}...${NC}"
if ! sudo git clone --branch "${GITHUB_BRANCH}" "https://gitee.nas.cpolar.cn/${GITHUB_REPO}.git" "$SOURCE_DIR" 2>&1; then
echo -e "${RED}克隆仓库失败,请检查网络连接和仓库地址${NC}"
echo -e "${YELLOW}仓库地址: https://gitee.nas.cpolar.cn/${GITHUB_REPO}.git${NC}"
show_build_alternatives
exit 1
fi
fi
# 设置目录权限(允许当前用户访问,但服务运行时是 root
sudo chown -R root:root "$SOURCE_DIR" 2>/dev/null || true
cd "$SOURCE_DIR"
# 配置 Git safe.directory解决所有权问题
sudo git config --global --add safe.directory "$SOURCE_DIR" 2>/dev/null || true
git config --global --add safe.directory "$SOURCE_DIR" 2>/dev/null || true
# 检查 vendor 目录是否存在(必须存在)
echo -e "${BLUE}检查 vendor 目录...${NC}"
if [ ! -d "$SOURCE_DIR/vendor" ] || [ ! -f "$SOURCE_DIR/vendor/modules.txt" ]; then
echo -e "${RED}错误: vendor 目录不存在或无效${NC}"
echo -e "${RED}请确保项目包含 vendor 目录,或先运行 ./vendor.sh 创建 vendor 目录${NC}"
echo -e "${YELLOW}vendor 目录路径: ${SOURCE_DIR}/vendor${NC}"
exit 1
fi
# vendor 目录存在,显示信息
VENDOR_COUNT=$(find "$SOURCE_DIR/vendor" -type d -mindepth 2 2>/dev/null | wc -l | tr -d '\n' || echo "0")
VENDOR_COUNT=${VENDOR_COUNT:-0}
echo -e "${GREEN}✓ vendor 目录存在${NC}"
echo -e "${BLUE}vendor 目录包含 ${GREEN}${VENDOR_COUNT}${NC} 个依赖包"
echo -e "${BLUE}编译时将使用 -mod=vendor 标志,无需网络连接${NC}"
# 构建包含 Go PATH 的环境变量
GO_PATH_ENV="PATH=\$PATH:/usr/local/go/bin"
if [ -d "/usr/local/go/bin" ]; then
GO_PATH_ENV="PATH=/usr/local/go/bin:\$PATH"
fi
# 设置 Go 环境变量(仅包含 PATH不使用代理
GO_ENV="$GO_PATH_ENV"
# 编译到临时文件(在用户有权限的目录),然后移动到目标位置
echo -e "${BLUE}编译二进制文件...${NC}"
TEMP_BINARY=$(mktemp)
BINARY_PATH="$SOURCE_DIR/agent"
# 使用 sudo 以 root 用户编译,直接输出到目标位置,强制使用 vendor
echo -e "${BLUE}开始编译(架构: ${ARCH}...${NC}"
echo -e "${BLUE}使用 vendor 目录编译(无需网络连接)...${NC}"
# 强制使用 vendor 模式编译
BUILD_FLAGS="-mod=vendor -v -buildvcs=false -ldflags='-w -s'"
if sudo bash -c "cd '$SOURCE_DIR' && $GO_ENV && GOOS=linux GOARCH=${ARCH} CGO_ENABLED=0 go build $BUILD_FLAGS -o '$BINARY_PATH' ./cmd/agent" 2>&1 | tee /tmp/go_build.log; then
if [ -f "$BINARY_PATH" ] && [ -s "$BINARY_PATH" ]; then
sudo chmod +x "$BINARY_PATH"
BINARY_SIZE=$(du -h "$BINARY_PATH" | cut -f1)
echo -e "${GREEN}✓ 编译成功 (文件大小: ${BINARY_SIZE})${NC}"
rm -f /tmp/go_build.log 2>/dev/null || true
else
echo -e "${RED}编译失败:未生成二进制文件${NC}"
echo -e "${YELLOW}编译日志:${NC}"
cat /tmp/go_build.log 2>/dev/null || true
show_build_alternatives
exit 1
fi
else
echo -e "${RED}编译失败,编译日志:${NC}"
cat /tmp/go_build.log 2>/dev/null || true
rm -f /tmp/go_build.log 2>/dev/null || true
show_build_alternatives
exit 1
fi
# 清理临时文件
rm -f "$TEMP_BINARY" 2>/dev/null || true
# 复制到安装目录(可选,保留在源码目录供 run.sh 使用)
sudo mkdir -p "$INSTALL_DIR"
sudo cp "$BINARY_PATH" "$INSTALL_DIR/$BINARY_NAME"
sudo chmod +x "$INSTALL_DIR/$BINARY_NAME"
echo -e "${GREEN}✓ 编译安装完成${NC}"
echo -e "${BLUE}源码目录: ${SOURCE_DIR}${NC}"
echo -e "${BLUE}二进制文件: ${INSTALL_DIR}/${BINARY_NAME}${NC}"
}
# 创建 systemd 服务
create_service() {
echo -e "${BLUE}创建 systemd 服务...${NC}"
# 确保启动脚本有执行权限
sudo chmod +x "$SOURCE_DIR/run.sh"
sudo chmod +x "$SOURCE_DIR/start-systemd.sh"
# 检测 Go 的安装路径
GO_PATH=""
if [ -d "/usr/local/go/bin" ]; then
GO_PATH="/usr/local/go/bin"
elif command -v go > /dev/null 2>&1; then
GO_BIN=$(command -v go)
GO_PATH=$(dirname "$GO_BIN")
fi
# 构建 PATH 环境变量
if [ -n "$GO_PATH" ]; then
ENV_PATH="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${GO_PATH}"
else
ENV_PATH="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
fi
sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null <<EOF
[Unit]
Description=LinkMaster Node Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$SOURCE_DIR
ExecStart=$SOURCE_DIR/start-systemd.sh
Restart=always
RestartSec=5
Environment="BACKEND_URL=$BACKEND_URL"
Environment="$ENV_PATH"
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
echo -e "${GREEN}✓ 服务创建完成${NC}"
}
# 配置防火墙规则
configure_firewall() {
echo -e "${BLUE}配置防火墙规则开放2200端口...${NC}"
PORT=2200
FIREWALL_CONFIGURED=false
# 检测 firewalld (CentOS/RHEL 7+, Fedora)
if command -v firewall-cmd > /dev/null 2>&1; then
if sudo systemctl is-active --quiet firewalld 2>/dev/null; then
echo -e "${BLUE}检测到 firewalld添加端口规则...${NC}"
if sudo firewall-cmd --permanent --add-port=${PORT}/tcp > /dev/null 2>&1; then
sudo firewall-cmd --reload > /dev/null 2>&1
echo -e "${GREEN}✓ firewalld 规则已添加${NC}"
FIREWALL_CONFIGURED=true
else
echo -e "${YELLOW}⚠ firewalld 规则添加失败(可能需要手动配置)${NC}"
fi
fi
fi
# 检测 ufw (Ubuntu/Debian)
if command -v ufw > /dev/null 2>&1; then
if sudo ufw status > /dev/null 2>&1; then
echo -e "${BLUE}检测到 ufw添加端口规则...${NC}"
if sudo ufw allow ${PORT}/tcp > /dev/null 2>&1; then
echo -e "${GREEN}✓ ufw 规则已添加${NC}"
FIREWALL_CONFIGURED=true
else
echo -e "${YELLOW}⚠ ufw 规则添加失败(可能需要手动配置)${NC}"
fi
fi
fi
# 检测 iptables较老的系统
if command -v iptables > /dev/null 2>&1 && [ "$FIREWALL_CONFIGURED" = false ]; then
# 检查是否有iptables规则说明防火墙可能在使用
if sudo iptables -L -n 2>/dev/null | grep -q "Chain INPUT"; then
echo -e "${BLUE}检测到 iptables添加端口规则...${NC}"
# 检查规则是否已存在
if ! sudo iptables -C INPUT -p tcp --dport ${PORT} -j ACCEPT > /dev/null 2>&1; then
if sudo iptables -I INPUT -p tcp --dport ${PORT} -j ACCEPT > /dev/null 2>&1; then
echo -e "${GREEN}✓ iptables 规则已添加${NC}"
# 尝试保存规则(不同系统保存方式不同)
if command -v iptables-save > /dev/null 2>&1; then
if [ -f /etc/redhat-release ]; then
sudo iptables-save > /etc/sysconfig/iptables 2>/dev/null || true
elif [ -f /etc/debian_version ]; then
sudo iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
fi
fi
FIREWALL_CONFIGURED=true
else
echo -e "${YELLOW}⚠ iptables 规则添加失败(可能需要手动配置)${NC}"
fi
else
echo -e "${GREEN}✓ iptables 规则已存在${NC}"
FIREWALL_CONFIGURED=true
fi
fi
fi
if [ "$FIREWALL_CONFIGURED" = false ]; then
echo -e "${YELLOW}⚠ 未检测到活动的防火墙,或防火墙未启用${NC}"
echo -e "${YELLOW} 如果节点无法远程访问,请手动开放端口 ${PORT}/tcp${NC}"
fi
}
# 登记节点调用心跳API获取节点信息
register_node() {
echo -e "${BLUE}登记节点到后端服务器...${NC}"
# 创建临时配置文件
CONFIG_FILE="$SOURCE_DIR/config.yaml"
sudo mkdir -p "$SOURCE_DIR"
# 创建基础配置文件
sudo tee "$CONFIG_FILE" > /dev/null <<EOF
server:
port: 2200
backend:
url: ${BACKEND_URL}
heartbeat:
interval: 60
debug: false
node:
id: 0
ip: ""
country: ""
province: ""
city: ""
isp: ""
EOF
# 调用心跳API获取节点信息
echo -e "${BLUE}发送心跳请求获取节点信息...${NC}"
echo -e "${BLUE}后端地址: ${BACKEND_URL}${NC}"
# 添加超时设置,避免长时间卡住
# 使用 set +e 临时禁用错误退出,因为心跳失败不应该阻止安装
set +e
RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 -X POST "${BACKEND_URL}/api/node/heartbeat" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "type=pingServer" 2>&1)
CURL_EXIT_CODE=$?
set -e # 重新启用错误退出
if [ $CURL_EXIT_CODE -eq 0 ]; then
# 尝试解析JSON响应
NODE_ID=$(echo "$RESPONSE" | grep -o '"node_id":[0-9]*' | grep -o '[0-9]*' | head -1)
NODE_IP=$(echo "$RESPONSE" | grep -o '"node_ip":"[^"]*"' | cut -d'"' -f4 | head -1)
COUNTRY=$(echo "$RESPONSE" | grep -o '"country":"[^"]*"' | cut -d'"' -f4 | head -1)
PROVINCE=$(echo "$RESPONSE" | grep -o '"province":"[^"]*"' | cut -d'"' -f4 | head -1)
CITY=$(echo "$RESPONSE" | grep -o '"city":"[^"]*"' | cut -d'"' -f4 | head -1)
ISP=$(echo "$RESPONSE" | grep -o '"isp":"[^"]*"' | cut -d'"' -f4 | head -1)
if [ -n "$NODE_ID" ] && [ "$NODE_ID" != "0" ] && [ -n "$NODE_IP" ]; then
# 更新配置文件
sudo tee "$CONFIG_FILE" > /dev/null <<EOF
server:
port: 2200
backend:
url: ${BACKEND_URL}
heartbeat:
interval: 60
debug: false
node:
id: ${NODE_ID}
ip: ${NODE_IP}
country: ${COUNTRY:-""}
province: ${PROVINCE:-""}
city: ${CITY:-""}
isp: ${ISP:-""}
EOF
echo -e "${GREEN}✓ 节点登记成功${NC}"
echo -e "${BLUE} 节点ID: ${NODE_ID}${NC}"
echo -e "${BLUE} 节点IP: ${NODE_IP}${NC}"
if [ -n "$COUNTRY" ] || [ -n "$PROVINCE" ] || [ -n "$CITY" ]; then
echo -e "${BLUE} 位置: ${COUNTRY:-""}/${PROVINCE:-""}/${CITY:-""}${NC}"
fi
else
echo -e "${YELLOW}⚠ 无法从响应中解析节点信息,将在服务启动时重试${NC}"
echo -e "${YELLOW} 响应: ${RESPONSE}${NC}"
fi
else
echo -e "${YELLOW}⚠ 心跳请求失败 (退出码: ${CURL_EXIT_CODE}),将在服务启动时重试${NC}"
echo -e "${YELLOW} 错误信息: ${RESPONSE}${NC}"
echo -e "${YELLOW} 提示: 请检查后端地址是否正确: ${BACKEND_URL}${NC}"
echo -e "${YELLOW} 测试连接: curl -v ${BACKEND_URL}/api/public/nodes/online${NC}"
fi
# 设置配置文件权限
sudo chmod 644 "$CONFIG_FILE"
}
# 启动服务
start_service() {
echo -e "${BLUE}启动服务...${NC}"
# 先检查服务文件是否存在
if [ ! -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then
echo -e "${RED}✗ 错误: 服务文件不存在${NC}"
echo -e "${YELLOW} 路径: /etc/systemd/system/${SERVICE_NAME}.service${NC}"
exit 1
fi
# 检查二进制文件是否存在
if [ ! -f "$SOURCE_DIR/agent" ] && [ ! -f "$INSTALL_DIR/$BINARY_NAME" ]; then
echo -e "${RED}✗ 错误: 二进制文件不存在${NC}"
echo -e "${YELLOW} 检查路径: $SOURCE_DIR/agent 或 $INSTALL_DIR/$BINARY_NAME${NC}"
exit 1
fi
# 启用服务(显示输出以便调试)
echo -e "${BLUE}启用服务...${NC}"
if ! sudo systemctl enable ${SERVICE_NAME} 2>&1; then
echo -e "${RED}✗ 启用服务失败${NC}"
exit 1
fi
# 重新加载 systemd
echo -e "${BLUE}重新加载 systemd...${NC}"
sudo systemctl daemon-reload
# 启动服务(显示输出以便调试)
echo -e "${BLUE}启动服务...${NC}"
if ! sudo systemctl restart ${SERVICE_NAME} 2>&1; then
echo -e "${RED}✗ 启动服务失败${NC}"
echo -e "${YELLOW}查看详细日志: sudo journalctl -u ${SERVICE_NAME} -n 100 --no-pager${NC}"
echo -e "${YELLOW}查看服务状态: sudo systemctl status ${SERVICE_NAME}${NC}"
exit 1
fi
# 等待服务启动
echo -e "${BLUE}等待服务启动...${NC}"
sleep 3
# 检查服务状态
if sudo systemctl is-active --quiet ${SERVICE_NAME} 2>/dev/null; then
echo -e "${GREEN}✓ 服务启动成功${NC}"
else
echo -e "${RED}✗ 服务启动失败${NC}"
echo -e "${YELLOW}服务状态:${NC}"
sudo systemctl status ${SERVICE_NAME} --no-pager -l || true
echo -e "${YELLOW}查看详细日志: sudo journalctl -u ${SERVICE_NAME} -n 100 --no-pager${NC}"
exit 1
fi
}
# 验证安装
verify_installation() {
echo -e "${BLUE}验证安装...${NC}"
# 检查进程
if pgrep -f "$BINARY_NAME" > /dev/null; then
echo -e "${GREEN}✓ 进程运行中${NC}"
else
echo -e "${YELLOW}⚠ 进程未运行${NC}"
fi
# 检查端口
if command -v netstat > /dev/null 2>&1; then
if netstat -tlnp 2>/dev/null | grep -q ":2200"; then
echo -e "${GREEN}✓ 端口 2200 已监听${NC}"
fi
elif command -v ss > /dev/null 2>&1; then
if ss -tlnp 2>/dev/null | grep -q ":2200"; then
echo -e "${GREEN}✓ 端口 2200 已监听${NC}"
fi
fi
# 健康检查(重试多次,给服务启动时间)
echo -e "${BLUE}等待服务启动并检查健康状态...${NC}"
HEALTH_CHECK_PASSED=false
for i in {1..10}; do
sleep 2
if curl -sf http://localhost:2200/api/health > /dev/null 2>&1; then
HEALTH_RESPONSE=$(curl -s http://localhost:2200/api/health 2>/dev/null || echo "")
if echo "$HEALTH_RESPONSE" | grep -q '"status":"ok"'; then
HEALTH_CHECK_PASSED=true
echo -e "${GREEN}✓ 健康检查通过${NC}"
break
fi
fi
if [ $i -lt 10 ]; then
echo -e "${BLUE}等待服务启动... ($i/10)${NC}"
fi
done
if [ "$HEALTH_CHECK_PASSED" = false ]; then
echo -e "${YELLOW}⚠ 健康检查未通过${NC}"
echo -e "${YELLOW}请检查服务日志: sudo journalctl -u ${SERVICE_NAME} -n 50${NC}"
echo -e "${YELLOW}或手动测试: curl http://localhost:2200/api/health${NC}"
fi
}
# 主安装流程
main() {
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} LinkMaster 节点端安装程序${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo -e "${BLUE}后端地址: ${BACKEND_URL}${NC}"
echo ""
echo -e "${BLUE}[1/8] 检测系统类型...${NC}"
detect_system
# 检查是否已安装,如果已安装则先卸载
if check_installed; then
echo -e "${BLUE}[2/8] 卸载已存在的服务...${NC}"
uninstall_service
else
echo -e "${BLUE}[2/8] 检查已安装服务...${NC}"
echo -e "${GREEN}✓ 未检测到已安装的服务${NC}"
fi
echo -e "${BLUE}[3/8] 检测并配置镜像源...${NC}"
detect_fastest_mirror
echo -e "${BLUE}[4/8] 安装系统依赖...${NC}"
install_dependencies
# 优先尝试从 Releases 下载二进制文件
echo -e "${BLUE}[5/8] 下载或编译二进制文件...${NC}"
if ! download_binary_from_releases; then
echo -e "${BLUE}从 Releases 下载失败,开始从源码编译...${NC}"
build_from_source
else
echo -e "${GREEN}✓ 使用预编译二进制文件,跳过编译步骤${NC}"
fi
echo -e "${BLUE}[6/8] 创建 systemd 服务...${NC}"
create_service
echo -e "${BLUE}[7/8] 配置防火墙规则...${NC}"
configure_firewall
echo -e "${BLUE}[8/8] 登记节点到后端服务器...${NC}"
register_node
echo -e "${BLUE}[9/9] 启动服务...${NC}"
start_service
echo -e "${BLUE}[10/10] 验证安装...${NC}"
verify_installation
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} 安装完成!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo -e "${BLUE}服务管理命令:${NC}"
echo " 查看状态: sudo systemctl status ${SERVICE_NAME}"
echo " 查看日志: sudo journalctl -u ${SERVICE_NAME} -f"
echo " 重启服务: sudo systemctl restart ${SERVICE_NAME}"
echo " 停止服务: sudo systemctl stop ${SERVICE_NAME}"
echo ""
echo -e "${BLUE}后端地址: ${BACKEND_URL}${NC}"
echo -e "${BLUE}节点端口: 2200${NC}"
echo ""
echo -e "${YELLOW}重要提示:${NC}"
echo " - 节点端直接连接后端服务器,不使用前端代理"
echo " - 确保后端地址可访问: curl ${BACKEND_URL}/api/public/nodes/online"
}
# 执行安装
main