DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Bash 后台命令失败仍返回成功:无参数 wait 不汇总子进程错误

Bash 开启 set -e,后台任务失败却仍显示成功时,检查是否使用无参数 wait。保存各任务 PID,逐个收集退出状态,并明确剩余任务和副作用。

脚本开了 set -e,后台命令已经失败,执行 wait 后却仍打印“完成”,CI 也显示成功。先看 wait 有没有指定后台进程的 PID:Bash 无参数 wait 等待后台任务,但正常等完后的返回值是零,不会自动把其中的失败汇总给父脚本。

启动 command & 时,父 shell 不等待最终结果,异步列表的状态为零。因此 set -e 既不能从启动时的状态知道子进程以后失败,也不能从无参 wait 的零状态得知失败。pipefail 处理管道,又不是这项后台任务状态的汇总器。

保存启动后的 PID,再等待那一个任务

下面用 false 说明状态传播,属于结构示例,未代表任何实际任务运行结果:

#!/usr/bin/env bash
set -e

false &
task_pid=$!

wait "$task_pid"
printf '%s\n' 'task completed successfully'

$! 表示最近一个后台任务的 PID,要在启动后及时保存;再启动其他后台任务会改变它。带 PID 的 wait 等待相应子进程,并返回其退出状态。在示例这种普通顶层命令位置,非零的 wait 可以使 set -e 终止后续执行。

不要由此认为 set -e 能覆盖所有上下文。wait 放在 if 条件、某些 AND/OR 列表或其他免退出位置时,规则不同。需要明确记录状态、清理资源或等待多个任务时,用显式分支处理,更容易确认实际结果。

PID 必须属于当前 shell 可等待的子进程。把启动和等待放进不同 shell,或者借助命令替换把 PID 字符串带回来,不会同时转移父子关系。wait 返回 127 等状态时,先确认任务来源,不能直接把它当作业务程序的原始错误码。

多个任务要逐个收集,不只等待最后一个

下面是两个受控脚本并行执行、等全部结束后统一返回失败的示例。脚本名需要替换,并先确认它们可以安全并行,不会互相覆盖输出或同时修改冲突数据:

#!/usr/bin/env bash
set -u

bash task-one.sh &
first_pid=$!
bash task-two.sh &
second_pid=$!

failed=0
for task_pid in "$first_pid" "$second_pid"; do
  if wait "$task_pid"; then
    :
  else
    task_rc=$?
    printf 'task pid=%s exited=%s\n' "$task_pid" "$task_rc" >&2
    failed=1
  fi
done

exit "$failed"

任务在启动时就并行运行,逐个 wait 不会把它们重新变成串行执行。第一项失败也会继续等待第二项,最后返回 1;示例保留每项原状态到日志,但没有让最终退出码等于某一个任务的原状态。

直接写 wait "$first_pid" "$second_pid",Bash 返回的是最后一个指定任务的状态,可能被最后成功的任务掩盖。无参数 wait 同样不能满足“任意一个失败,整组失败”。

不要用 if ! wait "$task_pid"; then task_rc=$? 取原失败码:! 已取反,分支中的状态会改变。上面的普通 if/else 在失败分支第一条语句保存 wait 的状态。

早退与取消是另一项任务管理决定

支持 wait -n 的 Bash 可以等待一项任务完成并取得其状态,但一次调用并不等于已经检查所有任务。不同版本的选项支持也不同;本例选择逐个已保存 PID,不依赖 -n 或 -p。

父脚本发现失败后立即退出,不保证其他后台进程已经结束。若需要取消剩余任务,应按程序支持的停止方式设计信号、超时、等待与收尾,确认是否有派生子进程和已提交的外部副作用。不要把向一个 PID 发信号,当作整个任务树已停止或写入已回滚。

后台脚本内部也要把失败传到退出状态。它自己忽略错误、最后一条命令返回零时,父进程的正确 wait 仍只会得到零;需要沿整条任务调用链核对。日志中出现错误文字也不能代替退出状态。

修改前保存任务脚本和调度方式,在隔离数据中由项目方验证首项失败、末项失败、两项成功、等待被中断等情形。成功标准是每项结果被收集、整组状态符合约定、任务结束后的资源和业务状态可解释。回退只恢复调度代码,不撤销后台任务已经完成的写入或发送动作,重新执行前需核对幂等与已有结果。