用 inotifywait 做 300ms 事件去抖:源码保存只触发一次构建

举报
yd_217513249 发表于 2026/11/01 01:33:12 2026/11/01
【摘要】 编辑器保存源码时 close_write 常连续触发,导致构建重复执行。本文用 inotifywait 监听目录,通过命名管道把事件送入去抖循环,以 300ms 超时窗口合并连续事件,只执行一次构建,并给出后台监听清理与忽略 .git 的最小 Shell 脚本。

用 inotifywait 做 300ms 事件去抖:源码保存只触发一次构建

问题场景

很多编辑器保存文件时,底层操作不止一次 write。常见做法是先写临时文件,再 rename 到目标位置;也有编辑器分块写入并多次 close。于是 inotify 会在很短时间内连续上报多个 close_write 或 moved_to 事件。如果对每个事件都直接执行构建,保存一次就可能编译多次,日志被刷屏,机器资源也被重复占用。

目标很明确:把短时间内的连续事件合并起来,等事件流安静下来后,只执行一次构建命令。

思路

用 inotifywait 常驻监听目标目录,把事件路径写入一个命名管道(FIFO)。主循环从管道读到第一个事件后不立即构建,而是继续在同一个管道上 read,并设置 300ms 超时。超时前每来一个新事件,都会重新计时,形成滑动窗口;直到 300ms 内没有新事件,才执行构建。

监听进程放在后台运行,脚本退出时通过 trap 杀掉它并删除临时管道,避免留下孤儿进程。

最小实现

#!/usr/bin/env bash
# watch-build.sh:监听目录,300ms 去抖后执行一次构建
set -uo pipefail

WATCH_DIR=${1:-.}
DEBOUNCE=${DEBOUNCE:-0.3}
BUILD_CMD=${BUILD_CMD:-make}

command -v inotifywait >/dev/null || { echo '缺少 inotify-tools' >&2; exit 1; }

TMPD=$(mktemp -d)
FIFO=$TMPD/events
mkfifo "$FIFO"

WATCH_PID=
cleanup() {
  if [ -n "$WATCH_PID" ]; then
    kill "$WATCH_PID" 2>/dev/null
    wait "$WATCH_PID" 2>/dev/null
    WATCH_PID=
  fi
  rm -rf "$TMPD"
}
trap cleanup EXIT INT TERM

inotifywait -m -r -q \
  -e close_write -e moved_to \
  --exclude '(^|/)\.git/' \
  --format '%w%f' "$WATCH_DIR" >"$FIFO" &
WATCH_PID=$!

printf '监听 %s,去抖 %ss,构建命令 %s\n' "$WATCH_DIR" "$DEBOUNCE" "$BUILD_CMD"

while IFS= read -r path; do
  # 吞掉随后到来的重复事件;每读到一个事件都会重新计时
  while IFS= read -r -t "$DEBOUNCE" _; do :; done
  printf '[watch] 变更: %s\n' "$path"
  if ( cd "$WATCH_DIR" && bash -c "$BUILD_CMD" ); then
    echo '[watch] 构建完成'
  else
    rc=$?
    printf '[watch] 构建失败,退出码 %s\n' "$rc" >&2
  fi
done <"$FIFO"

几个关键点:

  • 用命名管道连接监听进程和消费循环,读写两端职责清晰,便于在 trap 中统一清理。
  • 内层 read -t "$DEBOUNCE" 负责去抖。超时返回非零值,循环自然结束,因此脚本没有使用 set -e,否则超时会被当成错误退出。
  • close_write 覆盖常规写入,moved_to 覆盖“写临时文件再 rename”的原子保存方式;两者都纳入合并窗口。
  • --exclude '(^|/)\.git/' 用正则排除 .git 路径下的事件,减少无关触发。若你的 inotifywait 上报格式不同,可以先用 --format '%w%f' 打印路径确认匹配效果。
  • 构建命令在 WATCH_DIR 下通过 bash -c 执行,相对路径会以被监听目录为基准。

运行方法

chmod +x watch-build.sh

# 监听当前目录,默认执行 make
./watch-build.sh .

# 自定义构建命令与去抖时间
BUILD_CMD='npm run build' DEBOUNCE=0.5 ./watch-build.sh src

# 另开终端制造一次变更
echo 'test' >> src/a.txt

预期行为是:连续多次保存只在最后一次保存后约 300ms 触发一次构建,终端只打印一条变更路径和一次构建结果。

边界与限制

  • 持续事件可能让构建长期得不到执行。 当前实现是滑动窗口:只要事件间隔始终小于 300ms,计时就会一直重置。若目录内有高频写入,可增加“最长等待时间”,超过阈值就强制构建一次;最小示例未包含该逻辑。
  • 新建子目录不一定被自动纳入监听。 inotifywait -r 通常在启动时建立递归 watch,新建子目录是否自动加入取决于 inotify-tools 的版本与实现。创建新目录后,更稳妥的做法是重启脚本,或对新增目录单独建立监听。
  • 构建期间的事件不会丢失。 构建执行时,新事件会暂存在管道中;构建结束后被读出并合并成下一次构建。代价是可能多出一次构建,但不会漏掉变更。
  • 内核 watch 配额有限。 目录树很大时会触及 fs.inotify.max_user_watches 等限制,需要按需调整内核参数;排除 .git 主要减少事件输出,不能完全替代配额规划。
  • 小数秒超时依赖 bash 版本。 read -t 0.3 需要 bash 4 及以上;更旧的 shell 可改用后台 sleep 加 kill,或借助 timeout 命令实现类似效果。
  • 构建命令被视为受信任输入。 BUILD_CMD 交给 bash -c 执行,适合本地开发脚本;不要把外部不可信内容拼进该变量。
  • 退出清理依赖信号。 正常 Ctrl+C 或脚本退出会触发 trap 清理;若进程被 SIGKILL 强制杀死,trap 不会执行,需要手动检查残留的 inotifywait 进程。

这个脚本适合本地开发时的“保存即构建”场景,逻辑短、依赖少,也便于替换成测试、格式化或同步命令。真正接入 CI 时,仍建议由版本控制事件或流水线触发构建,而不是依赖文件系统事件。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。