reconFTW 弹性恢复与超时安全:`.inprogress` 哨兵、磁盘满防护与并行作业超时机制深度解析
reconFTW 弹性恢复与超时安全:.inprogress 哨兵、磁盘满防护与并行作业超时机制深度解析
reconFTW 是面向 bug bounty、渗透测试与安全研究场景的 Bash 侦察编排框架,其 Phase 1(Resilient Resume & Timeout Safety)集中解决长时间扫描面临的四类可靠性问题:中断后无法续跑、磁盘中途写满、并行批处理被卡死作业拖垮、DNS 枚举无限期挂起。本文基于 01-CONTEXT.md 的完整设计决策(D-01~D-17),结合 modules/core.sh、lib/parallel.sh、modules/utils.sh、modules/modes.sh 与 reconftw.cfg 的实际实现,讲清每个机制的触发链路、配置方法与源码级依据。读完你既能掌握 PRESERVE=true 续跑、PARALLEL_JOB_TIMEOUT_SECONDS 限时等实战配置,也能理解这套设计为何选"哨兵文件 + EXIT trap"而非 PID/时间戳方案。
一、Phase 1 的边界与目标:四项 v1 需求
Phase 1 要解决的核心问题:长时间扫描在遭遇中断、磁盘压力或卡死工具时,不能静默产出截断结果,也不能在续跑时浪费数小时重复劳动。按 ROADMAP.md 的定义,Phase 1 覆盖 REQUIREMENTS.md 中的四项 v1 需求,拆分为三个计划:
| 计划 | 机制 | 对应需求 |
|---|---|---|
| 01-01 | .inprogress 哨兵生命周期 + 续跑检测 |
RESIL-01 |
| 01-02 | 函数边界处周期性 df 检查(磁盘满中途防护) |
RESIL-02 |
| 01-03 | lib/parallel.sh 中 PARALLEL_JOB_TIMEOUT_SECONDS 强制执行 + reconftw.cfg DNS 超时默认值 |
RESIL-03、PERF-02 |
范围内改动面:生命周期包装位于 modules/core.sh 的 start_func/end_func,并行作业机制位于 lib/parallel.sh,磁盘辅助函数位于 modules/utils.sh,四项配置默认值位于 reconftw.cfg。
明确不在范围内(推迟到其他阶段):MIN_DISK_SPACE_GB 的 2-vs-5 不一致问题(DOCS-02,Phase 5)、逐工具线程上限(PERF-01,Phase 3)、针对新超时/心跳路径的并行覆盖测试(TEST-01,Phase 4)。
从 PROJECT.md 可以看到,Phase 1 的五项成果(RESIL-01/02/03、PERF-02 及 01-04/01-05 两个缺口修复)均已标记为 Validated,说明下述设计与实现已经落地为仓库中的真实代码。
二、.inprogress 哨兵生命周期与续跑检测(RESIL-01)
2.1 哨兵文件的形态:纯空文件(D-01)
设计决策 D-01 规定:哨兵文件是一个纯空文件 ${called_fn_dir}/.inprogress_<fn>。start_func 负责 touch 创建它;end_func 负责在 touch 既有的 .<fn> 成功检查点之前删除它。不携带任何 JSON 元数据、PID 或时间戳。
源码印证(modules/core.sh start_func,L1441-L1469):
function start_func() {
# Mid-run disk-full guard (D-07/D-09): abort BEFORE any state write
_check_disk_mid_run || _abort_disk_full
...
# Resume sentinel (RESIL-01 / D-01): touch .inprogress_<fn> as a crash-leftover marker
if [[ -n "${called_fn_dir:-}" ]]; then
touch "$called_fn_dir/.inprogress_${1}" 2>/dev/null || true
fi
log_json "INFO" "${1}" "Function started" "description=${2}"
...
}
对应 modules/core.sh end_func(L1471-L1507):
# Resume sentinel (RESIL-01 / D-01): remove .inprogress_<fn> BEFORE touching
# the .<fn> success checkpoint. A crash between the rm and the touch leaves
# .<fn> absent — the existing checkpoint guard re-enters the function on the
# next run. .<fn> is the source of truth; .inprogress_<fn> is a surface
# indicator only.
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
关键语义:.<fn> 是事实来源(决定是否重跑),.inprogress_<fn> 只是表面指示器(用于"发生了什么"的报告),不参与控制流。若函数在 rm 与 touch 之间崩溃,.<fn> 缺失,下次运行的检查点守卫会自然重入该函数——这正是透明续跑得以成立的原因(见 2.5)。
2.2 过时检测:clean-exit-gated trap(D-02)
D-02 规定过时检测采用以干净退出为门槛的 trap 清理机制:
- modules/modes.sh 的
start()(L13 起)初始化模块级标志_RECON_CLEAN_EXIT=false(实际在 L16),并安装一个独立的 EXIT-only trap 调用_cleanup_inprogress; - trap 清扫以
_RECON_CLEAN_EXIT=true为门槛,该值由end()(工作流收尾函数)的最后一条可执行语句设置; - 因此干净遍历不会残留任何
.inprogress_*文件; - 而 SIGINT/SIGTERM(经由
cleanup_on_exittrap 调用exit 130)不会翻转该标志,EXIT trap 以_RECON_CLEAN_EXIT=false触发,哨兵得以保留——下次运行看到哨兵并发出 WARN 续跑横幅。
trap 安装源码(modules/modes.sh L122-L123):
trap 'cleanup_on_exit' INT TERM
trap '_cleanup_inprogress' EXIT # silent EXIT-only sentinel sweep (D-02)
标志翻转源码(modules/modes.sh L526-L532,end() 收尾):
# Mark clean traversal so the EXIT trap's _cleanup_inprogress sweeps
# ... (bypasses end() — SIGINT/SIGTERM via cleanup_on_exit, an
# internal abort like _abort_disk_full's exit 1, or an unhandled error)
_RECON_CLEAN_EXIT=true
EXIT trap 实现(modules/utils.sh _cleanup_inprogress,L153-L157):
function _cleanup_inprogress() {
[[ "${_RECON_CLEAN_EXIT:-false}" == "true" ]] || return 0
[[ -n "${called_fn_dir:-}" ]] && rm -f "${called_fn_dir}"/.inprogress_* 2>/dev/null
return 0
}
注意,start() 中还已存在一个 ERR trap(modules/modes.sh L140 附近),新增的 EXIT trap 必须与它组合而非替换——可通过单个注册字符串链式挂多个 handler,或重构为同时调用两者的单一 _recon_exit_hook。
2.3 为何弃用 PID / 时间戳方案(D-03)
D-03 解释了选型的理由:项目是单操作员、单目标约束(见 PROJECT.md Constraints 与 CLAUDE.md),同一 called_fn_dir 永远不会被并发运行竞争。这一约束使得"目录里还留着哨兵 = 之前崩溃过"成为安全推断,从而完全不需要 PID 存活探测或时间戳过时窗口。PID 方案在长时运行机器上还会因 PID 回收而失效(旧进程的 PID 可能被新进程复用,导致误判存活)。
因此规划阶段明确要求:不要出于"保险"重新引入 PID/时间戳元数据——在单操作员约束下,更简单的设计才是正确的设计(见原文档 Specific Ideas 小节)。
2.4 续跑报告:单行横幅而非逐函数徽章(D-04)
D-04 规定:当 start() 时发现一个或多个 .inprogress_* 文件,在 recon 横幅顶部输出一行汇总:
WARN resume: N functions re-running after interruption (fn_a, fn_b)
之后静默继续——不做逐函数 RESUME 徽章,不在模块区内部增加额外噪声。JSONL 日志同步输出 level=WARN func=resume reason=inprogress_leftover funcs=fn_a,fn_b。
该设计刻意保持 OK/WARN/FAIL/SKIP 徽章词汇不变(徽章词汇表锁定于 CONVENTIONS.md),汇总行是唯一的续跑专属 UI。横幅渲染方式(用现有 _print_msg WARN 还是新 _print_resume_banner 辅助函数)属于规划层渲染选择;由于它是 WARN 级别,在默认 OUTPUT_VERBOSITY=1 下即可见,而 _print_error 路径的磁盘满错误则始终可见。
2.5 PRESERVE=false 清理与透明重执行(D-05 / D-06)
- D-05:
PRESERVE=false(默认)在运行开始前就会清空.<fn>检查点;同一个循环也必须清理任何孤儿.inprogress_*文件,保证PRESERVE=false的运行永不报告续跑。而PRESERVE=true时,trap 清理 + 残留检测逻辑正是续跑得以工作的机制。 - D-06:函数重执行是透明的。既有检查点守卫
[[ ! -f "$called_fn_dir/.${FUNCNAME[0]}" ]] || [[ $DIFF == true ]]本就会重跑.<fn>不存在的函数——崩溃的函数从未到达end_func,其.<fn>必然缺失,守卫自然重入。哨兵文件纯粹服务于"发生了什么"的表面报告,不驱动控制流。
这与 ROADMAP.md 的成功标准 1 完全一致:"中断 sub_brute 后以 PRESERVE=true 重跑,产生清晰的 .inprogress_sub_brute 指示,且仅重执行该函数——已完成的函数保留 .<func> 检查点并被跳过"。
三、磁盘满中途防护(RESIL-02)
3.1 检查节奏:每个函数边界(D-07)
D-07 规定检查节奏为每次 start_func / end_func 边界:
- 新增辅助函数
_check_disk_mid_run,位于 modules/utils.sh(紧挨既有check_disk_space()); start_func在既有函数体之前调用它;end_func在 touch 检查点之后调用它。
源码印证:start_func 第一行即 _check_disk_mid_run || _abort_disk_full(modules/core.sh L1444),end_func 在持久化 .status_<fn> 之后、收尾之前再次执行 _check_disk_mid_run || _abort_disk_full(modules/core.sh L1575),注释明确说明"在 .<fn> 与 .status_<fn> 可靠写入之后执行,让已完成的函数留下完整成功记录;在此中止可保护下一个函数不在无余量状态下运行"。
成本评估:每次调用约 1ms,一个扫描约 100 次函数边界,总开销可忽略;不引入后台监视进程。
3.2 阈值单一来源:复用 MIN_DISK_SPACE_GB(D-08)
D-08 规定阈值就是 pre-flight 检查所用的同一个 MIN_DISK_SPACE_GB(单一事实来源),实现如下(modules/utils.sh L449-L453):
# Mid-run disk-full check: thin wrapper around check_disk_space using MIN_DISK_SPACE_GB (D-07/D-08).
function _check_disk_mid_run() {
check_disk_space "${MIN_DISK_SPACE_GB:-5}" "${dir:-.}"
return $?
}
底层的 check_disk_space()(modules/utils.sh L432-L447)用 df -Pk 计算可用 GB,跨 macOS/Linux 可移植,返回 0/1 并填充 DISK_SPACE_INFO。Phase 5(DOCS-02)将解决 reconftw.cfg 中 MIN_DISK_SPACE_GB=2(reconftw.cfg L49)与 modules/modes.sh start() 中 ${MIN_DISK_SPACE_GB:-5} 回退默认值 5 之间的差异;Phase 1 刻意不碰这两处,运行期读到哪个值就用哪个值,也不引入独立的中途旋钮。
3.3 中止策略:整轮硬中止(D-09)
D-09 规定磁盘满时硬中止整个运行:_check_disk_mid_run 返回非零 → 调用 _abort_disk_full。实现(modules/utils.sh L455-L460):
function _abort_disk_full() {
_print_error "disk_full: aborting (${DISK_SPACE_INFO:-disk space exhausted})"
log_json "ERROR" "${FUNCNAME[1]:-main}" "Disk space exhausted" "reason=disk_full" "info=${DISK_SPACE_INFO:-unknown}"
exit 1
}
它输出 disk_full: aborting (avail=XGB, req=YGB at dir) 形式的错误、记录 log_json ERROR disk_full abort_run,然后 exit 1。此时 D-02 的 Bash EXIT trap 会在退出路上触发,清空所有 .inprogress_*——后续运行不会看到虚假的续跑条件。软中止与暂停等待方案均被否决:单操作员项目,快速失败(fail-fast)才是正确选择。
3.4 ENOSPC 检测机制:仅边界 df(D-10)
D-10 明确检测机制只有边界 df:不做 run_command 的 stderr 扫描(No space left on device),不做每个重定向的失败 trap。这是有意识接受的权衡:某个工具在单个函数内部耗尽磁盘(如 gotator 写出 2GB 词表)时,该函数可能产生截断输出,但下一次 start_func 会在造成进一步破坏前中止整个运行。pre-flight 5GB 余量 + Phase 5 的 DOCS-02 对齐让这种情况成为罕见边缘场景;对 v1 而言,全量逐写 trap 属于过度设计。
四、PARALLEL_JOB_TIMEOUT_SECONDS 强制执行(RESIL-03)
4.1 默认值:0(禁用、显式启用)(D-11)
D-11 规定默认值 PARALLEL_JOB_TIMEOUT_SECONDS=0(禁用、opt-in),随 reconftw.cfg 发布,并附文档注释明确建议:长扫描用 3600,CI 运行用 600。这样在运行的扫描零风险被过度激进的默认值杀死,且向后兼容。
实际配置(reconftw.cfg L330-L331):
PARALLEL_JOB_TIMEOUT_SECONDS=0 # 0 disables; e.g. 3600 for long scans, 600 for CI
PARALLEL_KILL_GRACE_SECONDS=10 # Seconds between TERM and KILL when enforcing PARALLEL_JOB_TIMEOUT_SECONDS
规划层面特意强调:不要提议非零默认值(见原文档 Specific Ideas)。
4.2 执行点:既有 batch-flush 心跳循环(D-12)
D-12 规定执行点位于既有的 batch-flush 心跳循环(lib/parallel.sh,parallel_funcs 的 batch 等待区)。该循环原本每 PARALLEL_HEARTBEAT_SECONDS(默认 20s)用 kill -0 ${batch_pids[$idx]} 轮询存活 PID;扩展后为每个存活 PID 计算 now - batch_starts[$idx],若超过 PARALLEL_JOB_TIMEOUT_SECONDS(且变量 > 0)则调用 _timeout_kill_job。
实际源码(lib/parallel.sh L516-L524,batch flush 心跳内):
for idx in "${!batch_pids[@]}"; do
if kill -0 "${batch_pids[$idx]}" 2>/dev/null; then
alive=1
job_dur=$((now - batch_starts[$idx]))
# Timeout enforcement (RESIL-03 / D-11..D-14): fires regardless of
# verbosity (CR-02 fix). Uses hoisted _to from outer scope.
if (( _to > 0 )) && (( job_dur > _to )); then
_timeout_kill_job "${batch_pids[$idx]}" "${batch_funcs[$idx]}" "$job_dur"
fi
...
CR-02 缺口修复使超时执行与 --quiet 无关(心跳循环在"超时启用 或 verbose 进度开启"时即运行,见 L508 与 L629 的循环条件),保证 CI 场景(--quiet)下超时依然生效。注意 lib/parallel.sh 的注释还记录了一个已知细节:PARALLEL_HEARTBEAT_SECONDS=0 会完全禁用该循环,从而也禁用超时执行(WR-05,计划在 Phase 5 DOCS-01 处理)。
4.3 杀进程行为:TERM 后 KILL(D-13)
D-13 规定先 TERM 再 KILL:新旋钮 PARALLEL_KILL_GRACE_SECONDS=10(默认)。_timeout_kill_job 先发 kill -TERM $pid,随后以 1 秒间隔轮询 kill -0 $pid 直至宽限期结束,若仍存活则 kill -KILL $pid。这符合 Unix 关停语义,并能对付无视 TERM 的工具。
实现(lib/parallel.sh _timeout_kill_job,L77-L105):
function _timeout_kill_job() {
local pid="$1" func_name="$2" duration_sec="$3"
local grace="${PARALLEL_KILL_GRACE_SECONDS:-10}"
[[ "$grace" =~ ^[0-9]+$ ]] || grace=10
_kill_tree "$pid" TERM
local i
for ((i=0; i<grace; i++)); do
kill -0 "$pid" 2>/dev/null || break
sleep 1
done
_kill_tree "$pid" KILL
...
CR-03 修复让 _timeout_kill_job 切换为进程树击杀:_kill_tree(lib/parallel.sh L55-L66)通过 pgrep -P 递归遍历先信号化叶子节点再信号化父节点,确保真正的底层外部工具(puredns、dnsx、ffuf、axiom-scan 等)被终止,而不只是包装子 shell——修复前的行为是只杀包装 PID,导致工具被孤儿化到 PID 1 后继续超时运行。若宿主机缺 pgrep,则优雅退化为仅杀包装(与补丁前行为一致),运行不失败。进程组击杀方案(kill -- -<pgid>)被否决,因为 modules/modes.sh L17 显式 set +m 禁用了作业控制,包装子 shell 没有独立 pgid。
4.4 超时报告:FAIL + reason=timeout(D-14)
D-14 规定被击杀作业的徽章为 FAIL,原因 timeout:持久化到 .status_<fn>(FAIL)与 .status_reason_<fn>(timeout),复用 modules/core.sh 既有的状态持久化模式(原文档引用 L1505-L1509,当前实现位于 end_func 的 L1547-L1552)。批计数器 failed++,于是 RECON_PARTIAL_RUN=true 由既有聚合器自动跟随。控制台徽章输出 FAIL func_name 600s (timeout)。
_timeout_kill_job 中的持久化与结构化日志(lib/parallel.sh L96-L104):
if [[ -n "${called_fn_dir:-}" ]]; then
printf "FAIL\n" >"${called_fn_dir}/.status_${func_name}" 2>/dev/null || true
printf "timeout\n" >"${called_fn_dir}/.status_reason_${func_name}" 2>/dev/null || true
fi
if declare -F log_json >/dev/null 2>&1; then
log_json "ERROR" "${func_name}" "Job timed out" "reason=timeout" "duration_sec=${duration_sec}"
fi
下游 _parallel_emit_job_output(lib/parallel.sh L235-L363)读取这两个文件并在 FAIL 徽章上渲染 reason: timeout,无需任何 schema 扩展。实际杀延迟 = 阈值 + 约 1s(心跳轮询节奏)+ PARALLEL_KILL_GRACE_SECONDS(此计算已写进 reconftw.cfg L328-L329 的注释)。
4.5 本地与 Axiom 通用(D-15)
D-15 强调超时对本地与 axiom 分布式作业同样适用——两者的并行批处理包装是同一代码路径,无特例。Axiom 无需改动:超时击杀发生在父 shell 的 parallel_funcs 心跳层、每个作业子 shell 之前;从并行批处理内部发起的 axiom 分布式作业在本地子 shell 层被击杀(axiom-scan 命令被终止,其本身会向远端节点发信号,这已足够)。
五、DNS 超时默认值(PERF-02)
5.1 默认值从 0 改为非零(D-16)
D-16 规定 reconftw.cfg 中两个 DNS 超时默认值从 0 改为:
DNS_BRUTE_TIMEOUT=6h # timeout/gtimeout duration for DNS bruteforce (0 disables hard-timeout). Default protects against hung resolvers.
DNS_RESOLVE_TIMEOUT=4h # timeout/gtimeout duration for DNS resolve (0 disables hard-timeout). Default protects against hung resolvers.
(当前实现位于 reconftw.cfg L414-L415;规划文档中的原始引用行号为 L387-L388,随阶段推进行号有位移。)
这两个值都经由 _run_dns_with_heartbeat(modules/utils.sh L1434 附近)透传,该辅助函数尊重 0(禁用)——非零的 h 后缀时长被 timeout/gtimeout 原样接受。辅助函数本身零代码改动,只改 cfg 默认值加内联文档注释。
5.2 超时触发后的呈现(D-17)
D-17 规定:硬超时触发时,既有的 _run_dns_with_heartbeat 已经返回非零并输出清晰的日志行,无需新增呈现逻辑——既有的徽章路径(end_func 的 WARN)已足够。JSONL 日志条目应携带 reason=dns_hard_timeout,让下游工具(Phase 4 测试、AI 报告)能够区分该原因。
这与 ROADMAP.md 成功标准 4 一致:"新装 reconftw.cfg 出厂即带非零 DNS_BRUTE_TIMEOUT=6h 与 DNS_RESOLVE_TIMEOUT=4h 默认值,卡死的 DNS 运行以日志化超时中止,而不是无限期阻塞"。它修复了 CONCERNS.md 中"两个硬超时守卫默认 0(禁用),大词表上的长 DNS 爆破可能无限期运行"的性能隐患。
六、可复用资产与集成点速查
6.1 既有可复用资产
| 资产 | 位置 | 在 Phase 1 中的作用 |
|---|---|---|
check_disk_space() |
modules/utils.sh L432 | 已返回 0/1 并填充 DISK_SPACE_INFO;_check_disk_mid_run 包装它,无需重实现 df 解析 |
start_func/end_func |
modules/core.sh L1441 / L1471 | 并行安全的逐函数开始时间戳与 record_func_timing 保留;哨兵操作插入指定位置 |
_run_dns_with_heartbeat |
modules/utils.sh L1434 | 已尊重 0=disabled 并接受 timeout/gtimeout 风格时长;DNS 硬超时只改 cfg |
| 心跳循环 | lib/parallel.sh L508-L544 / L629-L665 | 已迭代存活 PID;超时击杀检查插入其中 |
| 状态持久化模式 | modules/core.sh L1547-L1552 | .status_<fn> 与 .status_reason_<fn> 模式已存在;超时报告(D-14)直接复用 |
log_json 辅助函数 |
各模块 | D-09、D-14、D-17 复用同一调用形态 log_json LEVEL func msg key=val ... |
6.2 集成点清单
start()(modules/modes.sh L13):安装_cleanup_inprogress的 EXIT/INT/TERM trap(D-02)、在首个模块开始前输出续跑汇总横幅(D-04)、承载PRESERVE=false孤儿清理循环(D-05)。- 既有 ERR trap(modules/modes.sh L140 附近):新 EXIT trap 必须与之组合而非替换。
reconftw.cfg:本阶段四处配置——PARALLEL_JOB_TIMEOUT_SECONDS=0、PARALLEL_KILL_GRACE_SECONDS=10(并行旋钮应紧邻PARALLEL_HEARTBEAT_SECONDS文档区,实际位于 reconftw.cfg L328-L331),以及DNS_BRUTE_TIMEOUT=6h、DNS_RESOLVE_TIMEOUT=4h两处取值变更(reconftw.cfg L414-L415)。- Axiom 无改动:见 4.5 节。
6.3 既有模式约束
- 徽章词汇表锁定:
OK/WARN/FAIL/SKIP/CACHE/INFO/RUN(CONVENTIONS.md)。超时击杀 =FAIL(D-14),磁盘满中止 =FAIL(D-09),续跑提示 =WARN(D-04),不引入任何新徽章值。 - verbosity 门控:
OUTPUT_VERBOSITY=0/1/2已管控哪些_print_msg/notification调用可见。续跑汇总行是 WARN 级别,默认 verbosity 1 下可见;磁盘满错误走_print_error,始终可见。 - CLI-over-config 重应用:本阶段新增旋钮(如
PARALLEL_KILL_GRACE_SECONDS)不需要 CLI 标志,env-var/cfg 覆盖已足够;若日后加标志,遵循reconftw.sh的CLI_*重应用模式。 - Source guards:本阶段不新增 lib 文件,全部改动进既有 lib/parallel.sh、modules/core.sh、modules/utils.sh、modules/modes.sh、reconftw.cfg,通过编辑既有文件保持 source-guard 模式。
6.4 命名约定
D-01 之外的私有辅助函数名(_check_disk_mid_run、_abort_disk_full、_timeout_kill_job、_cleanup_inprogress)遵循 CONVENTIONS.md 的下划线前缀私有约定(当前源码均以 function 关键字声明,符合函数命名规则)。JSONL 日志的 key/value 拼写(reason=inprogress_leftover、reason=timeout、reason=dns_hard_timeout)沿用 modules/core.sh 既有模式。
七、推迟项与边界(明确不做的内容)
原文档 Deferred 小节列出的刻意推迟项,理解它们有助于避免在后续使用中误以为存在缺失功能:
- 逐工具超时覆盖(如
PARALLEL_TIMEOUT_DNS_BRUTE_SECONDS):v1 讨论后否决;用户可上调PARALLEL_JOB_TIMEOUT_SECONDS以容纳最慢工具。仅在真实调优需求出现后重新评估。 - 函数中段的磁盘后台心跳监视器:能在下一个边界前捕获单个函数烧盘,但鉴于 5GB pre-flight 余量被否决为过度设计;若遥测显示截断事件,可在未来"可观测性"里程碑考虑。
- 逐重定向 ENOSPC trap 包裹每个
>/>>:最彻底但侵入全代码库,v1 不在范围内。 MIN_DISK_SPACE_GB2-vs-5 对齐:明确归 Phase 5(DOCS-02);Phase 1 刻意不碰 reconftw.cfg 与 modules/modes.sh 的取值。- 新代码路径的测试:Phase 4(TEST-01)覆盖
parallel_funcs超时击杀路径;哨兵生命周期与磁盘满处理测试可并入该阶段——Phase 1 交付行为,Phase 4 交付覆盖率。从 ROADMAP.md 可以看到,Phase 1 还额外完成了 01-04(_RECON_CLEAN_EXIT门控,对应 RESIL-01/CR-01)与 01-05(超时执行脱离--quiet门控 +_kill_tree进程树击杀,对应 RESIL-03/CR-02、CR-03)两个缺口闭合计划。
八、实战配置速查
将以下配置应用于 reconftw.cfg 即可启用 Phase 1 的三大实战能力:
# --- 续跑(RESIL-01)---
# PRESERVE=true 时,中断后重跑会检测 .inprogress_* 哨兵并只重执行崩溃函数
PRESERVE=true
# --- 磁盘满中途防护(RESIL-02)---
# 阈值沿用 pre-flight 的 MIN_DISK_SPACE_GB(注意 cfg 写 2、modes.sh 回退 5 的差异归 Phase 5)
MIN_DISK_SPACE_GB=5
# --- 并行作业超时(RESIL-03)---
# 0 禁用;长扫描建议 3600,CI 建议 600
PARALLEL_JOB_TIMEOUT_SECONDS=600
# 先 TERM、宽限期后仍存活再 KILL 的间隔秒数
PARALLEL_KILL_GRACE_SECONDS=10
# --- DNS 硬超时(PERF-02)---
# 0 禁用硬超时;默认值已保护免受挂死 resolver 阻塞
DNS_BRUTE_TIMEOUT=6h
DNS_RESOLVE_TIMEOUT=4h
典型场景与预期行为:
- 中断续跑:
sub_brute执行中被 Ctrl-C → 重跑(PRESERVE=true)→ 横幅输出WARN resume: 1 functions re-running after interruption (sub_brute),其余已完函数因.<fn>存在被跳过。 - 磁盘写满:运行中途可用空间跌破
MIN_DISK_SPACE_GB→ 下一函数边界_check_disk_mid_run失败 →_abort_disk_full输出disk_full: aborting (...)并exit 1,EXIT trap 顺带清理哨兵,后续运行不误报续跑。 - 卡死工具:某并行作业超过
PARALLEL_JOB_TIMEOUT_SECONDS→_kill_tree TERM(宽限 10s)→_kill_tree KILL→ 徽章FAIL func_name 600s (timeout),failed++使RECON_PARTIAL_RUN=true,批次继续而非整体停摆。 - DNS 挂起:resolver 卡死导致 DNS 爆破超过
6h/ 解析超过4h→_run_dns_with_heartbeat经timeout/gtimeout终止并返回非零,日志携带reason=dns_hard_timeout,end_func以WARN徽章呈现。
九、总结
Phase 1 以"哨兵文件 + 干净退出门控 trap"替代 PID/时间戳方案,把续跑检测收敛为纯表面指示,让控制流继续由 .<fn> 检查点驱动;以边界 df 检查 + 硬中止拦截磁盘写满,避免截断输出污染后续管线;以 heartbeat 循环内的 TERM→KILL 进程树击杀为并行批处理提供每作业时限;并以 6h/4h 的 DNS 硬超时默认值堵住无限阻塞。四者共用既有徽章词汇、log_json 形态与状态持久化模式,未新增任何配置之外的运行面——整套设计在"单操作员、fail-fast、向后兼容"三大约束下,用最小的机制面换取确定性的恢复与中止语义。对希望在长时侦察中保住已投入计算、又能防止环境故障静默劣化输出的使用者而言,这四项配置与行为是优先理解的可靠性基座。