Files
s-hy2/scripts/common.sh
T
sindricn faf3fcf70a 更新
2025-09-24 16:17:07 +08:00

393 lines
8.9 KiB
Bash

#!/bin/bash
# 公共函数库 - 统一错误处理和日志记录
# 为所有脚本提供标准化的错误处理、日志记录和工具函数
# 适度的错误处理 (不使用 -e 避免意外退出)
set -uo pipefail
# 全局变量 (防止重复定义)
if [[ -z "${SCRIPT_NAME:-}" ]]; then
readonly SCRIPT_NAME="${0##*/}"
fi
if [[ -z "${LOG_DIR:-}" ]]; then
readonly LOG_DIR="/var/log/s-hy2"
fi
if [[ -z "${LOG_FILE:-}" ]]; then
readonly LOG_FILE="$LOG_DIR/s-hy2.log"
fi
if [[ -z "${PID_FILE:-}" ]]; then
readonly PID_FILE="/var/run/s-hy2.pid"
fi
# 颜色定义 (防止与主脚本冲突)
if [[ -z "${RED:-}" ]]; then
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly CYAN='\033[0;36m'
readonly NC='\033[0m'
fi
# 日志级别 (防止重复定义)
if [[ -z "${LOG_LEVEL_DEBUG:-}" ]]; then
readonly LOG_LEVEL_DEBUG=0
readonly LOG_LEVEL_INFO=1
readonly LOG_LEVEL_WARN=2
readonly LOG_LEVEL_ERROR=3
readonly LOG_LEVEL_FATAL=4
fi
# 当前日志级别 (默认为 INFO)
LOG_LEVEL=${LOG_LEVEL:-$LOG_LEVEL_INFO}
# 初始化日志系统
init_logging() {
# 创建日志目录
if [[ ! -d "$LOG_DIR" ]]; then
mkdir -p "$LOG_DIR" || {
echo "警告: 无法创建日志目录 $LOG_DIR" >&2
return 1
}
fi
# 设置日志文件权限
if [[ -f "$LOG_FILE" ]]; then
chmod 644 "$LOG_FILE"
else
touch "$LOG_FILE" && chmod 644 "$LOG_FILE"
fi
}
# 统一的日志记录函数
log_message() {
local level="$1"
local level_num="$2"
local color="$3"
shift 3
local message="$*"
# 检查日志级别
if [[ $level_num -lt $LOG_LEVEL ]]; then
return 0
fi
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 控制台输出(带颜色)
echo -e "${color}[${level}]${NC} $message" >&2
# 文件输出(无颜色)
if [[ -w "$LOG_FILE" ]] 2>/dev/null; then
echo "[$timestamp] [$SCRIPT_NAME:$$] [$level] $message" >> "$LOG_FILE"
fi
}
# 各级别日志函数
log_debug() {
log_message "DEBUG" $LOG_LEVEL_DEBUG "$CYAN" "$@"
}
log_info() {
log_message "INFO" $LOG_LEVEL_INFO "$BLUE" "$@"
}
log_warn() {
log_message "WARN" $LOG_LEVEL_WARN "$YELLOW" "$@"
}
log_error() {
log_message "ERROR" $LOG_LEVEL_ERROR "$RED" "$@"
}
log_fatal() {
log_message "FATAL" $LOG_LEVEL_FATAL "$RED" "$@"
}
log_success() {
log_message "SUCCESS" $LOG_LEVEL_INFO "$GREEN" "$@"
}
# 错误处理函数
error_exit() {
local message="$1"
local exit_code="${2:-1}"
log_error "$message"
# 执行清理
if command -v cleanup >/dev/null 2>&1; then
cleanup "$exit_code"
fi
exit "$exit_code"
}
# 设置错误陷阱
setup_error_handling() {
# 捕获各种信号
trap 'handle_error $? $LINENO' ERR
trap 'handle_signal INT "中断信号"' INT
trap 'handle_signal TERM "终止信号"' TERM
trap 'handle_signal HUP "挂起信号"' HUP
}
# 错误处理器
handle_error() {
local exit_code=$1
local line_number=$2
log_error "脚本执行失败: 退出码 $exit_code, 行号 $line_number"
# 显示调用栈
local i=0
while [[ ${FUNCNAME[$i]} ]]; do
log_debug "调用栈 $i: ${FUNCNAME[$i]} (${BASH_SOURCE[$i]}:${BASH_LINENO[$i]})"
((i++))
done
# 执行清理
if command -v cleanup >/dev/null 2>&1; then
cleanup "$exit_code"
fi
exit "$exit_code"
}
# 信号处理器
handle_signal() {
local signal="$1"
local description="$2"
log_info "接收到 $description ($signal), 正在清理..."
# 执行清理
if command -v cleanup >/dev/null 2>&1; then
cleanup 130
fi
exit 130
}
# 默认清理函数
cleanup() {
local exit_code="${1:-0}"
log_debug "执行清理操作 (退出码: $exit_code)"
# 清理临时文件
if [[ -n "${TEMP_FILES:-}" ]]; then
for temp_file in $TEMP_FILES; do
if [[ -f "$temp_file" ]]; then
rm -f "$temp_file"
log_debug "删除临时文件: $temp_file"
fi
done
fi
# 清理临时目录
if [[ -n "${TEMP_DIRS:-}" ]]; then
for temp_dir in $TEMP_DIRS; do
if [[ -d "$temp_dir" ]]; then
rm -rf "$temp_dir"
log_debug "删除临时目录: $temp_dir"
fi
done
fi
# 清理进程文件
if [[ -f "$PID_FILE" ]]; then
rm -f "$PID_FILE"
fi
}
# 创建安全临时文件
create_temp_file() {
local temp_file
temp_file=$(mktemp)
chmod 600 "$temp_file"
# 添加到清理列表
TEMP_FILES="${TEMP_FILES:-} $temp_file"
echo "$temp_file"
}
# 创建安全临时目录
create_temp_dir() {
local temp_dir
temp_dir=$(mktemp -d)
chmod 700 "$temp_dir"
# 添加到清理列表
TEMP_DIRS="${TEMP_DIRS:-} $temp_dir"
echo "$temp_dir"
}
# 检查命令是否存在
require_command() {
local cmd="$1"
local package="${2:-$cmd}"
if ! command -v "$cmd" >/dev/null 2>&1; then
error_exit "缺少必要的命令: $cmd (请安装 $package)"
fi
}
# 检查文件是否可读
require_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
error_exit "文件不存在: $file"
fi
if [[ ! -r "$file" ]]; then
error_exit "文件不可读: $file"
fi
}
# 检查目录是否可写
require_writable_dir() {
local dir="$1"
if [[ ! -d "$dir" ]]; then
error_exit "目录不存在: $dir"
fi
if [[ ! -w "$dir" ]]; then
error_exit "目录不可写: $dir"
fi
}
# 检查root权限
require_root() {
if [[ $EUID -ne 0 ]]; then
error_exit "此脚本需要root权限运行"
fi
}
# 等待用户确认
wait_for_user() {
echo ""
read -p "按回车键继续..." -r
}
# 检查Hysteria2是否已安装和配置
check_hysteria2_ready() {
local check_type="${1:-install}" # install, config, service
case $check_type in
"install")
if ! command -v hysteria >/dev/null 2>&1; then
log_warn "Hysteria2 未安装"
echo ""
echo -e "${YELLOW}提示:${NC}请先安装 Hysteria2"
echo " 返回主菜单选择 '1. 安装 Hysteria2'"
echo ""
wait_for_user
return 1
fi
;;
"config")
if [[ ! -f "/etc/hysteria/config.yaml" ]]; then
log_warn "Hysteria2 配置文件不存在"
echo ""
echo -e "${YELLOW}提示:${NC}请先配置 Hysteria2"
echo " 1. 返回主菜单选择 '2. 快速配置' 或 '3. 手动配置'"
echo " 2. 如未安装,请先选择 '1. 安装 Hysteria2'"
echo ""
wait_for_user
return 1
fi
;;
"service")
if ! systemctl is-enabled hysteria-server >/dev/null 2>&1; then
log_warn "Hysteria2 服务未启用"
echo ""
echo -e "${YELLOW}提示:${NC}请先启用服务"
echo " 返回主菜单选择 '7. 服务管理'"
echo ""
wait_for_user
return 1
fi
;;
esac
return 0
}
# 显示进度条
show_progress() {
local current="$1"
local total="$2"
local width=50
local percentage=$((current * 100 / total))
local completed=$((current * width / total))
printf "\r["
printf "%${completed}s" | tr ' ' '='
printf "%$((width - completed))s" | tr ' ' '-'
printf "] %d%% (%d/%d)" "$percentage" "$current" "$total"
}
# 并发控制
init_semaphore() {
local max_jobs="$1"
local semaphore_pipe="/tmp/semaphore.$$"
mkfifo "$semaphore_pipe"
exec 3<>"$semaphore_pipe"
for ((i=0; i<max_jobs; i++)); do
echo "token" >&3
done
# 添加到清理列表
TEMP_FILES="${TEMP_FILES:-} $semaphore_pipe"
}
acquire_semaphore() {
read -u 3
}
release_semaphore() {
echo "token" >&3
}
# 网络连接检查
check_internet_connection() {
local test_urls=(
"https://www.google.com"
"https://github.com"
"https://www.cloudflare.com"
)
for url in "${test_urls[@]}"; do
if curl -s --connect-timeout 5 --max-time 10 "$url" >/dev/null 2>&1; then
return 0
fi
done
return 1
}
# 导出函数供其他脚本使用
export -f init_logging
export -f log_debug log_info log_warn log_error log_fatal log_success
export -f error_exit setup_error_handling cleanup
export -f create_temp_file create_temp_dir
export -f require_command require_file require_writable_dir require_root
export -f wait_for_user show_progress
export -f init_semaphore acquire_semaphore release_semaphore
export -f check_internet_connection
# 自动初始化
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
# 被作为模块导入时自动初始化
init_logging
setup_error_handling
fi