DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

fs.watch 收到 rename 后,不再发现文件修改

Linux、macOS原子保存可能把文件换成新inode。检查真实保存方式,监听稳定父目录并重新读取当前内容,处理缺失文件名和读取竞态。

程序直接监听一个文件,编辑器保存后收到一次 rename,随后再修改同一路径却没有通知。在 Linux、macOS 上,这可能是保存时替换了文件:编辑器先写临时文件,再把它重命名到目标路径。文件名没有变,监听对象却可能仍是旧 inode。

先确认实际保存方式和运行文件系统。单个 rename 事件不能直接证明文件已经被替换;文件被移走、删除等情况也需要检查。

路径相同,不等于仍是同一个文件

Linux、macOS 上的 fs.watch(filename) 会把路径解析为 inode 并监听它。目标删除后重新创建,会得到新的 inode。原监听器可能报告删除,却不会自动转而监听新对象,所以之后写入同名文件也没有新的通知。

应比较保存前后的文件身份,以及编辑器是否启用了原子保存。Linux 上可查看 inode;应用已有 stat 诊断时,可记录目标的 dev、ino。不要仅凭修改时间变化判断监听对象是否相同,也不要为验证监听而直接改写生产配置。

如果监听器创建时就报 ENOSPC,应先查Linux 文件监听配额。已经收到事件、随后因为文件替换而失效,与创建监听项失败属于不同问题。

经常替换的文件,监听稳定的父目录

对于编辑器会替换的配置文件,可以监听其父目录,在相关事件到来后重新读取目标路径。这样处理的是“现在这个路径上有什么内容”,而不是假设每次保存都修改同一个 inode。

下面只演示事件筛选;scheduleConfigRead 是应用需要提供的函数,负责合并重复通知、读取与验证当前配置,并处理读取错误。它不是 Node 内置 API:

const { watch } = require('node:fs');
const path = require('node:path');

const target = path.resolve('config.json');
const directory = path.dirname(target);
const basename = path.basename(target);

const watcher = watch(directory, (eventType, filename) => {
  if (filename === null || filename === basename) {
    scheduleConfigRead(target);
  }
});

这段采用默认的 UTF-8 文件名编码。回调中的 filename 即使在支持的平台上也可能为 null,因此不能把缺失名称的事件全部丢掉;此时重新检查目标当前状态。

父目录必须确实存在并保持稳定,且应用有权访问。目录自身也被整体替换时,还需要重新建立对应监听。启动监听前后应协调一次当前内容检查,避免只等待未来事件而漏掉已有配置。

scheduleConfigRead 应按业务安排短暂合并和串行读取,避免多次保存使异步读取结果倒序覆盖。读取时文件可能暂时不存在,JSON 也可能不完整。应明确保留上一个有效配置还是拒绝继续运行,并记录必要的错误,不把事件回调当成“内容已经有效”的信号。

继续监听单文件时,要处理重新注册的空窗

也可以在确认目标身份改变后,关闭旧 watcher 并重新注册,但不能只在 rename 后无条件调用一次 watch。目标当时可能尚未存在;先 stat 后注册之间也仍有竞态,需要处理注册失败,并在注册完成后重新检查当前文件。

fs.watchFile() 使用 stat 轮询,可作为某些环境的替代方式。它需要权衡检查间隔、文件系统开销和刷新延迟,也不是逐次记录全部修改的接口。网络文件系统、虚拟化挂载上的事件监听可能另有可靠性限制,需按真实部署环境选择。

修改监听代码后,在隔离环境分别验证原地写入、原子替换、删除后恢复和快速连续保存。检查应用最终加载的内容,而不只是事件次数。监听通知可能合并,不能保证每个短暂中间状态都被应用读到。退出或重建监听时关闭旧 watcher,避免重复回调与资源积累。