mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-16 22:35:32 +08:00
fix: improve shell compatibility and process cleanup
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.
This commit is contained in:
@@ -119,12 +119,12 @@ reload_pm2
|
|||||||
|
|
||||||
if [[ $AutoStartBot == true ]]; then
|
if [[ $AutoStartBot == true ]]; then
|
||||||
log_with_style "INFO" "🤖 3. 启动 bot..."
|
log_with_style "INFO" "🤖 3. 启动 bot..."
|
||||||
nohup ql bot >$dir_log/bot.log 2>&1 &
|
nohup ql bot >"$dir_log/bot.log" 2>&1 &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EnableExtraShell == true ]]; then
|
if [[ $EnableExtraShell == true ]]; then
|
||||||
log_with_style "INFO" "🛠️ 4. 执行自定义脚本..."
|
log_with_style "INFO" "🛠️ 4. 执行自定义脚本..."
|
||||||
nohup ql extra >$dir_log/extra.log 2>&1 &
|
nohup ql extra >"$dir_log/extra.log" 2>&1 &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_with_style "SUCCESS" "🎉 容器启动成功!"
|
log_with_style "SUCCESS" "🎉 容器启动成功!"
|
||||||
|
|||||||
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
create_token() {
|
create_token() {
|
||||||
local token_command="ts-node-transpile-only ${dir_root}/back/token.ts"
|
|
||||||
local token_file="${dir_root}/static/build/token.js"
|
local token_file="${dir_root}/static/build/token.js"
|
||||||
if [[ -f $token_file ]]; then
|
if [[ -f $token_file ]]; then
|
||||||
token_command="node ${token_file}"
|
__ql_token__=$(node "$token_file")
|
||||||
|
else
|
||||||
|
__ql_token__=$(ts-node-transpile-only "${dir_root}/back/token.ts")
|
||||||
fi
|
fi
|
||||||
__ql_token__=$(eval "$token_command")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get_token() {
|
get_token() {
|
||||||
|
|||||||
+30
-28
@@ -39,16 +39,16 @@ random_delay() {
|
|||||||
gen_array_scripts() {
|
gen_array_scripts() {
|
||||||
local dir_current=$(pwd)
|
local dir_current=$(pwd)
|
||||||
local i="-1"
|
local i="-1"
|
||||||
cd $dir_scripts
|
cd "$dir_scripts" || return $?
|
||||||
for file in $(ls); do
|
for file in *; do
|
||||||
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
|
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
|
||||||
let i++
|
let i++
|
||||||
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
|
array_scripts[i]=$file
|
||||||
array_scripts_name[i]=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
array_scripts_name[i]=$(grep -- "new Env" "$file" | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||||
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
|
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
cd $dir_current
|
cd "$dir_current" || return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
## 使用说明
|
## 使用说明
|
||||||
@@ -72,11 +72,11 @@ usage() {
|
|||||||
## run nohup,$1:文件名,不含路径,带后缀
|
## run nohup,$1:文件名,不含路径,带后缀
|
||||||
run_nohup() {
|
run_nohup() {
|
||||||
local file_name=$1
|
local file_name=$1
|
||||||
nohup node $file_name &>$log_path &
|
nohup node "$file_name" &>"$log_path" &
|
||||||
}
|
}
|
||||||
|
|
||||||
env_str_to_array() {
|
env_str_to_array() {
|
||||||
. $file_env
|
. "$file_env"
|
||||||
local IFS="&"
|
local IFS="&"
|
||||||
read -ra array <<<"${!env_param}"
|
read -ra array <<<"${!env_param}"
|
||||||
array_length=${#array[@]}
|
array_length=${#array[@]}
|
||||||
@@ -122,7 +122,7 @@ enter_script_workdir() {
|
|||||||
_target_dir="${dir_scripts}/${work_dir}"
|
_target_dir="${dir_scripts}/${work_dir}"
|
||||||
fi
|
fi
|
||||||
if [[ -d "${_target_dir}" ]]; then
|
if [[ -d "${_target_dir}" ]]; then
|
||||||
cd "${_target_dir}"
|
cd "${_target_dir}" || return $?
|
||||||
if [[ ${file_param} =~ "/" ]]; then
|
if [[ ${file_param} =~ "/" ]]; then
|
||||||
local script_name="${file_param##*/}"
|
local script_name="${file_param##*/}"
|
||||||
if [[ "${use_dot_prefix}" == "true" ]]; then
|
if [[ "${use_dot_prefix}" == "true" ]]; then
|
||||||
@@ -136,13 +136,13 @@ enter_script_workdir() {
|
|||||||
t '警告:工作目录不存在 %s' "${_target_dir}"
|
t '警告:工作目录不存在 %s' "${_target_dir}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd $dir_scripts
|
cd "$dir_scripts" || return $?
|
||||||
if [[ ${file_param} =~ "/" ]]; then
|
if [[ ${file_param} =~ "/" ]]; then
|
||||||
local script_dir="${file_param%/*}"
|
local script_dir="${file_param%/*}"
|
||||||
local script_name="${file_param##*/}"
|
local script_name="${file_param##*/}"
|
||||||
|
|
||||||
if [[ -d ${script_dir} ]]; then
|
if [[ -d ${script_dir} ]]; then
|
||||||
cd ${script_dir}
|
cd "$script_dir" || return $?
|
||||||
if [[ "${use_dot_prefix}" == "true" ]]; then
|
if [[ "${use_dot_prefix}" == "true" ]]; then
|
||||||
file_param="./${script_name}"
|
file_param="./${script_name}"
|
||||||
else
|
else
|
||||||
@@ -159,12 +159,12 @@ run_normal() {
|
|||||||
random_delay "$file_param"
|
random_delay "$file_param"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
enter_script_workdir
|
enter_script_workdir || return $?
|
||||||
|
|
||||||
if [[ $isJsOrPythonFile == 'false' ]]; then
|
if [[ $isJsOrPythonFile == 'false' ]]; then
|
||||||
clear_non_sh_env
|
clear_non_sh_env
|
||||||
fi
|
fi
|
||||||
run_task_command $which_program $file_param "${script_params[@]}"
|
run_task_command ${which_program:+"$which_program"} "$file_param" "${script_params[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_env_split() {
|
handle_env_split() {
|
||||||
@@ -175,7 +175,7 @@ handle_env_split() {
|
|||||||
env_str_to_array
|
env_str_to_array
|
||||||
local tempArr=$(echo $num_param | sed "s/-max/-${array_length}/g" | sed "s/max-/${array_length}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
local tempArr=$(echo $num_param | sed "s/-max/-${array_length}/g" | sed "s/max-/${array_length}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||||
local runArr=($(eval echo $tempArr))
|
local runArr=($(eval echo $tempArr))
|
||||||
array_run=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
|
array_run=($(awk -v RS=' ' '!a[$1]++' <<<"${runArr[*]}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||||
@@ -192,10 +192,10 @@ run_concurrent() {
|
|||||||
time=$(date "+$mtime_format")
|
time=$(date "+$mtime_format")
|
||||||
single_log_time=$(format_log_time "$mtime_format" "$time")
|
single_log_time=$(format_log_time "$mtime_format" "$time")
|
||||||
|
|
||||||
enter_script_workdir
|
enter_script_workdir || return $?
|
||||||
|
|
||||||
local j=0
|
local j=0
|
||||||
for i in ${array_run[@]}; do
|
for i in "${array_run[@]}"; do
|
||||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((j + 1)).log"
|
single_log_path="$dir_log/$log_dir/${single_log_time}_$((j + 1)).log"
|
||||||
let j++
|
let j++
|
||||||
|
|
||||||
@@ -203,16 +203,16 @@ run_concurrent() {
|
|||||||
export "${env_param}=${array[$i - 1]}"
|
export "${env_param}=${array[$i - 1]}"
|
||||||
clear_non_sh_env
|
clear_non_sh_env
|
||||||
fi
|
fi
|
||||||
eval envParam="${env_param}" numParam="${i}" run_task_command $which_program $file_param "${script_params[@]}" &>$single_log_path &
|
envParam="${env_param}" numParam="${i}" run_task_command ${which_program:+"$which_program"} "$file_param" "${script_params[@]}" &>"$single_log_path" &
|
||||||
done
|
done
|
||||||
|
|
||||||
wait
|
wait
|
||||||
local k=0
|
local k=0
|
||||||
for i in ${array_run[@]}; do
|
for i in "${array_run[@]}"; do
|
||||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((k + 1)).log"
|
single_log_path="$dir_log/$log_dir/${single_log_time}_$((k + 1)).log"
|
||||||
let k++
|
let k++
|
||||||
cat $single_log_path
|
cat "$single_log_path"
|
||||||
[[ -f $single_log_path ]] && rm -f $single_log_path
|
[[ -f $single_log_path ]] && rm -f "$single_log_path"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ run_designated() {
|
|||||||
|
|
||||||
if [[ $isJsOrPythonFile == 'false' ]]; then
|
if [[ $isJsOrPythonFile == 'false' ]]; then
|
||||||
local n=0
|
local n=0
|
||||||
for i in ${array_run[@]}; do
|
for i in "${array_run[@]}"; do
|
||||||
array_str[n]=${array[$i - 1]}
|
array_str[n]=${array[$i - 1]}
|
||||||
let n++
|
let n++
|
||||||
done
|
done
|
||||||
@@ -241,9 +241,9 @@ run_designated() {
|
|||||||
clear_non_sh_env
|
clear_non_sh_env
|
||||||
fi
|
fi
|
||||||
|
|
||||||
enter_script_workdir
|
enter_script_workdir || return $?
|
||||||
|
|
||||||
envParam="${env_param}" numParam="${num_param}" run_task_command $which_program $file_param "${script_params[@]}"
|
envParam="${env_param}" numParam="${num_param}" run_task_command ${which_program:+"$which_program"} "$file_param" "${script_params[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
## 运行其他命令
|
## 运行其他命令
|
||||||
@@ -273,7 +273,7 @@ run_else() {
|
|||||||
if [[ -n "$script_for_dir" ]]; then
|
if [[ -n "$script_for_dir" ]]; then
|
||||||
local saved_file_param="$file_param"
|
local saved_file_param="$file_param"
|
||||||
file_param="$script_for_dir"
|
file_param="$script_for_dir"
|
||||||
enter_script_workdir true
|
enter_script_workdir true || return $?
|
||||||
local adjusted_script="$file_param"
|
local adjusted_script="$file_param"
|
||||||
file_param="$saved_file_param"
|
file_param="$saved_file_param"
|
||||||
|
|
||||||
@@ -289,17 +289,17 @@ run_else() {
|
|||||||
set -- "${new_args[@]}"
|
set -- "${new_args[@]}"
|
||||||
else
|
else
|
||||||
# 没有找到脚本参数,只 cd 到 scripts 目录
|
# 没有找到脚本参数,只 cd 到 scripts 目录
|
||||||
enter_script_workdir true
|
enter_script_workdir true || return $?
|
||||||
shift
|
shift
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# file_param 本身就是脚本,直接用 enter_script_workdir 处理
|
# file_param 本身就是脚本,直接用 enter_script_workdir 处理
|
||||||
enter_script_workdir true
|
enter_script_workdir true || return $?
|
||||||
shift
|
shift
|
||||||
fi
|
fi
|
||||||
|
|
||||||
clear_non_sh_env
|
clear_non_sh_env
|
||||||
run_task_command $which_program $file_param "$@"
|
run_task_command ${which_program:+"$which_program"} "$file_param" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_file() {
|
check_file() {
|
||||||
@@ -313,12 +313,14 @@ check_file() {
|
|||||||
export PREV_NODE_OPTIONS="${NODE_OPTIONS:=}"
|
export PREV_NODE_OPTIONS="${NODE_OPTIONS:=}"
|
||||||
export PREV_PYTHONPATH="${PYTHONPATH:=}"
|
export PREV_PYTHONPATH="${PYTHONPATH:=}"
|
||||||
if [[ $1 == *.js ]] || [[ $1 == *.ts ]] || [[ $1 == *.mjs ]]; then
|
if [[ $1 == *.js ]] || [[ $1 == *.ts ]] || [[ $1 == *.mjs ]]; then
|
||||||
export NODE_OPTIONS="-r ${file_preload_js} ${NODE_OPTIONS}"
|
local preload_path=${file_preload_js//\\/\\\\}
|
||||||
|
preload_path=${preload_path//\"/\\\"}
|
||||||
|
export NODE_OPTIONS="-r \"${preload_path}\" ${NODE_OPTIONS}"
|
||||||
else
|
else
|
||||||
export PYTHONPATH="${dir_preload}:${dir_config}:${PYTHONPATH}"
|
export PYTHONPATH="${dir_preload}:${dir_config}:${PYTHONPATH}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
. $file_env
|
. "$file_env"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -1,4 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# BSD grep accepts \d as a digit; GNU grep treats it as a literal d.
|
||||||
|
string=$(awk '$1 == "version:" { print $2; exit }' version.yaml) || exit 1
|
||||||
|
if [[ ! $string =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[[:alnum:].-]+)?(\+[[:alnum:].-]+)?$ ]]; then
|
||||||
|
printf 'Invalid release version: %s\n' "$string" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
version="v$string"
|
||||||
echo -e "开始发布"
|
echo -e "开始发布"
|
||||||
|
|
||||||
echo -e "切换master分支"
|
echo -e "切换master分支"
|
||||||
@@ -9,8 +17,6 @@ git push --set-upstream origin master -f
|
|||||||
echo -e "更新cdn文件"
|
echo -e "更新cdn文件"
|
||||||
ts-node-transpile-only sample/tool.ts
|
ts-node-transpile-only sample/tool.ts
|
||||||
|
|
||||||
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
|
|
||||||
version="v$string"
|
|
||||||
echo -e "当前版本$version"
|
echo -e "当前版本$version"
|
||||||
|
|
||||||
echo -e "删除已经存在的本地tag"
|
echo -e "删除已经存在的本地tag"
|
||||||
|
|||||||
+21
-16
@@ -3,42 +3,47 @@
|
|||||||
days=$1
|
days=$1
|
||||||
|
|
||||||
remove_js_log() {
|
remove_js_log() {
|
||||||
local log_full_path_list=$(find $dir_log -name "*.log")
|
local log log_date log_timestamp diff_time log_path encoded_path result
|
||||||
local diff_time
|
local now
|
||||||
for log in $log_full_path_list; do
|
now=$(date +%s) || return $?
|
||||||
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10)
|
while IFS= read -r -d '' log; do
|
||||||
|
log_date=${log##*/}
|
||||||
|
log_date=${log_date:0:10}
|
||||||
if ! [[ $log_date =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
if ! [[ $log_date =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
||||||
if [[ $is_macos -eq 1 ]]; then
|
if [[ $is_macos -eq 1 ]]; then
|
||||||
log_date=$(stat -f %Sm -t "%Y-%m-%d" "$log")
|
log_date=$(stat -f %Sm -t "%Y-%m-%d" "$log") || continue
|
||||||
else
|
else
|
||||||
log_date=$(stat -c %y "$log" | cut -d ' ' -f 1)
|
log_date=$(stat -c %y "$log" | cut -d ' ' -f 1)
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [[ $is_macos -eq 1 ]]; then
|
if [[ $is_macos -eq 1 ]]; then
|
||||||
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
|
# BSD date otherwise fills the omitted time from the current clock.
|
||||||
|
log_timestamp=$(date -j -f "%Y-%m-%d %H:%M:%S" "$log_date 00:00:00" +%s) || continue
|
||||||
else
|
else
|
||||||
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
|
log_timestamp=$(date +%s -d "$log_date") || continue
|
||||||
fi
|
fi
|
||||||
|
diff_time=$((now - log_timestamp))
|
||||||
if [[ $diff_time -gt $((${days} * 86400)) ]]; then
|
if [[ $diff_time -gt $((${days} * 86400)) ]]; then
|
||||||
local log_path=$(echo "$log" | sed "s,${dir_log}/,,g")
|
log_path=${log#"$dir_log"/}
|
||||||
local result=$(find_cron_api "log_path=$log_path")
|
encoded_path=$(printf '%s' "$log_path" | jq -sRr @uri) || continue
|
||||||
|
result=$(find_cron_api "log_path=$encoded_path")
|
||||||
t '查询文件 %s' "$log_path"
|
t '查询文件 %s' "$log_path"
|
||||||
if [[ -z $result ]]; then
|
if [[ -z $result ]]; then
|
||||||
t '删除中~'
|
t '删除中~'
|
||||||
rm -vf $log
|
rm -vf -- "$log"
|
||||||
else
|
else
|
||||||
t '正在被 %s 使用,跳过~' "$result"
|
t '正在被 %s 使用,跳过~' "$result"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done < <(find "$dir_log" -type f -name "*.log" -print0)
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_empty_dir() {
|
remove_empty_dir() {
|
||||||
cd $dir_log
|
local dir
|
||||||
for dir in $(ls); do
|
for dir in "$dir_log"/*/ "$dir_log"/.[!.]*/ "$dir_log"/..?*/; do
|
||||||
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
|
[[ -d "$dir" ]] || continue
|
||||||
rm -rf $dir
|
# rmdir also protects a directory populated between discovery and removal.
|
||||||
fi
|
rmdir -- "$dir" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-40
@@ -77,17 +77,20 @@ load_ql_envs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
import_config() {
|
import_config() {
|
||||||
[[ -f $file_config_user ]] && . $file_config_user
|
[[ -f $file_config_user ]] && . "$file_config_user"
|
||||||
[[ -f $dir_preload/lang_env.sh ]] && . $dir_preload/lang_env.sh
|
[[ -f $dir_preload/lang_env.sh ]] && . "$dir_preload/lang_env.sh"
|
||||||
|
|
||||||
# 加载语言包(bash 4+ 支持 declare -A,不兼容时回退输出中文 key)
|
# 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=${QL_LANG:-zh}
|
||||||
local lang_file="$dir_shell/lang/${lang}.sh"
|
local lang_file="$dir_shell/lang/${lang}.sh"
|
||||||
if [[ ${BASH_VERSINFO[0]} -ge 4 ]] && [[ -f $lang_file ]]; then
|
if (declare -gA __ql_lang_probe) 2>/dev/null; then
|
||||||
. $lang_file
|
if [[ -f $lang_file ]]; then
|
||||||
elif [[ ${BASH_VERSINFO[0]} -ge 4 ]]; then
|
. "$lang_file"
|
||||||
|
elif [[ -f "$dir_shell/lang/zh.sh" ]]; then
|
||||||
. "$dir_shell/lang/zh.sh"
|
. "$dir_shell/lang/zh.sh"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
load_ql_envs
|
load_ql_envs
|
||||||
command_timeout_time=${CommandTimeoutTime:-""}
|
command_timeout_time=${CommandTimeoutTime:-""}
|
||||||
@@ -132,7 +135,7 @@ unset_proxy() {
|
|||||||
make_dir() {
|
make_dir() {
|
||||||
local dir=$1
|
local dir=$1
|
||||||
if [[ ! -d $dir ]]; then
|
if [[ ! -d $dir ]]; then
|
||||||
mkdir -p $dir
|
mkdir -p -- "$dir"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,57 +179,57 @@ define_cmd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fix_config() {
|
fix_config() {
|
||||||
make_dir $dir_tmp
|
make_dir "$dir_tmp"
|
||||||
make_dir $dir_static
|
make_dir "$dir_static"
|
||||||
make_dir $dir_data
|
make_dir "$dir_data"
|
||||||
make_dir $dir_config
|
make_dir "$dir_config"
|
||||||
make_dir $dir_log
|
make_dir "$dir_log"
|
||||||
make_dir $dir_db
|
make_dir "$dir_db"
|
||||||
make_dir $dir_scripts
|
make_dir "$dir_scripts"
|
||||||
make_dir $dir_list_tmp
|
make_dir "$dir_list_tmp"
|
||||||
make_dir $dir_repo
|
make_dir "$dir_repo"
|
||||||
make_dir $dir_raw
|
make_dir "$dir_raw"
|
||||||
make_dir $dir_update_log
|
make_dir "$dir_update_log"
|
||||||
make_dir $dir_dep
|
make_dir "$dir_dep"
|
||||||
|
|
||||||
if [[ ! -s $file_config_user ]]; then
|
if [[ ! -s $file_config_user ]]; then
|
||||||
cp -f $file_config_sample $file_config_user
|
cp -f "$file_config_sample" "$file_config_user"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f $file_task_before ]]; then
|
if [[ ! -f $file_task_before ]]; then
|
||||||
cp -f $file_task_sample $file_task_before
|
cp -f "$file_task_sample" "$file_task_before"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f $file_task_after ]]; then
|
if [[ ! -f $file_task_after ]]; then
|
||||||
cp -f $file_task_sample $file_task_after
|
cp -f "$file_task_sample" "$file_task_after"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f $file_extra_shell ]]; then
|
if [[ ! -f $file_extra_shell ]]; then
|
||||||
cp -f $file_extra_sample $file_extra_shell
|
cp -f "$file_extra_sample" "$file_extra_shell"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $file_notify_py ]]; then
|
if [[ ! -s $file_notify_py ]]; then
|
||||||
cp -f $file_notify_py_sample $file_notify_py
|
cp -f "$file_notify_py_sample" "$file_notify_py"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $file_notify_js ]]; then
|
if [[ ! -s $file_notify_js ]]; then
|
||||||
cp -f $file_notify_js_sample $file_notify_js
|
cp -f "$file_notify_js_sample" "$file_notify_js"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $file_test_js ]]; then
|
if [[ ! -s $file_test_js ]]; then
|
||||||
cp -f $file_test_js_sample $file_test_js
|
cp -f "$file_test_js_sample" "$file_test_js"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $file_test_py ]]; then
|
if [[ ! -s $file_test_py ]]; then
|
||||||
cp -f $file_test_py_sample $file_test_py
|
cp -f "$file_test_py_sample" "$file_test_py"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $dep_notify_js ]]; then
|
if [[ ! -s $dep_notify_js ]]; then
|
||||||
cp -f $file_notify_js_sample $dep_notify_js
|
cp -f "$file_notify_js_sample" "$dep_notify_js"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $dep_notify_py ]]; then
|
if [[ ! -s $dep_notify_py ]]; then
|
||||||
cp -f $file_notify_py_sample $dep_notify_py
|
cp -f "$file_notify_py_sample" "$dep_notify_py"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -246,17 +249,17 @@ npm_install_2() {
|
|||||||
local dir_current=$(pwd)
|
local dir_current=$(pwd)
|
||||||
local dir_work=$1
|
local dir_work=$1
|
||||||
|
|
||||||
cd $dir_work
|
cd "$dir_work" || return $?
|
||||||
t '安装 %s 依赖包...\n' "$dir_work"
|
t '安装 %s 依赖包...\n' "$dir_work"
|
||||||
npm_install_sub
|
npm_install_sub
|
||||||
cd $dir_current
|
cd "$dir_current" || return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
diff_and_copy() {
|
diff_and_copy() {
|
||||||
local copy_source=$1
|
local copy_source=$1
|
||||||
local copy_to=$2
|
local copy_to=$2
|
||||||
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
|
if [[ ! -s $copy_to ]] || [[ $(diff "$copy_source" "$copy_to") ]]; then
|
||||||
cp -f $copy_source $copy_to
|
cp -f "$copy_source" "$copy_to"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,12 +268,13 @@ git_clone_scripts() {
|
|||||||
local dir="$2"
|
local dir="$2"
|
||||||
local branch="$3"
|
local branch="$3"
|
||||||
local proxy="$4"
|
local proxy="$4"
|
||||||
[[ $branch ]] && local part_cmd="-b $branch "
|
local branch_args=()
|
||||||
|
[[ $branch ]] && branch_args=(-b "$branch")
|
||||||
t '开始拉取仓库 %s 到 %s\n' "${uniq_path}" "$dir"
|
t '开始拉取仓库 %s 到 %s\n' "${uniq_path}" "$dir"
|
||||||
|
|
||||||
set_proxy "$proxy"
|
set_proxy "$proxy"
|
||||||
|
|
||||||
git clone -q --depth=1 $part_cmd $url $dir
|
git clone -q --depth=1 "${branch_args[@]}" "$url" "$dir"
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
|
|
||||||
unset_proxy
|
unset_proxy
|
||||||
@@ -283,7 +287,7 @@ random_range() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete_pm2() {
|
delete_pm2() {
|
||||||
cd $dir_root
|
cd "$dir_root" || return $?
|
||||||
# Try to delete PM2 processes, but don't fail if PM2 is not available
|
# Try to delete PM2 processes, but don't fail if PM2 is not available
|
||||||
pm2 delete ecosystem.config.js 2>/dev/null || true
|
pm2 delete ecosystem.config.js 2>/dev/null || true
|
||||||
# Also try to kill any directly spawned node processes
|
# Also try to kill any directly spawned node processes
|
||||||
@@ -291,7 +295,7 @@ delete_pm2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reload_pm2() {
|
reload_pm2() {
|
||||||
cd $dir_root
|
cd "$dir_root" || return $?
|
||||||
restore_env_vars
|
restore_env_vars
|
||||||
|
|
||||||
# Try to start PM2, but handle failures gracefully
|
# Try to start PM2, but handle failures gracefully
|
||||||
@@ -310,7 +314,7 @@ reload_pm2() {
|
|||||||
if [[ "$QL_CONTAINER" == "true" ]]; then
|
if [[ "$QL_CONTAINER" == "true" ]]; then
|
||||||
nohup node static/build/app.js >/proc/1/fd/1 2>/proc/1/fd/2 &
|
nohup node static/build/app.js >/proc/1/fd/1 2>/proc/1/fd/2 &
|
||||||
else
|
else
|
||||||
nohup node static/build/app.js >$dir_log/qinglong.log 2>&1 &
|
nohup node static/build/app.js >"$dir_log/qinglong.log" 2>&1 &
|
||||||
fi
|
fi
|
||||||
local node_pid=$!
|
local node_pid=$!
|
||||||
|
|
||||||
@@ -398,7 +402,7 @@ handle_task_start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run_task_before() {
|
run_task_before() {
|
||||||
. $file_task_before "$@"
|
. "$file_task_before" "$@"
|
||||||
|
|
||||||
if [[ ${task_before:=} ]]; then
|
if [[ ${task_before:=} ]]; then
|
||||||
t '执行前置命令\n'
|
t '执行前置命令\n'
|
||||||
@@ -408,7 +412,7 @@ run_task_before() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run_task_after() {
|
run_task_after() {
|
||||||
. $file_task_after "$@"
|
. "$file_task_after" "$@"
|
||||||
|
|
||||||
if [[ ${task_after:=} ]]; then
|
if [[ ${task_after:=} ]]; then
|
||||||
t '\n执行后置命令\n'
|
t '\n执行后置命令\n'
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ reload_pm2
|
|||||||
if [[ $command != "reload" ]]; then
|
if [[ $command != "reload" ]]; then
|
||||||
if [[ $AutoStartBot == true ]]; then
|
if [[ $AutoStartBot == true ]]; then
|
||||||
log_with_style "INFO" "🤖 4. 启动 bot..."
|
log_with_style "INFO" "🤖 4. 启动 bot..."
|
||||||
nohup ql bot >$dir_log/bot.log 2>&1 &
|
nohup ql bot >"$dir_log/bot.log" 2>&1 &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EnableExtraShell == true ]]; then
|
if [[ $EnableExtraShell == true ]]; then
|
||||||
log_with_style "INFO" "🛠️ 5. 执行自定义脚本..."
|
log_with_style "INFO" "🛠️ 5. 执行自定义脚本..."
|
||||||
nohup ql extra >$dir_log/extra.log 2>&1 &
|
nohup ql extra >"$dir_log/extra.log" 2>&1 &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pm2 startup
|
pm2 startup
|
||||||
|
|||||||
+29
-10
@@ -4,31 +4,46 @@
|
|||||||
set -e
|
set -e
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
|
# Configuration and language helpers are not loaded during bootstrap errors.
|
||||||
|
startup_message() {
|
||||||
|
local format="$1"
|
||||||
|
shift
|
||||||
|
printf -- "$format\n" "$@" >&2
|
||||||
|
}
|
||||||
|
|
||||||
if [[ ! $QL_DIR ]]; then
|
if [[ ! $QL_DIR ]]; then
|
||||||
npm_dir=$(npm root -g)
|
npm_dir=$(npm root -g 2>/dev/null) || npm_dir=""
|
||||||
pnpm_dir=$(pnpm root -g)
|
pnpm_dir=""
|
||||||
if [[ -d "$npm_dir/@whyour/qinglong" ]]; then
|
if command -v pnpm >/dev/null 2>&1; then
|
||||||
|
pnpm_dir=$(pnpm root -g 2>/dev/null) || pnpm_dir=""
|
||||||
|
fi
|
||||||
|
if [[ -n "$npm_dir" && -d "$npm_dir/@whyour/qinglong" ]]; then
|
||||||
QL_DIR="$npm_dir/@whyour/qinglong"
|
QL_DIR="$npm_dir/@whyour/qinglong"
|
||||||
elif [[ -d "$pnpm_dir/@whyour/qinglong" ]]; then
|
elif [[ -n "$pnpm_dir" && -d "$pnpm_dir/@whyour/qinglong" ]]; then
|
||||||
QL_DIR="$pnpm_dir/@whyour/qinglong"
|
QL_DIR="$pnpm_dir/@whyour/qinglong"
|
||||||
else
|
else
|
||||||
t '未找到 qinglong 模块,请先执行 npm i -g @whyour/qinglong 安装'
|
startup_message '未找到 qinglong 模块,请先执行 npm i -g @whyour/qinglong 安装'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $QL_DIR ]]; then
|
if [[ $QL_DIR ]]; then
|
||||||
t '请先手动设置 export QL_DIR=%s,环境变量,并手动添加到系统环境变量,然后再次执行命令 qinglong 启动服务' "$QL_DIR"
|
startup_message '请先手动设置 export QL_DIR=%s,环境变量,并手动添加到系统环境变量,然后再次执行命令 qinglong 启动服务' "$QL_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ $QL_DIR != /* ]]; then
|
||||||
|
startup_message 'QL_DIR 必须为绝对路径,例如 /ql'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ! $QL_DATA_DIR ]]; then
|
if [[ ! $QL_DATA_DIR ]]; then
|
||||||
t '请先手动设置数据存储目录 export QL_DATA_DIR 环境变量,目录必须以斜杠开头的绝对路径,并且以 /data 结尾,例如 /ql/data 并手动添加到系统环境变量'
|
startup_message '请先手动设置数据存储目录 export QL_DATA_DIR 环境变量,目录必须以斜杠开头的绝对路径,并且以 /data 结尾,例如 /ql/data 并手动添加到系统环境变量'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $QL_DATA_DIR != */data ]]; then
|
if [[ $QL_DATA_DIR != /*/data ]]; then
|
||||||
t 'QL_DATA_DIR 必须以 /data 结尾,例如 /ql/data,如果有历史数据,请新建 data 目录,把历史数据放到 data 目录中'
|
startup_message 'QL_DATA_DIR 必须为以 /data 结尾的绝对路径,例如 /ql/data,如果有历史数据,请新建 data 目录,把历史数据放到 data 目录中'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -38,7 +53,11 @@ if [[ $command != "reload" ]]; then
|
|||||||
# 安装依赖
|
# 安装依赖
|
||||||
os_name="${QL_OS_TYPE:-}"
|
os_name="${QL_OS_TYPE:-}"
|
||||||
if [ -z "$os_name" ]; then
|
if [ -z "$os_name" ]; then
|
||||||
|
if [[ -r /etc/os-release ]]; then
|
||||||
os_name=$(source /etc/os-release && echo "$ID")
|
os_name=$(source /etc/os-release && echo "$ID")
|
||||||
|
else
|
||||||
|
os_name=$(uname -s)
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 非 root 用户使用 sudo
|
# 非 root 用户使用 sudo
|
||||||
@@ -69,7 +88,7 @@ if [[ $command != "reload" ]]; then
|
|||||||
$SUDO apt-get install -y git curl wget tzdata perl openssl jq nginx procps netcat-openbsd openssh-client
|
$SUDO apt-get install -y git curl wget tzdata perl openssl jq nginx procps netcat-openbsd openssh-client
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
t '暂不支持此系统部署 %s' "$os_name"
|
startup_message '暂不支持此系统部署 %s' "$os_name"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
+17
-9
@@ -36,7 +36,10 @@ run_task_command() {
|
|||||||
) &
|
) &
|
||||||
timeout_watcher=$!
|
timeout_watcher=$!
|
||||||
set +m
|
set +m
|
||||||
trap '{
|
finish_task_timeout() {
|
||||||
|
local code="$1"
|
||||||
|
trap - EXIT
|
||||||
|
{
|
||||||
# Bash can report a killed job between any two cleanup commands.
|
# Bash can report a killed job between any two cleanup commands.
|
||||||
# Keep redirection active until the watchdog is fully reaped.
|
# Keep redirection active until the watchdog is fully reaped.
|
||||||
kill -KILL -- "-$timeout_watcher" || :
|
kill -KILL -- "-$timeout_watcher" || :
|
||||||
@@ -45,14 +48,19 @@ run_task_command() {
|
|||||||
if kill -TERM -- "-$timeout_worker" 2>/dev/null; then sleep 0.1; fi
|
if kill -TERM -- "-$timeout_worker" 2>/dev/null; then sleep 0.1; fi
|
||||||
kill -KILL -- "-$timeout_worker" 2>/dev/null || :
|
kill -KILL -- "-$timeout_worker" 2>/dev/null || :
|
||||||
wait "$timeout_worker" 2>/dev/null || :
|
wait "$timeout_worker" 2>/dev/null || :
|
||||||
rm -rf -- "$timeout_dir"' EXIT
|
rm -rf -- "$timeout_dir"
|
||||||
trap 'exit 130' INT
|
exit "$code"
|
||||||
trap 'exit 143' TERM
|
}
|
||||||
trap 'exit 129' HUP
|
# Older Bash can omit a subshell's EXIT trap in sourced pipelines. Call
|
||||||
trap 'exit 131' QUIT
|
# cleanup explicitly on completion/signals, retaining EXIT as a fallback.
|
||||||
|
trap 'finish_task_timeout "$?"' EXIT
|
||||||
|
trap 'finish_task_timeout 130' INT
|
||||||
|
trap 'finish_task_timeout 143' TERM
|
||||||
|
trap 'finish_task_timeout 129' HUP
|
||||||
|
trap 'finish_task_timeout 131' QUIT
|
||||||
# Do not clean up a timer whose process group has not initialized yet.
|
# Do not clean up a timer whose process group has not initialized yet.
|
||||||
while [[ ! -f "$timeout_dir/ready" ]]; do
|
while [[ ! -f "$timeout_dir/ready" ]]; do
|
||||||
kill -0 "$timeout_watcher" 2>/dev/null || exit 125
|
kill -0 "$timeout_watcher" 2>/dev/null || finish_task_timeout 125
|
||||||
sleep 0.001
|
sleep 0.001
|
||||||
done
|
done
|
||||||
wait "$timeout_worker"
|
wait "$timeout_worker"
|
||||||
@@ -60,8 +68,8 @@ run_task_command() {
|
|||||||
if [[ -f "$timeout_dir/expired" ]]; then
|
if [[ -f "$timeout_dir/expired" ]]; then
|
||||||
# Let the timer finish escalation even if the task leader exited first.
|
# Let the timer finish escalation even if the task leader exited first.
|
||||||
wait "$timeout_watcher" 2>/dev/null || :
|
wait "$timeout_watcher" 2>/dev/null || :
|
||||||
exit 124
|
finish_task_timeout 124
|
||||||
fi
|
fi
|
||||||
exit "$timeout_code"
|
finish_task_timeout "$timeout_code"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
dir_shell=$QL_DIR/shell
|
dir_shell=$QL_DIR/shell
|
||||||
. $dir_shell/share.sh
|
. "$dir_shell/share.sh"
|
||||||
. $dir_shell/api.sh
|
. "$dir_shell/api.sh"
|
||||||
. $dir_shell/task-timeout.sh
|
. "$dir_shell/task-timeout.sh"
|
||||||
|
|
||||||
trap 'single_hanle SIGINT' INT
|
trap 'single_hanle SIGINT' INT
|
||||||
trap 'single_hanle SIGTERM' TERM
|
trap 'single_hanle SIGTERM' TERM
|
||||||
@@ -48,7 +48,7 @@ handle_log_path() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z ${ID:=} ]]; then
|
if [[ -z ${ID:=} ]]; then
|
||||||
ID=$(cat $list_crontab_user | grep -E "$cmd_task.* $file_param" | perl -pe "s|.*ID=(.*) $cmd_task.* $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
ID=$(cat "$list_crontab_user" | grep -E "$cmd_task.* $file_param" | perl -pe "s|.*ID=(.*) $cmd_task.* $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
fi
|
fi
|
||||||
local suffix=""
|
local suffix=""
|
||||||
if [[ ! -z $ID ]]; then
|
if [[ ! -z $ID ]]; then
|
||||||
@@ -89,10 +89,10 @@ handle_log_path() {
|
|||||||
log_path="$real_log_path"
|
log_path="$real_log_path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cmd="2>&1 | tee -a $dir_log/$log_path"
|
printf -v cmd '2>&1 | tee -a %q' "$dir_log/$log_path"
|
||||||
make_dir "$dir_log/$log_dir"
|
make_dir "$dir_log/$log_dir"
|
||||||
if [[ "${no_tee:=}" == "true" ]]; then
|
if [[ "${no_tee:=}" == "true" ]]; then
|
||||||
cmd=">> $dir_log/$log_path 2>&1"
|
printf -v cmd '>> %q 2>&1' "$dir_log/$log_path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${real_time:=}" == "true" ]]; then
|
if [[ "${real_time:=}" == "true" ]]; then
|
||||||
@@ -162,5 +162,5 @@ define_program "${task_shell_params[@]}"
|
|||||||
handle_log_path "${task_shell_params[@]}"
|
handle_log_path "${task_shell_params[@]}"
|
||||||
init_begin_time
|
init_begin_time
|
||||||
|
|
||||||
eval . $dir_shell/otask.sh "$cmd"
|
eval '. "$dir_shell/otask.sh"' "$cmd"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+22
-16
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
dir_shell=$QL_DIR/shell
|
dir_shell=$QL_DIR/shell
|
||||||
. $dir_shell/share.sh
|
. "$dir_shell/share.sh"
|
||||||
. $dir_shell/api.sh
|
. "$dir_shell/api.sh"
|
||||||
load_ql_envs
|
load_ql_envs
|
||||||
. $dir_shell/env.sh
|
. "$dir_shell/env.sh"
|
||||||
|
|
||||||
send_mark=$dir_shell/send_mark
|
send_mark=$dir_shell/send_mark
|
||||||
|
|
||||||
@@ -165,17 +165,20 @@ update_raw() {
|
|||||||
t '开始下载:%s 保存路径:%s\n' "${raw_url}" "$dir_raw/${raw_file_name}"
|
t '开始下载:%s 保存路径:%s\n' "${raw_url}" "$dir_raw/${raw_file_name}"
|
||||||
|
|
||||||
set_proxy "$proxy"
|
set_proxy "$proxy"
|
||||||
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
|
if wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" "$raw_url"; then
|
||||||
|
exit_status=0
|
||||||
|
else
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
|
fi
|
||||||
unset_proxy
|
unset_proxy
|
||||||
|
|
||||||
if [[ $? -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
|
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}" || return $?
|
||||||
t '下载 %s 成功...\n' "${raw_file_name}"
|
t '下载 %s 成功...\n' "${raw_file_name}"
|
||||||
cd $dir_raw
|
cd "$dir_raw" || return $?
|
||||||
local filename="raw_${raw_file_name}"
|
local filename="raw_${raw_file_name}"
|
||||||
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
local cron_id=$(cat "$list_crontab_user" | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
cp -f $raw_file_name $dir_scripts/${filename}
|
cp -f "$raw_file_name" "$dir_scripts/${filename}" || return $?
|
||||||
if [[ -z $cron_id ]] && [[ ${autoAddCron} == true ]]; then
|
if [[ -z $cron_id ]] && [[ ${autoAddCron} == true ]]; then
|
||||||
cron_line=$(
|
cron_line=$(
|
||||||
perl -ne "{
|
perl -ne "{
|
||||||
@@ -202,6 +205,7 @@ update_raw() {
|
|||||||
else
|
else
|
||||||
t '下载 %s 失败,保留之前正常下载的版本...\n' "${raw_file_name}"
|
t '下载 %s 失败,保留之前正常下载的版本...\n' "${raw_file_name}"
|
||||||
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
|
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
|
||||||
|
return "$exit_status"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -209,7 +213,7 @@ update_raw() {
|
|||||||
## 调用用户自定义的extra.sh
|
## 调用用户自定义的extra.sh
|
||||||
run_extra_shell() {
|
run_extra_shell() {
|
||||||
if [[ -f $file_extra_shell ]]; then
|
if [[ -f $file_extra_shell ]]; then
|
||||||
. $file_extra_shell
|
. "$file_extra_shell"
|
||||||
else
|
else
|
||||||
t '%s文件不存在,跳过执行...\n' "$file_extra_shell"
|
t '%s文件不存在,跳过执行...\n' "$file_extra_shell"
|
||||||
fi
|
fi
|
||||||
@@ -266,7 +270,8 @@ show_service_logs() {
|
|||||||
# Winston writes the active runtime log by date. Follow the filename so
|
# Winston writes the active runtime log by date. Follow the filename so
|
||||||
# size-based rotations are handled without involving the PM2 log files.
|
# size-based rotations are handled without involving the PM2 log files.
|
||||||
local current_system_log="$system_log_dir/$(date +%F).log"
|
local current_system_log="$system_log_dir/$(date +%F).log"
|
||||||
tail -n 0 --retry -F "$current_system_log"
|
# GNU and BSD tail both support -F; --retry is GNU-only and implied by -F.
|
||||||
|
tail -n 0 -F "$current_system_log"
|
||||||
}
|
}
|
||||||
|
|
||||||
## 脚本用法
|
## 脚本用法
|
||||||
@@ -484,9 +489,9 @@ main() {
|
|||||||
local log_path="${log_dir}/${log_time}.log"
|
local log_path="${log_dir}/${log_time}.log"
|
||||||
local file_path="$dir_log/$log_path"
|
local file_path="$dir_log/$log_path"
|
||||||
|
|
||||||
cmd="2>&1 | tee -a $file_path"
|
printf -v cmd '2>&1 | tee -a %q' "$file_path"
|
||||||
if [[ "$no_tee" == "true" ]]; then
|
if [[ "$no_tee" == "true" ]]; then
|
||||||
cmd=">> $file_path 2>&1"
|
printf -v cmd '>> %q 2>&1' "$file_path"
|
||||||
fi
|
fi
|
||||||
if [[ "$real_time" == "true" ]]; then
|
if [[ "$real_time" == "true" ]]; then
|
||||||
cmd=""
|
cmd=""
|
||||||
@@ -532,19 +537,20 @@ main() {
|
|||||||
get_uniq_path "$p2"
|
get_uniq_path "$p2"
|
||||||
if [[ -n $p2 ]]; then
|
if [[ -n $p2 ]]; then
|
||||||
update_raw "$p2" "$p3" "$p4" "$p5"
|
update_raw "$p2" "$p3" "$p4" "$p5"
|
||||||
|
command_status=$?
|
||||||
else
|
else
|
||||||
t '命令输入错误...\n'
|
t '命令输入错误...\n'
|
||||||
eval usage $cmd
|
eval usage $cmd
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
rmlog)
|
rmlog)
|
||||||
eval . $dir_shell/rmlog.sh "$p2" $cmd
|
eval '. "$dir_shell/rmlog.sh" "$p2"' "$cmd"
|
||||||
;;
|
;;
|
||||||
bot)
|
bot)
|
||||||
eval . $dir_shell/bot.sh $cmd
|
eval '. "$dir_shell/bot.sh"' "$cmd"
|
||||||
;;
|
;;
|
||||||
check)
|
check)
|
||||||
eval . $dir_shell/check.sh $cmd
|
eval '. "$dir_shell/check.sh"' "$cmd"
|
||||||
;;
|
;;
|
||||||
resetlet)
|
resetlet)
|
||||||
eval update_auth_config "\\\"retries\\\":0" "重置登录错误次数" $cmd
|
eval update_auth_config "\\\"retries\\\":0" "重置登录错误次数" $cmd
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ test('ql log follows the current Winston system log without PM2', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(result.status, 0, result.stderr);
|
assert.equal(result.status, 0, result.stderr);
|
||||||
assert.equal(result.stdout, `tail <-n> <0> <--retry> <-F> <${currentLog}>\n`);
|
assert.equal(result.stdout, `tail <-n> <0> <-F> <${currentLog}>\n`);
|
||||||
assert.equal(fs.existsSync(path.join(testRoot, 'data/log/log')), false);
|
assert.equal(fs.existsSync(path.join(testRoot, 'data/log/log')), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,558 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const repo = path.resolve(__dirname, '../..');
|
||||||
|
const bash = process.env.QL_TEST_BASH || '/bin/bash';
|
||||||
|
const source = (file) => fs.readFileSync(path.join(repo, file), 'utf8');
|
||||||
|
function extract(file, name) {
|
||||||
|
const text = source(file);
|
||||||
|
const start = text.indexOf(`${name}() {`);
|
||||||
|
assert.ok(start >= 0, name);
|
||||||
|
return text.slice(start, text.indexOf('\n}', start) + 2);
|
||||||
|
}
|
||||||
|
function fixture(t, prefix = `ql compatibility '" $literal `) {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||||
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||||
|
const write = (name, content, executable = false) => {
|
||||||
|
const file = path.join(root, name);
|
||||||
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||||
|
fs.writeFileSync(file, content, { mode: executable ? 0o755 : 0o644 });
|
||||||
|
return file;
|
||||||
|
};
|
||||||
|
const run = (code, env = {}, args = [], cwd = root) =>
|
||||||
|
spawnSync(bash, ['-c', code, 'compatibility-test', ...args], {
|
||||||
|
cwd,
|
||||||
|
env: { ...process.env, ...env },
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
return { root, write, run };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [label, args] of [
|
||||||
|
['normal', ['nested folder/probe [1] $name.sh', '--']],
|
||||||
|
['explicit bash', ['bash', 'nested folder/probe [1] $name.sh']],
|
||||||
|
[
|
||||||
|
'designated',
|
||||||
|
['nested folder/probe [1] $name.sh', 'desi', 'QA_PANEL', '1-2', '--'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'concurrent',
|
||||||
|
['nested folder/probe [1] $name.sh', 'conc', 'QA_PANEL', '1-2', '--'],
|
||||||
|
],
|
||||||
|
['automatic Node', ['nested folder/probe [1] $name.js', '--']],
|
||||||
|
['explicit Node', ['node', 'nested folder/probe [1] $name.js']],
|
||||||
|
]) {
|
||||||
|
for (const logging of ['tee', 'file']) {
|
||||||
|
test(`${label} task keeps literal paths and arguments with ${logging} logging`, (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
for (const name of ['share.sh', 'task.sh', 'otask.sh', 'task-timeout.sh'])
|
||||||
|
f.write(`shell/${name}`, source(`shell/${name}`));
|
||||||
|
f.write(
|
||||||
|
'shell/api.sh',
|
||||||
|
'update_cron(){ :; }; record_cron_stat(){ :; }\n',
|
||||||
|
);
|
||||||
|
f.write('shell/preload/env.sh', 'export QA_PANEL="one&two"\n');
|
||||||
|
f.write('shell/preload/sitecustomize.js', '// fixture preloader\n');
|
||||||
|
f.write(
|
||||||
|
'data/config/config.sh',
|
||||||
|
'DefaultCronRule="0 0 * * *"\nCommandTimeoutTime="2s"\n',
|
||||||
|
);
|
||||||
|
f.write('data/config/task_before.sh', 'printf "BEFORE\\n"\n');
|
||||||
|
f.write('data/config/task_after.sh', 'printf "AFTER\\n"\n');
|
||||||
|
f.write('bin/pnpm', '#!/bin/bash\nprintf /fixture/global\n', true);
|
||||||
|
const literal = `argument with 'quotes' $literal ; echo NOT_EXECUTED`;
|
||||||
|
f.write(
|
||||||
|
'data/scripts/nested folder/probe [1] $name.sh',
|
||||||
|
'printf "ARG=%s\\nACCOUNT=%s\\n" "$1" "$QA_PANEL"\n',
|
||||||
|
);
|
||||||
|
f.write(
|
||||||
|
'data/scripts/nested folder/probe [1] $name.js',
|
||||||
|
'console.log("ARG=" + process.argv[2]);\n',
|
||||||
|
);
|
||||||
|
const started = Date.now();
|
||||||
|
const result = f.run(
|
||||||
|
'exec "$QA_BASH" "$QL_DIR/shell/task.sh" "$@"',
|
||||||
|
{
|
||||||
|
QA_BASH: bash,
|
||||||
|
QL_DIR: f.root,
|
||||||
|
QL_DATA_DIR: path.join(f.root, 'data'),
|
||||||
|
ID: '42',
|
||||||
|
no_delay: 'true',
|
||||||
|
real_time: 'false',
|
||||||
|
no_tee: logging === 'file' ? 'true' : 'false',
|
||||||
|
real_log_path: 'literal [log] $name.log',
|
||||||
|
PATH: path.join(f.root, 'bin') + path.delimiter + process.env.PATH,
|
||||||
|
},
|
||||||
|
[...args, literal],
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stderr, '');
|
||||||
|
assert.ok(
|
||||||
|
Date.now() - started < 1800,
|
||||||
|
'completion must not wait for the two-second watchdog',
|
||||||
|
);
|
||||||
|
const log = fs.readFileSync(
|
||||||
|
path.join(f.root, 'data/log/literal [log] $name.log'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
assert.ok(log.includes(`ARG=${literal}\n`), log);
|
||||||
|
assert.match(log, /AFTER\n/);
|
||||||
|
assert.match(log, /完成 ✅/);
|
||||||
|
if (label === 'concurrent') {
|
||||||
|
assert.match(log, /ACCOUNT=one\n/);
|
||||||
|
assert.match(log, /ACCOUNT=two\n/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('shared initialization copies configuration and hooks under a directory containing spaces', (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
f.write('shell/share.sh', source('shell/share.sh'));
|
||||||
|
for (const name of [
|
||||||
|
'config.sample.sh',
|
||||||
|
'task.sample.sh',
|
||||||
|
'extra.sample.sh',
|
||||||
|
'notify.py',
|
||||||
|
'notify.js',
|
||||||
|
'ql_sample.js',
|
||||||
|
'ql_sample.py',
|
||||||
|
])
|
||||||
|
f.write(
|
||||||
|
`sample/${name}`,
|
||||||
|
name === 'config.sample.sh'
|
||||||
|
? 'DefaultCronRule="0 0 * * *"\n'
|
||||||
|
: '# fixture\n',
|
||||||
|
);
|
||||||
|
const result = f.run(
|
||||||
|
'. "$QL_DIR/shell/share.sh"; fix_config; import_config; printf "%s" "$default_cron"',
|
||||||
|
{
|
||||||
|
QL_DIR: f.root,
|
||||||
|
QL_DATA_DIR: path.join(f.root, 'custom data'),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stderr, '');
|
||||||
|
assert.equal(result.stdout, '0 0 * * *');
|
||||||
|
assert.ok(
|
||||||
|
fs.existsSync(path.join(f.root, 'custom data/config/task_after.sh')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('token generation passes a compiled token filename literally', (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
f.write('static/build/token.js', 'console.log("fixture-token");\n');
|
||||||
|
const result = f.run(
|
||||||
|
extract('shell/api.sh', 'create_token') +
|
||||||
|
'\ncreate_token; printf "%s" "$__ql_token__"',
|
||||||
|
{ dir_root: f.root },
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stdout, 'fixture-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('script discovery keeps filenames with spaces, glob characters and leading dashes', (t) => {
|
||||||
|
const f = fixture(t, 'ql-script-list | ');
|
||||||
|
for (const name of ['-dash name.js', 'probe [1].js', 'sendNotify.js'])
|
||||||
|
f.write(`scripts/${name}`, "new Env('fixture');\n");
|
||||||
|
const result = f.run(
|
||||||
|
extract('shell/otask.sh', 'gen_array_scripts') +
|
||||||
|
'\ngen_array_scripts; printf "%s\\n" "${array_scripts[@]}"',
|
||||||
|
{
|
||||||
|
dir_scripts: path.join(f.root, 'scripts'),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stderr, '');
|
||||||
|
assert.deepEqual(result.stdout.trim().split('\n').sort(), [
|
||||||
|
'-dash name.js',
|
||||||
|
'probe [1].js',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('token generation passes the TypeScript fallback filename literally', (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
const result = f.run(
|
||||||
|
extract('shell/api.sh', 'create_token') +
|
||||||
|
`
|
||||||
|
ts-node-transpile-only(){ [[ "$1" == "$dir_root/back/token.ts" ]] || return 99; printf fallback-token; }
|
||||||
|
create_token || exit $?
|
||||||
|
printf '%s' "$__ql_token__"
|
||||||
|
`,
|
||||||
|
{ dir_root: f.root },
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stdout, 'fallback-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('repository cloning preserves a literal destination with and without a branch', (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
const origin = path.join(f.root, 'origin repo');
|
||||||
|
for (const args of [
|
||||||
|
['init', '-b', 'develop', origin],
|
||||||
|
['-C', origin, 'config', 'user.name', 'Fixture'],
|
||||||
|
['-C', origin, 'config', 'user.email', 'fixture@example.invalid'],
|
||||||
|
]) {
|
||||||
|
const result = spawnSync('git', args, { encoding: 'utf8' });
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(path.join(origin, 'probe.txt'), 'fixture');
|
||||||
|
for (const args of [
|
||||||
|
['-C', origin, 'add', '.'],
|
||||||
|
['-C', origin, 'commit', '-m', 'fixture'],
|
||||||
|
]) {
|
||||||
|
const result = spawnSync('git', args, { encoding: 'utf8' });
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
}
|
||||||
|
for (const branch of ['', 'develop']) {
|
||||||
|
const target = path.join(f.root, branch || 'default clone');
|
||||||
|
const result = f.run(
|
||||||
|
[
|
||||||
|
extract('shell/share.sh', 'set_proxy'),
|
||||||
|
extract('shell/share.sh', 'unset_proxy'),
|
||||||
|
extract('shell/share.sh', 'git_clone_scripts'),
|
||||||
|
't(){ :; }; git_clone_scripts "$QA_ORIGIN" "$QA_TARGET" "$QA_BRANCH"; exit "$exit_status"',
|
||||||
|
].join('\n'),
|
||||||
|
{ QA_ORIGIN: origin, QA_TARGET: target, QA_BRANCH: branch },
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(target, 'probe.txt'), 'utf8'),
|
||||||
|
'fixture',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const condition of [
|
||||||
|
'missing data',
|
||||||
|
'invalid data',
|
||||||
|
'invalid root',
|
||||||
|
'npm only',
|
||||||
|
'missing module',
|
||||||
|
'unsupported OS',
|
||||||
|
]) {
|
||||||
|
test(`native startup reports ${condition} without depending on translation initialization`, (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
const npmRoot = path.join(f.root, 'npm global');
|
||||||
|
fs.mkdirSync(path.join(npmRoot, '@whyour/qinglong'), { recursive: true });
|
||||||
|
f.write('bin/npm', '#!/bin/bash\nprintf "%s" "$QA_NPM_ROOT"\n', true);
|
||||||
|
f.write('bin/id', '#!/bin/bash\nprintf 1000\n', true);
|
||||||
|
const env = {
|
||||||
|
PATH: path.join(f.root, 'bin'),
|
||||||
|
QL_DIR: f.root,
|
||||||
|
QL_DATA_DIR: path.join(f.root, 'data'),
|
||||||
|
QA_NPM_ROOT: npmRoot,
|
||||||
|
QL_OS_TYPE: 'unsupported-fixture',
|
||||||
|
};
|
||||||
|
if (condition === 'missing data') env.QL_DATA_DIR = '';
|
||||||
|
if (condition === 'invalid data') env.QL_DATA_DIR = 'relative/data';
|
||||||
|
if (condition === 'invalid root') env.QL_DIR = 'relative';
|
||||||
|
if (condition === 'npm only' || condition === 'missing module')
|
||||||
|
env.QL_DIR = '';
|
||||||
|
if (condition === 'missing module')
|
||||||
|
fs.rmdirSync(path.join(npmRoot, '@whyour/qinglong'));
|
||||||
|
const result = f.run(source('shell/start.sh'), env);
|
||||||
|
assert.equal(result.status, 1, result.stderr);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
result.stderr,
|
||||||
|
/command not found|No such file|integer expression/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
result.stderr,
|
||||||
|
condition === 'npm only'
|
||||||
|
? /export QL_DIR=/
|
||||||
|
: condition === 'missing module'
|
||||||
|
? /npm i -g @whyour\/qinglong/
|
||||||
|
: condition === 'unsupported OS'
|
||||||
|
? /暂不支持此系统/
|
||||||
|
: condition === 'invalid root'
|
||||||
|
? /QL_DIR/
|
||||||
|
: /QL_DATA_DIR/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('language loading falls back when global associative arrays are unavailable', (t) => {
|
||||||
|
const f = fixture(t, 'ql-locale-compat-');
|
||||||
|
f.write(
|
||||||
|
'shell/lang/en.sh',
|
||||||
|
'printf loaded > "$QA_MARKER"\ndeclare -gA LANG_MESSAGES=([probe]=translated)\n',
|
||||||
|
);
|
||||||
|
const result = f.run(
|
||||||
|
source('shell/share.sh') +
|
||||||
|
`
|
||||||
|
declare(){ if [[ "$1" == -gA ]]; then return 2; fi; builtin declare "$@"; }
|
||||||
|
import_config
|
||||||
|
t probe
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
QL_DIR: f.root,
|
||||||
|
QL_LANG: 'en',
|
||||||
|
QA_MARKER: path.join(f.root, 'loaded'),
|
||||||
|
DefaultCronRule: '0 0 * * *',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stdout, 'probe\n');
|
||||||
|
assert.equal(fs.existsSync(path.join(f.root, 'loaded')), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'supported Bash keeps translated messages global after configuration loading',
|
||||||
|
{
|
||||||
|
skip:
|
||||||
|
spawnSync(bash, ['-c', 'declare -gA probe'], { stdio: 'ignore' })
|
||||||
|
.status !== 0,
|
||||||
|
},
|
||||||
|
(t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
f.write(
|
||||||
|
'shell/lang/en.sh',
|
||||||
|
'declare -gA LANG_MESSAGES=([probe]=translated)\n',
|
||||||
|
);
|
||||||
|
const result = f.run(
|
||||||
|
source('shell/share.sh') + '\nimport_config; t probe',
|
||||||
|
{
|
||||||
|
QL_DIR: f.root,
|
||||||
|
QL_LANG: 'en',
|
||||||
|
DefaultCronRule: '0 0 * * *',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stdout, 'translated\n');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const fail of [false, true]) {
|
||||||
|
test(`raw download ${
|
||||||
|
fail
|
||||||
|
? 'failure preserves the installed script'
|
||||||
|
: 'success installs the new script'
|
||||||
|
} and clears its proxy`, (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
f.write('raw/probe.js', 'previous-good-version');
|
||||||
|
f.write('scripts/raw_probe.js', 'previous-good-version');
|
||||||
|
f.write('crontab.list', '');
|
||||||
|
const result = f.run(
|
||||||
|
[
|
||||||
|
extract('shell/share.sh', 'set_proxy'),
|
||||||
|
extract('shell/share.sh', 'unset_proxy'),
|
||||||
|
extract('shell/update.sh', 'update_raw'),
|
||||||
|
`t(){ :; }
|
||||||
|
wget(){ printf '%s' "$QA_CONTENT" > "$4"; return "$QA_STATUS"; }
|
||||||
|
update_raw 'https://fixture.invalid/probe.js' 'http://fixture.invalid:8080' false false
|
||||||
|
result=$?
|
||||||
|
if env | grep -qE '^https?_proxy='; then exit 99; fi
|
||||||
|
exit "$result"`,
|
||||||
|
].join('\n'),
|
||||||
|
{
|
||||||
|
dir_raw: path.join(f.root, 'raw'),
|
||||||
|
dir_scripts: path.join(f.root, 'scripts'),
|
||||||
|
list_crontab_user: path.join(f.root, 'crontab.list'),
|
||||||
|
cmd_task: 'task',
|
||||||
|
uniq_path: 'probe',
|
||||||
|
QA_CONTENT: fail ? 'partial-download' : 'new-version',
|
||||||
|
QA_STATUS: fail ? '8' : '0',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(result.status, fail ? 8 : 0, result.stderr);
|
||||||
|
const expected = fail ? 'previous-good-version' : 'new-version';
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(f.root, 'raw/probe.js'), 'utf8'),
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(f.root, 'scripts/raw_probe.js'), 'utf8'),
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
assert.equal(fs.existsSync(path.join(f.root, 'raw/probe.js.new')), false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('ql raw propagates download failure without replacing either installed copy', (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
for (const name of ['share.sh', 'update.sh', 'env.sh'])
|
||||||
|
f.write(`shell/${name}`, source(`shell/${name}`));
|
||||||
|
f.write('shell/api.sh', '');
|
||||||
|
f.write('data/config/config.sh', 'DefaultCronRule="0 0 * * *"\n');
|
||||||
|
f.write('data/config/crontab.list', '');
|
||||||
|
f.write('data/raw/invalid_probe.js', 'old-version');
|
||||||
|
f.write('data/scripts/raw_invalid_probe.js', 'old-version');
|
||||||
|
f.write('bin/wget', '#!/bin/bash\nprintf partial > "$4"\nexit 8\n', true);
|
||||||
|
const result = f.run(
|
||||||
|
'exec "$QA_BASH" "$QL_DIR/shell/update.sh" raw https://fixture.invalid/probe.js "" false false',
|
||||||
|
{
|
||||||
|
QA_BASH: bash,
|
||||||
|
QL_DIR: f.root,
|
||||||
|
QL_DATA_DIR: path.join(f.root, 'data'),
|
||||||
|
ID: '',
|
||||||
|
PATH: path.join(f.root, 'bin') + path.delimiter + process.env.PATH,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 8, result.stderr);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(f.root, 'data/raw/invalid_probe.js'), 'utf8'),
|
||||||
|
'old-version',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(f.root, 'data/scripts/raw_invalid_probe.js'),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
'old-version',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dateTools = [
|
||||||
|
{
|
||||||
|
name: 'host',
|
||||||
|
command: 'date',
|
||||||
|
macos: process.platform === 'darwin' ? '1' : '0',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (
|
||||||
|
process.platform === 'darwin' &&
|
||||||
|
spawnSync('gdate', ['--version']).status === 0
|
||||||
|
)
|
||||||
|
dateTools.push({ name: 'GNU', command: 'gdate', macos: '0' });
|
||||||
|
for (const dateTool of dateTools) {
|
||||||
|
test(`${dateTool.name} retention handles literal filenames, active references and empty directories`, (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
const old = f.write('logs/2020-01-01 two [words].log', 'old');
|
||||||
|
const active = f.write('logs/2020-01-01 active & protected.log', 'active');
|
||||||
|
f.write('logs/keep/important.txt', 'keep');
|
||||||
|
fs.mkdirSync(path.join(f.root, 'logs/empty directory'));
|
||||||
|
const result = f.run(
|
||||||
|
`
|
||||||
|
t(){ :; }
|
||||||
|
date(){ command "$QA_DATE" "$@"; }
|
||||||
|
find_cron_api(){ printf '%s\n' "$1" >> "$QA_QUERIES"; [[ "$1" == *active* ]] && printf active; }
|
||||||
|
. "$QA_SCRIPT" 7
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
dir_log: path.join(f.root, 'logs'),
|
||||||
|
is_macos: dateTool.macos,
|
||||||
|
QA_DATE: dateTool.command,
|
||||||
|
QA_QUERIES: path.join(f.root, 'queries'),
|
||||||
|
QA_SCRIPT: path.join(repo, 'shell/rmlog.sh'),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stderr, '');
|
||||||
|
assert.equal(fs.existsSync(old), false);
|
||||||
|
assert.equal(fs.existsSync(active), true);
|
||||||
|
assert.equal(
|
||||||
|
fs.existsSync(path.join(f.root, 'logs/empty directory')),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.ok(fs.existsSync(path.join(f.root, 'logs/keep/important.txt')));
|
||||||
|
const queries = fs
|
||||||
|
.readFileSync(path.join(f.root, 'queries'), 'utf8')
|
||||||
|
.trim()
|
||||||
|
.split('\n');
|
||||||
|
for (const query of queries)
|
||||||
|
assert.equal(
|
||||||
|
new URLSearchParams(query).get('log_path').includes('.log'),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
queries.some(
|
||||||
|
(query) =>
|
||||||
|
new URLSearchParams(query).get('log_path') ===
|
||||||
|
'2020-01-01 active & protected.log',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`${dateTool.name} retention measures a dated log from local midnight`, (t) => {
|
||||||
|
const f = fixture(t, 'ql-retention-boundary-');
|
||||||
|
const file = f.write('logs/2026-01-01.log', 'boundary');
|
||||||
|
const result = f.run(
|
||||||
|
`
|
||||||
|
t(){ :; }; find_cron_api(){ :; }
|
||||||
|
date(){ if [[ "$*" == +%s ]]; then printf 1767830401; else command "$QA_DATE" "$@"; fi; }
|
||||||
|
. "$QA_SCRIPT" 7
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
dir_log: path.join(f.root, 'logs'),
|
||||||
|
is_macos: dateTool.macos,
|
||||||
|
QA_DATE: dateTool.command,
|
||||||
|
QA_SCRIPT: path.join(repo, 'shell/rmlog.sh'),
|
||||||
|
TZ: 'UTC',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(
|
||||||
|
fs.existsSync(file),
|
||||||
|
false,
|
||||||
|
'seven days plus one second is past retention',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('log streaming options are accepted by the actual platform tail', (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
const today = spawnSync('date', ['+%F'], { encoding: 'utf8' }).stdout.trim();
|
||||||
|
f.write(`syslog/${today}.log`, 'ready\n');
|
||||||
|
const result = f.run(
|
||||||
|
extract('shell/update.sh', 'show_service_logs') +
|
||||||
|
`
|
||||||
|
tail(){ printf '%s\\0' "$@"; }
|
||||||
|
show_service_logs --lines 0
|
||||||
|
`,
|
||||||
|
{ dir_data: f.root },
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
const args = result.stdout.split('\0').slice(0, -1);
|
||||||
|
const native = spawnSync('tail', args, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 250,
|
||||||
|
killSignal: 'SIGKILL',
|
||||||
|
});
|
||||||
|
assert.equal(native.error?.code, 'ETIMEDOUT', native.stderr);
|
||||||
|
assert.equal(native.stderr, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const version of ['2.23.0', '2.23.0-beta.1+build.2', 'invalid']) {
|
||||||
|
test(`legacy publishing ${
|
||||||
|
version === 'invalid'
|
||||||
|
? 'rejects an invalid version before Git changes'
|
||||||
|
: 'uses the complete version with GNU-compatible tools'
|
||||||
|
}`, (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
f.write('version.yaml', `version: ${version}\n`);
|
||||||
|
f.write(
|
||||||
|
'bin/git',
|
||||||
|
'#!/bin/bash\nprintf "%s\\n" "$*" >> "$QA_GIT_CALLS"\n',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
f.write('bin/ts-node-transpile-only', '#!/bin/bash\nexit 0\n', true);
|
||||||
|
const result = f.run(source('shell/pub.sh'), {
|
||||||
|
PATH:
|
||||||
|
path.join(f.root, 'bin') +
|
||||||
|
path.delimiter +
|
||||||
|
(process.platform === 'darwin' &&
|
||||||
|
fs.existsSync('/opt/homebrew/opt/grep/libexec/gnubin')
|
||||||
|
? '/opt/homebrew/opt/grep/libexec/gnubin:'
|
||||||
|
: '') +
|
||||||
|
process.env.PATH,
|
||||||
|
QA_GIT_CALLS: path.join(f.root, 'git-calls'),
|
||||||
|
});
|
||||||
|
if (version === 'invalid') {
|
||||||
|
assert.notEqual(result.status, 0);
|
||||||
|
assert.equal(fs.existsSync(path.join(f.root, 'git-calls')), false);
|
||||||
|
} else {
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.ok(
|
||||||
|
fs
|
||||||
|
.readFileSync(path.join(f.root, 'git-calls'), 'utf8')
|
||||||
|
.includes(`tag -a v${version} -m release v${version}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawn } = require('node:child_process');
|
||||||
|
const psTree = require('ps-tree');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const bash = process.env.QL_TEST_BASH || '/bin/bash';
|
||||||
|
const helper = path.resolve(__dirname, '../../shell/task-timeout.sh');
|
||||||
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
const tree = (pid) =>
|
||||||
|
new Promise((resolve, reject) =>
|
||||||
|
psTree(pid, (error, children) =>
|
||||||
|
error ? reject(error) : resolve(children),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
async function until(predicate, message) {
|
||||||
|
const deadline = Date.now() + 5000;
|
||||||
|
while (!(await predicate())) {
|
||||||
|
assert.ok(Date.now() < deadline, message);
|
||||||
|
await delay(20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function alive(pid) {
|
||||||
|
try {
|
||||||
|
process.kill(pid, 0);
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
try {
|
||||||
|
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
||||||
|
if (
|
||||||
|
['Z', 'X', 'x'].includes(
|
||||||
|
stat.slice(stat.lastIndexOf(')') + 2).split(' ')[0],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') return false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ESRCH') return false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mode of ['timeout', 'manual']) {
|
||||||
|
test(
|
||||||
|
`${mode} cleans up TERM-resistant children, grandchildren and the watchdog`,
|
||||||
|
{ timeout: 12000, skip: process.platform === 'win32' },
|
||||||
|
async (t) => {
|
||||||
|
const root = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'ql-timeout-processes-'),
|
||||||
|
);
|
||||||
|
const known = new Set();
|
||||||
|
let child;
|
||||||
|
t.after(async () => {
|
||||||
|
if (child) {
|
||||||
|
for (const p of await tree(child.pid).catch(() => []))
|
||||||
|
known.add(Number(p.PID));
|
||||||
|
}
|
||||||
|
for (const pid of known) {
|
||||||
|
if (pid > 1) {
|
||||||
|
try {
|
||||||
|
process.kill(pid, 'SIGKILL');
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(root, 'child.sh'),
|
||||||
|
`
|
||||||
|
trap '' TERM
|
||||||
|
printf '%s' "$$" > "$QA_ROOT/child"
|
||||||
|
sleep 30 &
|
||||||
|
printf '%s' "$!" > "$QA_ROOT/grandchild"
|
||||||
|
wait
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
child = spawn(
|
||||||
|
bash,
|
||||||
|
[
|
||||||
|
'-c',
|
||||||
|
`
|
||||||
|
. "$QA_HELPER"
|
||||||
|
task() {
|
||||||
|
trap '' TERM
|
||||||
|
/bin/sh -c 'printf "%s" "$PPID"' > "$QA_ROOT/worker"
|
||||||
|
"$QA_BASH" "$QA_ROOT/child.sh" &
|
||||||
|
wait
|
||||||
|
}
|
||||||
|
run_task_command task
|
||||||
|
exit $?
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
detached: true,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
QA_ROOT: root,
|
||||||
|
QA_HELPER: helper,
|
||||||
|
QA_BASH: bash,
|
||||||
|
command_timeout_time: mode === 'timeout' ? '1s' : '1h',
|
||||||
|
},
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
known.add(child.pid);
|
||||||
|
let result,
|
||||||
|
stderr = '';
|
||||||
|
child.stdout.resume();
|
||||||
|
child.stderr.on('data', (chunk) => (stderr += chunk));
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
result = { code, signal };
|
||||||
|
});
|
||||||
|
await until(
|
||||||
|
() =>
|
||||||
|
['worker', 'child', 'grandchild'].every((name) => {
|
||||||
|
const file = path.join(root, name);
|
||||||
|
return (
|
||||||
|
fs.existsSync(file) && Number(fs.readFileSync(file, 'utf8')) > 1
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
'task process tree did not initialize',
|
||||||
|
);
|
||||||
|
const descendants = await tree(child.pid);
|
||||||
|
for (const p of descendants) known.add(Number(p.PID));
|
||||||
|
for (const name of ['worker', 'child', 'grandchild'])
|
||||||
|
known.add(Number(fs.readFileSync(path.join(root, name), 'utf8')));
|
||||||
|
assert.ok(
|
||||||
|
known.size >= 6,
|
||||||
|
'must observe the wrapper, watchdog and task descendants',
|
||||||
|
);
|
||||||
|
if (mode === 'manual') {
|
||||||
|
const worker = Number(
|
||||||
|
fs.readFileSync(path.join(root, 'worker'), 'utf8'),
|
||||||
|
);
|
||||||
|
const wrapper = descendants.find((p) => Number(p.PID) === worker);
|
||||||
|
assert.ok(wrapper, 'worker must belong to this test process tree');
|
||||||
|
process.kill(Number(wrapper.PPID), 'SIGTERM');
|
||||||
|
}
|
||||||
|
await until(() => result, 'task wrapper did not exit');
|
||||||
|
assert.equal(result.code, mode === 'timeout' ? 124 : 143, stderr);
|
||||||
|
await until(
|
||||||
|
() => [...known].every((pid) => !alive(pid)),
|
||||||
|
'task or watchdog process survived cleanup',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user