
Token Monitor 文件监听实战Watcher 子进程隔离如何化解 macOS 文件描述符难题【免费下载链接】token-monitorLocal-first desktop widget for tracking token usage, costs, and limits across 43 AI coding tools—including Claude Code, Codex, Cursor, OpenCode, OpenClaw, and more—with multi-device sync.项目地址: https://gitcode.com/gh_mirrors/tok/token-monitorToken Monitor 是一款本地优先local-first的 AI 用量监控桌面组件实时追踪 Claude Code、Codex、Cursor 等 43 款 AI 编程工具的 token 消耗、成本与配额上限并支持多设备同步。本文深入剖析它的实时采集原理文件监听如何做到秒级感知、为什么把 Watcher 放进隔离子进程以及 macOS 上文件描述符耗尽EBADF这个经典难题是如何被化解的 采集总览文件监听 防抖 兜底全量对账Token Monitor 不依赖任何云端 API 抓包它监控的是各 AI 工具在本地写下的数据文件会话 JSONL、SQLite 数据库等。采集引擎的入口是 collector.js核心策略有三层文件监听主路径用 [chokidar](https://example.com 不存在-占位) 之类的原生文件系统事件监听各工具数据目录文件一变立刻触发防抖合并监听事件经watchDebounceMs默认 1.5s防抖并受watchMaxWaitMs上限 5s封顶——保证3~5 秒内更新的产品承诺又不会让持续写入的 Agent 把扫描排队到天荒地老周期性全量扫描兜底监听只是优化不是事实来源。每小时的全量对账扫描负责补齐漏掉的事件、新创建的客户端目录和跨天数据。事件到达后采集器会把变更路径反查回它属于哪个客户端只对受影响的客户端做一次定向--today增量扫描而不是每次都全量扫描所有工具——这是它能同时盯 40 多个工具仍保持轻快的关键。第一步监听器在哪里跑防抖怎么写真正创建 chokidar 实例的工厂函数是openWatch()collector.js。几个值得注意的参数ignoreInitial: true不回放历史事件只关心从现在开始的变化awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 200 }等文件写完500ms 稳定再发事件避免读到写了一半的会话文件轮询模式兜底{ usePolling: true, interval: 2000, binaryInterval: 5000 }用于原生事件不可用或描述符耗尽的场景。事件回调handleWatchEvent()collector.js做了三件关键事归属定位clientsForWatchPath()把文件路径映射回客户端作为分区键防抖调度scheduleTick()在防抖窗口内合并事件一次 tick 只扫一遍所有变更过的客户端自触发拦截直接丢弃SQLite -shm旁路文件的事件后文细讲。关键抉择为什么必须用子进程而不是 Worker 线程这是本项目最有含金量的架构决策答案分两层。第一层close()的同步卡顿chokidar 的close()在调用线程上是同步的且开销随监听目录数量超线性增长——真实测量548 个目录约 1.1 秒1820 个目录约 12 秒。而每次增删一个被跟踪的客户端监听根目录都要重建。如果监听器跑在主进程/主线程上每次开关客户端都会把组件卡死约一秒。所以第一步就是把它挪出拥有者线程。第二层macOS 的文件描述符悬崖仅仅挪到一个 worker线程不够。原因藏在 macOS 的一个细节里在 macOS 上chokidar 对每个被监听的文件各持有一个文件描述符fd。当监听的文件多到把OPEN_MAX10240以内的 fd 编号全部占满后系统为下一个子进程分配的 IPC 管道编号会超过OPEN_MAXposix_spawn直接拒绝——结果就是 Token Monitor 每次拉起 tokscale 扫描子进程都失败报EBADF即 issue #520。而 worker线程和宿主进程共享同一张描述符表线程里开再多 fd 也等价于宿主自己开了。解决办法是fork 一个独立子进程那些描述符和 chokidar 的原生内存分配全部留在子进程里宿主进程干干净净spawn永远能拿到合法的低编号管道。这个决策完整地记录在 watcherHost.js 的文件头注释里还顺带澄清了一个诱人的替代方案❌unwatch()不是解药——它只停止事件分发描述符照样保留实测关闭前后都是 2613 个 fd✅ 真正的释放只发生在监听器彻底close()或进程退出时。协作机制Coordinator × Worker 的latest-wins协议隔离边界由 watcherHost.js 中的WatcherProcess 协调器coordinator实现子进程那一端则是 watcherWorker.js。整套协议可以概括为几条铁律1. 一次只活一个 workeracquire()每次被调用即监听根集变化都会递增revision版本号。替换 worker 之前协调器必须先等旧 worker 确认退出——这是同步close()时代旧监听器完全消失后才开新的这一不变量在进程间的复刻。Linux 上 inotify 预算是按用户共享的编辑器也在用两套描述符重叠可能直接触发耗尽。2. Latest-wins不排队worker 端watcherWorker.js用一个desired槽 pump()循环处理配置一次close()可能跑好几分钟用户在这期间改了三四个设置worker 不会把中间状态一一回放只在旧树关闭后检查是不是又有更新的请求了是就跳过、直接应用最新的一份。3. 串行化只发生在close()上代码里特意注明ready事件不作为生命周期步骤来等待否则会卡住整个 pump 直到首轮扫描完成只有close()需要串行因为只有它持有必须在新监听前消失的描述符。4. 子进程不能比宿主活得久watcherWorker.js 监听 IPCdisconnect事件宿主退出或崩溃后子进程自动process.exit(0)不会变成抱着一堆描述符的孤儿进程。在 Electron 下fork 时设置ELECTRON_RUN_AS_NODE1watcherHost.js让子进程以纯 Node 身份运行避免 macOS 上多出一个 Dock 图标。5. 降级是粘性的若子进程异常退出或terminate()迟迟得不到退出确认10 秒看门狗超时说明描述符无法假定已释放协调器会回落到进程内监听in-process host而且此时强制使用轮询模式并记一条watcher-host-fallback诊断事件——因为宿主自己持有描述符 原生事件正是当初出问题的组合。轮询还有上限保护openWatch()启动前会先清点将要覆盖的路径数超过WATCH_POLLING_ENTRY_LIMIT直接拒绝over N paths to poll而不是让轮询把磁盘 stat 打爆。细节防御三个自伤场景的解法好的实时采集一半功夫花在防自己咬自己SQLite-shm自监听循环只读打开一个 WAL 模式的 SQLite 库仍会让 SQLite 重写db-shm共享内存索引。对监听器来说这和真实数据变化无法区分——于是出现监听事件 → 定向扫描 → shm 被重写 → 监听事件……的死循环实测某客户端停止状态下每 5 分钟 142 个幽灵事件。解法在handleWatchEvent()里按SELF_WATCHED_SQLITE_SIDECAR_CLIENTS白名单精准丢弃这些旁路文件的事件collector.js真正的数据信号仍然来自数据库本体和-wal文件监听器自身是 tokscale 的产物自同步写入的 tokscale 缓存目录刻意不监听避免我们写的东西又触发我们环境变量开关TOKEN_MONITOR_WATCH_IN_PROCESS可强制进程内监听测试与排障用TOKEN_MONITOR_WATCH_POLLING可覆盖原生/轮询的默认选择watcherHost.js。小结Token Monitor 的实时采集架构可以浓缩成一张决策链问题解法源码close()同步卡顿冻结 UI监听器移出拥有者线程watcherHost.jsmacOS fd 占满导致EBADF用子进程而非线程隔离描述符watcherHost.js描述符重叠风险单 worker 退出屏障 latest-winswatcherWorker.jsworker 崩溃进程内轮询粘性降级 诊断事件collector.js自触发死循环精确忽略-shm旁路文件collector.js更完整的采集、监听与自同步设计说明可参考 docs/architecture.md多设备同步部分则可阅读 docs/API.md 与 worker/README.md。【免费下载链接】token-monitorLocal-first desktop widget for tracking token usage, costs, and limits across 43 AI coding tools—including Claude Code, Codex, Cursor, OpenCode, OpenClaw, and more—with multi-device sync.项目地址: https://gitcode.com/gh_mirrors/tok/token-monitor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考