#!/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 # 颜色输出 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.tuna.tsinghua.edu.cn" "mirrors.huaweicloud.com" "mirrors.163.com" "archive.ubuntu.com" ) # CentOS/RHEL 镜像源列表 CENTOS_MIRRORS=( "mirrors.tuna.tsinghua.edu.cn" "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 使用 -W,Linux 使用 -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 < /dev/null </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 < /dev/null < /dev/null < /dev/null </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 hash(target_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" 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 # 查找解压后的二进制文件(优先查找 agent,然后是 agent-*) local binary_file="" if [ -f "./agent" ] && [ -x "./agent" ]; then binary_file="./agent" else binary_file=$(find . -maxdepth 1 -type f \( -name "agent" -o -name "agent-*" -o -name "Agent" -o -name "Agent-*" \) ! -name "*.tar.gz" ! -name "*.zip" 2>/dev/null | head -1) 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 local binary_file=$(find . -maxdepth 1 -type f \( -name "agent*.exe" -o -name "Agent*.exe" \) 2>/dev/null | head -1) fi if [ -z "$binary_file" ] || [ ! -f "$binary_file" ]; then echo -e "${YELLOW}⚠ 未找到解压后的二进制文件,将使用源码编译${NC}" echo -e "${YELLOW} 解压目录内容:${NC}" ls -la "$temp_dir" 2>/dev/null || true rm -rf "$temp_dir" return 1 fi # 验证二进制文件是否可执行 if [ ! -x "$binary_file" ]; then chmod +x "$binary_file" 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_file" 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 # 保存下载的二进制文件到临时位置 local downloaded_binary="${temp_dir}/downloaded_agent" sudo cp "$binary_file" "$downloaded_binary" sudo chmod +x "$downloaded_binary" # 验证复制后的文件 if [ ! -f "$downloaded_binary" ] || [ ! -x "$downloaded_binary" ]; then echo -e "${YELLOW}⚠ 二进制文件验证失败,将使用源码编译${NC}" rm -rf "$temp_dir" return 1 fi # 清理临时下载文件 rm -f "$download_file" # 克隆仓库(用于获取 run.sh 和 start-systemd.sh 等脚本) echo -e "${BLUE}克隆仓库以获取启动脚本...${NC}" if [ -d "$SOURCE_DIR" ]; then sudo rm -rf "$SOURCE_DIR" fi 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" ] && [ -d "$SOURCE_DIR/.git" ]; then cd "$SOURCE_DIR" || { echo -e "${YELLOW}⚠ 无法切换到源码目录,跳过验证${NC}" cd /tmp || true } # 获取当前分支的最新 commit hash local current_commit=$(git rev-parse HEAD 2>/dev/null || echo "") if [ -n "$current_commit" ]; then # 截取短 commit hash 用于显示(前7位) local release_commit_short="" local current_commit_short=$(echo "$current_commit" | cut -c1-7) if [ "${#release_commit}" -eq 40 ]; then release_commit_short=$(echo "$release_commit" | cut -c1-7) else release_commit_short="$release_commit" fi echo -e "${BLUE} Release commit: ${release_commit_short}${NC}" echo -e "${BLUE} 当前代码 commit: ${current_commit_short}${NC}" # 对比 commit hash(只有当 release_commit 是完整的 commit hash 时才对比) if [ "${#release_commit}" -eq 40 ] && [ "${#current_commit}" -eq 40 ]; then if [ "$release_commit" != "$current_commit" ]; then echo -e "${YELLOW}⚠ Commit hash 不匹配,二进制文件可能不是最新代码编译的${NC}" echo -e "${YELLOW} Release 基于较旧的代码,将使用源码编译最新版本${NC}" # 保留已克隆的仓库目录,供 build_from_source 复用 cd /tmp || true rm -rf "$temp_dir" return 1 else echo -e "${GREEN}✓ Commit hash 匹配,二进制文件是最新代码编译的${NC}" fi else echo -e "${YELLOW}⚠ 无法获取有效的 commit hash 进行对比,跳过验证${NC}" fi else echo -e "${YELLOW}⚠ 无法获取当前代码的 commit hash,跳过验证${NC}" fi # 返回临时目录 cd /tmp || true else echo -e "${YELLOW}⚠ 源码目录不存在,跳过验证${NC}" fi else echo -e "${YELLOW}⚠ 无法获取 release 的 commit hash,跳过验证${NC}" fi # 用下载的二进制文件覆盖克隆目录中的文件 sudo cp "$downloaded_binary" "$SOURCE_DIR/agent" sudo chmod +x "$SOURCE_DIR/agent" # 复制到安装目录 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}/agent${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 < /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 <&1) if [ $? -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 < /dev/null 2>&1 sudo systemctl restart ${SERVICE_NAME} # 等待服务启动 sleep 3 # 检查服务状态 if sudo systemctl is-active --quiet ${SERVICE_NAME}; then echo -e "${GREEN}✓ 服务启动成功${NC}" else echo -e "${RED}✗ 服务启动失败${NC}" echo -e "${YELLOW}查看日志: sudo journalctl -u ${SERVICE_NAME} -n 50${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 "" detect_system # 检查是否已安装,如果已安装则先卸载 if check_installed; then uninstall_service fi # 检测并配置最快的镜像源(在安装依赖之前) detect_fastest_mirror install_dependencies # 优先尝试从 Releases 下载二进制文件 if ! download_binary_from_releases; then echo -e "${BLUE}从 Releases 下载失败,开始从源码编译...${NC}" build_from_source else echo -e "${GREEN}✓ 使用预编译二进制文件,跳过编译步骤${NC}" fi create_service configure_firewall register_node start_service 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