mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-16 22:35:32 +08:00
Preserve literal paths and arguments, use portable command options, validate startup configuration, and retain scripts when downloads fail. Ensure task watchdogs and process groups are cleaned up across Bash versions, with compatibility and process lifecycle regression coverage.
459 lines
12 KiB
Bash
Executable File
459 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
## 目录
|
||
export dir_root=$QL_DIR
|
||
export dir_tmp=$dir_root/.tmp
|
||
export dir_data=$dir_root/data
|
||
|
||
if [[ ${QL_DATA_DIR:=} ]]; then
|
||
export dir_data="${QL_DATA_DIR%/}"
|
||
fi
|
||
|
||
export dir_shell=$dir_root/shell
|
||
export dir_preload=$dir_shell/preload
|
||
export dir_sample=$dir_root/sample
|
||
export dir_static=$dir_root/static
|
||
export dir_config=$dir_data/config
|
||
export dir_scripts=$dir_data/scripts
|
||
export dir_repo=$dir_data/repo
|
||
export dir_raw=$dir_data/raw
|
||
export dir_log=$dir_data/log
|
||
export dir_db=$dir_data/db
|
||
export dir_dep=$dir_data/deps
|
||
export dir_list_tmp=$dir_log/.tmp
|
||
export dir_update_log=$dir_log/update
|
||
export ql_static_repo=$dir_repo/static
|
||
|
||
## 文件
|
||
export file_config_sample=$dir_sample/config.sample.sh
|
||
export file_env=$dir_preload/env.sh
|
||
export file_preload_js=$dir_preload/sitecustomize.js
|
||
export file_sharecode=$dir_config/sharecode.sh
|
||
export file_config_user=$dir_config/config.sh
|
||
export file_auth_sample=$dir_sample/auth.sample.json
|
||
export file_auth_user=$dir_config/auth.json
|
||
export file_auth_token=$dir_config/token.json
|
||
export file_extra_shell=$dir_config/extra.sh
|
||
export file_task_before=$dir_config/task_before.sh
|
||
export file_task_before_js=$dir_config/task_before.js
|
||
export file_task_before_py=$dir_config/task_before.py
|
||
export file_task_after=$dir_config/task_after.sh
|
||
export file_task_sample=$dir_sample/task.sample.sh
|
||
export file_extra_sample=$dir_sample/extra.sample.sh
|
||
export file_notify_js_sample=$dir_sample/notify.js
|
||
export file_notify_py_sample=$dir_sample/notify.py
|
||
export file_test_js_sample=$dir_sample/ql_sample.js
|
||
export file_test_py_sample=$dir_sample/ql_sample.py
|
||
export file_notify_py=$dir_scripts/notify.py
|
||
export file_notify_js=$dir_scripts/sendNotify.js
|
||
export file_test_js=$dir_scripts/ql_sample.js
|
||
export file_test_py=$dir_scripts/ql_sample.py
|
||
export dep_notify_py=$dir_dep/notify.py
|
||
export dep_notify_js=$dir_dep/sendNotify.js
|
||
|
||
## 清单文件
|
||
list_crontab_user=$dir_config/crontab.list
|
||
list_crontab_sample=$dir_sample/crontab.sample.list
|
||
list_own_scripts=$dir_list_tmp/own_scripts.list
|
||
list_own_user=$dir_list_tmp/own_user.list
|
||
list_own_add=$dir_list_tmp/own_add.list
|
||
list_own_drop=$dir_list_tmp/own_drop.list
|
||
|
||
link_name=(
|
||
task
|
||
ql
|
||
)
|
||
|
||
init_env() {
|
||
export NODE_PATH="/usr/local/bin:/usr/local/lib/node_modules"
|
||
export PYTHONUNBUFFERED=1
|
||
}
|
||
|
||
load_ql_envs() {
|
||
ql_base_url=${QlBaseUrl:-"/"}
|
||
ql_port=${QlPort:-"5700"}
|
||
ql_grpc_port=${QlGrpcPort:-"5500"}
|
||
current_branch=${QL_BRANCH:-""}
|
||
}
|
||
|
||
import_config() {
|
||
[[ -f $file_config_user ]] && . "$file_config_user"
|
||
[[ -f $dir_preload/lang_env.sh ]] && . "$dir_preload/lang_env.sh"
|
||
|
||
# Language packs need global associative arrays (Bash 4.2+). Probe the
|
||
# capability so Bash 3.2 and 4.0/4.1 retain the Chinese-key fallback.
|
||
local lang=${QL_LANG:-zh}
|
||
local lang_file="$dir_shell/lang/${lang}.sh"
|
||
if (declare -gA __ql_lang_probe) 2>/dev/null; then
|
||
if [[ -f $lang_file ]]; then
|
||
. "$lang_file"
|
||
elif [[ -f "$dir_shell/lang/zh.sh" ]]; then
|
||
. "$dir_shell/lang/zh.sh"
|
||
fi
|
||
fi
|
||
|
||
load_ql_envs
|
||
command_timeout_time=${CommandTimeoutTime:-""}
|
||
file_extensions=${RepoFileExtensions:-"js py"}
|
||
proxy_url=${ProxyUrl:-""}
|
||
|
||
if [[ -n "${DefaultCronRule}" ]]; then
|
||
default_cron="${DefaultCronRule}"
|
||
else
|
||
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
|
||
fi
|
||
}
|
||
|
||
t() {
|
||
local key="$1"
|
||
shift
|
||
local msg
|
||
if declare -p LANG_MESSAGES &>/dev/null; then
|
||
msg="${LANG_MESSAGES["$key"]}"
|
||
fi
|
||
[[ -z $msg ]] && msg="$key"
|
||
# shellcheck disable=SC2059
|
||
printf -- "$msg\n" "$@"
|
||
}
|
||
|
||
set_proxy() {
|
||
local proxy="$1"
|
||
if [[ $proxy ]]; then
|
||
proxy_url="$proxy"
|
||
fi
|
||
if [[ $proxy_url ]]; then
|
||
export http_proxy="${proxy_url}"
|
||
export https_proxy="${proxy_url}"
|
||
fi
|
||
}
|
||
|
||
unset_proxy() {
|
||
unset http_proxy
|
||
unset https_proxy
|
||
}
|
||
|
||
make_dir() {
|
||
local dir=$1
|
||
if [[ ! -d $dir ]]; then
|
||
mkdir -p -- "$dir"
|
||
fi
|
||
}
|
||
|
||
detect_termux() {
|
||
if [[ $PATH == *com.termux* ]]; then
|
||
is_termux=1
|
||
else
|
||
is_termux=0
|
||
fi
|
||
}
|
||
|
||
detect_macos() {
|
||
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
|
||
}
|
||
|
||
gen_random_num() {
|
||
local divi=$1
|
||
echo $((${RANDOM} % $divi))
|
||
}
|
||
|
||
define_cmd() {
|
||
local cmd_prefix cmd_suffix
|
||
if type task &>/dev/null; then
|
||
cmd_suffix=""
|
||
if [[ -f "$dir_shell/task.sh" ]]; then
|
||
cmd_prefix=""
|
||
else
|
||
cmd_prefix="bash "
|
||
fi
|
||
else
|
||
cmd_suffix=".sh"
|
||
if [[ -f "$dir_shell/task.sh" ]]; then
|
||
cmd_prefix="$dir_shell/"
|
||
else
|
||
cmd_prefix="bash $dir_shell/"
|
||
fi
|
||
fi
|
||
for ((i = 0; i < ${#link_name[*]}; i++)); do
|
||
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
|
||
done
|
||
}
|
||
|
||
fix_config() {
|
||
make_dir "$dir_tmp"
|
||
make_dir "$dir_static"
|
||
make_dir "$dir_data"
|
||
make_dir "$dir_config"
|
||
make_dir "$dir_log"
|
||
make_dir "$dir_db"
|
||
make_dir "$dir_scripts"
|
||
make_dir "$dir_list_tmp"
|
||
make_dir "$dir_repo"
|
||
make_dir "$dir_raw"
|
||
make_dir "$dir_update_log"
|
||
make_dir "$dir_dep"
|
||
|
||
if [[ ! -s $file_config_user ]]; then
|
||
cp -f "$file_config_sample" "$file_config_user"
|
||
fi
|
||
|
||
if [[ ! -f $file_task_before ]]; then
|
||
cp -f "$file_task_sample" "$file_task_before"
|
||
fi
|
||
|
||
if [[ ! -f $file_task_after ]]; then
|
||
cp -f "$file_task_sample" "$file_task_after"
|
||
fi
|
||
|
||
if [[ ! -f $file_extra_shell ]]; then
|
||
cp -f "$file_extra_sample" "$file_extra_shell"
|
||
fi
|
||
|
||
if [[ ! -s $file_notify_py ]]; then
|
||
cp -f "$file_notify_py_sample" "$file_notify_py"
|
||
fi
|
||
|
||
if [[ ! -s $file_notify_js ]]; then
|
||
cp -f "$file_notify_js_sample" "$file_notify_js"
|
||
fi
|
||
|
||
if [[ ! -s $file_test_js ]]; then
|
||
cp -f "$file_test_js_sample" "$file_test_js"
|
||
fi
|
||
|
||
if [[ ! -s $file_test_py ]]; then
|
||
cp -f "$file_test_py_sample" "$file_test_py"
|
||
fi
|
||
|
||
if [[ ! -s $dep_notify_js ]]; then
|
||
cp -f "$file_notify_js_sample" "$dep_notify_js"
|
||
fi
|
||
|
||
if [[ ! -s $dep_notify_py ]]; then
|
||
cp -f "$file_notify_py_sample" "$dep_notify_py"
|
||
fi
|
||
|
||
}
|
||
|
||
npm_install_sub() {
|
||
if [ $is_termux -eq 1 ]; then
|
||
npm install --production --no-bin-links
|
||
elif ! type pnpm &>/dev/null; then
|
||
npm install --production
|
||
else
|
||
pnpm install --loglevel error --production
|
||
fi
|
||
exit_status=$?
|
||
}
|
||
|
||
npm_install_2() {
|
||
local dir_current=$(pwd)
|
||
local dir_work=$1
|
||
|
||
cd "$dir_work" || return $?
|
||
t '安装 %s 依赖包...\n' "$dir_work"
|
||
npm_install_sub
|
||
cd "$dir_current" || return $?
|
||
}
|
||
|
||
diff_and_copy() {
|
||
local copy_source=$1
|
||
local copy_to=$2
|
||
if [[ ! -s $copy_to ]] || [[ $(diff "$copy_source" "$copy_to") ]]; then
|
||
cp -f "$copy_source" "$copy_to"
|
||
fi
|
||
}
|
||
|
||
git_clone_scripts() {
|
||
local url="$1"
|
||
local dir="$2"
|
||
local branch="$3"
|
||
local proxy="$4"
|
||
local branch_args=()
|
||
[[ $branch ]] && branch_args=(-b "$branch")
|
||
t '开始拉取仓库 %s 到 %s\n' "${uniq_path}" "$dir"
|
||
|
||
set_proxy "$proxy"
|
||
|
||
git clone -q --depth=1 "${branch_args[@]}" "$url" "$dir"
|
||
exit_status=$?
|
||
|
||
unset_proxy
|
||
}
|
||
|
||
random_range() {
|
||
local beg=$1
|
||
local end=$2
|
||
echo $((RANDOM % ($end - $beg) + $beg))
|
||
}
|
||
|
||
delete_pm2() {
|
||
cd "$dir_root" || return $?
|
||
# Try to delete PM2 processes, but don't fail if PM2 is not available
|
||
pm2 delete ecosystem.config.js 2>/dev/null || true
|
||
# Also try to kill any directly spawned node processes
|
||
pkill -f "node.*static/build/app.js" 2>/dev/null || true
|
||
}
|
||
|
||
reload_pm2() {
|
||
cd "$dir_root" || return $?
|
||
restore_env_vars
|
||
|
||
# Try to start PM2, but handle failures gracefully
|
||
if pm2 flush &>/dev/null && pm2 startOrGracefulReload ecosystem.config.js --update-env; then
|
||
return 0
|
||
else
|
||
local exit_code=$?
|
||
t '警告: PM2 启动失败 (退出码: %s),可能是由于硬件不兼容' "$exit_code"
|
||
t '正在尝试直接使用 Node.js 启动服务...'
|
||
|
||
# Kill any existing node processes for qinglong
|
||
pkill -f "node.*static/build/app.js" 2>/dev/null || true
|
||
|
||
# Start node directly in the background. In containers, keep application
|
||
# output on the container streams instead of duplicating Winston logs on disk.
|
||
if [[ "$QL_CONTAINER" == "true" ]]; then
|
||
nohup node static/build/app.js >/proc/1/fd/1 2>/proc/1/fd/2 &
|
||
else
|
||
nohup node static/build/app.js >"$dir_log/qinglong.log" 2>&1 &
|
||
fi
|
||
local node_pid=$!
|
||
|
||
t '已使用 Node.js 直接启动服务 (PID: %s)' "$node_pid"
|
||
t '注意: 使用此模式时,部分 PM2 管理功能将不可用'
|
||
return 0
|
||
fi
|
||
}
|
||
|
||
diff_time() {
|
||
local format="$1"
|
||
local begin_time="$2"
|
||
local end_time="$3"
|
||
|
||
if [[ $is_macos -eq 1 ]]; then
|
||
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
|
||
else
|
||
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
||
fi
|
||
echo "$diff_time"
|
||
}
|
||
|
||
format_time() {
|
||
local format="$1"
|
||
local time="$2"
|
||
|
||
if [[ $is_macos -eq 1 ]]; then
|
||
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
|
||
else
|
||
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
|
||
fi
|
||
}
|
||
|
||
format_log_time() {
|
||
local format="$1"
|
||
local time="$2"
|
||
|
||
if [[ $is_macos -eq 1 ]]; then
|
||
echo $(python3 -c 'from datetime import datetime; print(datetime.now().strftime("%Y-%m-%d-%H-%M-%S-%f")[:-3])')
|
||
else
|
||
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
|
||
fi
|
||
}
|
||
|
||
format_timestamp() {
|
||
local format="$1"
|
||
local time="$2"
|
||
|
||
if [[ $is_macos -eq 1 ]]; then
|
||
echo $(date -j -f "$format" "$time" "+%s")
|
||
else
|
||
echo $(date -d "$time" "+%s")
|
||
fi
|
||
}
|
||
|
||
get_env_array() {
|
||
exported_variables=()
|
||
# Preserve the legacy export-line/second-field rules without evaluating values.
|
||
local export_name_program='/^export / { name = $2; sub(/=.*/, "", name); print name }'
|
||
if [[ ${BASH_VERSINFO[0]} -ge 4 ]]; then
|
||
builtin mapfile -t exported_variables < <(awk "$export_name_program" "$file_env")
|
||
else
|
||
# macOS still ships Bash 3, which does not provide mapfile.
|
||
while IFS= read -r line; do
|
||
exported_variables+=("$line")
|
||
done < <(awk "$export_name_program" "$file_env")
|
||
fi
|
||
}
|
||
|
||
clear_env() {
|
||
for var in "${exported_variables[@]}"; do
|
||
unset "$var"
|
||
done
|
||
}
|
||
|
||
handle_task_start() {
|
||
local error_message=""
|
||
if [[ $ID ]]; then
|
||
local error=$(update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp")
|
||
if [[ $error ]]; then
|
||
error_message=", 任务状态更新失败(${error})"
|
||
fi
|
||
fi
|
||
t '## 开始执行... %s\n' "${begin_time}${error_message}"
|
||
}
|
||
|
||
run_task_before() {
|
||
. "$file_task_before" "$@"
|
||
|
||
if [[ ${task_before:=} ]]; then
|
||
t '执行前置命令\n'
|
||
eval "${task_before%;}"
|
||
t '\n执行前置命令结束\n'
|
||
fi
|
||
}
|
||
|
||
run_task_after() {
|
||
. "$file_task_after" "$@"
|
||
|
||
if [[ ${task_after:=} ]]; then
|
||
t '\n执行后置命令\n'
|
||
eval "${task_after%;}"
|
||
t '\n执行后置命令结束'
|
||
fi
|
||
}
|
||
|
||
handle_task_end() {
|
||
local etime end_time end_timestamp
|
||
if [[ $is_macos -ne 1 && $time_format == '%Y-%m-%d %H:%M:%S' ]]; then
|
||
IFS='|' read -r end_time end_timestamp < <(date "+$time_format|%s")
|
||
else
|
||
etime=$(date "+$time_format")
|
||
end_time=$(format_time "$time_format" "$etime")
|
||
end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||
fi
|
||
local diff_time=$(($end_timestamp - $begin_timestamp))
|
||
local exit_code="${_task_exit_code:-0}"
|
||
[[ "$diff_time" == 0 ]] && diff_time=1
|
||
|
||
if [[ $ID ]]; then
|
||
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time" "$exit_code")
|
||
if [[ $error ]]; then
|
||
error_message=", 状态更新失败(${error})"
|
||
fi
|
||
fi
|
||
|
||
record_cron_stat "$ID" "${exit_code:-0}" "$diff_time"
|
||
|
||
if [[ "${MANUAL:=}" == "true" ]]; then
|
||
t '\n## 已停止 🛑... %s 耗时 %s 秒%s' "$end_time" "$diff_time" "${error_message:=} "
|
||
elif [[ $exit_code -eq 0 ]]; then
|
||
t '\n## 完成 ✅... %s 耗时 %s 秒%s' "$end_time" "$diff_time" "${error_message:=} "
|
||
else
|
||
t '\n## 失败 ❌(退出码 %s)... %s 耗时 %s 秒%s' "$exit_code" "$end_time" "$diff_time" "${error_message:=} "
|
||
fi
|
||
}
|
||
|
||
init_env
|
||
detect_termux
|
||
detect_macos
|
||
define_cmd
|