reconFTW 弹性恢复与超时安全:`.inprogress` 哨兵、磁盘满防护与并行作业超时机制深度解析

原创2026-09-25 13:04:531,003 阅读
文章标签:渗透测试网络安全应用安全

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_exit trap 调用 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_GB 2-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

典型场景与预期行为:

  1. 中断续跑:sub_brute 执行中被 Ctrl-C → 重跑(PRESERVE=true)→ 横幅输出 WARN resume: 1 functions re-running after interruption (sub_brute),其余已完函数因 .<fn> 存在被跳过。
  2. 磁盘写满:运行中途可用空间跌破 MIN_DISK_SPACE_GB → 下一函数边界 _check_disk_mid_run 失败 → _abort_disk_full 输出 disk_full: aborting (...) 并 exit 1,EXIT trap 顺带清理哨兵,后续运行不误报续跑。
  3. 卡死工具:某并行作业超过 PARALLEL_JOB_TIMEOUT_SECONDS → _kill_tree TERM(宽限 10s)→ _kill_tree KILL → 徽章 FAIL func_name 600s (timeout),failed++ 使 RECON_PARTIAL_RUN=true,批次继续而非整体停摆。
  4. 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、向后兼容"三大约束下,用最小的机制面换取确定性的恢复与中止语义。对希望在长时侦察中保住已投入计算、又能防止环境故障静默劣化输出的使用者而言,这四项配置与行为是优先理解的可靠性基座。

登录后查看全文
reconftw