reconFTW 弹性恢复与超时安全专项代码审查:`01-resilient-resume-timeout-safety` 阶段评审报告解析
reconFTW 弹性恢复与超时安全专项代码审查:01-resilient-resume-timeout-safety 阶段评审报告解析
本指南围绕 reconFTW 开源仓库中
.planning/phases/01-resilient-resume-timeout-safety/01-REVIEW.md评审报告展开,深入解读该阶段针对「异常中断后自动恢复(resume)」与「并行任务超时安全(timeout safety)」两大目标引入的三项关键修复(CR-01 EXIT 陷阱门控、CR-02 静默模式下超时失效、CR-03 进程树清理),并对照源码剖析其实现原理、遗留告警(WR-01/02/03)与改进建议。读完本文,你将掌握 reconFTW 的断点续跑哨兵机制、并行心跳超时闭环,以及如何从源码层面验证这类弹性修复的正确性与边界。
一、评审报告背景:这是"复审"而非首次评审
01-REVIEW.md 是 01 阶段(resilient-resume-timeout-safety,即"弹性恢复 + 超时安全")的第二次代码评审,评审时间 2026-05-13T14:30:00Z,深度为 standard,共复查 4 个文件:
- lib/parallel.sh — 并行执行核心(心跳、超时、进程树清理)
- modules/core.sh —
start_func/end_func/start_subfunc/end_subfunc等函数生命周期 - modules/modes.sh —
start()/end()工作流编排与 EXIT 陷阱安装 - modules/utils.sh —
_cleanup_inprogress/_abort_disk_full等清理与中止逻辑
复审结论:上一轮评审提出的 3 个 Critical(CR-01/02/03)已由 01-04/01-05 计划正确修复并通过全流程追踪验证;本阶段无新 BLOCKER,剩余 3 个 Warning(WR-01/02/03)与 3 个 Info(IN-01/02/03),总发现 6 项,状态为 issues_found。
二、核心机制:断点续跑的哨兵体系(.inprogress_<fn> 与 .<fn>)
要理解 CR-01 修复,必须先弄清 reconFTW 的恢复机制。仓库在 called_fn_dir(默认 Recon/<domain>/.called_fn)下用两类隐藏文件做"检查点":
| 哨兵文件 | 写入时机 | 语义 |
|---|---|---|
.inprogress_<fn> |
start_func() 执行开头 touch |
函数正在运行/异常中断的"进行中"标记(表面指示器) |
.<fn> |
end_func() 结束前 touch |
函数成功完成,是唯一的事实来源(source of truth) |
关键设计:.<fn> 才是 checkpoint 的依据,.inprogress_<fn> 只是断点提示。end_func 中两者严格有序——先 rm .inprogress_<fn> 再 touch .<fn>;若在 rm 与 touch 之间崩溃,则 .<fn> 缺失,下一次运行会依据 checkpoint 守卫重新进入该函数,实现断点续跑。
从源码看,哨兵写入集中在 modules/core.sh 与 modules/core.sh:
# start_func() 内(modules/core.sh:1459-1461)
if [[ -n "${called_fn_dir:-}" ]]; then
touch "$called_fn_dir/.inprogress_${1}" 2>/dev/null || true
fi
# end_func() 内(modules/core.sh:1504-1507)
if [[ -n "${called_fn_dir:-}" ]]; then
rm -f "$called_fn_dir/.inprogress_${fn}" 2>/dev/null || true
touch "$called_fn_dir/.${fn}" 2>/dev/null || true
fi
配套的恢复提示(resume banner)位于 modules/modes.sh:start() 在非 FORCE_RESCAN 场景下扫描 $called_fn_dir/.inprogress_*,若存在则输出一行 WARN resume: N functions re-running after interruption (...) 并通过 log_json 记录,明确告知用户哪些函数将被重跑。
三、CR-01:EXIT 陷阱门控——SIGINT/SIGTERM 不再破坏恢复哨兵
3.1 缺陷原貌
在修复前,EXIT 陷阱无条件清理 .inprogress_*,导致用户按 Ctrl-C(SIGINT)中断后,哨兵被清空,下次运行无法提示断点续跑——中断后恢复的提示缺口(SC1)。
3.2 修复方案
引入全局标志 _RECON_CLEAN_EXIT 作为"干净退出"门控:
- 初始化为 false:modules/modes.sh,
start()首行即_RECON_CLEAN_EXIT=false,每次-l列表循环迭代都会重新复位; - 唯一置 true 的点:modules/modes.sh,作为
end()的最后一条语句执行; - 消费方:modules/utils.sh 的
_cleanup_inprogress,首行[[ "${_RECON_CLEAN_EXIT:-false}" == "true" ]] || return 0,只有标志为 true 才执行rm -f "$called_fn_dir"/.inprogress_*。
EXIT 陷阱的安装位于 modules/modes.sh:
trap 'cleanup_on_exit' INT TERM
trap '_cleanup_inprogress' EXIT # silent EXIT-only sentinel sweep (D-02)
3.3 行为矩阵(可验证)
| 退出路径 | _RECON_CLEAN_EXIT |
EXIT 陷阱是否清哨兵 | 结果 |
|---|---|---|---|
正常遍历完成(end() 执行到最后) |
true |
是 | 无哨兵残留,干净收尾 |
SIGINT/SIGTERM(cleanup_on_exit 路径) |
false |
否 | 哨兵存活 → 下次运行显示 resume banner |
磁盘满中止 _abort_disk_full 的 exit 1 |
false |
否 | 哨兵存活 → 下次运行恢复 |
| 未处理错误 | false |
否 | 哨兵存活 → 下次运行恢复 |
评审报告逐一流式追踪了全部模式流程(-r、-s、-p、-a、-w、-n、-z、-c、monitor、report-only、multi),确认:每个调用 start() 的工作流都调用 end(),因此正常路径标志必为 true;绕过 end() 的路径标志恒为 false,哨兵必然保留。同时 FORCE_RESCAN=true 在 modules/modes.sh 会主动 rm -f "$called_fn_dir"/.inprogress_*,保证强制重扫不会继承伪造的恢复状态。
四、CR-02:静默模式下并行超时失效——心跳与快照解耦
4.1 缺陷原貌
修复前,心跳循环只有在"显示进度快照"的开关开启时才运行;--quiet(OUTPUT_VERBOSITY=0)下循环直接跳过,PARALLEL_JOB_TIMEOUT_SECONDS 形同虚设。CI 场景常以 --quiet 运行(见 reconftw.cfg 中关于超时配置用于 CI 的说明),超时失效意味着挂死任务无人清理。
4.2 修复方案
在 lib/parallel.sh(第一处心跳循环)与 lib/parallel.sh(第二处循环)将循环进入条件与快照显示条件解耦:
local _loop_active=false
[[ "${PARALLEL_MODE:-true}" == "true" ]] && [[ "$hb" =~ ^[0-9]+$ ]] && ((hb > 0)) && _loop_active=true
if [[ "$_loop_active" == "true" ]] && { [[ "$_verbose_progress" == "true" ]] || (( _to > 0 )); }; then
- 只要
hb(PARALLEL_HEARTBEAT_SECONDS)> 0,循环即进入; _verbose_progress(verbosity ≥ 1)仅决定是否渲染快照;- 超时检查(lib/parallel.sh 与 lib/parallel.sh)与 verbosity 无关,只要
_to > 0就逐次迭代触发。
评审验证:OUTPUT_VERBOSITY=0 且 PARALLEL_JOB_TIMEOUT_SECONDS=600 时,循环照常运行且 _timeout_kill_job 被调用。注意一个边界:PARALLEL_HEARTBEAT_SECONDS=0 会彻底禁用循环,连带禁用超时强制(报告标注为 WR-05 遗留事项)。
五、CR-03:超时仅杀包装子 shell——进程树清理 _kill_tree
5.1 缺陷原貌
parallel_funcs 将每个函数放入后台子 shell(wrapper)执行;修复前 _timeout_kill_job 只对 wrapper PID 发信号,真正的外部工具(puredns、dnsx、ffuf、axiom-scan 等)成为孤儿进程被 PID 1 收养,继续越过超时执行。
5.2 修复方案
新增递归助手 lib/parallel.sh _kill_tree,用 pgrep -P 先深后浅遍历进程树,先杀子进程再杀父进程,防父进程收到信号后重新拉起子进程;pgrep 缺失时优雅降级为仅杀 wrapper(等价于补丁前行为):
function _kill_tree() {
local parent="$1" sig="${2:-TERM}"
local child
if command -v pgrep >/dev/null 2>&1; then
for child in $(pgrep -P "$parent" 2>/dev/null); do
_kill_tree "$child" "$sig"
done
fi
kill "-$sig" "$parent" 2>/dev/null || true
}
_timeout_kill_job(lib/parallel.sh)按 D-13 的 TERM→轮询→KILL 三段式执行:先 _kill_tree "$pid" TERM,以 PARALLEL_KILL_GRACE_SECONDS(默认 10 秒,配置见 reconftw.cfg)逐秒 kill -0 轮询,超宽限后 _kill_tree "$pid" KILL。随后按 D-14 持久化失败状态(printf "FAIL\n" > .status_<fn> 与 printf "timeout\n" > .status_reason_<fn>),并输出 ERROR 级 log_json——与 end_func 写入同路径文件,使 _parallel_emit_job_output 无需扩展 schema 即可在 FAIL 徽章上渲染 timeout 原因。
报告还确认了设计约束:start() 显式 set +m 禁用作业控制(modules/modes.sh),wrapper 子 shell 没有独立进程组,因此否决了 kill -- -<pgid> 进程组方案;pgrep -P 在 Linux(procps)与 macOS(BSD pgrep)均可用(已做系统测试)。
5.3 相关配置速查
# reconftw.cfg:328-331
PARALLEL_JOB_TIMEOUT_SECONDS=0 # 0 禁用;如 3600 用于长扫描、600 用于 CI
PARALLEL_KILL_GRACE_SECONDS=10 # TERM 与 KILL 之间的宽限秒数
实际 kill 延迟 ≈ 阈值 + 心跳轮询约 1 秒 + PARALLEL_KILL_GRACE_SECONDS。相关并行参数还有 PARALLEL_MAX_JOBS=4、PARALLEL_HEARTBEAT_SECONDS=20 等(lib/parallel.sh),磁盘中止阈值 MIN_DISK_SPACE_GB 默认 2(reconftw.cfg,0 可禁用检查)。
六、遗留告警(Warnings):WR-01/02/03 剖析与修复建议
WR-01:end_func/end_subfunc 中未加守卫的 touch
上一轮 CR-04 发现:modules/core.sh:1485 的 rm 已移入 [[ -n "${called_fn_dir:-}" ]] 守卫,但相邻的 touch "$called_fn_dir/.${fn}" 仍裸奔;end_subfunc 第 1572 行的 touch "$called_fn_dir/.${2}" 同样未守卫。若 called_fn_dir 为空(自定义工作流、替代入口、不调用 start() 的测试运行器),touch /.${fn} 在非 root 下报权限错误,在 root 下则静默在文件系统根目录创建文件。建议修复:
# modules/core.sh end_func,约 1482-1485 行
if [[ -n "${called_fn_dir:-}" ]]; then
rm -f "$called_fn_dir/.inprogress_${fn}" 2>/dev/null || true
touch "$called_fn_dir/.${fn}" 2>/dev/null || true
fi
end_subfunc(约 1572 行)同样将 touch 移入守卫并追加 || true。需说明的是:当前仓库 end_func 的 touch 已带 2>/dev/null || true 且已处于 called_fn_dir 守卫内(modules/core.sh),评审反映的是评审时点(2026-05-13)的快照,此后实现已趋近建议形态;end_subfunc(modules/core.sh)的 touch 已加 2>/dev/null || true 但仍在守卫内,实践中应保持守卫与抑制错误两者同时具备。
WR-02:过期注释——EXIT 陷阱"会清 .inprogress_*"的表述失真
CR-01 修复后,_abort_disk_full 的 exit 1 不会置 true 标志,EXIT 陷阱实际不清理哨兵;但 modules/core.sh:1420-1421(start_func 头部)与 modules/utils.sh:455(_abort_disk_full 头部)的注释仍描述修复前的"无条件清理"行为,会误导维护者推断陷阱链。报告中给出的对齐文案要点:磁盘守卫在任何状态写入前中止,exit 1 使 _RECON_CLEAN_EXIT=false,EXIT 陷阱的 _cleanup_inprogress 因此不扫描,其他在飞函数的既有哨兵得以存活并驱动下次运行的 resume banner。此为纯文档修复,无行为变更。
WR-03:_timeout_kill_job 跨心跳迭代重复触发
心跳循环 for idx in "${!batch_pids<a href="https://link.gitcode.com/i/039dcf3eb9634095e36408c1068e006b" target="_blank">@]}" 中,若 wrapper PID 进入 zombie 态,kill -0 在多数内核(尤其 Linux)仍返回成功,下一轮外层迭代会再次触发 _timeout_kill_job:重复走 _kill_tree(此时树已近全死,信号为 no-op)、重复写 .status_<fn>=FAIL 与 .status_reason_<fn>=timeout、重复输出 ERROR 级 log_json,并浪费一次 10 秒宽限轮询——期间外层心跳被阻塞,无法检查其他仍在运行的任务是否超时。建议在心跳循环内维护局部关联数组跟踪已发信号 PID(local -A _timed_out_pids=()),触发条件追加 [[ -z "${_timed_out_pids[${batch_pids[$idx]}]:-}" ]],触发后置位;两处循环([lib/parallel.sh 与 lib/parallel.sh)同步修改。
七、Info 级观察:IN-01/02/03(非缺陷,供后续优化)
- IN-01:
start_subfunc不写.inprogress_<fn>(modules/core.sh)。哨兵体系只覆盖start_func/end_func;sub_passive、sub_crt、sub_active、sub_brute等子函数写.<fn>与.status_<fn>但无.inprogress_<fn>,SIGINT 中断子函数时 resume banner 无法点名具体子函数(父函数subdomains_full受保护,重跑仍正确)。可选方案:在start_subfunc/end_subfunc镜像 inprogress touch/rm,或加注释明确子函数仅依赖.<fn>。 - IN-02:
_kill_tree递归无深度上限(lib/parallel.sh)。reconFTW 并行 wrapper 实际只有 2-3 层(subshell → 工具 → 管道工具的孙进程,如subfinder | anew),bash 默认栈(内核约 8MB,约 5000-10000 帧)足以支撑;但对 fork 炸弹类工具或深层 axiom 工具链存在理论风险。可选加固:depth参数 +PARALLEL_KILL_MAX_DEPTH(建议 16)上限。 - IN-03:resume banner 用
ls -1展开 glob(modules/modes.sh)。mapfile -t _leftover < <(ls -1 "${called_fn_dir}"/.inprogress_* 2>/dev/null)在 glob 不匹配时是 fork + stderr 噪音模式,可改为纯 bash 的shopt -s nullglob+ 数组直接展开。属风格优化,非缺陷。
八、测试基线与本阶段的验证缺口
报告明确指出:01-04/01-05 的两轮 SUMMARY 保持了 246/246 单元测试 + 34/34 安全测试(bats) 基线全部通过;但未新增覆盖这三项修复的测试文件——按 01-CONTEXT.md 的约定,新增测试被显式推迟到 Phase 4 / TEST-01。对读者意味着:验证 CR-01/02/03 目前依赖代码审查追踪(如逐模式确认 start()/end() 配对、verbosity 0 场景手动验证超时触发),未来应在 Phase 4 补齐集成测试。仓库现有测试体系可参考 tests/ 下的 bats 文件(如 test_monitor.bats、test_parallel.bats),以及 tests/integration/test_checkpoint.bats 等。
九、小结:如何用这份评审驱动你的修改
本阶段修复的共性方法论值得复用于后续重构:
- 门控而非条件删除:用布尔标志(
_RECON_CLEAN_EXIT)区分"干净退出"与"异常退出",使清理语义可预测、可单点修改; - 解耦职责:心跳循环的"存在"与"显示"分离,让超时强制不再依赖 UI verbosity;
- 纵深防御:信号清理从"杀 wrapper"升级为"递归杀进程树",同时保留 pgrep 缺失时的降级路径;
- 文档同步:行为变更后必须同步审计相邻注释(WR-02 的教训),否则后续维护者的分析路径会被误导。
在你提交新改动前,建议对照本文 WR-01/02/03 的检查清单自查,并将 IN-02 的深度上限、IN-03 的 nullglob 风格一并纳入你的代码规范。
参考文件索引(仓库相对路径):
- 评审报告原文:.planning/phases/01-resilient-resume-timeout-safety/01-REVIEW.md
- 并行核心:lib/parallel.sh(
_kill_treeL55-66、_timeout_kill_jobL77-105、心跳循环 L500-545/L621-665) - 函数生命周期:modules/core.sh(
start_funcL1441-1469、end_funcL1471-1578、start_subfuncL1580-1591、end_subfuncL1593+) - 工作流编排与陷阱:modules/modes.sh(
start()L13-190、end()末尾 L526-533) - 清理与中止:modules/utils.sh(
cleanup_on_exitL116-146、_cleanup_inprogressL153-157、_check_disk_mid_runL450-453、_abort_disk_fullL456-460) - 配置参考:reconftw.cfg(
MIN_DISK_SPACE_GBL49、超时参数 L328-331)