用 inotifywait 做 300ms 事件去抖:源码保存只触发一次构建
【摘要】 编辑器保存源码时 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)