fix: 修复任务生命周期与调度就绪,优化执行和构建开销 (#3069)

* fix: harden task lifecycle and scheduler readiness

* fix: confine log writes to the configured log directory

* fix: verify complete build artifacts and untracked inputs

* fix: reconcile scheduler state and make stop win startup races

* fix: isolate cron generations and serialize scheduler recovery
This commit is contained in:
whyour
2026-09-13 00:32:41 +08:00
committed by GitHub
parent d62d8f3025
commit a94e665054
61 changed files with 4214 additions and 608 deletions
+34 -13
View File
@@ -10,16 +10,39 @@ create_token() {
}
get_token() {
if [[ -f $file_auth_token ]]; then
__ql_token__=$(cat $file_auth_token | jq -r .value)
local expiration=$(cat $file_auth_token | jq -r .expiration)
local currentTimeStamp=$(date +%s)
if [[ $currentTimeStamp -ge $expiration ]]; then
create_token
local token_data expiration token_value
local currentTimeStamp=${EPOCHSECONDS:-$(date +%s)}
if [[ -f "$file_auth_token" ]] && token_data=$(jq -er \
'select((.expiration | type) == "number" and (.value | type) == "string" and (.value | length) > 0 and (.value | test("[\r\n]") | not)) | .expiration, .value' \
"$file_auth_token" 2>/dev/null); then
expiration=${token_data%%$'\n'*}
token_value=${token_data#*$'\n'}
if [[ "$expiration" =~ ^[1-9][0-9]{0,10}$ && -n "$token_value" && "$token_value" != *$'\n'* && "$token_value" != *$'\r'* ]] && \
(( currentTimeStamp < expiration )); then
__ql_token__=$token_value
return 0
fi
else
create_token
fi
create_token
}
# Read the status response once; preserve multiline error messages and the
# legacy code/message variables used by the shell API callers.
ql_parse_status_response() {
# Both task status and statistics endpoints normally return this exact body.
# Match the whole document; all other JSON and malformed responses still
# use jq. Keep its missing-message result for existing callers.
if [[ "$1" == '{"code":200}' ]]; then
code=200
message=null
return 0
fi
local parsed
code=""
message=""
parsed=$(jq -r '.code, .message' <<< "$1") || return $?
code=${parsed%%$'\n'*}
message=${parsed#*$'\n'}
}
add_cron_api() {
@@ -142,7 +165,7 @@ update_cron() {
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local exitCode="${7:-}"
local currentTimeStamp=$(date +%s)
local currentTimeStamp=${EPOCHSECONDS:-$(date +%s)}
local dataRaw="{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime"
if [[ -n $exitCode ]]; then
dataRaw="${dataRaw},\"exit_code\":$exitCode"
@@ -156,8 +179,7 @@ update_cron() {
--data-raw "$dataRaw" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
ql_parse_status_response "$api" || true
if [[ $code != 200 ]]; then
if [[ ! $message ]]; then
message="$api"
@@ -240,8 +262,7 @@ record_cron_stat() {
--data-raw "{\"ref_id\":$ref_id,\"code\":$exit_code,\"elapsed\":$elapsed}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
ql_parse_status_response "$api" || true
if [[ $code != 200 ]]; then
if [[ ! $message ]]; then
message="$api"
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# Cache discovery only, never the installed packages. Ordinary installs at the
# same root are visible immediately. TTL bounds changes outside known inputs.
ql_node_path_cache_key() {
local pnpm_bin node_bin name directory file
pnpm_bin=$(type -P pnpm) || return 1
node_bin=$(type -P node) || return 1
{
printf '%s\n' 'v1' "$EUID" "$PWD" "$PATH" "${HOME:-}" \
"${PNPM_HOME:-}" "${XDG_CONFIG_HOME:-}" "${XDG_DATA_HOME:-}" \
"$pnpm_bin" "$node_bin"
for name in ${!npm_config_@} ${!NPM_CONFIG_@}; do
printf '%s=%s\n' "$name" "${!name}"
done
# Follow executable symlinks, detecting upgrades at the same command path.
stat -Lc '%n:%i:%s:%Y:%Z' "$pnpm_bin" "$node_bin" 2>/dev/null || \
stat -Lf '%N:%i:%z:%m:%c' "$pnpm_bin" "$node_bin" 2>/dev/null
directory=$PWD
while :; do
for file in "$directory/.npmrc" "$directory/pnpm-workspace.yaml"; do
if [[ -f "$file" ]]; then
printf '%s\n' "$file"
cat -- "$file"
printf '\n'
fi
done
[[ "$directory" == / ]] && break
directory=${directory%/*}
[[ -n "$directory" ]] || directory=/
done
for file in "${HOME:-}/.npmrc" \
"${XDG_CONFIG_HOME:-${HOME:-}/.config}/pnpm/rc" \
"${npm_config_userconfig:-${NPM_CONFIG_USERCONFIG:-/dev/null}}" \
"${npm_config_globalconfig:-${NPM_CONFIG_GLOBALCONFIG:-/dev/null}}"; do
if [[ -f "$file" ]]; then
printf '%s\n' "$file"
cat -- "$file"
printf '\n'
fi
done
} | cksum
}
ql_read_node_path_cache() {
local cache="$1" key="$2" now
local stored_key="" stored_at="" stored_path=""
now=${EPOCHSECONDS:-$(date +%s)}
if [[ -f "$cache" && ! -L "$cache" && -O "$cache" ]]; then
if ! {
IFS= read -r stored_key && IFS= read -r stored_at && IFS= read -r stored_path
} < "$cache"; then
return 1
fi
if [[ "$stored_key" == "$key" && "$stored_at" =~ ^(0|[1-9][0-9]{0,10})$ && "$stored_path" == /* ]] && \
(( now >= stored_at && now - stored_at < 60 )); then
printf '%s\n' "$stored_path"
return 0
fi
fi
return 1
}
# Keep the lock descriptor in a subshell so sourcing this helper never changes
# the caller's descriptors. A crashed refresher releases the kernel lock.
ql_refresh_node_global_path() (
local cache="$1" key="$2" now result previous_umask
local lock="${cache}.lock"
previous_umask=$(umask)
umask 077
if type -P flock &>/dev/null && mkdir -p -- "$dir_tmp" 2>/dev/null; then
if [[ ! -L "$lock" && ( ! -e "$lock" || ( -f "$lock" && -O "$lock" ) ) ]] && \
{ exec 9>> "$lock"; } 2>/dev/null; then
# Bound the wait; absent/unsupported flock or contention falls back to
# independent discovery. Never remove the lock file while waiters exist.
if flock -w 2 9 2>/dev/null; then
if ql_read_node_path_cache "$cache" "$key"; then
return 0
fi
fi
fi
fi
# Private lock creation must not change pnpm's inherited creation mask.
umask "$previous_umask"
now=${EPOCHSECONDS:-$(date +%s)}
result=$(pnpm root -g 9>&- 2>/dev/null) || return $?
# Never cache failed, empty, multiline or non-absolute answers.
if [[ "$result" == /* && "$result" != *$'\n'* && "$result" != *$'\r'* ]]; then
(
umask 077
mkdir -p -- "$dir_tmp" || exit 0
local temporary
temporary=$(mktemp "${cache}.XXXXXX") || exit 0
if printf '%s\n%s\n%s\n' "$key" "$now" "$result" > "$temporary"; then
mv -f -- "$temporary" "$cache" || rm -f -- "$temporary"
else
rm -f -- "$temporary"
fi
) 2>/dev/null
fi
printf '%s\n' "$result"
)
ql_get_node_global_path() {
if [[ "${QL_NODE_PATH_CACHE:-1}" == 0 ]]; then
pnpm root -g 2>/dev/null
return $?
fi
local key cache
cache="${dir_tmp}/pnpm-root-${EUID}.cache"
key=$(ql_node_path_cache_key) || { pnpm root -g 2>/dev/null; return $?; }
if ql_read_node_path_cache "$cache" "$key"; then
return 0
fi
ql_refresh_node_global_path "$cache" "$key"
}
+7 -1
View File
@@ -95,7 +95,13 @@ append_node_dependency_path() {
# 用户依赖目录加入 NODE_PATH,替代 symlink 到 node_modules 的方式
export NODE_PATH="${NODE_PATH:+${NODE_PATH}:}${dir_dep}"
local pnpm_global_path=$(pnpm root -g 2>/dev/null)
local pnpm_global_path
if [[ -f "$dir_shell/node_path_cache.sh" ]]; then
. "$dir_shell/node_path_cache.sh"
pnpm_global_path=$(ql_get_node_global_path) || true
else
pnpm_global_path=$(pnpm root -g 2>/dev/null) || true
fi
if [[ -n "$pnpm_global_path" ]]; then
export QL_NODE_GLOBAL_PATH="$pnpm_global_path"
export NODE_PATH="${NODE_PATH:+${NODE_PATH}:}${pnpm_global_path}"
+18 -6
View File
@@ -368,9 +368,16 @@ format_timestamp() {
get_env_array() {
exported_variables=()
while IFS= read -r line; do
exported_variables+=("$line")
done < <(grep '^export ' $file_env | awk '{print $2}' | cut -d= -f1)
# 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() {
@@ -411,9 +418,14 @@ run_task_after() {
}
handle_task_end() {
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
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
+13 -3
View File
@@ -52,8 +52,14 @@ handle_log_path() {
fi
fi
time=$(date "+$mtime_format")
log_time=$(format_log_time "$mtime_format" "$time")
if [[ $is_macos -ne 1 && $mtime_format == '%Y-%m-%d %H:%M:%S.%3N' ]]; then
# Render both representations from one clock snapshot, without parsing it
# again in a second date process.
IFS='|' read -r time log_time < <(date "+$mtime_format|%Y-%m-%d-%H-%M-%S-%3N")
else
time=$(date "+$mtime_format")
log_time=$(format_log_time "$mtime_format" "$time")
fi
if [[ -z $log_name ]]; then
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
@@ -124,7 +130,11 @@ format_params() {
}
init_begin_time() {
begin_time=$(format_time "$time_format" "$time")
if [[ $is_macos -ne 1 && $mtime_format == '%Y-%m-%d %H:%M:%S.%3N' && $time_format == '%Y-%m-%d %H:%M:%S' ]]; then
begin_time=${time%.*}
else
begin_time=$(format_time "$time_format" "$time")
fi
begin_timestamp=$(format_timestamp "$time_format" "$time")
}